Compare commits

...

34 Commits

Author SHA1 Message Date
Joe Savona
fcf9bc817c [compiler] Sketch of new InferReferenceEffects equivalent
[ghstack-poisoned]
2025-05-12 17:01:10 -07:00
Joe Savona
c3b87cc620 [compiler] Sketch of new mutation/aliasing effects
[ghstack-poisoned]
2025-05-12 17:01:04 -07:00
Joe Savona
b0b09a74a8 Update on "[compiler] Repro for imprecise memo due to closure capturing changes"
Syncing this stack internally there is a small percentage of files that lose memoization, generally for callbacks. The repro here tries to get at the core pattern, where a parameter escapes into a mutable return value. This makes the callback appear mutable, and means that calls like array.map aren't able to optimize as well — even if the array itself is transitively immutable.

The challenge is that we can't really distinguish between just capturing and true mutation right now — AnalyzeFunctions kind of has to pick one, and consider both a mutation.

[ghstack-poisoned]
2025-05-12 17:01:02 -07:00
Joe Savona
f40a865bc9 Update base for Update on "[compiler] Repro for imprecise memo due to closure capturing changes"
Syncing this stack internally there is a small percentage of files that lose memoization, generally for callbacks. The repro here tries to get at the core pattern, where a parameter escapes into a mutable return value. This makes the callback appear mutable, and means that calls like array.map aren't able to optimize as well — even if the array itself is transitively immutable.

The challenge is that we can't really distinguish between just capturing and true mutation right now — AnalyzeFunctions kind of has to pick one, and consider both a mutation.

[ghstack-poisoned]
2025-05-12 17:01:01 -07:00
Joe Savona
bfff07b450 [compiler] Repro for imprecise memo due to closure capturing changes
Syncing this stack internally there is a small percentage of files that lose memoization, generally for callbacks. The repro here tries to get at the core pattern, where a parameter escapes into a mutable return value. This makes the callback appear mutable, and means that calls like array.map aren't able to optimize as well — even if the array itself is transitively immutable.

The challenge is that we can't really distinguish between just capturing and true mutation right now — AnalyzeFunctions kind of has to pick one, and consider both a mutation.

[ghstack-poisoned]
2025-05-12 11:56:39 -07:00
Joe Savona
5fc610efb3 [compiler] avoid empty switch case bodies
[ghstack-poisoned]
2025-05-12 11:56:33 -07:00
Joe Savona
68ab155cfe [compiler] allow local fixtures to be excluded from git w "nocommit" prefix
[ghstack-poisoned]
2025-05-12 11:56:25 -07:00
Joe Savona
4afa39de4e Update on "[compiler] Fix for PropertyStore object effect"
Fix for the issue in the previous PR. Long-term the ideal thing would be to make InferMutableRanges smarter about Store effects, and recognize that they are also transitive mutations of whatever was captured into the object. So in the following:

```
const x = {y: {z: {}}};
x.y.z.key = value;
```

That the `PropertyStore z . 'key' = value` is a transitive mutation of x and all three object expressions (x, x.y, x.y.z).

But for now it's simpler to stick to the original idea of Store only counting if we know that the type is an object.

[ghstack-poisoned]
2025-05-12 11:56:21 -07:00
Joe Savona
275866f3fc Update base for Update on "[compiler] Fix for PropertyStore object effect"
Fix for the issue in the previous PR. Long-term the ideal thing would be to make InferMutableRanges smarter about Store effects, and recognize that they are also transitive mutations of whatever was captured into the object. So in the following:

```
const x = {y: {z: {}}};
x.y.z.key = value;
```

That the `PropertyStore z . 'key' = value` is a transitive mutation of x and all three object expressions (x, x.y, x.y.z).

But for now it's simpler to stick to the original idea of Store only counting if we know that the type is an object.

[ghstack-poisoned]
2025-05-12 11:56:20 -07:00
Joe Savona
45a889cb87 Update on "[compiler] Fix for PropertyStore object effect"
Fix for the issue in the previous PR. Long-term the ideal thing would be to make InferMutableRanges smarter about Store effects, and recognize that they are also transitive mutations of whatever was captured into the object. So in the following:

```
const x = {y: {z: {}}};
x.y.z.key = value;
```

That the `PropertyStore z . 'key' = value` is a transitive mutation of x and all three object expressions (x, x.y, x.y.z).

But for now it's simpler to stick to the original idea of Store only counting if we know that the type is an object.

[ghstack-poisoned]
2025-05-09 11:21:51 -07:00
Joe Savona
23222b4b36 Update base for Update on "[compiler] Fix for PropertyStore object effect"
Fix for the issue in the previous PR. Long-term the ideal thing would be to make InferMutableRanges smarter about Store effects, and recognize that they are also transitive mutations of whatever was captured into the object. So in the following:

```
const x = {y: {z: {}}};
x.y.z.key = value;
```

That the `PropertyStore z . 'key' = value` is a transitive mutation of x and all three object expressions (x, x.y, x.y.z).

But for now it's simpler to stick to the original idea of Store only counting if we know that the type is an object.

[ghstack-poisoned]
2025-05-09 11:21:50 -07:00
Joe Savona
98b02b93f9 Update on "[compiler] Fix for PropertyStore object effect"
Fix for the issue in the previous PR. Long-term the ideal thing would be to make InferMutableRanges smarter about Store effects, and recognize that they are also transitive mutations of whatever was captured into the object. So in the following:

```
const x = {y: {z: {}}};
x.y.z.key = value;
```

That the `PropertyStore z . 'key' = value` is a transitive mutation of x and all three object expressions (x, x.y, x.y.z).

But for now it's simpler to stick to the original idea of Store only counting if we know that the type is an object.

[ghstack-poisoned]
2025-05-09 10:42:52 -07:00
Joe Savona
52a34e4fa0 Update base for Update on "[compiler] Fix for PropertyStore object effect"
Fix for the issue in the previous PR. Long-term the ideal thing would be to make InferMutableRanges smarter about Store effects, and recognize that they are also transitive mutations of whatever was captured into the object. So in the following:

```
const x = {y: {z: {}}};
x.y.z.key = value;
```

That the `PropertyStore z . 'key' = value` is a transitive mutation of x and all three object expressions (x, x.y, x.y.z).

But for now it's simpler to stick to the original idea of Store only counting if we know that the type is an object.

[ghstack-poisoned]
2025-05-09 10:42:51 -07:00
Joe Savona
5bf3eb6085 Update on "[compiler] Fix for PropertyStore object effect"
Fix for the issue in the previous PR. Long-term the ideal thing would be to make InferMutableRanges smarter about Store effects, and recognize that they are also transitive mutations of whatever was captured into the object. So in the following:

```
const x = {y: {z: {}}};
x.y.z.key = value;
```

That the `PropertyStore z . 'key' = value` is a transitive mutation of x and all three object expressions (x, x.y, x.y.z).

But for now it's simpler to stick to the original idea of Store only counting if we know that the type is an object.

[ghstack-poisoned]
2025-05-09 10:40:57 -07:00
Joe Savona
b9367e0e3b Update base for Update on "[compiler] Fix for PropertyStore object effect"
Fix for the issue in the previous PR. Long-term the ideal thing would be to make InferMutableRanges smarter about Store effects, and recognize that they are also transitive mutations of whatever was captured into the object. So in the following:

```
const x = {y: {z: {}}};
x.y.z.key = value;
```

That the `PropertyStore z . 'key' = value` is a transitive mutation of x and all three object expressions (x, x.y, x.y.z).

But for now it's simpler to stick to the original idea of Store only counting if we know that the type is an object.

[ghstack-poisoned]
2025-05-09 10:40:57 -07:00
Joe Savona
90ed7e8228 [compiler] Fix for PropertyStore object effect
Fix for the issue in the previous PR. Long-term the ideal thing would be to make InferMutableRanges smarter about Store effects, and recognize that they are also transitive mutations of whatever was captured into the object. So in the following:

```
const x = {y: {z: {}}};
x.y.z.key = value;
```

That the `PropertyStore z . 'key' = value` is a transitive mutation of x and all three object expressions (x, x.y, x.y.z).

But for now it's simpler to stick to the original idea of Store only counting if we know that the type is an object.

[ghstack-poisoned]
2025-05-09 10:36:17 -07:00
Joe Savona
a25a7e1873 [compiler] Fixture tests for PropertyStore effects
Adds fixture tests to demonstrate an issue in changing PropertyStore to always have a Store effect on its object operand, regardless of the operand type. The issue is that if we're doing a PropertyStore on a nested value, that has be considered a transitive mutation of the parent object:

```
const x = {y: {z: {}}};
x.y.z.key = 'value'; // this has to be a mutation of `x`
```

Fix in the next PR.

[ghstack-poisoned]
2025-05-09 10:36:12 -07:00
Joe Savona
206e7e93d8 Update on "[compiler] Move co-mutation range extension to InferMutableRanges"
We've occassionally added logic that extends mutable ranges into InferReactiveScopeVariables to handle a specific case, but inevitably discover that the logic needs to be part of the InferMutableRanges fixpoint loop. That happened in the past with extending the range of phi operands to account for subsequent mutations, which I moved to InferMutableRanges a while back. But InferReactiveScopeVariables also has logic to group co-mutations in the same scope, which also extends ranges of the co-mutating operands to have the same end point. Recently mofeiz found some cases where this is insufficient, where a closure captures a value that could change via a co-mutation, and where failure to extend the ranges in the fixpoint meant the function expression appeared independently memoizable when it wasn't.

The fix is to make InferMutableRanges update ranges to account for co-mutations. That is relatively straightforward, but not enough! The problem is that the fixpoint loop stopped once the alias sets coalesced, but co-mutations only affect ranges and not aliases. So the other part of the fix is to have the fixpoint condition use a custom canonicalization that describes each identifiers root _and_ the mutable range of that root.

[ghstack-poisoned]
2025-05-09 10:36:11 -07:00
Joe Savona
63e29f78e3 Update base for Update on "[compiler] Move co-mutation range extension to InferMutableRanges"
We've occassionally added logic that extends mutable ranges into InferReactiveScopeVariables to handle a specific case, but inevitably discover that the logic needs to be part of the InferMutableRanges fixpoint loop. That happened in the past with extending the range of phi operands to account for subsequent mutations, which I moved to InferMutableRanges a while back. But InferReactiveScopeVariables also has logic to group co-mutations in the same scope, which also extends ranges of the co-mutating operands to have the same end point. Recently mofeiz found some cases where this is insufficient, where a closure captures a value that could change via a co-mutation, and where failure to extend the ranges in the fixpoint meant the function expression appeared independently memoizable when it wasn't.

The fix is to make InferMutableRanges update ranges to account for co-mutations. That is relatively straightforward, but not enough! The problem is that the fixpoint loop stopped once the alias sets coalesced, but co-mutations only affect ranges and not aliases. So the other part of the fix is to have the fixpoint condition use a custom canonicalization that describes each identifiers root _and_ the mutable range of that root.

[ghstack-poisoned]
2025-05-09 10:36:10 -07:00
Joe Savona
ec552a5011 Update on "[compiler] Move co-mutation range extension to InferMutableRanges"
We've occassionally added logic that extends mutable ranges into InferReactiveScopeVariables to handle a specific case, but inevitably discover that the logic needs to be part of the InferMutableRanges fixpoint loop. That happened in the past with extending the range of phi operands to account for subsequent mutations, which I moved to InferMutableRanges a while back. But InferReactiveScopeVariables also has logic to group co-mutations in the same scope, which also extends ranges of the co-mutating operands to have the same end point. Recently mofeiz found some cases where this is insufficient, where a closure captures a value that could change via a co-mutation, and where failure to extend the ranges in the fixpoint meant the function expression appeared independently memoizable when it wasn't.

The fix is to make InferMutableRanges update ranges to account for co-mutations. That is relatively straightforward, but not enough! The problem is that the fixpoint loop stopped once the alias sets coalesced, but co-mutations only affect ranges and not aliases. So the other part of the fix is to have the fixpoint condition use a custom canonicalization that describes each identifiers root _and_ the mutable range of that root.

