diff --git a/compiler/forget/src/CompilerError.ts b/compiler/forget/src/CompilerError.ts
index c725ae20bd..ef1675f3e2 100644
--- a/compiler/forget/src/CompilerError.ts
+++ b/compiler/forget/src/CompilerError.ts
@@ -128,6 +128,19 @@ export class CompilerError extends Error {
throw errors;
}
+ static invalidInput(reason: string, loc: SourceLocation): never {
+ const errors = new CompilerError();
+ errors.pushErrorDetail(
+ new CompilerErrorDetail({
+ codeframe: null,
+ loc: typeof loc === "symbol" ? null : loc,
+ reason,
+ severity: ErrorSeverity.InvalidInput,
+ })
+ );
+ throw errors;
+ }
+
constructor(...args: any[]) {
super(...args);
}
diff --git a/compiler/forget/src/HIR/HIR.ts b/compiler/forget/src/HIR/HIR.ts
index 9a1e1e374d..34c5c6a8d7 100644
--- a/compiler/forget/src/HIR/HIR.ts
+++ b/compiler/forget/src/HIR/HIR.ts
@@ -449,7 +449,7 @@ export type Phi = {
* Type inference does not currently guarantee that {@link PropertyCall.property}
* is a FunctionType.
*/
-type PropertyCall = {
+export type PropertyCall = {
kind: "PropertyCall";
receiver: Place;
property: Place;
diff --git a/compiler/forget/src/HIR/PrintHIR.ts b/compiler/forget/src/HIR/PrintHIR.ts
index 9139f7e885..e582c8be54 100644
--- a/compiler/forget/src/HIR/PrintHIR.ts
+++ b/compiler/forget/src/HIR/PrintHIR.ts
@@ -564,7 +564,7 @@ function printScope(scope: ReactiveScope | null): string {
return `${scope !== null ? `_@${scope.id}` : ""}`;
}
-function printType(type: Type): string {
+export function printType(type: Type): string {
if (type.kind === "Type") return "";
// TODO(mofeiZ): add debugName for generated ids
if (type.kind === "Object" && type.shapeId != null) {
diff --git a/compiler/forget/src/Inference/InferReferenceEffects.ts b/compiler/forget/src/Inference/InferReferenceEffects.ts
index 9c0d356f47..9dbd0253f7 100644
--- a/compiler/forget/src/Inference/InferReferenceEffects.ts
+++ b/compiler/forget/src/Inference/InferReferenceEffects.ts
@@ -18,12 +18,17 @@ import {
isObjectType,
Phi,
Place,
+ PropertyCall,
+ Type,
ValueKind,
} from "../HIR/HIR";
+import { FunctionSignature } from "../HIR/ObjectShape";
import {
+ printIdentifier,
printMixedHIR,
printPlace,
printSourceLocation,
+ printType,
} from "../HIR/PrintHIR";
import {
eachInstructionOperand,
@@ -263,6 +268,18 @@ class InferenceState {
* value is already frozen or is immutable.
*/
reference(place: Place, effectKind: Effect): void {
+ this.#referenceImpl(place, effectKind, false);
+ }
+
+ /**
+ * Throwing version of {@link reference}, which throws with an error
+ * if we record a mutate effect on an immutable value.
+ */
+ referenceAndCheckError(place: Place, effectKind: Effect): void {
+ this.#referenceImpl(place, effectKind, true);
+ }
+
+ #referenceImpl(place: Place, effectKind: Effect, shouldError: boolean): void {
const values = this.#variables.get(place.identifier.id);
if (values === undefined) {
place.effect = effectKind === Effect.Mutate ? Effect.Mutate : Effect.Read;
@@ -292,6 +309,14 @@ class InferenceState {
) {
effect = Effect.Mutate;
} else {
+ if (shouldError) {
+ CompilerError.invalidInput(
+ `InferReferenceEffects: inferred mutation of known immutable value ${printIdentifier(
+ place.identifier
+ )}${printType(place.identifier.type)} (${valueKind})`,
+ place.loc
+ );
+ }
effect = Effect.Read;
}
break;
@@ -554,7 +579,7 @@ function mergeValues(a: ValueKind, b: ValueKind): ValueKind {
* recording references on the @param state according to JS semantics.
*/
function inferBlock(
- _env: Environment,
+ env: Environment,
state: InferenceState,
block: BasicBlock
): void {
@@ -660,25 +685,39 @@ function inferBlock(
continue;
}
case "PropertyCall": {
- if (!state.isDefined(instrValue.receiver)) {
- // TODO @josephsavona: improve handling of globals
- const value: InstructionValue = {
- kind: "Primitive",
- loc: instrValue.loc,
- value: undefined,
- };
- state.initialize(value, ValueKind.Frozen);
- state.define(instrValue.receiver, value);
- }
+ invariant(
+ state.isDefined(instrValue.receiver),
+ "[InferReferenceEffects] Internal error: receiver of PropertyCall should have been defined by corresponding PropertyLoad"
+ );
- state.reference(instrValue.receiver, Effect.Mutate);
state.reference(instrValue.property, Effect.Read);
- for (const arg of instrValue.args) {
- if (arg.kind === "Identifier") {
- state.reference(arg, Effect.Mutate);
- } else {
- state.reference(arg.place, Effect.Mutate);
+
+ const signature = getFunctionCallSignature(
+ env,
+ instrValue.property.identifier.type
+ );
+ if (signature !== null) {
+ const effects = getFunctionCallEffects(
+ instrValue,
+ signature,
+ Effect.Mutate
+ );
+ for (const [place, effect] of effects) {
+ state.referenceAndCheckError(place, effect);
}
+ state.referenceAndCheckError(
+ instrValue.receiver,
+ signature.calleeEffect
+ );
+ } else {
+ for (const arg of instrValue.args) {
+ if (arg.kind === "Identifier") {
+ state.reference(arg, Effect.Mutate);
+ } else {
+ state.reference(arg.place, Effect.Mutate);
+ }
+ }
+ state.reference(instrValue.receiver, Effect.Mutate);
}
state.initialize(instrValue, ValueKind.Mutable);
state.define(instr.lvalue, instrValue);
@@ -904,3 +943,61 @@ function hasContextRefOperand(
}
return false;
}
+
+function getFunctionCallSignature(
+ env: Environment,
+ type: Type
+): FunctionSignature | null {
+ if (type.kind !== "Function") {
+ return null;
+ }
+ return env.getFunctionSignature(type);
+}
+
+/**
+ * Make a best attempt at matching arguments of a PropertyCall to its FunctionSignature,
+ * calling back to `defaultEffect` when we are unable to.
+ *
+ * @param fn
+ * @param sig
+ * @param defaultEffect In the case that inference fails, all arguments will be inferred
+ * as defaultEffect
+ * @returns Inferred effects of function arguments
+ */
+function getFunctionCallEffects(
+ fn: PropertyCall,
+ sig: FunctionSignature,
+ defaultEffect: Effect
+): Array<[Place, Effect]> {
+ const inferredEffects: Array<[Place, Effect | null]> = fn.args.map(
+ (arg, idx) => {
+ const argPlace = arg.kind === "Identifier" ? arg : arg.place;
+ if (idx < sig.positionalParams.length) {
+ // Only infer effects when there is a direct mapping positional arg --> positional param
+ // Otherwise, return null to indicate inference failed
+ if (arg.kind === "Identifier") {
+ return [argPlace, sig.positionalParams[idx]];
+ } else {
+ return [argPlace, null];
+ }
+ } else if (sig.restParam !== null) {
+ return [argPlace, sig.restParam];
+ } else {
+ // If there are more arguments than positional arguments, we'll also assume
+ // that inference failed
+ return [argPlace, null];
+ }
+ }
+ );
+
+ let results: Array<[Place, Effect]>;
+ if (inferredEffects.some(([_, effect]) => effect === null)) {
+ // If inference failed for any argument, give up on inference for all arguments
+ results = inferredEffects.map(([arg, _]) => {
+ return [arg, defaultEffect];
+ });
+ } else {
+ results = inferredEffects as Array<[Place, Effect]>;
+ }
+ return results;
+}
diff --git a/compiler/forget/src/__tests__/fixtures/compiler/array-at-closure.expect.md b/compiler/forget/src/__tests__/fixtures/compiler/array-at-closure.expect.md
new file mode 100644
index 0000000000..8e418fad9b
--- /dev/null
+++ b/compiler/forget/src/__tests__/fixtures/compiler/array-at-closure.expect.md
@@ -0,0 +1,52 @@
+
+## Input
+
+```javascript
+function Component(props) {
+ const x = foo(props.x);
+ const fn = function () {
+ const arr = [...bar(props)];
+ return arr.at(x);
+ };
+ const fnResult = fn();
+ return fnResult;
+}
+
+```
+
+## Code
+
+```javascript
+function Component(props) {
+ const $ = React.unstable_useMemoCache(5);
+ const c_0 = $[0] !== props.x;
+ let t0;
+ if (c_0) {
+ t0 = foo(props.x);
+ $[0] = props.x;
+ $[1] = t0;
+ } else {
+ t0 = $[1];
+ }
+ const x = t0;
+ const c_2 = $[2] !== props;
+ const c_3 = $[3] !== x;
+ let t1;
+ if (c_2 || c_3) {
+ const fn = function () {
+ const arr = [...bar(props)];
+ return arr.at(x);
+ };
+ t1 = fn();
+ $[2] = props;
+ $[3] = x;
+ $[4] = t1;
+ } else {
+ t1 = $[4];
+ }
+ const fnResult = t1;
+ return fnResult;
+}
+
+```
+
\ No newline at end of file
diff --git a/compiler/forget/src/__tests__/fixtures/compiler/array-at-closure.js b/compiler/forget/src/__tests__/fixtures/compiler/array-at-closure.js
new file mode 100644
index 0000000000..244b834756
--- /dev/null
+++ b/compiler/forget/src/__tests__/fixtures/compiler/array-at-closure.js
@@ -0,0 +1,9 @@
+function Component(props) {
+ const x = foo(props.x);
+ const fn = function () {
+ const arr = [...bar(props)];
+ return arr.at(x);
+ };
+ const fnResult = fn();
+ return fnResult;
+}
diff --git a/compiler/forget/src/__tests__/fixtures/compiler/array-at-effect.expect.md b/compiler/forget/src/__tests__/fixtures/compiler/array-at-effect.expect.md
new file mode 100644
index 0000000000..e8ee5dad01
--- /dev/null
+++ b/compiler/forget/src/__tests__/fixtures/compiler/array-at-effect.expect.md
@@ -0,0 +1,70 @@
+
+## Input
+
+```javascript
+// arrayInstance.at should have the following effects:
+// - read on arg0
+// - read on receiver
+// - mutate on lvalue
+function ArrayAtTest(props) {
+ const arr = [foo(props.x)];
+ const result = arr.at(bar(props.y));
+ return result;
+}
+
+```
+
+## Code
+
+```javascript
+// arrayInstance.at should have the following effects:
+// - read on arg0
+// - read on receiver
+// - mutate on lvalue
+function ArrayAtTest(props) {
+ const $ = React.unstable_useMemoCache(9);
+ const c_0 = $[0] !== props.x;
+ let t0;
+ if (c_0) {
+ t0 = foo(props.x);
+ $[0] = props.x;
+ $[1] = t0;
+ } else {
+ t0 = $[1];
+ }
+ const c_2 = $[2] !== t0;
+ let t1;
+ if (c_2) {
+ t1 = [t0];
+ $[2] = t0;
+ $[3] = t1;
+ } else {
+ t1 = $[3];
+ }
+ const arr = t1;
+ const c_4 = $[4] !== props.y;
+ let t2;
+ if (c_4) {
+ t2 = bar(props.y);
+ $[4] = props.y;
+ $[5] = t2;
+ } else {
+ t2 = $[5];
+ }
+ const c_6 = $[6] !== arr;
+ const c_7 = $[7] !== t2;
+ let t3;
+ if (c_6 || c_7) {
+ t3 = arr.at(t2);
+ $[6] = arr;
+ $[7] = t2;
+ $[8] = t3;
+ } else {
+ t3 = $[8];
+ }
+ const result = t3;
+ return result;
+}
+
+```
+
\ No newline at end of file
diff --git a/compiler/forget/src/__tests__/fixtures/compiler/array-at-effect.js b/compiler/forget/src/__tests__/fixtures/compiler/array-at-effect.js
new file mode 100644
index 0000000000..2e2e78e50e
--- /dev/null
+++ b/compiler/forget/src/__tests__/fixtures/compiler/array-at-effect.js
@@ -0,0 +1,9 @@
+// arrayInstance.at should have the following effects:
+// - read on arg0
+// - read on receiver
+// - mutate on lvalue
+function ArrayAtTest(props) {
+ const arr = [foo(props.x)];
+ const result = arr.at(bar(props.y));
+ return result;
+}
diff --git a/compiler/forget/src/__tests__/fixtures/compiler/array-at-mutate-after-capture.expect.md b/compiler/forget/src/__tests__/fixtures/compiler/array-at-mutate-after-capture.expect.md
new file mode 100644
index 0000000000..6500c946c3
--- /dev/null
+++ b/compiler/forget/src/__tests__/fixtures/compiler/array-at-mutate-after-capture.expect.md
@@ -0,0 +1,47 @@
+
+## Input
+
+```javascript
+// x's mutable range should extend to `mutate(y)`
+
+function Component(props) {
+ let x = [42, {}];
+ const idx = foo(props.b);
+ let y = x.at(idx);
+ mutate(y);
+
+ return x;
+}
+
+```
+
+## Code
+
+```javascript
+// x's mutable range should extend to `mutate(y)`
+
+function Component(props) {
+ const $ = React.unstable_useMemoCache(2);
+ let t0;
+ if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
+ t0 = {};
+ $[0] = t0;
+ } else {
+ t0 = $[0];
+ }
+ let t1;
+ if ($[1] === Symbol.for("react.memo_cache_sentinel")) {
+ t1 = [42, t0];
+ $[1] = t1;
+ } else {
+ t1 = $[1];
+ }
+ const x = t1;
+ const idx = foo(props.b);
+ const y = x.at(idx);
+ mutate(y);
+ return x;
+}
+
+```
+
\ No newline at end of file
diff --git a/compiler/forget/src/__tests__/fixtures/compiler/array-at-mutate-after-capture.js b/compiler/forget/src/__tests__/fixtures/compiler/array-at-mutate-after-capture.js
new file mode 100644
index 0000000000..9553132e1a
--- /dev/null
+++ b/compiler/forget/src/__tests__/fixtures/compiler/array-at-mutate-after-capture.js
@@ -0,0 +1,10 @@
+// x's mutable range should extend to `mutate(y)`
+
+function Component(props) {
+ let x = [42, {}];
+ const idx = foo(props.b);
+ let y = x.at(idx);
+ mutate(y);
+
+ return x;
+}
diff --git a/compiler/forget/src/__tests__/fixtures/compiler/array-property-call.expect.md b/compiler/forget/src/__tests__/fixtures/compiler/array-property-call.expect.md
index 8f897264d4..fa0d1c60f2 100644
--- a/compiler/forget/src/__tests__/fixtures/compiler/array-property-call.expect.md
+++ b/compiler/forget/src/__tests__/fixtures/compiler/array-property-call.expect.md
@@ -16,43 +16,49 @@ function Component(props) {
```javascript
function Component(props) {
- const $ = React.unstable_useMemoCache(10);
+ const $ = React.unstable_useMemoCache(11);
const c_0 = $[0] !== props.a;
const c_1 = $[1] !== props.b;
- const c_2 = $[2] !== props.c;
let t0;
let a;
- let x;
- if (c_0 || c_1 || c_2) {
+ if (c_0 || c_1) {
a = [props.a, props.b, "hello"];
- x = a.push(42);
- t0 = a.at(props.c);
+ t0 = a.push(42);
$[0] = props.a;
$[1] = props.b;
- $[2] = props.c;
- $[3] = t0;
- $[4] = a;
- $[5] = x;
+ $[2] = t0;
+ $[3] = a;
} else {
- t0 = $[3];
- a = $[4];
- x = $[5];
+ t0 = $[2];
+ a = $[3];
}
- const y = t0;
- const c_6 = $[6] !== a;
- const c_7 = $[7] !== x;
- const c_8 = $[8] !== y;
+ const x = t0;
+ const c_4 = $[4] !== a;
+ const c_5 = $[5] !== props.c;
let t1;
- if (c_6 || c_7 || c_8) {
- t1 = { a, x, y };
- $[6] = a;
- $[7] = x;
- $[8] = y;
- $[9] = t1;
+ if (c_4 || c_5) {
+ t1 = a.at(props.c);
+ $[4] = a;
+ $[5] = props.c;
+ $[6] = t1;
} else {
- t1 = $[9];
+ t1 = $[6];
}
- return t1;
+ const y = t1;
+ const c_7 = $[7] !== a;
+ const c_8 = $[8] !== x;
+ const c_9 = $[9] !== y;
+ let t2;
+ if (c_7 || c_8 || c_9) {
+ t2 = { a, x, y };
+ $[7] = a;
+ $[8] = x;
+ $[9] = y;
+ $[10] = t2;
+ } else {
+ t2 = $[10];
+ }
+ return t2;
}
```
diff --git a/compiler/forget/src/__tests__/fixtures/compiler/array-push-effect.expect.md b/compiler/forget/src/__tests__/fixtures/compiler/array-push-effect.expect.md
new file mode 100644
index 0000000000..15c6128ebb
--- /dev/null
+++ b/compiler/forget/src/__tests__/fixtures/compiler/array-push-effect.expect.md
@@ -0,0 +1,71 @@
+
+## Input
+
+```javascript
+// arrayInstance.push should have the following effects:
+// - read on all args (rest parameter)
+// - mutate on receiver
+function Component(props) {
+ const x = foo(props.x);
+ const y = { y: props.y };
+ const arr = [];
+ arr.push({});
+ arr.push(x, y);
+ return arr;
+}
+
+```
+
+## Code
+
+```javascript
+// arrayInstance.push should have the following effects:
+// - read on all args (rest parameter)
+// - mutate on receiver
+function Component(props) {
+ const $ = React.unstable_useMemoCache(8);
+ const c_0 = $[0] !== props.x;
+ let t0;
+ if (c_0) {
+ t0 = foo(props.x);
+ $[0] = props.x;
+ $[1] = t0;
+ } else {
+ t0 = $[1];
+ }
+ const x = t0;
+ const c_2 = $[2] !== props.y;
+ let t1;
+ if (c_2) {
+ t1 = { y: props.y };
+ $[2] = props.y;
+ $[3] = t1;
+ } else {
+ t1 = $[3];
+ }
+ const y = t1;
+ const c_4 = $[4] !== x;
+ const c_5 = $[5] !== y;
+ let arr;
+ if (c_4 || c_5) {
+ arr = [];
+ let t2;
+ if ($[7] === Symbol.for("react.memo_cache_sentinel")) {
+ t2 = {};
+ $[7] = t2;
+ } else {
+ t2 = $[7];
+ }
+ arr.push(t2);
+ arr.push(x, y);
+ $[4] = x;
+ $[5] = y;
+ $[6] = arr;
+ } else {
+ arr = $[6];
+ }
+ return arr;
+}
+
+```
+
\ No newline at end of file
diff --git a/compiler/forget/src/__tests__/fixtures/compiler/array-push-effect.js b/compiler/forget/src/__tests__/fixtures/compiler/array-push-effect.js
new file mode 100644
index 0000000000..b91eff8642
--- /dev/null
+++ b/compiler/forget/src/__tests__/fixtures/compiler/array-push-effect.js
@@ -0,0 +1,11 @@
+// arrayInstance.push should have the following effects:
+// - read on all args (rest parameter)
+// - mutate on receiver
+function Component(props) {
+ const x = foo(props.x);
+ const y = { y: props.y };
+ const arr = [];
+ arr.push({});
+ arr.push(x, y);
+ return arr;
+}
diff --git a/compiler/forget/src/__tests__/fixtures/compiler/assignment-variations-complex-lvalue-array.expect.md b/compiler/forget/src/__tests__/fixtures/compiler/assignment-variations-complex-lvalue-array.expect.md
index 8e4ab75469..fd20e1d99c 100644
--- a/compiler/forget/src/__tests__/fixtures/compiler/assignment-variations-complex-lvalue-array.expect.md
+++ b/compiler/forget/src/__tests__/fixtures/compiler/assignment-variations-complex-lvalue-array.expect.md
@@ -15,16 +15,24 @@ function foo() {
```javascript
function foo() {
- const $ = React.unstable_useMemoCache(1);
- let a;
+ const $ = React.unstable_useMemoCache(2);
+ let t0;
if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
- a = [[1]];
- const first = a.at(0);
- first.set(0, 2);
- $[0] = a;
+ t0 = [1];
+ $[0] = t0;
} else {
- a = $[0];
+ t0 = $[0];
}
+ let t1;
+ if ($[1] === Symbol.for("react.memo_cache_sentinel")) {
+ t1 = [t0];
+ $[1] = t1;
+ } else {
+ t1 = $[1];
+ }
+ const a = t1;
+ const first = a.at(0);
+ first.set(0, 2);
return a;
}
diff --git a/compiler/forget/src/__tests__/fixtures/compiler/error.mutate-after-aliased-freeze.expect.md b/compiler/forget/src/__tests__/fixtures/compiler/error.mutate-after-aliased-freeze.expect.md
new file mode 100644
index 0000000000..65f5d9a57d
--- /dev/null
+++ b/compiler/forget/src/__tests__/fixtures/compiler/error.mutate-after-aliased-freeze.expect.md
@@ -0,0 +1,31 @@
+
+## Input
+
+```javascript
+function Component(props) {
+ let x = [];
+ let y = x;
+
+ if (props.p1) {
+ x = [];
+ }
+
+ let _ = ;
+
+ // y is MaybeFrozen at this point, since it may alias to x
+ // (which is the above line freezes)
+ y.push(props.p2);
+
+ return ;
+}
+
+```
+
+
+## Error
+
+```
+[ReactForget] InvalidInput: InferReferenceEffects: inferred mutation of known immutable value $42:TObject (frozen) (13:13)
+```
+
+
\ No newline at end of file
diff --git a/compiler/forget/src/__tests__/fixtures/compiler/error.mutate-after-aliased-freeze.js b/compiler/forget/src/__tests__/fixtures/compiler/error.mutate-after-aliased-freeze.js
new file mode 100644
index 0000000000..8b08aaa215
--- /dev/null
+++ b/compiler/forget/src/__tests__/fixtures/compiler/error.mutate-after-aliased-freeze.js
@@ -0,0 +1,16 @@
+function Component(props) {
+ let x = [];
+ let y = x;
+
+ if (props.p1) {
+ x = [];
+ }
+
+ let _ = ;
+
+ // y is MaybeFrozen at this point, since it may alias to x
+ // (which is the above line freezes)
+ y.push(props.p2);
+
+ return ;
+}
diff --git a/compiler/forget/src/__tests__/fixtures/compiler/error.mutate-after-freeze.expect.md b/compiler/forget/src/__tests__/fixtures/compiler/error.mutate-after-freeze.expect.md
new file mode 100644
index 0000000000..491ed4e8fe
--- /dev/null
+++ b/compiler/forget/src/__tests__/fixtures/compiler/error.mutate-after-freeze.expect.md
@@ -0,0 +1,25 @@
+
+## Input
+
+```javascript
+function Component(props) {
+ let x = [];
+
+ let _ = ;
+
+ // x is Frozen at this point
+ x.push(props.p2);
+
+ return
{_}
;
+}
+
+```
+
+
+## Error
+
+```
+[ReactForget] InvalidInput: InferReferenceEffects: inferred mutation of known immutable value $25:TObject (frozen) (7:7)
+```
+
+
\ No newline at end of file
diff --git a/compiler/forget/src/__tests__/fixtures/compiler/error.mutate-after-freeze.js b/compiler/forget/src/__tests__/fixtures/compiler/error.mutate-after-freeze.js
new file mode 100644
index 0000000000..a40fbcb31e
--- /dev/null
+++ b/compiler/forget/src/__tests__/fixtures/compiler/error.mutate-after-freeze.js
@@ -0,0 +1,10 @@
+function Component(props) {
+ let x = [];
+
+ let _ = ;
+
+ // x is Frozen at this point
+ x.push(props.p2);
+
+ return