diff --git a/compiler/packages/babel-plugin-react-compiler/src/ReactiveScopes/CodegenReactiveFunction.ts b/compiler/packages/babel-plugin-react-compiler/src/ReactiveScopes/CodegenReactiveFunction.ts index f7da5229548ab..4cdefed8208d0 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/ReactiveScopes/CodegenReactiveFunction.ts +++ b/compiler/packages/babel-plugin-react-compiler/src/ReactiveScopes/CodegenReactiveFunction.ts @@ -1810,41 +1810,70 @@ function codegenInstructionValue( case 'MethodCall': { const isHook = getHookKind(cx.env, instrValue.property.identifier) != null; - const memberExpr = codegenPlaceToExpression(cx, instrValue.property); - CompilerError.invariant( - t.isMemberExpression(memberExpr) || - t.isOptionalMemberExpression(memberExpr), - { - reason: - '[Codegen] Internal error: MethodCall::property must be an unpromoted + unmemoized MemberExpression. ' + - `Got a \`${memberExpr.type}\``, - description: null, - loc: memberExpr.loc ?? null, - suggestions: null, - }, - ); - CompilerError.invariant( - t.isNodesEquivalent( - memberExpr.object, - codegenPlaceToExpression(cx, instrValue.receiver), - ), - { - reason: - '[Codegen] Internal error: Forget should always generate MethodCall::property ' + - 'as a MemberExpression of MethodCall::receiver', - description: null, - loc: memberExpr.loc ?? null, - suggestions: null, - }, - ); - const args = instrValue.args.map(arg => codegenArgument(cx, arg)); - value = createCallExpression( - cx.env, - memberExpr, - args, - instrValue.loc, - isHook, - ); + /** + * We need to check if the property was memoized. If it has, we should reconstruct the + * MemberExpression. + */ + let memberExpr: t.Expression; + const tmp = cx.temp.get(instrValue.property.identifier.declarationId); + if (tmp != null && tmp.type === 'Identifier') { + /** + * We can't reconstruct the MemberExpression from just the identifier, so we work around + * this by allowing an Identifier here. + */ + memberExpr = tmp; + } else if (tmp != null) { + memberExpr = convertValueToExpression(tmp); + } else { + memberExpr = codegenPlaceToExpression(cx, instrValue.property); + } + + // Reconstruct the MemberExpression if we previously saw an Identifier. + if (memberExpr.type === 'Identifier') { + const args = instrValue.args.map(arg => codegenArgument(cx, arg)); + value = createCallExpression( + cx.env, + memberExpr, + args, + instrValue.loc, + isHook, + ); + } else { + CompilerError.invariant( + t.isMemberExpression(memberExpr) || + t.isOptionalMemberExpression(memberExpr), + { + reason: + '[Codegen] Internal error: MethodCall::property must be an unpromoted + unmemoized MemberExpression. ' + + `Got a \`${memberExpr.type}\``, + description: null, + loc: memberExpr.loc ?? null, + suggestions: null, + }, + ); + CompilerError.invariant( + t.isNodesEquivalent( + memberExpr.object, + codegenPlaceToExpression(cx, instrValue.receiver), + ), + { + reason: + '[Codegen] Internal error: Forget should always generate MethodCall::property ' + + 'as a MemberExpression of MethodCall::receiver', + description: null, + loc: memberExpr.loc ?? null, + suggestions: null, + }, + ); + const args = instrValue.args.map(arg => codegenArgument(cx, arg)); + value = createCallExpression( + cx.env, + memberExpr, + args, + instrValue.loc, + isHook, + ); + } break; } case 'NewExpression': { diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/codegen-methodcall-readonly-receiver.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/codegen-methodcall-readonly-receiver.expect.md new file mode 100644 index 0000000000000..1540dea9fc425 --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/codegen-methodcall-readonly-receiver.expect.md @@ -0,0 +1,69 @@ + +## Input + +```javascript +import {PrimitiveBox} from 'shared-runtime'; + +function Component({value, realmax}) { + const box = new PrimitiveBox(value); + const maxValue = Math.max(box.get(), realmax); + // ^^^^^^^^^ should not be separated into static call + return
{maxValue}
; +} + +export const FIXTURE_ENTRYPOINT = { + fn: Component, + params: [{value: 42, realmax: 100}], + isComponent: true, +}; + +``` + +## Code + +```javascript +import { c as _c } from "react/compiler-runtime"; +import { PrimitiveBox } from "shared-runtime"; + +function Component(t0) { + const $ = _c(6); + const { value, realmax } = t0; + let t1; + let t2; + let t3; + if ($[0] !== value) { + const box = new PrimitiveBox(value); + t1 = Math; + t2 = t1.max; + t3 = box.get(); + $[0] = value; + $[1] = t1; + $[2] = t2; + $[3] = t3; + } else { + t1 = $[1]; + t2 = $[2]; + t3 = $[3]; + } + const maxValue = t2(t3, realmax); + let t4; + if ($[4] !== maxValue) { + t4 =
{maxValue}
; + $[4] = maxValue; + $[5] = t4; + } else { + t4 = $[5]; + } + return t4; +} + +export const FIXTURE_ENTRYPOINT = { + fn: Component, + params: [{ value: 42, realmax: 100 }], + isComponent: true, +}; + +``` + +### Eval output +(kind: ok)
100
\ No newline at end of file diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/codegen-methodcall-readonly-receiver.js b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/codegen-methodcall-readonly-receiver.js new file mode 100644 index 0000000000000..7ca503d37541c --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/codegen-methodcall-readonly-receiver.js @@ -0,0 +1,14 @@ +import {PrimitiveBox} from 'shared-runtime'; + +function Component({value, realmax}) { + const box = new PrimitiveBox(value); + const maxValue = Math.max(box.get(), realmax); + // ^^^^^^^^^ should not be separated into static call + return
{maxValue}
; +} + +export const FIXTURE_ENTRYPOINT = { + fn: Component, + params: [{value: 42, realmax: 100}], + isComponent: true, +}; diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/codegen-methodcall.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/codegen-methodcall.expect.md new file mode 100644 index 0000000000000..f1132ae58b6c5 --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/codegen-methodcall.expect.md @@ -0,0 +1,85 @@ + +## Input + +```javascript +function foo() { + return { + bar() { + return 3.14; + }, + }; +} + +const YearsAndMonthsSince = () => { + const diff = foo(); + const months = Math.floor(diff.bar()); + return <>{months}; +}; + +export const FIXTURE_ENTRYPOINT = { + fn: YearsAndMonthsSince, + params: [], + isComponent: true, +}; + +``` + +## Code + +```javascript +import { c as _c } from "react/compiler-runtime"; +function foo() { + const $ = _c(1); + let t0; + if ($[0] === Symbol.for("react.memo_cache_sentinel")) { + t0 = { + bar() { + return 3.14; + }, + }; + $[0] = t0; + } else { + t0 = $[0]; + } + return t0; +} + +const YearsAndMonthsSince = () => { + const $ = _c(4); + let t0; + let t1; + let t2; + if ($[0] === Symbol.for("react.memo_cache_sentinel")) { + const diff = foo(); + t0 = Math; + t1 = t0.floor; + t2 = diff.bar(); + $[0] = t0; + $[1] = t1; + $[2] = t2; + } else { + t0 = $[0]; + t1 = $[1]; + t2 = $[2]; + } + const months = t1(t2); + let t3; + if ($[3] === Symbol.for("react.memo_cache_sentinel")) { + t3 = <>{months}; + $[3] = t3; + } else { + t3 = $[3]; + } + return t3; +}; + +export const FIXTURE_ENTRYPOINT = { + fn: YearsAndMonthsSince, + params: [], + isComponent: true, +}; + +``` + +### Eval output +(kind: ok) 3 \ No newline at end of file diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/codegen-methodcall.js b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/codegen-methodcall.js new file mode 100644 index 0000000000000..a46959eef4f66 --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/codegen-methodcall.js @@ -0,0 +1,19 @@ +function foo() { + return { + bar() { + return 3.14; + }, + }; +} + +const YearsAndMonthsSince = () => { + const diff = foo(); + const months = Math.floor(diff.bar()); + return <>{months}; +}; + +export const FIXTURE_ENTRYPOINT = { + fn: YearsAndMonthsSince, + params: [], + isComponent: true, +}; diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.bug-invariant-codegen-methodcall.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.bug-invariant-codegen-methodcall.expect.md deleted file mode 100644 index 4ea831de8751e..0000000000000 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.bug-invariant-codegen-methodcall.expect.md +++ /dev/null @@ -1,31 +0,0 @@ - -## Input - -```javascript -const YearsAndMonthsSince = () => { - const diff = foo(); - const months = Math.floor(diff.bar()); - return <>{months}; -}; - -``` - - -## Error - -``` -Found 1 error: - -Invariant: [Codegen] Internal error: MethodCall::property must be an unpromoted + unmemoized MemberExpression. Got a `Identifier` - -error.bug-invariant-codegen-methodcall.ts:3:17 - 1 | const YearsAndMonthsSince = () => { - 2 | const diff = foo(); -> 3 | const months = Math.floor(diff.bar()); - | ^^^^^^^^^^ [Codegen] Internal error: MethodCall::property must be an unpromoted + unmemoized MemberExpression. Got a `Identifier` - 4 | return <>{months}; - 5 | }; - 6 | -``` - - \ No newline at end of file diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.bug-invariant-codegen-methodcall.js b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.bug-invariant-codegen-methodcall.js deleted file mode 100644 index 948182653cbe0..0000000000000 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.bug-invariant-codegen-methodcall.js +++ /dev/null @@ -1,5 +0,0 @@ -const YearsAndMonthsSince = () => { - const diff = foo(); - const months = Math.floor(diff.bar()); - return <>{months}; -}; diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.todo-nested-method-calls-lower-property-load-into-temporary.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.todo-nested-method-calls-lower-property-load-into-temporary.expect.md deleted file mode 100644 index 67d6c4f4e0549..0000000000000 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.todo-nested-method-calls-lower-property-load-into-temporary.expect.md +++ /dev/null @@ -1,39 +0,0 @@ - -## Input - -```javascript -import {makeArray} from 'shared-runtime'; - -const other = [0, 1]; -function Component({}) { - const items = makeArray(0, 1, 2, null, 4, false, 6); - const max = Math.max(2, items.push(5), ...other); - return max; -} - -export const FIXTURE_ENTRYPOINT = { - fn: Component, - params: [{}], -}; - -``` - - -## Error - -``` -Found 1 error: - -Invariant: [Codegen] Internal error: MethodCall::property must be an unpromoted + unmemoized MemberExpression. Got a `Identifier` - -error.todo-nested-method-calls-lower-property-load-into-temporary.ts:6:14 - 4 | function Component({}) { - 5 | const items = makeArray(0, 1, 2, null, 4, false, 6); -> 6 | const max = Math.max(2, items.push(5), ...other); - | ^^^^^^^^ [Codegen] Internal error: MethodCall::property must be an unpromoted + unmemoized MemberExpression. Got a `Identifier` - 7 | return max; - 8 | } - 9 | -``` - - \ No newline at end of file diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/nested-method-calls-lower-property-load-into-temporary.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/nested-method-calls-lower-property-load-into-temporary.expect.md new file mode 100644 index 0000000000000..638e993014912 --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/nested-method-calls-lower-property-load-into-temporary.expect.md @@ -0,0 +1,62 @@ + +## Input + +```javascript +import {makeArray} from 'shared-runtime'; + +const other = [0, 1]; +function Component({}) { + const items = makeArray(0, 1, 2, null, 4, false, 6); + const max = Math.max(2, items.push(5), ...other); + return max; +} + +export const FIXTURE_ENTRYPOINT = { + fn: Component, + params: [{}], +}; + +``` + +## Code + +```javascript +import { c as _c } from "react/compiler-runtime"; +import { makeArray } from "shared-runtime"; + +const other = [0, 1]; +function Component(t0) { + const $ = _c(4); + let t1; + let t2; + let t3; + let t4; + if ($[0] === Symbol.for("react.memo_cache_sentinel")) { + const items = makeArray(0, 1, 2, null, 4, false, 6); + t1 = Math; + t2 = t1.max; + t3 = 2; + t4 = items.push(5); + $[0] = t1; + $[1] = t2; + $[2] = t3; + $[3] = t4; + } else { + t1 = $[0]; + t2 = $[1]; + t3 = $[2]; + t4 = $[3]; + } + const max = t2(t3, t4, ...other); + return max; +} + +export const FIXTURE_ENTRYPOINT = { + fn: Component, + params: [{}], +}; + +``` + +### Eval output +(kind: ok) 8 \ No newline at end of file diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.todo-nested-method-calls-lower-property-load-into-temporary.js b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/nested-method-calls-lower-property-load-into-temporary.js similarity index 100% rename from compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.todo-nested-method-calls-lower-property-load-into-temporary.js rename to compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/nested-method-calls-lower-property-load-into-temporary.js diff --git a/compiler/packages/snap/src/sprout/shared-runtime-type-provider.ts b/compiler/packages/snap/src/sprout/shared-runtime-type-provider.ts index 58b007c1c7355..2c16c8ba43569 100644 --- a/compiler/packages/snap/src/sprout/shared-runtime-type-provider.ts +++ b/compiler/packages/snap/src/sprout/shared-runtime-type-provider.ts @@ -196,6 +196,29 @@ export function makeSharedRuntimeTypeProvider({ ], }, }, + PrimitiveBox: { + kind: 'object', + properties: { + get: { + kind: 'function', + positionalParams: [], + restParam: null, + calleeEffect: EffectEnum.Read, + returnType: {kind: 'type', name: 'Primitive'}, + returnValueKind: ValueKindEnum.Primitive, + aliasing: { + receiver: '@receiver', + params: ['@value'], + rest: null, + returns: '@return', + temporaries: [], + effects: [ + {kind: 'CreateFrom', from: '@value', into: '@return'}, + ], + }, + }, + }, + }, }, }; } else if (moduleName === 'ReactCompilerTest') { diff --git a/compiler/packages/snap/src/sprout/shared-runtime.ts b/compiler/packages/snap/src/sprout/shared-runtime.ts index f37ca82709022..2ae5bcc61d3ed 100644 --- a/compiler/packages/snap/src/sprout/shared-runtime.ts +++ b/compiler/packages/snap/src/sprout/shared-runtime.ts @@ -421,4 +421,22 @@ export function typedMutate(x: any, v: any = null): void { x.property = v; } +type PrimitiveValue = + | number + | string + | boolean + | symbol + | null + | undefined + | bigint; +export class PrimitiveBox { + value: T; + constructor(value: T) { + this.value = value; + } + get(): T { + return this.value; + } +} + export default typedLog;