[ghstack-poisoned]
2025-05-09 09:01:13 -07:00
Joe Savona
390d516127 Update base for Update on "[compiler] Move co-mutation range extension to InferMutableRanges"
We've occassionally added logic that extends mutable ranges into InferReactiveScopeVariables to handle a specific case, but inevitably discover that the logic needs to be part of the InferMutableRanges fixpoint loop. That happened in the past with extending the range of phi operands to account for subsequent mutations, which I moved to InferMutableRanges a while back. But InferReactiveScopeVariables also has logic to group co-mutations in the same scope, which also extends ranges of the co-mutating operands to have the same end point. Recently mofeiz found some cases where this is insufficient, where a closure captures a value that could change via a co-mutation, and where failure to extend the ranges in the fixpoint meant the function expression appeared independently memoizable when it wasn't.

The fix is to make InferMutableRanges update ranges to account for co-mutations. That is relatively straightforward, but not enough! The problem is that the fixpoint loop stopped once the alias sets coalesced, but co-mutations only affect ranges and not aliases. So the other part of the fix is to have the fixpoint condition use a custom canonicalization that describes each identifiers root _and_ the mutable range of that root.

[ghstack-poisoned]
2025-05-09 09:01:13 -07:00
Joe Savona
3b83903ca1 [compiler] Move co-mutation range extension to InferMutableRanges
We've occassionally added logic that extends mutable ranges into InferReactiveScopeVariables to handle a specific case, but inevitably discover that the logic needs to be part of the InferMutableRanges fixpoint loop. That happened in the past with extending the range of phi operands to account for subsequent mutations, which I moved to InferMutableRanges a while back. But InferReactiveScopeVariables also has logic to group co-mutations in the same scope, which also extends ranges of the co-mutating operands to have the same end point. Recently @mofeiz found some cases where this is insufficient, where a closure captures a value that could change via a co-mutation, and where failure to extend the ranges in the fixpoint meant the function expression appeared independently memoizable when it wasn't.

The fix is to make InferMutableRanges update ranges to account for co-mutations. That is relatively straightforward, but not enough! The problem is that the fixpoint loop stopped once the alias sets coalesced, but co-mutations only affect ranges and not aliases. So the other part of the fix is to have the fixpoint condition use a custom canonicalization that describes each identifiers root _and_ the mutable range of that root.

[ghstack-poisoned]
2025-05-08 16:24:24 -07:00
Joe Savona
1fec32eeeb Update on "[compiler][wip] Infer alias effects for function expressions"
This is a stab at addressing a pattern that mofeiz and I have both stumbled across. Today, FunctionExpression's context list describes values from the outer context that are accessed in the function, and with what effect they were accessed. This allows us to describe the fact that a value from the outer context is known to be mutated inside a function expression, or is known to be captured (aliased) into some other value in the function expression. However, the basic `Effect` kind is insufficient to describe the full semantics. Notably, it doesn't let us describe more complex aliasing relationships.

From an example mofeiz added:

```js
const x = {};
const y = {};
const f = () => {
  const a = [y];
  const b = x;
  // this sets y.x = x
  a[0].x = b;
}
f();
mutate(y.x);  // which means this mutates x!
```

Here, the Effect on the context operands are `[mutate y, read x]`. The `mutate y` is bc of the array push. But the `read x` is surprising — `x` is captured into `y`, but there is no subsequent mutation of y or x, so we consider this a read. But as the comments indicate, the final line mutates x! We need to reflect the fact that even though x isn't mutated inside the function, it is aliased into y, such that if y is subsequently mutated that this should count as a mutation of x too.

The idea of this PR is to extend the FunctionEffect type with a CaptureEffect variant which lists out the aliasing groups that occur inside the function expression. This allows us to bubble up the results of alias analysis from inside a function. The idea is to:

* Return the alias sets from InferMutableRanges
* Augment them with capturing of the form above, handling cases such as the `a[0].x = b`
* For each alias group, record a CaptureEffect for any group that contains 2+ context operands
* Extend the alias sets in the _outer_ function with the CaptureEffect sets from FunctionExpression/ObjectMethod instructions.

This isn't quite right yet, just sharing early hacking.

[ghstack-poisoned]
2025-05-08 16:24:22 -07:00
Joe Savona
fd0df94b1a Update base for Update on "[compiler][wip] Infer alias effects for function expressions"
This is a stab at addressing a pattern that mofeiz and I have both stumbled across. Today, FunctionExpression's context list describes values from the outer context that are accessed in the function, and with what effect they were accessed. This allows us to describe the fact that a value from the outer context is known to be mutated inside a function expression, or is known to be captured (aliased) into some other value in the function expression. However, the basic `Effect` kind is insufficient to describe the full semantics. Notably, it doesn't let us describe more complex aliasing relationships.

From an example mofeiz added:

```js
const x = {};
const y = {};
const f = () => {
  const a = [y];
  const b = x;
  // this sets y.x = x
  a[0].x = b;
}
f();
mutate(y.x);  // which means this mutates x!
```

Here, the Effect on the context operands are `[mutate y, read x]`. The `mutate y` is bc of the array push. But the `read x` is surprising — `x` is captured into `y`, but there is no subsequent mutation of y or x, so we consider this a read. But as the comments indicate, the final line mutates x! We need to reflect the fact that even though x isn't mutated inside the function, it is aliased into y, such that if y is subsequently mutated that this should count as a mutation of x too.

The idea of this PR is to extend the FunctionEffect type with a CaptureEffect variant which lists out the aliasing groups that occur inside the function expression. This allows us to bubble up the results of alias analysis from inside a function. The idea is to:

* Return the alias sets from InferMutableRanges
* Augment them with capturing of the form above, handling cases such as the `a[0].x = b`
* For each alias group, record a CaptureEffect for any group that contains 2+ context operands
* Extend the alias sets in the _outer_ function with the CaptureEffect sets from FunctionExpression/ObjectMethod instructions.

This isn't quite right yet, just sharing early hacking.

[ghstack-poisoned]
2025-05-08 16:24:22 -07:00
Joe Savona
97442d79cb Update on "[compiler][wip] Infer alias effects for function expressions"
This is a stab at addressing a pattern that mofeiz and I have both stumbled across. Today, FunctionExpression's context list describes values from the outer context that are accessed in the function, and with what effect they were accessed. This allows us to describe the fact that a value from the outer context is known to be mutated inside a function expression, or is known to be captured (aliased) into some other value in the function expression. However, the basic `Effect` kind is insufficient to describe the full semantics. Notably, it doesn't let us describe more complex aliasing relationships.

From an example mofeiz added:

```js
const x = {};
const y = {};
const f = () => {
  const a = [y];
  const b = x;
  // this sets y.x = x
  a[0].x = b;
}
f();
mutate(y.x);  // which means this mutates x!
```

Here, the Effect on the context operands are `[mutate y, read x]`. The `mutate y` is bc of the array push. But the `read x` is surprising — `x` is captured into `y`, but there is no subsequent mutation of y or x, so we consider this a read. But as the comments indicate, the final line mutates x! We need to reflect the fact that even though x isn't mutated inside the function, it is aliased into y, such that if y is subsequently mutated that this should count as a mutation of x too.

The idea of this PR is to extend the FunctionEffect type with a CaptureEffect variant which lists out the aliasing groups that occur inside the function expression. This allows us to bubble up the results of alias analysis from inside a function. The idea is to:

* Return the alias sets from InferMutableRanges
* Augment them with capturing of the form above, handling cases such as the `a[0].x = b`
* For each alias group, record a CaptureEffect for any group that contains 2+ context operands
* Extend the alias sets in the _outer_ function with the CaptureEffect sets from FunctionExpression/ObjectMethod instructions.

This isn't quite right yet, just sharing early hacking.

[ghstack-poisoned]
2025-05-08 13:04:04 -07:00
Joe Savona
7936a99a2d Update base for Update on "[compiler][wip] Infer alias effects for function expressions"
This is a stab at addressing a pattern that mofeiz and I have both stumbled across. Today, FunctionExpression's context list describes values from the outer context that are accessed in the function, and with what effect they were accessed. This allows us to describe the fact that a value from the outer context is known to be mutated inside a function expression, or is known to be captured (aliased) into some other value in the function expression. However, the basic `Effect` kind is insufficient to describe the full semantics. Notably, it doesn't let us describe more complex aliasing relationships.

From an example mofeiz added:

```js
const x = {};
const y = {};
const f = () => {
  const a = [y];
  const b = x;
  // this sets y.x = x
  a[0].x = b;
}
f();
mutate(y.x);  // which means this mutates x!
```

Here, the Effect on the context operands are `[mutate y, read x]`. The `mutate y` is bc of the array push. But the `read x` is surprising — `x` is captured into `y`, but there is no subsequent mutation of y or x, so we consider this a read. But as the comments indicate, the final line mutates x! We need to reflect the fact that even though x isn't mutated inside the function, it is aliased into y, such that if y is subsequently mutated that this should count as a mutation of x too.

The idea of this PR is to extend the FunctionEffect type with a CaptureEffect variant which lists out the aliasing groups that occur inside the function expression. This allows us to bubble up the results of alias analysis from inside a function. The idea is to:

* Return the alias sets from InferMutableRanges
* Augment them with capturing of the form above, handling cases such as the `a[0].x = b`
* For each alias group, record a CaptureEffect for any group that contains 2+ context operands
* Extend the alias sets in the _outer_ function with the CaptureEffect sets from FunctionExpression/ObjectMethod instructions.

This isn't quite right yet, just sharing early hacking.

[ghstack-poisoned]
2025-05-08 13:04:03 -07:00
Joe Savona
c3a733fe2d Update on "[compiler][wip] Infer alias effects for function expressions"
This is a stab at addressing a pattern that mofeiz and I have both stumbled across. Today, FunctionExpression's context list describes values from the outer context that are accessed in the function, and with what effect they were accessed. This allows us to describe the fact that a value from the outer context is known to be mutated inside a function expression, or is known to be captured (aliased) into some other value in the function expression. However, the basic `Effect` kind is insufficient to describe the full semantics. Notably, it doesn't let us describe more complex aliasing relationships.

From an example mofeiz added:

```js
const x = {};
const y = {};
const f = () => {
  const a = [y];
  const b = x;
  // this sets y.x = x
  a[0].x = b;
}
f();
mutate(y.x);  // which means this mutates x!
```

Here, the Effect on the context operands are `[mutate y, read x]`. The `mutate y` is bc of the array push. But the `read x` is surprising — `x` is captured into `y`, but there is no subsequent mutation of y or x, so we consider this a read. But as the comments indicate, the final line mutates x! We need to reflect the fact that even though x isn't mutated inside the function, it is aliased into y, such that if y is subsequently mutated that this should count as a mutation of x too.

The idea of this PR is to extend the FunctionEffect type with a CaptureEffect variant which lists out the aliasing groups that occur inside the function expression. This allows us to bubble up the results of alias analysis from inside a function. The idea is to:

* Return the alias sets from InferMutableRanges
* Augment them with capturing of the form above, handling cases such as the `a[0].x = b`
* For each alias group, record a CaptureEffect for any group that contains 2+ context operands
* Extend the alias sets in the _outer_ function with the CaptureEffect sets from FunctionExpression/ObjectMethod instructions.

This isn't quite right yet, just sharing early hacking.

[ghstack-poisoned]
2025-05-08 12:45:15 -07:00
Joe Savona
05e75d8055 Update base for Update on "[compiler][wip] Infer alias effects for function expressions"
This is a stab at addressing a pattern that mofeiz and I have both stumbled across. Today, FunctionExpression's context list describes values from the outer context that are accessed in the function, and with what effect they were accessed. This allows us to describe the fact that a value from the outer context is known to be mutated inside a function expression, or is known to be captured (aliased) into some other value in the function expression. However, the basic `Effect` kind is insufficient to describe the full semantics. Notably, it doesn't let us describe more complex aliasing relationships.

From an example mofeiz added:

```js
const x = {};
const y = {};
const f = () => {
  const a = [y];
  const b = x;
  // this sets y.x = x
  a[0].x = b;
}
f();
mutate(y.x);  // which means this mutates x!
```

Here, the Effect on the context operands are `[mutate y, read x]`. The `mutate y` is bc of the array push. But the `read x` is surprising — `x` is captured into `y`, but there is no subsequent mutation of y or x, so we consider this a read. But as the comments indicate, the final line mutates x! We need to reflect the fact that even though x isn't mutated inside the function, it is aliased into y, such that if y is subsequently mutated that this should count as a mutation of x too.

The idea of this PR is to extend the FunctionEffect type with a CaptureEffect variant which lists out the aliasing groups that occur inside the function expression. This allows us to bubble up the results of alias analysis from inside a function. The idea is to:

* Return the alias sets from InferMutableRanges
* Augment them with capturing of the form above, handling cases such as the `a[0].x = b`
* For each alias group, record a CaptureEffect for any group that contains 2+ context operands
* Extend the alias sets in the _outer_ function with the CaptureEffect sets from FunctionExpression/ObjectMethod instructions.

This isn't quite right yet, just sharing early hacking.

[ghstack-poisoned]
2025-05-08 12:45:14 -07:00
Joe Savona
cd2537c6a6 [compiler][wip] Infer alias effects for function expressions
This is a stab at addressing a pattern that @mofeiz and I have both stumbled across. Today, FunctionExpression's context list describes values from the outer context that are accessed in the function, and with what effect they were accessed. This allows us to describe the fact that a value from the outer context is known to be mutated inside a function expression, or is known to be captured (aliased) into some other value in the function expression. However, the basic `Effect` kind is insufficient to describe the full semantics. Notably, it doesn't let us describe more complex aliasing relationships.

From an example @mofeiz added:

```js
const x = {};
const y = {};
const f = () => {
  const a = [y];
  const b = x;
  // this sets y.x = x
  a[0].x = b;
}
f();
mutate(y.x);  // which means this mutates x!
```

Here, the Effect on the context operands are `[mutate y, read x]`. The `mutate y` is bc of the array push. But the `read x` is surprising — `x` is captured into `y`, but there is no subsequent mutation of y or x, so we consider this a read. But as the comments indicate, the final line mutates x! We need to reflect the fact that even though x isn't mutated inside the function, it is aliased into y, such that if y is subsequently mutated that this should count as a mutation of x too.

The idea of this PR is to extend the FunctionEffect type with a CaptureEffect variant which lists out the aliasing groups that occur inside the function expression. This allows us to bubble up the results of alias analysis from inside a function. The idea is to:

* Return the alias sets from InferMutableRanges
* Augment them with capturing of the form above, handling cases such as the `a[0].x = b`
* For each alias group, record a CaptureEffect for any group that contains 2+ context operands
* Extend the alias sets in the _outer_ function with the CaptureEffect sets from FunctionExpression/ObjectMethod instructions.

This isn't quite right yet, just sharing early hacking.

[ghstack-poisoned]
2025-05-07 17:45:38 -07:00
Joe Savona
3d646a8993 Update on "[compiler] Correctly infer context mutation places as outer (context) places"
The issue in the previous PR was due to a ContextMutation function effect having a place that wasn't one of the functions' context variables. What was happening is that the `getContextRefOperand()` helper wasn't following aliases. If an operand had a context type, we recorded the operand as the context place — but instead we should be looking through to the context places of the abstract value.

With this change the fixture now fails for a different reason — we infer this as a mutation of `params` and reject it because `params` is frozen (hook return value). This case is clearly a false positive: the mutation is on the outer, new `nextParams` object and can't possibly mutate `params`. Need to think more about what to do here but this is clearly more precise in terms of which variable we record as the context variable.

[ghstack-poisoned]
2025-05-07 17:45:35 -07:00
Joe Savona
15307de4e2 Update base for Update on "[compiler] Correctly infer context mutation places as outer (context) places"
The issue in the previous PR was due to a ContextMutation function effect having a place that wasn't one of the functions' context variables. What was happening is that the `getContextRefOperand()` helper wasn't following aliases. If an operand had a context type, we recorded the operand as the context place — but instead we should be looking through to the context places of the abstract value.

With this change the fixture now fails for a different reason — we infer this as a mutation of `params` and reject it because `params` is frozen (hook return value). This case is clearly a false positive: the mutation is on the outer, new `nextParams` object and can't possibly mutate `params`. Need to think more about what to do here but this is clearly more precise in terms of which variable we record as the context variable.

[ghstack-poisoned]
2025-05-07 17:45:35 -07:00
Joe Savona
e9b19a6732 [compiler] Correctly infer context mutation places as outer (context) places
The issue in the previous PR was due to a ContextMutation function effect having a place that wasn't one of the functions' context variables. What was happening is that the `getContextRefOperand()` helper wasn't following aliases. If an operand had a context type, we recorded the operand as the context place — but instead we should be looking through to the context places of the abstract value.

With this change the fixture now fails for a different reason — we infer this as a mutation of `params` and reject it because `params` is frozen (hook return value). This case is clearly a false positive: the mutation is on the outer, new `nextParams` object and can't possibly mutate `params`. Need to think more about what to do here but this is clearly more precise in terms of which variable we record as the context variable.

[ghstack-poisoned]
2025-05-03 16:25:55 +09:00
Joe Savona
0ee6891337 Update on "[compiler] Repro for false positive ValidateNoFreezingKnownMutableFunctions"
Found when testing the new validation from #33079 internally. I haven't fully debugged, but somehow the combination of the effect function *accessing* a ref and also calling a second function which has a purely local mutation triggers the validation. Even though the called second function only mutates local variables. If i remove the ref access in the effect function, the error goes away.

Anyway I'll keep debugging, putting up a repro for now.

[ghstack-poisoned]
2025-05-03 16:25:52 +09:00
Joe Savona
18e4165a94 [compiler] Repro for false positive ValidateNoFreezingKnownMutableFunctions
Found when testing the new validation from #33079 internally. I haven't fully debugged, but somehow the combination of the effect function *accessing* a ref and also calling a second function which has a purely local mutation triggers the validation. Even though the called second function only mutates local variables. If i remove the ref access in the effect function, the error goes away.

Anyway I'll keep debugging, putting up a repro for now.

[ghstack-poisoned]
2025-05-03 09:58:15 +09:00
35 changed files with 1642 additions and 380 deletions

3
compiler/.gitignore vendored
View File

@@ -6,6 +6,9 @@
debug/
target/
nocommit*.js
nocommit*.expect.md
# These are backup files generated by rustfmt
**/*.rs.bk

View File

@@ -225,6 +225,7 @@ export function lower(
loc: func.node.loc ?? GeneratedSource,
env,
effects: null,
aliasingEffects: null,
directives,
});
}

View File

@@ -280,6 +280,7 @@ export type HIRFunction = {
returnType: Type;
context: Array<Place>;
effects: Array<FunctionEffect> | null;
aliasingEffects: Array<AliasingEffect> | null;
body: HIR;
generator: boolean;
async: boolean;
@@ -300,6 +301,10 @@ export type FunctionEffect =
places: ReadonlySet<Place>;
effect: Effect;
loc: SourceLocation;
}
| {
kind: 'CaptureEffect';
places: ReadonlySet<Place>;
};
/*
@@ -1430,6 +1435,46 @@ export const ValueKindSchema = z.enum([
ValueKind.Context,
]);
export type AliasingEffect =
/**
* Freezes the operand if not already frozen
*/
| {kind: 'Freeze'; place: Place}
/**
* Known mutation of a value, which may effect that value or values that it contains.
* This is rare!
*/
| {kind: 'MutateTransitive'; place: Place}
/**
* Known mutation of a specific value, targeting only that specific value but not
* values that it contains.
*
* Example: `array.push(item)` mutates the array but does not mutate items stored in the array.
*/
| {kind: 'MutateLocal'; place: Place}
/**
* Possible mutation of a specific value
*/
| {kind: 'ConditionallyMutate'; place: Place}
/**
* Direct aliasing of one identifier by another identifier
* Examples: `x = y` (from y -> to x) or phis (from operand -> to phi)
*/
| {kind: 'Alias'; from: Place; to: Place}
/**
* Direct aliasing of an identifier (or a sub-path), or storing a value/sub-path
* into a part of another object.
*
* One of from.path and/or to.path must be non-null (else this is equivalent to Alias)
*/
| {
kind: 'Capture';
from: {place: Place; path: '*' | null};
to: {place: Place; path: '*' | null};
}
// Known mutation of a global
| {kind: 'MutateGlobal'; place: Place};
// The effect with which a value is modified.
export enum Effect {
// Default value: not allowed after lifetime inference

View File

@@ -179,8 +179,59 @@ export type FunctionSignature = {
impure?: boolean;
canonicalName?: string;
aliasing?: AliasingSignature;
};
export type LifetimeId = number;
export type AliasingSignature = {
receiver: LifetimeId;
params: Array<LifetimeId> | null;
restParam: LifetimeId;
returns: LifetimeId;
effects: Array<AliasingSignatureEffect>;
};
export type AliasingSignatureEffect =
/**
* Freezes the operand if not already frozen
*/
| {kind: 'Freeze'; place: LifetimeId}
/**
* Known mutation of a value, which may effect that value or values that it contains.
* This is rare!
*/
| {kind: 'MutateTransitive'; place: LifetimeId}
/**
* Known mutation of a specific value, targeting only that specific value but not
* values that it contains.
*
* Example: `array.push(item)` mutates the array but does not mutate items stored in the array.
*/
| {kind: 'MutateLocal'; place: LifetimeId}
/**
* Possible mutation of a specific value
*/
| {kind: 'ConditionallyMutate'; place: LifetimeId}
/**
* Direct aliasing of one identifier by another identifier
* Examples: `x = y` (from y -> to x) or phis (from operand -> to phi)
*/
| {kind: 'Alias'; from: LifetimeId; to: LifetimeId}
/**
* Direct aliasing of an identifier (or a sub-path), or storing a value/sub-path
* into a part of another object.
*
* One of from.path and/or to.path must be non-null (else this is equivalent to Alias)
*/
| {
kind: 'Capture';
from: {place: LifetimeId; path: '*' | null};
to: {place: LifetimeId; path: '*' | null};
}
// Known mutation of a global
| {kind: 'MutateGlobal'; place: LifetimeId};
/*
* Shape of an {@link FunctionType} if {@link ObjectShape.functionType} is present,
* or {@link ObjectType} otherwise.

View File

@@ -546,12 +546,23 @@ export function printInstructionValue(instrValue: ReactiveValue): string {
const effects =
instrValue.loweredFunc.func.effects
?.map(effect => {
if (effect.kind === 'ContextMutation') {
return `ContextMutation places=[${[...effect.places]
.map(place => printPlace(place))
.join(', ')}] effect=${effect.effect}`;
} else {
return `GlobalMutation`;
switch (effect.kind) {
case 'ContextMutation': {
return `ContextMutation places=[${[...effect.places]
.map(place => printPlace(place))
.join(', ')}] effect=${effect.effect}`;
}
case 'GlobalMutation': {
return 'GlobalMutation';
}
case 'ReactMutation': {
return 'ReactMutation';
}
case 'CaptureEffect': {
return `CaptureEffect places=[${[...effect.places]
.map(place => printPlace(place))
.join(', ')}]`;
}
}
})
.join(', ') ?? '';
@@ -720,7 +731,7 @@ function isMutable(range: MutableRange): boolean {
}
const DEBUG_MUTABLE_RANGES = false;
function printMutableRange(identifier: Identifier): string {
export function printMutableRange(identifier: Identifier): string {
if (DEBUG_MUTABLE_RANGES) {
// if debugging, print both the identifier and scope range if they differ
const range = identifier.mutableRange;

View File

@@ -11,6 +11,7 @@ import {
HIRFunction,
Identifier,
LoweredFunction,
Place,
isRefOrRefValue,
makeInstructionId,
} from '../HIR';
@@ -19,6 +20,12 @@ import {inferReactiveScopeVariables} from '../ReactiveScopes';
import {rewriteInstructionKindsBasedOnReassignment} from '../SSA';
import {inferMutableRanges} from './InferMutableRanges';
import inferReferenceEffects from './InferReferenceEffects';
import DisjointSet from '../Utils/DisjointSet';
import {
eachInstructionLValue,
eachInstructionValueOperand,
} from '../HIR/visitors';
import {Iterable_some} from '../Utils/utils';
export default function analyseFunctions(func: HIRFunction): void {
for (const [_, block] of func.body.blocks) {
@@ -26,8 +33,8 @@ export default function analyseFunctions(func: HIRFunction): void {
switch (instr.value.kind) {
case 'ObjectMethod':
case 'FunctionExpression': {
lower(instr.value.loweredFunc.func);
infer(instr.value.loweredFunc);
const aliases = lower(instr.value.loweredFunc.func);
infer(instr.value.loweredFunc, aliases);
/**
* Reset mutable range for outer inferReferenceEffects
@@ -44,11 +51,11 @@ export default function analyseFunctions(func: HIRFunction): void {
}
}
function lower(func: HIRFunction): void {
function lower(func: HIRFunction): DisjointSet<Identifier> {
analyseFunctions(func);
inferReferenceEffects(func, {isFunctionExpression: true});
deadCodeElimination(func);
inferMutableRanges(func);
const aliases = inferMutableRanges(func);
rewriteInstructionKindsBasedOnReassignment(func);
inferReactiveScopeVariables(func);
func.env.logger?.debugLogIRs?.({
@@ -56,9 +63,61 @@ function lower(func: HIRFunction): void {
name: 'AnalyseFunction (inner)',
value: func,
});
inferAliasesForCapturing(func, aliases);
return aliases;
}
function infer(loweredFunc: LoweredFunction): void {
/**
* The alias sets returned by InferMutableRanges() accounts only for aliases that
* are known to mutate together. Notably this skips cases where a value is captured
* into some other value, but neither is subsequently mutated. An example is pushing
* a mutable value onto an array, where neither the array or value are subsequently
* mutated.
*
* This function extends the aliases sets to account for such capturing, so that we
* can detect cases where one of the values in a set is mutated later (in an outer function)
* we can correctly infer them as mutating together.
*/
function inferAliasesForCapturing(
fn: HIRFunction,
aliases: DisjointSet<Identifier>,
): void {
for (const block of fn.body.blocks.values()) {
for (const instr of block.instructions) {
const {lvalue, value} = instr;
const hasCapture =
lvalue.effect === Effect.Store ||
Iterable_some(
eachInstructionValueOperand(value),
operand => operand.effect === Effect.Capture,
);
if (!hasCapture) {
continue;
}
const operands: Array<Identifier> = [];
for (const lvalue of eachInstructionLValue(instr)) {
operands.push(lvalue.identifier);
}
for (const operand of eachInstructionValueOperand(instr.value)) {
if (
operand.effect === Effect.Store ||
operand.effect === Effect.Mutate ||
operand.effect === Effect.Capture
) {
operands.push(operand.identifier);
}
}
if (operands.length > 1) {
aliases.union(operands);
}
}
}
}
function infer(
loweredFunc: LoweredFunction,
aliases: DisjointSet<Identifier>,
): void {
for (const operand of loweredFunc.func.context) {
const identifier = operand.identifier;
CompilerError.invariant(operand.effect === Effect.Unknown, {
@@ -85,6 +144,23 @@ function infer(loweredFunc: LoweredFunction): void {
operand.effect = Effect.Read;
}
}
const contextIdentifiers = new Map(
loweredFunc.func.context.map(place => [place.identifier, place]),
);
for (const set of aliases.buildSets()) {
const contextOperands: Set<Place> = new Set(
[...set]
.map(identifier => contextIdentifiers.get(identifier))
.filter(place => place != null) as Array<Place>,
);
if (contextOperands.size !== 0) {
loweredFunc.func.effects ??= [];
loweredFunc.func.effects?.push({
kind: 'CaptureEffect',
places: contextOperands,
});
}
}
}
function isMutatedOrReassigned(id: Identifier): boolean {

View File

@@ -60,6 +60,10 @@ function inferInstr(
alias = instrValue.value;
break;
}
case 'IteratorNext': {
alias = instrValue.collection;
break;
}
default:
return;
}

View File

@@ -0,0 +1,33 @@
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
import {HIRFunction, Identifier} from '../HIR/HIR';
import DisjointSet from '../Utils/DisjointSet';
export function inferAliasForFunctionCaptureEffects(
func: HIRFunction,
aliases: DisjointSet<Identifier>,
): void {
for (const [_, block] of func.body.blocks) {
for (const instr of block.instructions) {
const {value} = instr;
if (
value.kind !== 'FunctionExpression' &&
value.kind !== 'ObjectMethod'
) {
continue;
}
const loweredFunction = value.loweredFunc.func;
for (const effect of loweredFunction.effects ?? []) {
if (effect.kind !== 'CaptureEffect') {
continue;
}
aliases.union([...effect.places].map(place => place.identifier));
}
}
}
}

View File

@@ -95,45 +95,58 @@ function inheritFunctionEffects(
return effects
.flatMap(effect => {
if (effect.kind === 'GlobalMutation' || effect.kind === 'ReactMutation') {
return [effect];
} else {
const effects: Array<FunctionEffect | null> = [];
CompilerError.invariant(effect.kind === 'ContextMutation', {
reason: 'Expected ContextMutation',
loc: null,
});
/**
* Contextual effects need to be replayed against the current inference
* state, which may know more about the value to which the effect applied.
* The main cases are:
* 1. The mutated context value is _still_ a context value in the current scope,
* so we have to continue propagating the original context mutation.
* 2. The mutated context value is a mutable value in the current scope,
* so the context mutation was fine and we can skip propagating the effect.
* 3. The mutated context value is an immutable value in the current scope,
* resulting in a non-ContextMutation FunctionEffect. We propagate that new,
* more detailed effect to the current function context.
*/
for (const place of effect.places) {
if (state.isDefined(place)) {
const replayedEffect = inferOperandEffect(state, {
...place,
loc: effect.loc,
effect: effect.effect,
});
if (replayedEffect != null) {
if (replayedEffect.kind === 'ContextMutation') {
// Case 1, still a context value so propagate the original effect
effects.push(effect);
} else {
// Case 3, immutable value so propagate the more precise effect
effects.push(replayedEffect);
}
} // else case 2, local mutable value so this effect was fine
}
switch (effect.kind) {
case 'GlobalMutation':
case 'ReactMutation': {
return [effect];
}
case 'ContextMutation': {
const effects: Array<FunctionEffect | null> = [];
CompilerError.invariant(effect.kind === 'ContextMutation', {
reason: 'Expected ContextMutation',
loc: null,
});
/**
* Contextual effects need to be replayed against the current inference
* state, which may know more about the value to which the effect applied.
* The main cases are:
* 1. The mutated context value is _still_ a context value in the current scope,
* so we have to continue propagating the original context mutation.
* 2. The mutated context value is a mutable value in the current scope,
* so the context mutation was fine and we can skip propagating the effect.
* 3. The mutated context value is an immutable value in the current scope,
* resulting in a non-ContextMutation FunctionEffect. We propagate that new,
* more detailed effect to the current function context.
*/
for (const place of effect.places) {
if (state.isDefined(place)) {
const replayedEffect = inferOperandEffect(state, {
...place,
loc: effect.loc,
effect: effect.effect,
});
if (replayedEffect != null) {
if (replayedEffect.kind === 'ContextMutation') {
// Case 1, still a context value so propagate the original effect
effects.push(effect);
} else {
// Case 3, immutable value so propagate the more precise effect
effects.push(replayedEffect);
}
} // else case 2, local mutable value so this effect was fine
}
}
return effects;
}
case 'CaptureEffect': {
return [];
}
default: {
assertExhaustive(
effect,
`Unexpected effect kind '${(effect as any).kind}'`,
);
}
return effects;
}
})
.filter((effect): effect is FunctionEffect => effect != null);
@@ -298,26 +311,31 @@ export function inferTerminalFunctionEffects(
export function transformFunctionEffectErrors(
functionEffects: Array<FunctionEffect>,
): Array<CompilerErrorDetailOptions> {
return functionEffects.map(eff => {
switch (eff.kind) {
case 'ReactMutation':
case 'GlobalMutation': {
return eff.error;
return functionEffects
.map(eff => {
switch (eff.kind) {
case 'ReactMutation':
case 'GlobalMutation': {
return eff.error;
}
case 'ContextMutation': {
return {
severity: ErrorSeverity.Invariant,
reason: `Unexpected ContextMutation in top-level function effects`,
loc: eff.loc,
};
}
case 'CaptureEffect': {
return null;
}
default:
assertExhaustive(
eff,
`Unexpected function effect kind \`${(eff as any).kind}\``,
);
}
case 'ContextMutation': {
return {
severity: ErrorSeverity.Invariant,
reason: `Unexpected ContextMutation in top-level function effects`,
loc: eff.loc,
};
}
default:
assertExhaustive(
eff,
`Unexpected function effect kind \`${(eff as any).kind}\``,
);
}
});
})
.filter(eff => eff != null) as Array<CompilerErrorDetailOptions>;
}
function isEffectSafeOutsideRender(effect: FunctionEffect): boolean {

View File

@@ -5,16 +5,21 @@
* LICENSE file in the root directory of this source tree.
*/
import prettyFormat from 'pretty-format';
import {HIRFunction, Identifier} from '../HIR/HIR';
import DisjointSet from '../Utils/DisjointSet';
import {inferAliasForUncalledFunctions} from './InerAliasForUncalledFunctions';
import {inferAliases} from './InferAlias';
import {inferAliasForFunctionCaptureEffects} from './InferAliasesForFunctionCaptureEffects';
import {inferAliasForPhis} from './InferAliasForPhis';
import {inferAliasForStores} from './InferAliasForStores';
import {inferMutableLifetimes} from './InferMutableLifetimes';
import {inferMutableRangesForAlias} from './InferMutableRangesForAlias';
import {inferMutableRangesForComutation} from './InferMutableRangesForComutation';
import {inferTryCatchAliases} from './InferTryCatchAliases';
import {printIdentifier, printMutableRange} from '../HIR/PrintHIR';
export function inferMutableRanges(ir: HIRFunction): void {
export function inferMutableRanges(ir: HIRFunction): DisjointSet<Identifier> {
// Infer mutable ranges for non fields
inferMutableLifetimes(ir, false);
@@ -30,18 +35,22 @@ export function inferMutableRanges(ir: HIRFunction): void {
* Eagerly canonicalize so that if nothing changes we can bail out
* after a single iteration
*/
let prevAliases: Map<Identifier, Identifier> = aliases.canonicalize();
let prevAliases: Map<Identifier, string> = canonicalize(aliases);
while (true) {
// Infer mutable ranges for aliases that are not fields
inferMutableRangesForAlias(ir, aliases);
inferMutableRangesForComutation(ir);
// Update aliasing information of fields
inferAliasForStores(ir, aliases);
inferAliasForFunctionCaptureEffects(ir, aliases);
// Update aliasing information of phis
inferAliasForPhis(ir, aliases);
const nextAliases = aliases.canonicalize();
const nextAliases = canonicalize(aliases);
if (areEqualMaps(prevAliases, nextAliases)) {
break;
}
@@ -73,20 +82,57 @@ export function inferMutableRanges(ir: HIRFunction): void {
* but does not modify values that `y` "contains" such as the
* object literal or `z`.
*/
prevAliases = aliases.canonicalize();
prevAliases = canonicalize(aliases);
while (true) {
inferMutableRangesForAlias(ir, aliases);
inferMutableRangesForComutation(ir);
inferAliasForPhis(ir, aliases);
inferAliasForUncalledFunctions(ir, aliases);
const nextAliases = aliases.canonicalize();
const nextAliases = canonicalize(aliases);
if (areEqualMaps(prevAliases, nextAliases)) {
break;
}
prevAliases = nextAliases;
}
return aliases;
}
function areEqualMaps<T>(a: Map<T, T>, b: Map<T, T>): boolean {
export function debugAliases(aliases: DisjointSet<Identifier>): void {
console.log(
prettyFormat(
aliases
.buildSets()
.map(set =>
[...set].map(
ident => printIdentifier(ident) + printMutableRange(ident),
),
),
),
);
}
/**
* Canonicalizes the alias set and mutable range information calculated at the current time.
* The returned value maps each identifier in the program to the root identifier of its alias
* set and the the mutable range of that set.
*
* This ensures that we fixpoint the mutable ranges themselves and not just the alias sets.
*/
function canonicalize(
aliases: DisjointSet<Identifier>,
): Map<Identifier, string> {
const entries = new Map<Identifier, string>();
aliases.forEach((item, root) => {
entries.set(
item,
`${root.id}:${root.mutableRange.start}:${root.mutableRange.end}`,
);
});
return entries;
}
function areEqualMaps<T, U>(a: Map<T, U>, b: Map<T, U>): boolean {
if (a.size !== b.size) {
return false;
}

View File

@@ -0,0 +1,91 @@
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
import {
HIRFunction,
Identifier,
isRefOrRefValue,
makeInstructionId,
} from '../HIR';
import {eachInstructionOperand} from '../HIR/visitors';
import {isMutable} from '../ReactiveScopes/InferReactiveScopeVariables';
/**
* Finds instructions with operands that co-mutate and extends all their mutable ranges
* to end at the same point (the highest `end` value of the group). Note that the
* alias sets used in InferMutableRanges are meant for values that strictly alias:
* a mutation of one value in the set would directly modify the same object as some
* other value in the set.
*
* However, co-mutation can cause an alias to one object to be stored within another object,
* for example:
*
* ```
* const a = {};
* const b = {};
* const f = () => b.c; //
* setProperty(a, 'b', b); // equiv to a.b = b
*
* a.b.c = 'c'; // this mutates b!
* ```
*
* Here, the co-mutation in `setProperty(a, 'b', b)` means that a reference to b may be stored
* in a, vice-versa, or both. We need to extend the mutable range of both a and b to reflect
* the fact the values may mutate together.
*
* Previously this was implemented in InferReactiveScopeVariables, but that is too late:
* we need this to be part of the InferMutableRanges fixpoint iteration to account for functions
* like `f` in the example, which capture a reference to a value that may change later. `f`
* cannot be independently memoized from the `setProperty()` call due to the co-mutation.
*
* See aliased-capture-mutate and aliased-capture-aliased-mutate fixtures for examples.
*/
export function inferMutableRangesForComutation(fn: HIRFunction): void {
for (const block of fn.body.blocks.values()) {
for (const instr of block.instructions) {
let operands: Array<Identifier> | null = null;
for (const operand of eachInstructionOperand(instr)) {
if (
isMutable(instr, operand) &&
operand.identifier.mutableRange.start > 0
) {
if (
instr.value.kind === 'FunctionExpression' ||
instr.value.kind === 'ObjectMethod'
) {
if (operand.identifier.type.kind === 'Primitive') {
continue;
}
}
operands ??= [];
operands.push(operand.identifier);
}
}
if (operands != null) {
// Find the last instruction which mutates any of the mutable operands
let lastMutatingInstructionId = makeInstructionId(0);
for (const id of operands) {
if (id.mutableRange.end > lastMutatingInstructionId) {
lastMutatingInstructionId = id.mutableRange.end;
}
}
/**
* Update all mutable operands's mutable ranges to end at the same point
*/
for (const id of operands) {
if (
id.mutableRange.end < lastMutatingInstructionId &&
!isRefOrRefValue(id)
) {
id.mutableRange.end = lastMutatingInstructionId;
}
}
}
}
}
}

View File

@@ -0,0 +1,556 @@
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
import {isFunctionExpression} from '@babel/types';
import {CompilerError} from '..';
import {
AliasingEffect,
BasicBlock,
BlockId,
CallExpression,
Environment,
HIRFunction,
Instruction,
InstructionValue,
isArrayType,
isMapType,
isSetType,
MethodCall,
NewExpression,
Place,
SpreadPattern,
} from '../HIR';
import {
eachInstructionValueLValue,
eachInstructionValueOperand,
eachTerminalSuccessor,
} from '../HIR/visitors';
import {Result} from '../Utils/Result';
import {getFunctionCallSignature} from './InferReferenceEffects';
import {
AliasingSignature,
FunctionSignature,
LifetimeId,
} from '../HIR/ObjectShape';
import {assertExhaustive, getOrInsertWith} from '../Utils/utils';
export function inferMutationAliasingEffects(
fn: HIRFunction,
{isFunctionExpression}: {isFunctionExpression: boolean} = {
isFunctionExpression: false,
},
): Result<Array<AliasingEffect>, CompilerError> {
const initialState = InferenceState.empty(fn.env, isFunctionExpression);
// Map of blocks to the last (merged) incoming state that was processed
const statesByBlock: Map<BlockId, InferenceState> = new Map();
/*
* Multiple predecessors may be visited prior to reaching a given successor,
* so track the list of incoming state for each successor block.
* These are merged when reaching that block again.
*/
const queuedStates: Map<BlockId, InferenceState> = new Map();
function queue(blockId: BlockId, state: InferenceState): void {
let queuedState = queuedStates.get(blockId);
if (queuedState != null) {
// merge the queued states for this block
state = queuedState.merge(state) ?? queuedState;
queuedStates.set(blockId, state);
} else {
/*
* this is the first queued state for this block, see whether
* there are changed relative to the last time it was processed.
*/
const prevState = statesByBlock.get(blockId);
const nextState = prevState != null ? prevState.merge(state) : state;
if (nextState != null) {
queuedStates.set(blockId, nextState);
}
}
}
queue(fn.body.entry, initialState);
const signatureCache: Map<Instruction, Array<AliasingEffect>> = new Map();
while (queuedStates.size !== 0) {
for (const [blockId, block] of fn.body.blocks) {
const incomingState = queuedStates.get(blockId);
queuedStates.delete(blockId);
if (incomingState == null) {
continue;
}
statesByBlock.set(blockId, incomingState);
const state = incomingState.clone();
inferBlock(state, block, signatureCache);
for (const nextBlockId of eachTerminalSuccessor(block.terminal)) {
queue(nextBlockId, state);
}
}
}
}
function inferBlock(
state: InferenceState,
block: BasicBlock,
signatureCache: Map<Instruction, Array<AliasingEffect>>,
): void {
for (const instr of block.instructions) {
let signature = signatureCache.get(instr);
if (signature == null) {
signature = computeSignatureForInstruction(state.env, instr);
signatureCache.set(instr, signature);
}
const effects = applySignature(signature, state);
instr.effects = effects;
}
}
/**
* Applies the signature to the given state to determine the precise set of effects
* that will occur in practice. This takes into account the inferred state of each
* variable. For example, the signature may have a `ConditionallyMutate x` effect.
* Here, we check the abstract type of `x` and either record a `Mutate x` if x is mutable
* or no effect if x is a primitive, global, or frozen.
*
* This phase may also emit errors, for example MutateLocal on a frozen value is invalid.
*/
function applySignature(
signature: Array<AliasingEffect>,
state: InferenceState,
): Array<AliasingEffect> {}
class InferenceState {
env: Environment;
isFunctionExpression: boolean;
constructor(env: Environment, isFunctionExpression: boolean) {
this.env = env;
this.isFunctionExpression = isFunctionExpression;
}
merge(state: InferenceState): InferenceState | null {}
clone(): InferenceState {}
static empty(
env: Environment,
isFunctionExpression: boolean,
): InferenceState {
return new InferenceState(env, isFunctionExpression);
}
}
/**
* Computes an effect signature for the instruction _without_ looking at the inference state,
* and only using the semantics of the instructions and the inferred types. The idea is to make
* it easy to check that the semantics of each instruction are preserved by describing only the
* effects and not making decisions based on the inference state.
*
* Then in applySignature(), above, we refine this signature based on the inference state.
*
* NOTE: this function is designed to be cached so it's only computed once upon first visiting
* an instruction.
*/
function computeSignatureForInstruction(
env: Environment,
instr: Instruction,
): Array<AliasingEffect> {
const {lvalue, value} = instr;
const effects: Array<AliasingEffect> = [];
switch (value.kind) {
case 'ArrayExpression': {
// All elements are captured into part of the output value
for (const element of value.elements) {
let operand: Place;
if (element.kind === 'Identifier') {
operand = element;
} else if (element.kind === 'Spread') {
operand = element.place;
} else {
continue;
}
effects.push({
kind: 'Capture',
from: {place: operand, path: null},
to: {place: lvalue, path: '*'},
});
}
break;
}
case 'ObjectExpression': {
for (const property of value.properties) {
const operand =
property.kind === 'ObjectProperty' ? property.place : property.place;
effects.push({
kind: 'Capture',
from: {place: operand, path: null},
to: {place: lvalue, path: '*'},
});
}
break;
}
case 'Await': {
// Potentially mutates the receiver (awaiting it changes its state and can run side effects)
effects.push({kind: 'ConditionallyMutate', place: value.value});
/**
* Data from the promise may be returned into the result, but await does not directly return
* the promise itself
*/
effects.push({
kind: 'Capture',
from: {place: value.value, path: '*'},
to: {place: lvalue, path: null},
});
break;
}
case 'TaggedTemplateExpression': {
CompilerError.throwTodo({
reason: `Handle TaggedTemplateExpression in new inference`,
loc: instr.loc,
});
}
case 'NewExpression':
case 'CallExpression':
case 'MethodCall': {
let callee;
let mutatesCallee = false;
if (value.kind === 'NewExpression') {
callee = value.callee;
mutatesCallee = false;
} else if (value.kind === 'CallExpression') {
callee = value.callee;
mutatesCallee = true;
} else if (value.kind === 'MethodCall') {
callee = value.property;
mutatesCallee = false;
} else {
assertExhaustive(
value,
`Unexpected value kind '${(value as any).kind}'`,
);
}
const signature = getFunctionCallSignature(env, callee.identifier.type);
if (signature != null && signature.aliasing != null) {
effects.push(
...computeEffectsForSignature(
signature.aliasing,
lvalue,
callee,
value.args,
),
);
} else {
/**
* If no signature then by default:
* - All operands are conditionally mutated, except some instruction
* variants are assumed to not mutate the callee (such as `new`)
* - All operands are captured into (but not directly aliased as)
* every other argument.
*/
for (const operand of eachInstructionValueOperand(value)) {
if (operand !== callee || mutatesCallee) {
effects.push({kind: 'ConditionallyMutate', place: operand});
}
for (const other of eachInstructionValueOperand(value)) {
if (other === operand) {
continue;
}
effects.push({
kind: 'Capture',
from: {place: operand, path: null},
to: {place: other, path: '*'},
});
}
}
}
break;
}
case 'PropertyDelete':
case 'ComputedDelete': {
// Mutates the object by removing the property, no aliasing
effects.push({kind: 'MutateLocal', place: value.object});
break;
}
case 'PropertyLoad':
case 'ComputedLoad': {
effects.push({
kind: 'Capture',
from: {place: value.object, path: '*'},
to: {place: lvalue, path: null},
});
break;
}
case 'PropertyStore':
case 'ComputedStore': {
effects.push({kind: 'MutateLocal', place: value.object});
effects.push({
kind: 'Capture',
from: {place: value.value, path: null},
to: {place: value.object, path: '*'},
});
effects.push({kind: 'Alias', from: value.value, to: lvalue});
break;
}
case 'PostfixUpdate':
case 'PrefixUpdate': {
effects.push({kind: 'MutateLocal', place: value.value});
break;
}
case 'ObjectMethod':
case 'FunctionExpression': {
const functionEffects = value.loweredFunc.func.aliasingEffects;
if (functionEffects != null) {
effects.push(...functionEffects);
}
break;
}
case 'GetIterator': {
if (
isArrayType(value.collection.identifier) ||
isMapType(value.collection.identifier) ||
isSetType(value.collection.identifier)
) {
/*
* Builtin collections are known to return a fresh iterator on each call,
* so the iterator does not alias the collection
*/
effects.push({
kind: 'Capture',
from: {place: value.collection, path: '*'},
to: {place: lvalue, path: '*'},
});
} else {
/*
* Otherwise, the object may return itself as the iterator, so we have to
* assume that the result directly aliases the collection. Further, the
* method to get the iterator could potentially mutate the collection
*/
effects.push({kind: 'Alias', from: value.collection, to: lvalue});
effects.push({kind: 'ConditionallyMutate', place: value.collection});
}
break;
}
case 'IteratorNext': {
/*
* Technically advancing an iterator will always mutate it (for any reasonable implementation)
* But because we create an alias from the collection to the iterator if we don't know the type,
* then it's possible the iterator is aliased to a frozen value and we wouldn't want to error.
* so we mark this as conditional mutation to allow iterating frozen values.
*/
effects.push({kind: 'ConditionallyMutate', place: value.iterator});
// Extracts part of the original collection into the result
effects.push({
kind: 'Capture',
from: {place: value.iterator, path: '*'},
to: {place: lvalue, path: null},
});
break;
}
case 'NextPropertyOf': {
// no effects
break;
}
case 'JsxExpression':
case 'JsxFragment': {
for (const operand of eachInstructionValueOperand(value)) {
effects.push({kind: 'Freeze', place: operand});
effects.push({
kind: 'Capture',
from: {place: operand, path: null},
to: {place: lvalue, path: '*'},
});
}
break;
}
case 'DeclareContext':
case 'DeclareLocal': {
// no effects
break;
}
case 'Destructure': {
for (const patternLValue of eachInstructionValueLValue(value)) {
effects.push({
kind: 'Capture',
from: {place: value.value, path: '*'},
to: {place: patternLValue, path: null},
});
}
effects.push({kind: 'Alias', from: value.value, to: lvalue});
break;
}
case 'LoadContext': {
effects.push({
kind: 'Capture',
from: {place: value.place, path: '*'},
to: {place: lvalue, path: null},
});
break;
}
case 'LoadLocal': {
effects.push({kind: 'Alias', from: value.place, to: lvalue});
break;
}
case 'StoreContext': {
effects.push({kind: 'MutateLocal', place: value.lvalue.place});
effects.push({
kind: 'Capture',
from: {place: value.value, path: null},
to: {place: value.lvalue.place, path: '*'},
});
effects.push({kind: 'Alias', from: value.value, to: lvalue});
break;
}
case 'StoreGlobal': {
effects.push({kind: 'MutateGlobal', place: value.value});
break;
}
case 'StoreLocal': {
effects.push({kind: 'Alias', from: value.value, to: value.lvalue.place});
effects.push({kind: 'Alias', from: value.value, to: lvalue});
break;
}
case 'TypeCastExpression': {
effects.push({kind: 'Alias', from: value.value, to: lvalue});
break;
}
case 'BinaryExpression':
case 'Debugger':
case 'FinishMemoize':
case 'JSXText':
case 'LoadGlobal':
case 'MetaProperty':
case 'Primitive':
case 'RegExpLiteral':
case 'StartMemoize':
case 'TemplateLiteral':
case 'UnaryExpression':
case 'UnsupportedNode': {
// no effects
break;
}
}
return effects;
}
function computeEffectsForSignature(
signature: AliasingSignature,
lvalue: Place,
receiver: Place,
args: Array<Place | SpreadPattern>,
): Array<AliasingEffect> {
// Build substitutions
const substitutions: Map<LifetimeId, Array<Place>> = new Map();
substitutions.set(signature.receiver, [receiver]);
substitutions.set(signature.returns, [lvalue]);
const params = signature.params;
for (let i = 0; i < args.length; i++) {
const arg = args[i];
if (params == null || i >= params.length || arg.kind === 'Spread') {
const place = arg.kind === 'Identifier' ? arg : arg.place;
getOrInsertWith(substitutions, signature.restParam, () => []).push(place);
} else {
const param = params[i];
substitutions.set(param, [arg]);
}
}
// Apply substitutions
const effects: Array<AliasingEffect> = [];
for (const effect of signature.effects) {
switch (effect.kind) {
case 'Alias': {
const from = substitutions.get(effect.from);
const to = substitutions.get(effect.to);
if (from == null || to == null) {
continue;
}
for (const fromPlace of from) {
for (const toPlace of to) {
effects.push({kind: 'Alias', from: fromPlace, to: toPlace});
}
}
break;
}
case 'Capture': {
const from = substitutions.get(effect.from.place);
const to = substitutions.get(effect.to.place);
if (from == null || to == null) {
continue;
}
for (const fromPlace of from) {
for (const toPlace of to) {
effects.push({
kind: 'Capture',
from: {place: fromPlace, path: effect.from.path},
to: {place: toPlace, path: effect.to.path},
});
}
}
break;
}
case 'ConditionallyMutate': {
const places = substitutions.get(effect.place);
if (places == null) {
continue;
}
for (const place of places) {
effects.push({kind: 'ConditionallyMutate', place});
}
break;
}
case 'MutateGlobal': {
const places = substitutions.get(effect.place);
if (places == null) {
continue;
}
for (const place of places) {
effects.push({kind: 'MutateGlobal', place});
}
break;
}
case 'Freeze': {
const places = substitutions.get(effect.place);
if (places == null) {
continue;
}
for (const place of places) {
effects.push({kind: 'Freeze', place});
}
break;
}
case 'MutateLocal': {
const places = substitutions.get(effect.place);
if (places == null) {
continue;
}
for (const place of places) {
effects.push({kind: 'MutateLocal', place});
}
break;
}
case 'MutateTransitive': {
const places = substitutions.get(effect.place);
if (places == null) {
continue;
}
for (const place of places) {
effects.push({kind: 'MutateTransitive', place});
}
break;
}
default: {
assertExhaustive(
effect,
`Unexpected effect kind '${(effect as any).kind}'`,
);
}
}
}
return effects;
}

View File

@@ -31,13 +31,13 @@ import {
isArrayType,
isMapType,
isMutableEffect,
isObjectType,
isSetType,
isObjectType,
} from '../HIR/HIR';
import {FunctionSignature} from '../HIR/ObjectShape';
import {
printIdentifier,
printMixedHIR,
printInstructionValue,
printPlace,
printSourceLocation,
} from '../HIR/PrintHIR';
@@ -48,7 +48,7 @@ import {
eachTerminalOperand,
eachTerminalSuccessor,
} from '../HIR/visitors';
import {assertExhaustive} from '../Utils/utils';
import {assertExhaustive, retainWhere} from '../Utils/utils';
import {
inferTerminalFunctionEffects,
inferInstructionFunctionEffects,
@@ -521,7 +521,7 @@ class InferenceState {
* `expected valueKind to be 'Mutable' but found to be \`${valueKind}\``
* );
*/
effect = isObjectType(place.identifier) ? Effect.Store : Effect.Mutate;
effect = Effect.Store;
break;
}
case Effect.Capture: {
@@ -669,7 +669,10 @@ class InferenceState {
}
for (const [value, kind] of this.#values) {
const id = identify(value);
result.values[id] = {kind, value: printMixedHIR(value)};
result.values[id] = {
abstract: this.debugAbstractValue(kind),
value: printInstructionValue(value),
};
}
for (const [variable, values] of this.#variables) {
result.variables[`$${variable}`] = [...values].map(identify);
@@ -677,6 +680,14 @@ class InferenceState {
return result;
}
debugAbstractValue(value: AbstractValue): any {
return {
kind: value.kind,
context: [...value.context].map(printPlace),
reason: [...value.reason],
};
}
inferPhi(phi: Phi): void {
const values: Set<InstructionValue> = new Set();
for (const [_, operand] of phi.operands) {
@@ -902,19 +913,11 @@ function inferBlock(
break;
}
case 'ArrayExpression': {
const contextRefOperands = getContextRefOperand(state, instrValue);
const valueKind: AbstractValue =
contextRefOperands.length > 0
? {
kind: ValueKind.Context,
reason: new Set([ValueReason.Other]),
context: new Set(contextRefOperands),
}
: {
kind: ValueKind.Mutable,
reason: new Set([ValueReason.Other]),
context: new Set(),
};
const valueKind: AbstractValue = {
kind: ValueKind.Mutable,
reason: new Set([ValueReason.Other]),
context: new Set(),
};
for (const element of instrValue.elements) {
if (element.kind === 'Spread') {
@@ -935,6 +938,7 @@ function inferBlock(
let _: 'Hole' = element.kind;
}
}
state.initialize(instrValue, valueKind);
state.define(instr.lvalue, instrValue);
instr.lvalue.effect = Effect.Store;
@@ -954,19 +958,11 @@ function inferBlock(
break;
}
case 'ObjectExpression': {
const contextRefOperands = getContextRefOperand(state, instrValue);
const valueKind: AbstractValue =
contextRefOperands.length > 0
? {
kind: ValueKind.Context,
reason: new Set([ValueReason.Other]),
context: new Set(contextRefOperands),
}
: {
kind: ValueKind.Mutable,
reason: new Set([ValueReason.Other]),
context: new Set(),
};
const valueKind: AbstractValue = {
kind: ValueKind.Mutable,
reason: new Set([ValueReason.Other]),
context: new Set(),
};
for (const property of instrValue.properties) {
switch (property.kind) {
@@ -1190,6 +1186,35 @@ function inferBlock(
);
hasMutableOperand ||= isMutableEffect(operand.effect, operand.loc);
}
/*
* Filter CaptureEffects to remove values that are immutable and don't
* need to be tracked for aliasing
*/
const effects = instrValue.loweredFunc.func.effects;
if (effects != null && effects.length !== 0) {
retainWhere(effects, effect => {
if (effect.kind !== 'CaptureEffect') {
return true;
}
const places: Set<Place> = new Set();
for (const place of effect.places) {
const kind = state.kind(place);
if (
kind.kind === ValueKind.Context ||
kind.kind === ValueKind.MaybeFrozen ||
kind.kind === ValueKind.Mutable
) {
places.add(place);
}
}
if (places.size === 0) {
return false;
}
effect.places = places;
return true;
});
}
/*
* If a closure did not capture any mutable values, then we can consider it to be
* frozen, which allows it to be independently memoized.
@@ -1280,20 +1305,18 @@ function inferBlock(
break;
}
case 'PropertyStore': {
const effect =
state.kind(instrValue.object).kind === ValueKind.Context
? Effect.ConditionallyMutate
: Effect.Capture;
state.referenceAndRecordEffects(
freezeActions,
instrValue.value,
effect,
Effect.Capture,
ValueReason.Other,
);
state.referenceAndRecordEffects(
freezeActions,
instrValue.object,
Effect.Store,
isObjectType(instrValue.object.identifier)
? Effect.Store
: Effect.Mutate,
ValueReason.Other,
);
@@ -1320,25 +1343,21 @@ function inferBlock(
state.referenceAndRecordEffects(
freezeActions,
instrValue.object,
Effect.Read,
Effect.Capture,
ValueReason.Other,
);
const lvalue = instr.lvalue;
lvalue.effect = Effect.ConditionallyMutate;
lvalue.effect = Effect.Store;
state.initialize(instrValue, state.kind(instrValue.object));
state.define(lvalue, instrValue);
continuation = {kind: 'funeffects'};
break;
}
case 'ComputedStore': {
const effect =
state.kind(instrValue.object).kind === ValueKind.Context
? Effect.ConditionallyMutate
: Effect.Capture;
state.referenceAndRecordEffects(
freezeActions,
instrValue.value,
effect,
Effect.Capture,
ValueReason.Other,
);
state.referenceAndRecordEffects(
@@ -1350,7 +1369,9 @@ function inferBlock(
state.referenceAndRecordEffects(
freezeActions,
instrValue.object,
Effect.Store,
isObjectType(instrValue.object.identifier)
? Effect.Store
: Effect.Mutate,
ValueReason.Other,
);
@@ -1387,7 +1408,7 @@ function inferBlock(
state.referenceAndRecordEffects(
freezeActions,
instrValue.object,
Effect.Read,
Effect.Capture,
ValueReason.Other,
);
state.referenceAndRecordEffects(
@@ -1397,7 +1418,7 @@ function inferBlock(
ValueReason.Other,
);
const lvalue = instr.lvalue;
lvalue.effect = Effect.ConditionallyMutate;
lvalue.effect = Effect.Store;
state.initialize(instrValue, state.kind(instrValue.object));
state.define(lvalue, instrValue);
continuation = {kind: 'funeffects'};
@@ -1811,7 +1832,9 @@ function inferBlock(
state.isDefined(operand) &&
((operand.identifier.type.kind === 'Function' &&
state.isFunctionExpression) ||
state.kind(operand).kind === ValueKind.Context)
state.kind(operand).kind === ValueKind.Context ||
(state.kind(operand).kind === ValueKind.Mutable &&
state.isFunctionExpression))
) {
/**
* Returned values should only be typed as 'frozen' if they are both (1)
@@ -1838,22 +1861,6 @@ function inferBlock(
);
}
function getContextRefOperand(
state: InferenceState,
instrValue: InstructionValue,
): Array<Place> {
const result = [];
for (const place of eachInstructionValueOperand(instrValue)) {
if (
state.isDefined(place) &&
state.kind(place).kind === ValueKind.Context
) {
result.push(place);
}
}
return result;
}
export function getFunctionCallSignature(
env: Environment,
type: Type,

View File

@@ -252,6 +252,7 @@ function emitSelectorFn(env: Environment, keys: Array<string>): Instruction {
returnType: makeType(),
context: [],
effects: null,
aliasingEffects: null,
body: {
entry: block.id,
blocks: new Map([[block.id, block]]),

View File

@@ -368,6 +368,7 @@ function emitOutlinedFn(
returnType: makeType(),
context: [],
effects: null,
aliasingEffects: null,
body: {
entry: block.id,
blocks: new Map([[block.id, block]]),

View File

@@ -1183,7 +1183,7 @@ function codegenTerminal(
? codegenPlaceToExpression(cx, case_.test)
: null;
const block = codegenBlock(cx, case_.block!);
return t.switchCase(test, [block]);
return t.switchCase(test, block.body.length === 0 ? [] : [block]);
}),
);
}

View File

@@ -0,0 +1,71 @@
## Input
```javascript
// @flow @enableTransitivelyFreezeFunctionExpressions:false
import {arrayPush, setPropertyByKey, Stringify} from 'shared-runtime';
function useFoo({a, b}: {a: number, b: number}) {
const x = [];
const y = {value: a};
arrayPush(x, y); // x and y co-mutate
const y_alias = y;
const cb = () => y_alias.value;
setPropertyByKey(x[0], 'value', b); // might overwrite y.value
return <Stringify cb={cb} shouldInvokeFns={true} />;
}
export const FIXTURE_ENTRYPOINT = {
fn: useFoo,
params: [{a: 2, b: 10}],
sequentialRenders: [
{a: 2, b: 10},
{a: 2, b: 11},
],
};
```
## Code
```javascript
import { c as _c } from "react/compiler-runtime";
import { arrayPush, setPropertyByKey, Stringify } from "shared-runtime";
function useFoo(t0) {
const $ = _c(3);
const { a, b } = t0;
let t1;
if ($[0] !== a || $[1] !== b) {
const x = [];
const y = { value: a };
arrayPush(x, y);
const y_alias = y;
const cb = () => y_alias.value;
setPropertyByKey(x[0], "value", b);
t1 = <Stringify cb={cb} shouldInvokeFns={true} />;
$[0] = a;
$[1] = b;
$[2] = t1;
} else {
t1 = $[2];
}
return t1;
}
export const FIXTURE_ENTRYPOINT = {
fn: useFoo,
params: [{ a: 2, b: 10 }],
sequentialRenders: [
{ a: 2, b: 10 },
{ a: 2, b: 11 },
],
};
```
### Eval output
(kind: ok) <div>{"cb":{"kind":"Function","result":10},"shouldInvokeFns":true}</div>
<div>{"cb":{"kind":"Function","result":11},"shouldInvokeFns":true}</div>

View File

@@ -0,0 +1,22 @@
// @flow @enableTransitivelyFreezeFunctionExpressions:false
import {arrayPush, setPropertyByKey, Stringify} from 'shared-runtime';
function useFoo({a, b}: {a: number, b: number}) {
const x = [];
const y = {value: a};
arrayPush(x, y); // x and y co-mutate
const y_alias = y;
const cb = () => y_alias.value;
setPropertyByKey(x[0], 'value', b); // might overwrite y.value
return <Stringify cb={cb} shouldInvokeFns={true} />;
}
export const FIXTURE_ENTRYPOINT = {
fn: useFoo,
params: [{a: 2, b: 10}],
sequentialRenders: [
{a: 2, b: 10},
{a: 2, b: 11},
],
};

View File

@@ -5,19 +5,6 @@
// @flow @enableTransitivelyFreezeFunctionExpressions:false
import {setPropertyByKey, Stringify} from 'shared-runtime';
/**
* Variation of bug in `bug-aliased-capture-aliased-mutate`
* Found differences in evaluator results
* Non-forget (expected):
* (kind: ok)
* <div>{"cb":{"kind":"Function","result":2},"shouldInvokeFns":true}</div>
* <div>{"cb":{"kind":"Function","result":3},"shouldInvokeFns":true}</div>
* Forget:
* (kind: ok)
* <div>{"cb":{"kind":"Function","result":2},"shouldInvokeFns":true}</div>
* <div>{"cb":{"kind":"Function","result":2},"shouldInvokeFns":true}</div>
*/
function useFoo({a}: {a: number, b: number}) {
const arr = [];
const obj = {value: a};
@@ -46,7 +33,7 @@ import { c as _c } from "react/compiler-runtime";
import { setPropertyByKey, Stringify } from "shared-runtime";
function useFoo(t0) {
const $ = _c(4);
const $ = _c(2);
const { a } = t0;
let t1;
if ($[0] !== a) {
@@ -55,15 +42,7 @@ function useFoo(t0) {
setPropertyByKey(obj, "arr", arr);
const obj_alias = obj;
let t2;
if ($[2] !== obj_alias.arr.length) {
t2 = () => obj_alias.arr.length;
$[2] = obj_alias.arr.length;
$[3] = t2;
} else {
t2 = $[3];
}
const cb = t2;
const cb = () => obj_alias.arr.length;
for (let i = 0; i < a; i++) {
arr.push(i);
}
@@ -84,4 +63,7 @@ export const FIXTURE_ENTRYPOINT = {
};
```
### Eval output
(kind: ok) <div>{"cb":{"kind":"Function","result":2},"shouldInvokeFns":true}</div>
<div>{"cb":{"kind":"Function","result":3},"shouldInvokeFns":true}</div>

View File

@@ -1,19 +1,6 @@
// @flow @enableTransitivelyFreezeFunctionExpressions:false
import {setPropertyByKey, Stringify} from 'shared-runtime';
/**
* Variation of bug in `bug-aliased-capture-aliased-mutate`
* Found differences in evaluator results
* Non-forget (expected):
* (kind: ok)
* <div>{"cb":{"kind":"Function","result":2},"shouldInvokeFns":true}</div>
* <div>{"cb":{"kind":"Function","result":3},"shouldInvokeFns":true}</div>
* Forget:
* (kind: ok)
* <div>{"cb":{"kind":"Function","result":2},"shouldInvokeFns":true}</div>
* <div>{"cb":{"kind":"Function","result":2},"shouldInvokeFns":true}</div>
*/
function useFoo({a}: {a: number, b: number}) {
const arr = [];
const obj = {value: a};

View File

@@ -23,34 +23,18 @@ export const FIXTURE_ENTRYPOINT = {
```javascript
import { c as _c } from "react/compiler-runtime";
function Component(props) {
const $ = _c(6);
const $ = _c(2);
let t0;
if ($[0] !== props.a) {
t0 = { a: props.a };
const item = { a: props.a };
const items = [item];
t0 = items.map(_temp);
$[0] = props.a;
$[1] = t0;
} else {
t0 = $[1];
}
const item = t0;
let t1;
if ($[2] !== item) {
t1 = [item];
$[2] = item;
$[3] = t1;
} else {
t1 = $[3];
}
const items = t1;
let t2;
if ($[4] !== items) {
t2 = items.map(_temp);
$[4] = items;
$[5] = t2;
} else {
t2 = $[5];
}
const mapped = t2;
const mapped = t0;
return mapped;
}
function _temp(item_0) {

View File

@@ -50,8 +50,7 @@ function Component(props) {
console.log(handlers.value);
break bb0;
}
default: {
}
default:
}
t0 = handlers;

View File

@@ -1,107 +0,0 @@
## Input
```javascript
// @flow @enableTransitivelyFreezeFunctionExpressions:false
import {arrayPush, setPropertyByKey, Stringify} from 'shared-runtime';
/**
* 1. `InferMutableRanges` derives the mutable range of identifiers and their
* aliases from `LoadLocal`, `PropertyLoad`, etc
* - After this pass, y's mutable range only extends to `arrayPush(x, y)`
* - We avoid assigning mutable ranges to loads after y's mutable range, as
* these are working with an immutable value. As a result, `LoadLocal y` and
* `PropertyLoad y` do not get mutable ranges
* 2. `InferReactiveScopeVariables` extends mutable ranges and creates scopes,
* as according to the 'co-mutation' of different values
* - Here, we infer that
* - `arrayPush(y, x)` might alias `x` and `y` to each other
* - `setPropertyKey(x, ...)` may mutate both `x` and `y`
* - This pass correctly extends the mutable range of `y`
* - Since we didn't run `InferMutableRange` logic again, the LoadLocal /
* PropertyLoads still don't have a mutable range
*
* Note that the this bug is an edge case. Compiler output is only invalid for:
* - function expressions with
* `enableTransitivelyFreezeFunctionExpressions:false`
* - functions that throw and get retried without clearing the memocache
*
* Found differences in evaluator results
* Non-forget (expected):
* (kind: ok)
* <div>{"cb":{"kind":"Function","result":10},"shouldInvokeFns":true}</div>
* <div>{"cb":{"kind":"Function","result":11},"shouldInvokeFns":true}</div>
* Forget:
* (kind: ok)
* <div>{"cb":{"kind":"Function","result":10},"shouldInvokeFns":true}</div>
* <div>{"cb":{"kind":"Function","result":10},"shouldInvokeFns":true}</div>
*/
function useFoo({a, b}: {a: number, b: number}) {
const x = [];
const y = {value: a};
arrayPush(x, y); // x and y co-mutate
const y_alias = y;
const cb = () => y_alias.value;
setPropertyByKey(x[0], 'value', b); // might overwrite y.value
return <Stringify cb={cb} shouldInvokeFns={true} />;
}
export const FIXTURE_ENTRYPOINT = {
fn: useFoo,
params: [{a: 2, b: 10}],
sequentialRenders: [
{a: 2, b: 10},
{a: 2, b: 11},
],
};
```
## Code
```javascript
import { c as _c } from "react/compiler-runtime";
import { arrayPush, setPropertyByKey, Stringify } from "shared-runtime";
function useFoo(t0) {
const $ = _c(5);
const { a, b } = t0;
let t1;
if ($[0] !== a || $[1] !== b) {
const x = [];
const y = { value: a };
arrayPush(x, y);
const y_alias = y;
let t2;
if ($[3] !== y_alias.value) {
t2 = () => y_alias.value;
$[3] = y_alias.value;
$[4] = t2;
} else {
t2 = $[4];
}
const cb = t2;
setPropertyByKey(x[0], "value", b);
t1 = <Stringify cb={cb} shouldInvokeFns={true} />;
$[0] = a;
$[1] = b;
$[2] = t1;
} else {
t1 = $[2];
}
return t1;
}
export const FIXTURE_ENTRYPOINT = {
fn: useFoo,
params: [{ a: 2, b: 10 }],
sequentialRenders: [
{ a: 2, b: 10 },
{ a: 2, b: 11 },
],
};
```

View File

@@ -1,53 +0,0 @@
// @flow @enableTransitivelyFreezeFunctionExpressions:false
import {arrayPush, setPropertyByKey, Stringify} from 'shared-runtime';
/**
* 1. `InferMutableRanges` derives the mutable range of identifiers and their
* aliases from `LoadLocal`, `PropertyLoad`, etc
* - After this pass, y's mutable range only extends to `arrayPush(x, y)`
* - We avoid assigning mutable ranges to loads after y's mutable range, as
* these are working with an immutable value. As a result, `LoadLocal y` and
* `PropertyLoad y` do not get mutable ranges
* 2. `InferReactiveScopeVariables` extends mutable ranges and creates scopes,
* as according to the 'co-mutation' of different values
* - Here, we infer that
* - `arrayPush(y, x)` might alias `x` and `y` to each other
* - `setPropertyKey(x, ...)` may mutate both `x` and `y`
* - This pass correctly extends the mutable range of `y`
* - Since we didn't run `InferMutableRange` logic again, the LoadLocal /
* PropertyLoads still don't have a mutable range
*
* Note that the this bug is an edge case. Compiler output is only invalid for:
* - function expressions with
* `enableTransitivelyFreezeFunctionExpressions:false`
* - functions that throw and get retried without clearing the memocache
*
* Found differences in evaluator results
* Non-forget (expected):
* (kind: ok)
* <div>{"cb":{"kind":"Function","result":10},"shouldInvokeFns":true}</div>
* <div>{"cb":{"kind":"Function","result":11},"shouldInvokeFns":true}</div>
* Forget:
* (kind: ok)
* <div>{"cb":{"kind":"Function","result":10},"shouldInvokeFns":true}</div>
* <div>{"cb":{"kind":"Function","result":10},"shouldInvokeFns":true}</div>
*/
function useFoo({a, b}: {a: number, b: number}) {
const x = [];
const y = {value: a};
arrayPush(x, y); // x and y co-mutate
const y_alias = y;
const cb = () => y_alias.value;
setPropertyByKey(x[0], 'value', b); // might overwrite y.value
return <Stringify cb={cb} shouldInvokeFns={true} />;
}
export const FIXTURE_ENTRYPOINT = {
fn: useFoo,
params: [{a: 2, b: 10}],
sequentialRenders: [
{a: 2, b: 10},
{a: 2, b: 11},
],
};

View File

@@ -0,0 +1,171 @@
## Input
```javascript
import {ValidateMemoization} from 'shared-runtime';
const Codes = {
en: {name: 'English'},
ja: {name: 'Japanese'},
ko: {name: 'Korean'},
zh: {name: 'Chinese'},
};
function Component(a) {
let keys;
if (a) {
keys = Object.keys(Codes);
} else {
return null;
}
const options = keys.map(code => {
const country = Codes[code];
return {
name: country.name,
code,
};
});
return (
<>
<ValidateMemoization inputs={[]} output={keys} onlyCheckCompiled={true} />
<ValidateMemoization
inputs={[]}
output={options}
onlyCheckCompiled={true}
/>
</>
);
}
export const FIXTURE_ENTRYPOINT = {
fn: Component,
params: [{a: false}],
sequentialRenders: [
{a: false},
{a: true},
{a: true},
{a: false},
{a: true},
{a: false},
],
};
```
## Code
```javascript
import { c as _c } from "react/compiler-runtime";
import { ValidateMemoization } from "shared-runtime";
const Codes = {
en: { name: "English" },
ja: { name: "Japanese" },
ko: { name: "Korean" },
zh: { name: "Chinese" },
};
function Component(a) {
const $ = _c(13);
let keys;
let t0;
let t1;
if ($[0] !== a) {
t1 = Symbol.for("react.early_return_sentinel");
bb0: {
if (a) {
keys = Object.keys(Codes);
} else {
t1 = null;
break bb0;
}
t0 = keys.map(_temp);
}
$[0] = a;
$[1] = t0;
$[2] = t1;
$[3] = keys;
} else {
t0 = $[1];
t1 = $[2];
keys = $[3];
}
if (t1 !== Symbol.for("react.early_return_sentinel")) {
return t1;
}
const options = t0;
let t2;
if ($[4] === Symbol.for("react.memo_cache_sentinel")) {
t2 = [];
$[4] = t2;
} else {
t2 = $[4];
}
let t3;
if ($[5] !== keys) {
t3 = (
<ValidateMemoization inputs={t2} output={keys} onlyCheckCompiled={true} />
);
$[5] = keys;
$[6] = t3;
} else {
t3 = $[6];
}
let t4;
if ($[7] === Symbol.for("react.memo_cache_sentinel")) {
t4 = [];
$[7] = t4;
} else {
t4 = $[7];
}
let t5;
if ($[8] !== options) {
t5 = (
<ValidateMemoization
inputs={t4}
output={options}
onlyCheckCompiled={true}
/>
);
$[8] = options;
$[9] = t5;
} else {
t5 = $[9];
}
let t6;
if ($[10] !== t3 || $[11] !== t5) {
t6 = (
<>
{t3}
{t5}
</>
);
$[10] = t3;
$[11] = t5;
$[12] = t6;
} else {
t6 = $[12];
}
return t6;
}
function _temp(code) {
const country = Codes[code];
return { name: country.name, code };
}
export const FIXTURE_ENTRYPOINT = {
fn: Component,
params: [{ a: false }],
sequentialRenders: [
{ a: false },
{ a: true },
{ a: true },
{ a: false },
{ a: true },
{ a: false },
],
};
```

View File

@@ -0,0 +1,47 @@
import {ValidateMemoization} from 'shared-runtime';
const Codes = {
en: {name: 'English'},
ja: {name: 'Japanese'},
ko: {name: 'Korean'},
zh: {name: 'Chinese'},
};
function Component(a) {
let keys;
if (a) {
keys = Object.keys(Codes);
} else {
return null;
}
const options = keys.map(code => {
const country = Codes[code];
return {
name: country.name,
code,
};
});
return (
<>
<ValidateMemoization inputs={[]} output={keys} onlyCheckCompiled={true} />
<ValidateMemoization
inputs={[]}
output={options}
onlyCheckCompiled={true}
/>
</>
);
}
export const FIXTURE_ENTRYPOINT = {
fn: Component,
params: [{a: false}],
sequentialRenders: [
{a: false},
{a: true},
{a: true},
{a: false},
{a: true},
{a: false},
],
};

View File

@@ -67,8 +67,7 @@ function Component(props) {
case "b": {
break bb1;
}
case "c": {
}
case "c":
default: {
x = 6;
}

View File

@@ -0,0 +1,83 @@
## Input
```javascript
function Component({a, b, c}) {
// This is an object version of array-access-assignment.js
// Meant to confirm that object expressions and PropertyStore/PropertyLoad with strings
// works equivalently to array expressions and property accesses with numeric indices
const x = {zero: a};
const y = {zero: null, one: b};
const z = {zero: {}, one: {}, two: {zero: c}};
x.zero = y.one;
z.zero.zero = x.zero;
return {zero: x, one: z};
}
export const FIXTURE_ENTRYPOINT = {
fn: Component,
params: [{a: 1, b: 20, c: 300}],
sequentialRenders: [
{a: 2, b: 20, c: 300},
{a: 3, b: 20, c: 300},
{a: 3, b: 21, c: 300},
{a: 3, b: 22, c: 300},
{a: 3, b: 22, c: 301},
],
};
```
## Code
```javascript
import { c as _c } from "react/compiler-runtime";
function Component(t0) {
const $ = _c(6);
const { a, b, c } = t0;
let t1;
if ($[0] !== a || $[1] !== b || $[2] !== c) {
const x = { zero: a };
let t2;
if ($[4] !== b) {
t2 = { zero: null, one: b };
$[4] = b;
$[5] = t2;
} else {
t2 = $[5];
}
const y = t2;
const z = { zero: {}, one: {}, two: { zero: c } };
x.zero = y.one;
z.zero.zero = x.zero;
t1 = { zero: x, one: z };
$[0] = a;
$[1] = b;
$[2] = c;
$[3] = t1;
} else {
t1 = $[3];
}
return t1;
}
export const FIXTURE_ENTRYPOINT = {
fn: Component,
params: [{ a: 1, b: 20, c: 300 }],
sequentialRenders: [
{ a: 2, b: 20, c: 300 },
{ a: 3, b: 20, c: 300 },
{ a: 3, b: 21, c: 300 },
{ a: 3, b: 22, c: 300 },
{ a: 3, b: 22, c: 301 },
],
};
```
### Eval output
(kind: ok) {"zero":{"zero":20},"one":{"zero":{"zero":20},"one":{},"two":{"zero":300}}}
{"zero":{"zero":20},"one":{"zero":{"zero":20},"one":{},"two":{"zero":300}}}
{"zero":{"zero":21},"one":{"zero":{"zero":21},"one":{},"two":{"zero":300}}}
{"zero":{"zero":22},"one":{"zero":{"zero":22},"one":{},"two":{"zero":300}}}
{"zero":{"zero":22},"one":{"zero":{"zero":22},"one":{},"two":{"zero":301}}}

View File

@@ -0,0 +1,23 @@
function Component({a, b, c}) {
// This is an object version of array-access-assignment.js
// Meant to confirm that object expressions and PropertyStore/PropertyLoad with strings
// works equivalently to array expressions and property accesses with numeric indices
const x = {zero: a};
const y = {zero: null, one: b};
const z = {zero: {}, one: {}, two: {zero: c}};
x.zero = y.one;
z.zero.zero = x.zero;
return {zero: x, one: z};
}
export const FIXTURE_ENTRYPOINT = {
fn: Component,
params: [{a: 1, b: 20, c: 300}],
sequentialRenders: [
{a: 2, b: 20, c: 300},
{a: 3, b: 20, c: 300},
{a: 3, b: 21, c: 300},
{a: 3, b: 22, c: 300},
{a: 3, b: 22, c: 301},
],
};

View File

@@ -0,0 +1,89 @@
## Input
```javascript
// @validateNoFreezingKnownMutableFunctions
import {useCallback, useEffect, useRef} from 'react';
import {useHook} from 'shared-runtime';
function Component() {
const params = useHook();
const update = useCallback(
partialParams => {
const nextParams = {
...params,
...partialParams,
};
// Due to how we previously represented ObjectExpressions in InferReferenceEffects,
// this was recorded as a mutation of a context value (`params`) which then made
// the function appear ineligible for freezing when passing to useEffect below.
nextParams.param = 'value';
console.log(nextParams);
},
[params]
);
const ref = useRef(null);
useEffect(() => {
if (ref.current === null) {
update();
}
}, [update]);
return 'ok';
}
```
## Code
```javascript
import { c as _c } from "react/compiler-runtime"; // @validateNoFreezingKnownMutableFunctions
import { useCallback, useEffect, useRef } from "react";
import { useHook } from "shared-runtime";
function Component() {
const $ = _c(5);
const params = useHook();
let t0;
if ($[0] !== params) {
t0 = (partialParams) => {
const nextParams = { ...params, ...partialParams };
nextParams.param = "value";
console.log(nextParams);
};
$[0] = params;
$[1] = t0;
} else {
t0 = $[1];
}
const update = t0;
const ref = useRef(null);
let t1;
let t2;
if ($[2] !== update) {
t1 = () => {
if (ref.current === null) {
update();
}
};
t2 = [update];
$[2] = update;
$[3] = t1;
$[4] = t2;
} else {
t1 = $[3];
t2 = $[4];
}
useEffect(t1, t2);
return "ok";
}
```
### Eval output
(kind: exception) Fixture not implemented

View File

@@ -0,0 +1,30 @@
// @validateNoFreezingKnownMutableFunctions
import {useCallback, useEffect, useRef} from 'react';
import {useHook} from 'shared-runtime';
function Component() {
const params = useHook();
const update = useCallback(
partialParams => {
const nextParams = {
...params,
...partialParams,
};
// Due to how we previously represented ObjectExpressions in InferReferenceEffects,
// this was recorded as a mutation of a context value (`params`) which then made
// the function appear ineligible for freezing when passing to useEffect below.
nextParams.param = 'value';
console.log(nextParams);
},
[params]
);
const ref = useRef(null);
useEffect(() => {
if (ref.current === null) {
update();
}
}, [update]);
return 'ok';
}

View File

@@ -50,10 +50,8 @@ function Component(props) {
case 1: {
break bb0;
}
case 2: {
}
default: {
}
case 2:
default:
}
} else {
if (props.cond2) {

View File

@@ -41,8 +41,7 @@ function foo() {
case 2: {
break bb0;
}
default: {
}
default:
}
}

View File

@@ -43,22 +43,17 @@ export const FIXTURE_ENTRYPOINT = {
```javascript
function foo(x) {
bb0: switch (x) {
case 0: {
}
case 1: {
}
case 0:
case 1:
case 2: {
break bb0;
}
case 3: {
break bb0;
}
case 4: {
}
case 5: {
}
default: {
}
case 4:
case 5:
default:
}
}

View File

@@ -453,8 +453,6 @@ const skipFilter = new Set([
'inner-function/nullable-objects/bug-invalid-array-map-manual',
'bug-object-expression-computed-key-modified-during-after-construction-hoisted-sequence-expr',
`bug-capturing-func-maybealias-captured-mutate`,
'bug-aliased-capture-aliased-mutate',
'bug-aliased-capture-mutate',
'bug-functiondecl-hoisting',
'bug-type-inference-control-flow',
'fbt/bug-fbt-plural-multiple-function-calls',
@@ -485,6 +483,7 @@ const skipFilter = new Set([
'todo.lower-context-access-array-destructuring',
'lower-context-selector-simple',
'lower-context-acess-multiple',
'bug-separate-memoization-due-to-callback-capturing',
]);
export default skipFilter;