Compare commits

...

77 Commits

Author SHA1 Message Date
Joe Savona
c2c10efd13 Update base for Update on "[compiler] Prep for making new/call/etc use Apply effects"
[ghstack-poisoned]
2025-06-09 11:11:03 -07:00
Joe Savona
38e929cb0c Update base for Update on "[compiler] Prep for making new/call/etc use Apply effects"
[ghstack-poisoned]
2025-06-06 18:05:29 -07:00
Joe Savona
1f023ebbbe Update base for Update on "[compiler] Prep for making new/call/etc use Apply effects"
[ghstack-poisoned]
2025-06-06 12:57:56 -07:00
Joe Savona
4922869278 Update base for Update on "[compiler] Prep for making new/call/etc use Apply effects"
[ghstack-poisoned]
2025-06-06 12:42:05 -07:00
Joe Savona
421571966c Update base for Update on "[compiler] Prep for making new/call/etc use Apply effects"
[ghstack-poisoned]
2025-06-06 09:56:40 -07:00
Joe Savona
9e5ce15dfe Update base for Update on "[compiler] Prep for making new/call/etc use Apply effects"
[ghstack-poisoned]
2025-06-05 22:13:12 -07:00
Joe Savona
a76977f644 Update base for Update on "[compiler] Prep for making new/call/etc use Apply effects"
[ghstack-poisoned]
2025-06-05 14:16:52 -07:00
Joe Savona
4ae090860c Update base for Update on "[compiler] Prep for making new/call/etc use Apply effects"
[ghstack-poisoned]
2025-06-05 14:00:23 -07:00
Joe Savona
354d6d88de Update base for Update on "[compiler] Prep for making new/call/etc use Apply effects"
[ghstack-poisoned]
2025-06-04 17:00:45 -07:00
Joe Savona
93755d3075 Update base for Update on "[compiler] Prep for making new/call/etc use Apply effects"
[ghstack-poisoned]
2025-06-04 16:47:57 -07:00
Joe Savona
7482763995 Update base for Update on "[compiler] Prep for making new/call/etc use Apply effects"
[ghstack-poisoned]
2025-06-03 17:00:34 -07:00
Joe Savona
6b561f679d Update base for Update on "[compiler] Prep for making new/call/etc use Apply effects"
[ghstack-poisoned]
2025-06-03 11:33:38 -07:00
Joe Savona
852dec879f Update base for Update on "[compiler] Prep for making new/call/etc use Apply effects"
[ghstack-poisoned]
2025-06-03 09:01:13 -07:00
Joe Savona
c98c55bf34 Update base for Update on "[compiler] Prep for making new/call/etc use Apply effects"
[ghstack-poisoned]
2025-06-01 17:19:37 -07:00
Joe Savona
333fd8d49d Update base for Update on "[compiler] Prep for making new/call/etc use Apply effects"
[ghstack-poisoned]
2025-05-30 16:29:48 -07:00
Joe Savona
35ae2fa72d Update base for Update on "[compiler] Prep for making new/call/etc use Apply effects"
[ghstack-poisoned]
2025-05-30 15:57:09 -07:00
Joe Savona
c28dc88c0c Update base for Update on "[compiler] Prep for making new/call/etc use Apply effects"
[ghstack-poisoned]
2025-05-30 11:33:37 -07:00
Joe Savona
ac2340e479 [compiler] Bailout on mutations of frozen/global values
[ghstack-poisoned]
2025-05-29 17:30:37 -07:00
Joe Savona
eb8d0c27b0 [compiler] comments and todos
[ghstack-poisoned]
2025-05-29 17:30:33 -07:00
Joe Savona
bc01ae2157 Update on "[compiler] Translate legacy FunctionSignature into new AliasingEffects"
To help bootstrap the new inference model, this PR adds a helper that takes a legacy FunctionSignature and converts into a list of (new) AliasingEffects. This conversion tries to make explicit all the implicit handling of InferReferenceEffects and previous FunctionSignature.

For example, the signature for Array.proto.pop has a calleeEffect of `Store`. Nowhere does it say that the receiver flows into the result! There's an implicit behavior that the receiver flows into the result. The new function makes this explicit by emitting a `Capture receiver -> lvalue` effect.

So far I confirmed that this works for Array.proto.push() if i hard code the inference to ignore new-style aliasing signatures. I'll continue to refine it going forward as I start running the new inference on more fixtures.

[ghstack-poisoned]
2025-05-29 17:30:31 -07:00
Joe Savona
3def726cac Update base for Update on "[compiler] Translate legacy FunctionSignature into new AliasingEffects"
To help bootstrap the new inference model, this PR adds a helper that takes a legacy FunctionSignature and converts into a list of (new) AliasingEffects. This conversion tries to make explicit all the implicit handling of InferReferenceEffects and previous FunctionSignature.

For example, the signature for Array.proto.pop has a calleeEffect of `Store`. Nowhere does it say that the receiver flows into the result! There's an implicit behavior that the receiver flows into the result. The new function makes this explicit by emitting a `Capture receiver -> lvalue` effect.

So far I confirmed that this works for Array.proto.push() if i hard code the inference to ignore new-style aliasing signatures. I'll continue to refine it going forward as I start running the new inference on more fixtures.

[ghstack-poisoned]
2025-05-29 17:30:30 -07:00
Joe Savona
fd162bdd9d [compiler] Translate legacy FunctionSignature into new AliasingEffects
To help bootstrap the new inference model, this PR adds a helper that takes a legacy FunctionSignature and converts into a list of (new) AliasingEffects. This conversion tries to make explicit all the implicit handling of InferReferenceEffects and previous FunctionSignature.

For example, the signature for Array.proto.pop has a calleeEffect of `Store`. Nowhere does it say that the receiver flows into the result! There's an implicit behavior that the receiver flows into the result. The new function makes this explicit by emitting a `Capture receiver -> lvalue` effect.

So far I confirmed that this works for Array.proto.push() if i hard code the inference to ignore new-style aliasing signatures. I'll continue to refine it going forward as I start running the new inference on more fixtures.

[ghstack-poisoned]
2025-05-28 22:27:22 -07:00
Joe Savona
0d6822a1da Update on "[compiler] First example of an aliasing signature (array push)"
Adds an aliasing signature for Array.prototype.push and fixes up the logic for consuming these signatures during effect inference. As the test fixture shows, we correctly model the capturing. Mutable values that are captured into the array count as co-mutated if the array itself is transitively mutated later, while mutable values captured after such transitive mutation are not considered co-mutated.

The implementation is based on the fact that the final `push` call is only locally mutating the array. During the phase that looks at local mutations we are only tracking direct aliases, and the push doesn't directly alias the items to the array, it only captures them.

[ghstack-poisoned]
2025-05-28 22:27:20 -07:00
Joe Savona
93e24f467e Update base for Update on "[compiler] First example of an aliasing signature (array push)"
Adds an aliasing signature for Array.prototype.push and fixes up the logic for consuming these signatures during effect inference. As the test fixture shows, we correctly model the capturing. Mutable values that are captured into the array count as co-mutated if the array itself is transitively mutated later, while mutable values captured after such transitive mutation are not considered co-mutated.

The implementation is based on the fact that the final `push` call is only locally mutating the array. During the phase that looks at local mutations we are only tracking direct aliases, and the push doesn't directly alias the items to the array, it only captures them.

[ghstack-poisoned]
2025-05-28 22:27:20 -07:00
Joe Savona
c207ff562a [compiler] First example of an aliasing signature (array push)
Adds an aliasing signature for Array.prototype.push and fixes up the logic for consuming these signatures during effect inference. As the test fixture shows, we correctly model the capturing. Mutable values that are captured into the array count as co-mutated if the array itself is transitively mutated later, while mutable values captured after such transitive mutation are not considered co-mutated.

The implementation is based on the fact that the final `push` call is only locally mutating the array. During the phase that looks at local mutations we are only tracking direct aliases, and the push doesn't directly alias the items to the array, it only captures them.

[ghstack-poisoned]
2025-05-28 17:38:49 -07:00
Joe Savona
cdd2d3f2f9 Update on "[compiler] Delay mutation of function expr context variables until function is called"
See comments in code. The idea is that rather than immediately processing function expression effects when declaring the function, we record Capture effects for context variables that may be captured/mutated in the function. Then, transitive mutations of the function value itself will extend the range of these values via the normal captured value comutation inference established earlier in the stack (if capture a -> b, then transitiveMutate(b) => mutate(a)). So capture contextVar -> function and transitiveMutate(function) => mutate(contextVar).

[ghstack-poisoned]
2025-05-28 17:38:47 -07:00
Joe Savona
51d5aa3f33 Update base for Update on "[compiler] Delay mutation of function expr context variables until function is called"
See comments in code. The idea is that rather than immediately processing function expression effects when declaring the function, we record Capture effects for context variables that may be captured/mutated in the function. Then, transitive mutations of the function value itself will extend the range of these values via the normal captured value comutation inference established earlier in the stack (if capture a -> b, then transitiveMutate(b) => mutate(a)). So capture contextVar -> function and transitiveMutate(function) => mutate(contextVar).

[ghstack-poisoned]
2025-05-28 17:38:46 -07:00
Joe Savona
8c70807cec [compiler] Delay mutation of function expr context variables until function is called
See comments in code. The idea is that rather than immediately processing function expression effects when declaring the function, we record Capture effects for context variables that may be captured/mutated in the function. Then, transitive mutations of the function value itself will extend the range of these values via the normal captured value comutation inference established earlier in the stack (if capture a -> b, then transitiveMutate(b) => mutate(a)). So capture contextVar -> function and transitiveMutate(function) => mutate(contextVar).

[ghstack-poisoned]
2025-05-28 16:29:32 -07:00
Joe Savona
a219df1c98 Update on "[compiler] Add ImmutableCapture effect, CreateFrom no longer needs Capture"
ImmutableCapture allows us to record information flow for escape analysis purposes, without impacting mutable range analysis. All of Alias, Capture, and CreateFrom now downgrade to ImmutableCapture if the `from` value is frozen. Globals and primitives are conceptually copy types and don't record any information flow at all. I guess we could add a `Copy` effect if we wanted but i haven't seen a need for it yet.

Related, previously CreateFrom was always paired with Capture when constructing effects. But I had also coded up the inference to sort of treat them the same. So this diff makes that official, and means CreateFrom is a valid third way to represent data flow and not just creation. When applied, CreateFrom will turn into: ImmutableCapture for frozen values, Capture for mutable values, and disappear for globals/primitives.

A final tweak is to change range inference to ensure that range.start is non-zero if the value is mutated.

[ghstack-poisoned]
2025-05-28 16:29:30 -07:00
Joe Savona
1a733b8946 Update base for Update on "[compiler] Add ImmutableCapture effect, CreateFrom no longer needs Capture"
ImmutableCapture allows us to record information flow for escape analysis purposes, without impacting mutable range analysis. All of Alias, Capture, and CreateFrom now downgrade to ImmutableCapture if the `from` value is frozen. Globals and primitives are conceptually copy types and don't record any information flow at all. I guess we could add a `Copy` effect if we wanted but i haven't seen a need for it yet.

Related, previously CreateFrom was always paired with Capture when constructing effects. But I had also coded up the inference to sort of treat them the same. So this diff makes that official, and means CreateFrom is a valid third way to represent data flow and not just creation. When applied, CreateFrom will turn into: ImmutableCapture for frozen values, Capture for mutable values, and disappear for globals/primitives.

A final tweak is to change range inference to ensure that range.start is non-zero if the value is mutated.

[ghstack-poisoned]
2025-05-28 16:29:30 -07:00
Joe Savona
74c236a99d [compiler] Add ImmutableCapture effect, CreateFrom no longer needs Capture
ImmutableCapture allows us to record information flow for escape analysis purposes, without impacting mutable range analysis. All of Alias, Capture, and CreateFrom now downgrade to ImmutableCapture if the `from` value is frozen. Globals and primitives are conceptually copy types and don't record any information flow at all. I guess we could add a `Copy` effect if we wanted but i haven't seen a need for it yet.

Related, previously CreateFrom was always paired with Capture when constructing effects. But I had also coded up the inference to sort of treat them the same. So this diff makes that official, and means CreateFrom is a valid third way to represent data flow and not just creation. When applied, CreateFrom will turn into: ImmutableCapture for frozen values, Capture for mutable values, and disappear for globals/primitives.

A final tweak is to change range inference to ensure that range.start is non-zero if the value is mutated.

[ghstack-poisoned]
2025-05-28 15:42:38 -07:00
Joe Savona
f9bb6067ab Update on "[compiler] Improve inference of function expression mutation/aliasing effects"
Distinguish the various forms of mutation, and do basic replaying of these effects when the function is created (imperfect, temporary).

[ghstack-poisoned]
2025-05-28 15:42:36 -07:00
Joe Savona
94409c39f3 Update base for Update on "[compiler] Improve inference of function expression mutation/aliasing effects"
Distinguish the various forms of mutation, and do basic replaying of these effects when the function is created (imperfect, temporary).

[ghstack-poisoned]
2025-05-28 15:42:35 -07:00
Joe Savona
e8d39119b1 [compiler] Improve inference of function expression mutation/aliasing effects
Distinguish the various forms of mutation, and do basic replaying of these effects when the function is created (imperfect, temporary).

[ghstack-poisoned]
2025-05-27 16:31:02 -07:00
Joe Savona
ff14c671fd Update on "[compiler] Alternate pipeline for new mutability model"
This PR gets a first fixture working end-to-end with the new mutability and aliasing model. Key changes:

* Add a feature flag to enable the model. When enabled we no longer call InferReferenceEffects or InferMutableRanges, and instead use the new equivalents.
* Adds a pass that infers Place-specific effects based on mutable ranges and instruction effects. This is necessary to satisfy existing code that requires operand effects to be populated.
* Adds a pass that infers the outwardly-visible capturing/aliasing behavior of a function expression. The idea is that this can bubble up and be used in conjunction with the `Apply` effect to get precise inference of things like `array.map(() => { ... })`.

[ghstack-poisoned]
2025-05-27 16:31:00 -07:00
Joe Savona
ca0f0e006d Update base for Update on "[compiler] Alternate pipeline for new mutability model"
This PR gets a first fixture working end-to-end with the new mutability and aliasing model. Key changes:

* Add a feature flag to enable the model. When enabled we no longer call InferReferenceEffects or InferMutableRanges, and instead use the new equivalents.
* Adds a pass that infers Place-specific effects based on mutable ranges and instruction effects. This is necessary to satisfy existing code that requires operand effects to be populated.
* Adds a pass that infers the outwardly-visible capturing/aliasing behavior of a function expression. The idea is that this can bubble up and be used in conjunction with the `Apply` effect to get precise inference of things like `array.map(() => { ... })`.

[ghstack-poisoned]
2025-05-27 16:30:59 -07:00
Joe Savona
05a8b6a717 [compiler] Alternate pipeline for new mutability model
This PR gets a first fixture working end-to-end with the new mutability and aliasing model. Key changes:

* Add a feature flag to enable the model. When enabled we no longer call InferReferenceEffects or InferMutableRanges, and instead use the new equivalents.
* Adds a pass that infers Place-specific effects based on mutable ranges and instruction effects. This is necessary to satisfy existing code that requires operand effects to be populated.
* Adds a pass that infers the outwardly-visible capturing/aliasing behavior of a function expression. The idea is that this can bubble up and be used in conjunction with the `Apply` effect to get precise inference of things like `array.map(() => { ... })`.

[ghstack-poisoned]
2025-05-27 14:23:38 -07:00
Joe Savona
58e449ee0e [compiler] Add HIRFunction.returns: Place
[ghstack-poisoned]
2025-05-27 14:23:33 -07:00
Joe Savona
61bd21cd18 Update on "[compiler] Foundation of new mutability and aliasing (alternate take)"
Alternate take at a new mutability and alising model, aiming to replace `InferReferenceEffects` and `InferMutableRanges`. My initial passes at this were more complicated than necessary, and I've iterated to refine and distill this down to the core concepts. There are two effects that track information flow: `capture` and `alias`:

* Given distinct values A and B. After capture A -> B, mutate(B) does *not* modify A. This more precisely captures the semantic of the previous `Store` effect. As an example, `array.push(item)` has the effect `capture item -> array` and `mutate(array)`. The array is modified, not the item.
* Given distinct values A and B. After alias A -> B, mutate(B) *does* modify A. This is because B now refers to the same value as A.
* Given distinct values A and B, after *either* capture A -> B *or* alias A -> B, transitiveMutate(B) counts as a mutation of A. Transitive mutation is the default, and places that previously used `Store` will use non-transitive mutate effects instead.

Conceptually "capture A -> B" means that a reference to A was "captured" (or stored) within A, but there is not a directly aliasing. Whereas "alias A -> B" means literal value aliasing.

The idea is that our previous sequential fixpoint loops in InferMutableRanges can instead work by first looking at transitive mutations, then look at non-transitive mutations. And aliasing groups can be built purely based on the `alias` effect.

Lots more to do here but the structure is coming together.

[ghstack-poisoned]
2025-05-27 14:23:31 -07:00
Joe Savona
3e67c7e39f Update base for Update on "[compiler] Foundation of new mutability and aliasing (alternate take)"
Alternate take at a new mutability and alising model, aiming to replace `InferReferenceEffects` and `InferMutableRanges`. My initial passes at this were more complicated than necessary, and I've iterated to refine and distill this down to the core concepts. There are two effects that track information flow: `capture` and `alias`:

* Given distinct values A and B. After capture A -> B, mutate(B) does *not* modify A. This more precisely captures the semantic of the previous `Store` effect. As an example, `array.push(item)` has the effect `capture item -> array` and `mutate(array)`. The array is modified, not the item.
* Given distinct values A and B. After alias A -> B, mutate(B) *does* modify A. This is because B now refers to the same value as A.
* Given distinct values A and B, after *either* capture A -> B *or* alias A -> B, transitiveMutate(B) counts as a mutation of A. Transitive mutation is the default, and places that previously used `Store` will use non-transitive mutate effects instead.

Conceptually "capture A -> B" means that a reference to A was "captured" (or stored) within A, but there is not a directly aliasing. Whereas "alias A -> B" means literal value aliasing.

The idea is that our previous sequential fixpoint loops in InferMutableRanges can instead work by first looking at transitive mutations, then look at non-transitive mutations. And aliasing groups can be built purely based on the `alias` effect.

Lots more to do here but the structure is coming together.

[ghstack-poisoned]
2025-05-27 14:23:31 -07:00
Joe Savona
6cc26518f3 Update on "[compiler] Foundation of new mutability and aliasing (alternate take)"
Alternate take at a new mutability and alising model, aiming to replace `InferReferenceEffects` and `InferMutableRanges`. My initial passes at this were more complicated than necessary, and I've iterated to refine and distill this down to the core concepts. There are two effects that track information flow: `capture` and `alias`:

* Given distinct values A and B. After capture A -> B, mutate(B) does *not* modify A. This more precisely captures the semantic of the previous `Store` effect. As an example, `array.push(item)` has the effect `capture item -> array` and `mutate(array)`. The array is modified, not the item.
* Given distinct values A and B. After alias A -> B, mutate(B) *does* modify A. This is because B now refers to the same value as A.
* Given distinct values A and B, after *either* capture A -> B *or* alias A -> B, transitiveMutate(B) counts as a mutation of A. Transitive mutation is the default, and places that previously used `Store` will use non-transitive mutate effects instead.

Conceptually "capture A -> B" means that a reference to A was "captured" (or stored) within A, but there is not a directly aliasing. Whereas "alias A -> B" means literal value aliasing.

The idea is that our previous sequential fixpoint loops in InferMutableRanges can instead work by first looking at transitive mutations, then look at non-transitive mutations. And aliasing groups can be built purely based on the `alias` effect.

Lots more to do here but the structure is coming together.

[ghstack-poisoned]
2025-05-23 16:58:18 -07:00
Joe Savona
79a13a29c5 Update base for Update on "[compiler] Foundation of new mutability and aliasing (alternate take)"
Alternate take at a new mutability and alising model, aiming to replace `InferReferenceEffects` and `InferMutableRanges`. My initial passes at this were more complicated than necessary, and I've iterated to refine and distill this down to the core concepts. There are two effects that track information flow: `capture` and `alias`:

* Given distinct values A and B. After capture A -> B, mutate(B) does *not* modify A. This more precisely captures the semantic of the previous `Store` effect. As an example, `array.push(item)` has the effect `capture item -> array` and `mutate(array)`. The array is modified, not the item.
* Given distinct values A and B. After alias A -> B, mutate(B) *does* modify A. This is because B now refers to the same value as A.
* Given distinct values A and B, after *either* capture A -> B *or* alias A -> B, transitiveMutate(B) counts as a mutation of A. Transitive mutation is the default, and places that previously used `Store` will use non-transitive mutate effects instead.

Conceptually "capture A -> B" means that a reference to A was "captured" (or stored) within A, but there is not a directly aliasing. Whereas "alias A -> B" means literal value aliasing.

The idea is that our previous sequential fixpoint loops in InferMutableRanges can instead work by first looking at transitive mutations, then look at non-transitive mutations. And aliasing groups can be built purely based on the `alias` effect.

Lots more to do here but the structure is coming together.

[ghstack-poisoned]
2025-05-23 16:58:17 -07:00
Joe Savona
80ae4e96ae [compiler] Foundation of new mutability and aliasing (alternate take)
Alternate take at a new mutability and alising model, aiming to replace `InferReferenceEffects` and `InferMutableRanges`. My initial passes at this were more complicated than necessary, and I've iterated to refine and distill this down to the core concepts. There are two effects that track information flow: `capture` and `alias`:

* Given distinct values A and B. After capture A -> B, mutate(B) does *not* modify A. This more precisely captures the semantic of the previous `Store` effect. As an example, `array.push(item)` has the effect `capture item -> array` and `mutate(array)`. The array is modified, not the item.
* Given distinct values A and B. After alias A -> B, mutate(B) *does* modify A. This is because B now refers to the same value as A.
* Given distinct values A and B, after *either* capture A -> B *or* alias A -> B, transitiveMutate(B) counts as a mutation of A.

Conceptually "capture A -> B" means that a reference to A was "captured" (or stored) within A, but there is not a directly aliasing. Whereas "alias A -> B" means literal value aliasing.

The idea is that our previous sequential fixpoint loops in InferMutableRanges can instead work by first looking at transitive mutations, then look at non-transitive mutations. And aliasing groups can be built purely based on the `alias` effect.

Lots more to do here but the structure is coming together.

[ghstack-poisoned]
2025-05-22 16:57:29 -07:00
Joe Savona
f5cd556ffa 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-22 16:57:27 -07:00
Joe Savona
0c7e645dd3 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-22 16:57:26 -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
60 changed files with 3907 additions and 431 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

@@ -104,6 +104,8 @@ import {validateNoImpureFunctionsInRender} from '../Validation/ValidateNoImpureF
import {CompilerError} from '..';
import {validateStaticComponents} from '../Validation/ValidateStaticComponents';
import {validateNoFreezingKnownMutableFunctions} from '../Validation/ValidateNoFreezingKnownMutableFunctions';
import {inferMutationAliasingEffects} from '../Inference/InferMutationAliasingEffects';
import {inferMutationAliasingRanges} from '../Inference/InferMutationAliasingRanges';
export type CompilerPipelineValue =
| {kind: 'ast'; name: string; value: CodegenFunction}
@@ -226,15 +228,27 @@ function runWithEnvironment(
analyseFunctions(hir);
log({kind: 'hir', name: 'AnalyseFunctions', value: hir});
const fnEffectErrors = inferReferenceEffects(hir);
if (env.isInferredMemoEnabled) {
if (fnEffectErrors.length > 0) {
CompilerError.throw(fnEffectErrors[0]);
if (!env.config.enableNewMutationAliasingModel) {
const fnEffectErrors = inferReferenceEffects(hir);
if (env.isInferredMemoEnabled) {
if (fnEffectErrors.length > 0) {
CompilerError.throw(fnEffectErrors[0]);
}
}
log({kind: 'hir', name: 'InferReferenceEffects', value: hir});
} else {
const mutabilityAliasingErrors = inferMutationAliasingEffects(hir);
log({kind: 'hir', name: 'InferMutationAliasingEffects', value: hir});
if (env.isInferredMemoEnabled) {
if (mutabilityAliasingErrors.isErr()) {
throw mutabilityAliasingErrors.unwrapErr();
}
}
}
log({kind: 'hir', name: 'InferReferenceEffects', value: hir});
validateLocalsNotReassignedAfterRender(hir);
if (!env.config.enableNewMutationAliasingModel) {
validateLocalsNotReassignedAfterRender(hir);
}
// Note: Has to come after infer reference effects because "dead" code may still affect inference
deadCodeElimination(hir);
@@ -248,8 +262,13 @@ function runWithEnvironment(
pruneMaybeThrows(hir);
log({kind: 'hir', name: 'PruneMaybeThrows', value: hir});
inferMutableRanges(hir);
log({kind: 'hir', name: 'InferMutableRanges', value: hir});
if (!env.config.enableNewMutationAliasingModel) {
inferMutableRanges(hir);
log({kind: 'hir', name: 'InferMutableRanges', value: hir});
} else {
inferMutationAliasingRanges(hir);
log({kind: 'hir', name: 'InferMutationAliasingRanges', value: hir});
}
if (env.isInferredMemoEnabled) {
if (env.config.assertValidMutableRanges) {

View File

@@ -47,7 +47,7 @@ import {
makeType,
promoteTemporary,
} from './HIR';
import HIRBuilder, {Bindings} from './HIRBuilder';
import HIRBuilder, {Bindings, createTemporaryPlace} from './HIRBuilder';
import {BuiltInArrayId} from './ObjectShape';
/*
@@ -218,6 +218,7 @@ export function lower(
fnType: parent == null ? env.fnType : 'Other',
returnTypeAnnotation: null, // TODO: extract the actual return type node if present
returnType: makeType(),
returns: createTemporaryPlace(env, func.node.loc ?? GeneratedSource),
body: builder.build(),
context,
generator: func.node.generator === true,
@@ -225,6 +226,7 @@ export function lower(
loc: func.node.loc ?? GeneratedSource,
env,
effects: null,
aliasingEffects: null,
directives,
});
}
@@ -1235,6 +1237,7 @@ function lowerStatement(
kind: 'Debugger',
loc,
},
effects: null,
loc,
});
return;
@@ -1892,6 +1895,7 @@ function lowerExpression(
place: leftValue,
loc: exprLoc,
},
effects: null,
loc: exprLoc,
});
builder.terminateWithContinuation(
@@ -2827,6 +2831,7 @@ function lowerOptionalCallExpression(
args,
loc,
},
effects: null,
loc,
});
} else {
@@ -2840,6 +2845,7 @@ function lowerOptionalCallExpression(
args,
loc,
},
effects: null,
loc,
});
}
@@ -3466,9 +3472,10 @@ function lowerValueToTemporary(
const place: Place = buildTemporaryPlace(builder, value.loc);
builder.push({
id: makeInstructionId(0),
value: value,
loc: value.loc,
lvalue: {...place},
value: value,
effects: null,
loc: value.loc,
});
return place;
}

View File

@@ -243,6 +243,11 @@ export const EnvironmentConfigSchema = z.object({
*/
enableUseTypeAnnotations: z.boolean().default(false),
/**
* Enable a new model for mutability and aliasing inference
*/
enableNewMutationAliasingModel: z.boolean().default(false),
/**
* Enables inference of optional dependency chains. Without this flag
* a property chain such as `props?.items?.foo` will infer as a dep on

View File

@@ -13,6 +13,7 @@ import {Environment, ReactFunctionType} from './Environment';
import type {HookKind} from './ObjectShape';
import {Type, makeType} from './Types';
import {z} from 'zod';
import {AliasingEffect} from '../Inference/InferMutationAliasingEffects';
/*
* *******************************************************************************************
@@ -100,6 +101,7 @@ export type ReactiveInstruction = {
id: InstructionId;
lvalue: Place | null;
value: ReactiveValue;
effects?: Array<AliasingEffect> | null; // TODO make non-optional
loc: SourceLocation;
};
@@ -278,12 +280,14 @@ export type HIRFunction = {
params: Array<Place | SpreadPattern>;
returnTypeAnnotation: t.FlowType | t.TSType | null;
returnType: Type;
returns: Place;
context: Array<Place>;
effects: Array<FunctionEffect> | null;
body: HIR;
generator: boolean;
async: boolean;
directives: Array<string>;
aliasingEffects?: Array<AliasingEffect> | null;
};
export type FunctionEffect =
@@ -300,6 +304,10 @@ export type FunctionEffect =
places: ReadonlySet<Place>;
effect: Effect;
loc: SourceLocation;
}
| {
kind: 'CaptureEffect';
places: ReadonlySet<Place>;
};
/*
@@ -645,12 +653,18 @@ export type Instruction = {
lvalue: Place;
value: InstructionValue;
loc: SourceLocation;
effects: Array<AliasingEffect> | null;
};
export function todoPopulateAliasingEffects(): Array<AliasingEffect> | null {
return null;
}
export type TInstruction<T extends InstructionValue> = {
id: InstructionId;
lvalue: Place;
value: T;
effects: Array<AliasingEffect> | null;
loc: SourceLocation;
};

View File

@@ -12,6 +12,7 @@ import {
GeneratedSource,
HIRFunction,
Instruction,
Place,
} from './HIR';
import {markPredecessors} from './HIRBuilder';
import {terminalFallthrough, terminalHasFallthrough} from './visitors';
@@ -80,20 +81,22 @@ export function mergeConsecutiveBlocks(fn: HIRFunction): void {
suggestions: null,
});
const operand = Array.from(phi.operands.values())[0]!;
const lvalue: Place = {
kind: 'Identifier',
identifier: phi.place.identifier,
effect: Effect.ConditionallyMutate,
reactive: false,
loc: GeneratedSource,
};
const instr: Instruction = {
id: predecessor.terminal.id,
lvalue: {
kind: 'Identifier',
identifier: phi.place.identifier,
effect: Effect.ConditionallyMutate,
reactive: false,
loc: GeneratedSource,
},
lvalue: {...lvalue},
value: {
kind: 'LoadLocal',
place: {...operand},
loc: GeneratedSource,
},
effects: [{kind: 'Alias', from: {...operand}, into: {...lvalue}}],
loc: GeneratedSource,
};
predecessor.instructions.push(instr);

View File

@@ -6,10 +6,21 @@
*/
import {CompilerError} from '../CompilerError';
import {Effect, ValueKind, ValueReason} from './HIR';
import {AliasingSignature} from '../Inference/InferMutationAliasingEffects';
import {
Effect,
GeneratedSource,
makeDeclarationId,
makeIdentifierId,
makeInstructionId,
Place,
ValueKind,
ValueReason,
} from './HIR';
import {
BuiltInType,
FunctionType,
makeType,
ObjectType,
PolyType,
PrimitiveType,
@@ -179,6 +190,8 @@ export type FunctionSignature = {
impure?: boolean;
canonicalName?: string;
aliasing?: AliasingSignature | null;
};
/*
@@ -302,6 +315,28 @@ addObject(BUILTIN_SHAPES, BuiltInArrayId, [
returnType: PRIMITIVE_TYPE,
calleeEffect: Effect.Store,
returnValueKind: ValueKind.Primitive,
aliasing: {
receiver: makeIdentifierId(0),
params: [],
rest: makeIdentifierId(1),
returns: makeIdentifierId(2),
effects: [
// Push directly mutates the array itself
{kind: 'Mutate', value: signatureArgument(0)},
// The arguments are captured into the array
{
kind: 'Capture',
from: signatureArgument(1),
into: signatureArgument(0),
},
// Returns the new length, a primitive
{
kind: 'Create',
into: signatureArgument(2),
value: ValueKind.Primitive,
},
],
},
}),
],
[
@@ -332,6 +367,7 @@ addObject(BUILTIN_SHAPES, BuiltInArrayId, [
returnValueKind: ValueKind.Mutable,
noAlias: true,
mutableOnlyIfOperandsAreMutable: true,
aliasing: null,
}),
],
[
@@ -1169,3 +1205,22 @@ export const DefaultNonmutatingHook = addHook(
},
'DefaultNonmutatingHook',
);
export function signatureArgument(id: number): Place {
const place: Place = {
kind: 'Identifier',
effect: Effect.Unknown,
loc: GeneratedSource,
reactive: false,
identifier: {
declarationId: makeDeclarationId(id),
id: makeIdentifierId(id),
loc: GeneratedSource,
mutableRange: {start: makeInstructionId(0), end: makeInstructionId(0)},
name: null,
scope: null,
type: makeType(),
},
};
return place;
}

View File

@@ -35,6 +35,11 @@ import type {
Type,
} from './HIR';
import {GotoVariant, InstructionKind} from './HIR';
import {
AliasedPlace,
AliasingEffect,
AliasingSignature,
} from '../Inference/InferMutationAliasingEffects';
export type Options = {
indent: number;
@@ -67,13 +72,15 @@ export function printFunction(fn: HIRFunction): string {
})
.join(', ') +
')';
} else {
definition += '()';
}
if (definition.length !== 0) {
output.push(definition);
}
output.push(printType(fn.returnType));
output.push(printHIR(fn.body));
output.push(`: ${printType(fn.returnType)} @ ${printPlace(fn.returns)}`);
output.push(...fn.directives);
output.push(printHIR(fn.body));
return output.join('\n');
}
@@ -151,7 +158,10 @@ export function printMixedHIR(
export function printInstruction(instr: ReactiveInstruction): string {
const id = `[${instr.id}]`;
const value = printInstructionValue(instr.value);
let value = printInstructionValue(instr.value);
if (instr.effects != null) {
value += `\n ${instr.effects.map(printAliasingEffect).join('\n ')}`;
}
if (instr.lvalue !== null) {
return `${id} ${printPlace(instr.lvalue)} = ${value}`;
@@ -546,17 +556,31 @@ 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(', ') ?? '';
const type = printType(instrValue.loweredFunc.func.returnType).trim();
value = `${kind} ${name} @context[${context}] @effects[${effects}]${type !== '' ? ` return${type}` : ''}:\n${fn}`;
const aliasingEffects =
instrValue.loweredFunc.func.aliasingEffects
?.map(printAliasingEffect)
?.join(', ') ?? '';
value = `${kind} ${name} @context[${context}] @effects[${effects}] @aliasingEffects=[${aliasingEffects}]\n${fn}`;
break;
}
case 'TaggedTemplateExpression': {
@@ -720,7 +744,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;
@@ -922,3 +946,67 @@ function getFunctionName(
return defaultValue;
}
}
export function printAliasingEffect(effect: AliasingEffect): string {
switch (effect.kind) {
case 'Alias': {
return `Alias ${printPlaceForAliasEffect(effect.into)} = ${printPlaceForAliasEffect(effect.from)}`;
}
case 'Capture': {
return `Capture ${printPlaceForAliasEffect(effect.into)} <- ${printPlaceForAliasEffect(effect.from)}`;
}
case 'ImmutableCapture': {
return `ImmutableCapture ${printPlaceForAliasEffect(effect.into)} <- ${printPlaceForAliasEffect(effect.from)}`;
}
case 'Create': {
return `Create ${printPlaceForAliasEffect(effect.into)} = ${effect.value}`;
}
case 'CreateFrom': {
return `Create ${printPlaceForAliasEffect(effect.into)} = kindOf(${printPlaceForAliasEffect(effect.from)})`;
}
case 'Apply': {
const params = effect.params.map(printAliasedPlace).join(', ');
const rest = effect.rest != null ? printAliasedPlace(effect.rest) : '';
const returns = printAliasedPlace(effect.returns);
return `Apply ${returns} = ${printAliasedPlace(effect.function)} as ${effect.receiver} ( ${params} ${params.length !== 0 && rest !== '' ? ', ...' : ''}${rest})`;
}
case 'Freeze': {
return `Freeze ${printPlaceForAliasEffect(effect.value)} ${effect.reason}`;
}
case 'Mutate':
case 'MutateConditionally':
case 'MutateTransitive':
case 'MutateTransitiveConditionally': {
return `${effect.kind} ${printPlaceForAliasEffect(effect.value)}`;
}
default: {
assertExhaustive(effect, `Unexpected kind '${(effect as any).kind}'`);
}
}
}
function printPlaceForAliasEffect(place: Place): string {
return printIdentifier(place.identifier);
}
function printAliasedPlace(place: AliasedPlace): string {
return place.kind + ' ' + printPlaceForAliasEffect(place.place);
}
export function printAliasingSignature(signature: AliasingSignature): string {
const tokens: Array<string> = ['function '];
tokens.push('(');
tokens.push('this=$' + String(signature.receiver));
for (const param of signature.params) {
tokens.push(', $' + String(param));
}
if (signature.rest != null) {
tokens.push(`, ...$${String(signature.rest)}`);
}
tokens.push('): ');
tokens.push('$' + String(signature.returns) + ':');
for (const effect of signature.effects) {
tokens.push('\n ' + printAliasingEffect(effect));
}
return tokens.join('');
}

View File

@@ -10,7 +10,9 @@ import {
Effect,
HIRFunction,
Identifier,
IdentifierId,
LoweredFunction,
Place,
isRefOrRefValue,
makeInstructionId,
} from '../HIR';
@@ -19,6 +21,15 @@ 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 {assertExhaustive, Iterable_some} from '../Utils/utils';
import {inferMutationAliasingEffects} from './InferMutationAliasingEffects';
import {inferMutationAliasingFunctionEffects} from './InferMutationAliasingFunctionEffects';
import {inferMutationAliasingRanges} from './InferMutationAliasingRanges';
export default function analyseFunctions(func: HIRFunction): void {
for (const [_, block] of func.body.blocks) {
@@ -26,8 +37,12 @@ export default function analyseFunctions(func: HIRFunction): void {
switch (instr.value.kind) {
case 'ObjectMethod':
case 'FunctionExpression': {
lower(instr.value.loweredFunc.func);
infer(instr.value.loweredFunc);
if (!func.env.config.enableNewMutationAliasingModel) {
const aliases = lower(instr.value.loweredFunc.func);
infer(instr.value.loweredFunc, aliases);
} else {
lowerWithMutationAliasing(instr.value.loweredFunc.func);
}
/**
* Reset mutable range for outer inferReferenceEffects
@@ -44,11 +59,70 @@ export default function analyseFunctions(func: HIRFunction): void {
}
}
function lower(func: HIRFunction): void {
function lowerWithMutationAliasing(fn: HIRFunction): void {
analyseFunctions(fn);
inferMutationAliasingEffects(fn, {isFunctionExpression: true});
deadCodeElimination(fn);
inferMutationAliasingRanges(fn);
rewriteInstructionKindsBasedOnReassignment(fn);
inferReactiveScopeVariables(fn);
fn.env.logger?.debugLogIRs?.({
kind: 'hir',
name: 'AnalyseFunction (inner)',
value: fn,
});
const effects = inferMutationAliasingFunctionEffects(fn);
fn.aliasingEffects = effects;
const capturedOrMutated = new Set<IdentifierId>();
for (const effect of effects ?? []) {
switch (effect.kind) {
case 'Alias':
case 'Capture':
case 'CreateFrom': {
capturedOrMutated.add(effect.from.identifier.id);
break;
}
case 'Apply': {
capturedOrMutated.add(effect.function.place.identifier.id);
break;
}
case 'Mutate':
case 'MutateConditionally':
case 'MutateTransitive':
case 'MutateTransitiveConditionally': {
capturedOrMutated.add(effect.value.identifier.id);
break;
}
case 'Create':
case 'Freeze':
case 'ImmutableCapture': {
// no-op
break;
}
default: {
assertExhaustive(
effect,
`Unexpected effect kind ${(effect as any).kind}`,
);
}
}
}
for (const operand of fn.context) {
if (capturedOrMutated.has(operand.identifier.id)) {
operand.effect = Effect.Capture;
} else {
operand.effect = Effect.Read;
}
}
}
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 +130,61 @@ function lower(func: HIRFunction): void {
name: 'AnalyseFunction (inner)',
value: func,
});
inferAliasesForCapturing(func, aliases);
return aliases ?? new DisjointSet();
}
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 +211,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

@@ -197,6 +197,7 @@ function makeManualMemoizationMarkers(
deps: depsList,
loc: fnExpr.loc,
},
effects: null,
loc: fnExpr.loc,
},
{
@@ -208,6 +209,7 @@ function makeManualMemoizationMarkers(
decl: {...memoDecl},
loc: fnExpr.loc,
},
effects: null,
loc: fnExpr.loc,
},
];

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

@@ -29,6 +29,7 @@ import {
isSetStateType,
isFireFunctionType,
makeScopeId,
todoPopulateAliasingEffects,
} from '../HIR';
import {collectHoistablePropertyLoadsInInnerFn} from '../HIR/CollectHoistablePropertyLoads';
import {collectOptionalChainSidemap} from '../HIR/CollectOptionalChainDependencies';
@@ -236,9 +237,10 @@ export function inferEffectDependencies(fn: HIRFunction): void {
newInstructions.push({
id: makeInstructionId(0),
loc: GeneratedSource,
lvalue: {...depsPlace, effect: Effect.Mutate},
effects: todoPopulateAliasingEffects(),
value: deps,
loc: GeneratedSource,
});
// Step 2: push the inferred deps array as an argument of the useEffect
@@ -249,9 +251,10 @@ export function inferEffectDependencies(fn: HIRFunction): void {
// Global functions have no reactive dependencies, so we can insert an empty array
newInstructions.push({
id: makeInstructionId(0),
loc: GeneratedSource,
lvalue: {...depsPlace, effect: Effect.Mutate},
effects: todoPopulateAliasingEffects(),
value: deps,
loc: GeneratedSource,
});
value.args.push({...depsPlace, effect: Effect.Freeze});
rewriteInstrs.set(instr.id, newInstructions);
@@ -316,21 +319,25 @@ function writeDependencyToInstructions(
const instructions: Array<Instruction> = [];
let currValue = createTemporaryPlace(env, GeneratedSource);
currValue.reactive = reactive;
const dependencyPlace: Place = {
kind: 'Identifier',
identifier: dep.identifier,
effect: Effect.Capture,
reactive,
loc: loc,
};
instructions.push({
id: makeInstructionId(0),
loc: GeneratedSource,
lvalue: {...currValue, effect: Effect.Mutate},
value: {
kind: 'LoadLocal',
place: {
kind: 'Identifier',
identifier: dep.identifier,
effect: Effect.Capture,
reactive,
loc: loc,
},
place: {...dependencyPlace},
loc: loc,
},
effects: [
{kind: 'Alias', from: {...dependencyPlace}, into: {...currValue}},
],
});
for (const path of dep.path) {
if (path.optional) {
@@ -359,6 +366,7 @@ function writeDependencyToInstructions(
property: path.property,
loc: loc,
},
effects: [{kind: 'Capture', from: {...currValue}, into: {...nextValue}}],
});
currValue = nextValue;
}

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,33 +311,38 @@ 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 {
return effect.kind === 'GlobalMutation';
}
function getWriteErrorReason(abstractValue: AbstractValue): string {
export function getWriteErrorReason(abstractValue: AbstractValue): string {
if (abstractValue.reason.has(ValueReason.Global)) {
return 'Writing to a variable defined outside a component or hook is not allowed. Consider using an effect';
} else if (abstractValue.reason.has(ValueReason.JsxCaptured)) {

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} 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,58 @@ 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)}:${ident.mutableRange.start}:${ident.mutableRange.end}`,
),
),
),
);
}
/**
* 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.
*/
export 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;
}
export 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,221 @@
/**
* 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, IdentifierId, Place, ScopeId} from '../HIR';
import {getOrInsertDefault} from '../Utils/utils';
import {AliasingEffect} from './InferMutationAliasingEffects';
export function inferMutationAliasingFunctionEffects(
fn: HIRFunction,
): Array<AliasingEffect> | null {
const effects: Array<AliasingEffect> = [];
/**
* Map used to identify tracked variables: params, context vars, return value
* This is used to detect mutation/capturing/aliasing of params/context vars
*/
const tracked = new Map<IdentifierId, Place>();
tracked.set(fn.returns.identifier.id, fn.returns);
/**
* For each reactive scope we track whether there are known/conditional local and transitive
* mutations. This is used to recover precise mutation effects for each of the params and
* context variables.
*/
const trackedScopes = new Map<
ScopeId,
{local: MutationKind; transitive: MutationKind}
>();
for (const operand of fn.context) {
tracked.set(operand.identifier.id, operand);
if (operand.identifier.scope != null) {
getOrInsertDefault(trackedScopes, operand.identifier.scope.id, {
local: MutationKind.None,
transitive: MutationKind.None,
});
}
}
for (const param of fn.params) {
const place = param.kind === 'Identifier' ? param : param.place;
tracked.set(place.identifier.id, place);
if (place.identifier.scope != null) {
getOrInsertDefault(trackedScopes, place.identifier.scope.id, {
local: MutationKind.None,
transitive: MutationKind.None,
});
}
}
/**
* Track capturing/aliasing of context vars and params into each other and into the return.
* We don't need to track locals and intermediate values, since we're only concerned with effects
* as they relate to arguments visible outside the function.
*
* For each aliased identifier we track capture/alias/createfrom and then merge this with how
* the value is used. Eg capturing an alias => capture. See joinEffects() helper.
*/
type AliasedIdentifier = {
kind: 'Capture' | 'Alias' | 'CreateFrom';
place: Place;
};
const dataFlow = new Map<IdentifierId, Array<AliasedIdentifier>>();
/*
* Check for aliasing of tracked values. Also joins the effects of how the value is
* used (@param kind) with the aliasing type of each value
*/
function lookup(
place: Place,
kind: AliasedIdentifier['kind'],
): Array<AliasedIdentifier> | null {
if (tracked.has(place.identifier.id)) {
return [{kind, place}];
}
return (
dataFlow.get(place.identifier.id)?.map(aliased => ({
kind: joinEffects(aliased.kind, kind),
place: aliased.place,
})) ?? null
);
}
// todo: fixpoint
for (const block of fn.body.blocks.values()) {
for (const phi of block.phis) {
const operands: Array<AliasedIdentifier> = [];
for (const operand of phi.operands.values()) {
const inputs = lookup(operand, 'Alias');
if (inputs != null) {
operands.push(...inputs);
}
}
if (operands.length !== 0) {
dataFlow.set(phi.place.identifier.id, operands);
}
}
for (const instr of block.instructions) {
if (instr.effects == null) continue;
for (const effect of instr.effects) {
if (
effect.kind === 'Capture' ||
effect.kind === 'Alias' ||
effect.kind === 'CreateFrom'
) {
const from = lookup(effect.from, effect.kind);
if (from == null) {
continue;
}
const into = lookup(effect.into, 'Alias');
if (into == null) {
getOrInsertDefault(dataFlow, effect.into.identifier.id, []).push(
...from,
);
} else {
for (const aliased of into) {
getOrInsertDefault(
dataFlow,
aliased.place.identifier.id,
[],
).push(...from);
}
}
} else if (
effect.kind === 'Mutate' &&
effect.value.identifier.scope != null &&
trackedScopes.has(effect.value.identifier.scope.id)
) {
const scope = trackedScopes.get(effect.value.identifier.scope.id)!;
scope.local = MutationKind.Definite;
} else if (
effect.kind === 'MutateConditionally' &&
effect.value.identifier.scope != null &&
trackedScopes.has(effect.value.identifier.scope.id)
) {
const scope = trackedScopes.get(effect.value.identifier.scope.id)!;
scope.local = Math.max(MutationKind.Conditional, scope.local);
} else if (
effect.kind === 'MutateTransitive' &&
effect.value.identifier.scope != null &&
trackedScopes.has(effect.value.identifier.scope.id)
) {
const scope = trackedScopes.get(effect.value.identifier.scope.id)!;
scope.transitive = MutationKind.Definite;
} else if (
effect.kind === 'MutateTransitiveConditionally' &&
effect.value.identifier.scope != null &&
trackedScopes.has(effect.value.identifier.scope.id)
) {
const scope = trackedScopes.get(effect.value.identifier.scope.id)!;
scope.transitive = Math.max(
MutationKind.Conditional,
scope.transitive,
);
}
}
}
if (block.terminal.kind === 'return') {
const from = lookup(block.terminal.value, 'Alias');
if (from != null) {
getOrInsertDefault(dataFlow, fn.returns.identifier.id, []).push(
...from,
);
}
}
}
// Create mutation effects based on observed mutation types
for (const value of tracked.values()) {
if (
value.identifier.id === fn.returns.identifier.id ||
value.identifier.scope == null
) {
continue;
}
const scope = trackedScopes.get(value.identifier.scope.id)!;
if (scope.local === MutationKind.Definite) {
effects.push({kind: 'Mutate', value});
} else if (scope.local === MutationKind.Conditional) {
effects.push({kind: 'MutateConditionally', value});
}
if (scope.transitive === MutationKind.Definite) {
effects.push({kind: 'MutateTransitive', value});
} else if (scope.transitive === MutationKind.Conditional) {
effects.push({kind: 'MutateTransitiveConditionally', value});
}
}
// Create aliasing effects based on observed data flow
for (const [into, from] of dataFlow) {
const input = tracked.get(into);
if (input == null) {
continue;
}
for (const aliased of from) {
const effect = {kind: aliased.kind, from: aliased.place, into: input};
effects.push(effect);
}
}
return effects;
}
enum MutationKind {
None = 0,
Conditional = 1,
Definite = 2,
}
type AliasingKind = 'Alias' | 'Capture' | 'CreateFrom';
function joinEffects(
effect1: AliasingKind,
effect2: AliasingKind,
): AliasingKind {
if (effect1 === 'Capture' || effect2 === 'Capture') {
return 'Capture';
} else {
return 'Alias';
}
}

View File

@@ -0,0 +1,232 @@
/**
* 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 {
Effect,
HIRFunction,
Identifier,
IdentifierId,
makeInstructionId,
} from '../HIR/HIR';
import {
eachInstructionLValue,
eachInstructionValueOperand,
eachTerminalOperand,
} from '../HIR/visitors';
import DisjointSet from '../Utils/DisjointSet';
import {assertExhaustive} from '../Utils/utils';
import {inferMutableRangesForAlias} from './InferMutableRangesForAlias';
/**
* Infers mutable ranges for all values.
*/
export function inferMutationAliasingRanges(fn: HIRFunction): void {
/**
* Part 1
* Infer ranges for transitive mutations, which includes mutations that affect
* captured references and not just direct aliases. We build a distjoing set
* that tracks capturing and direct aliasing, and look at transitive mutations
* only.
*/
const captures = new DisjointSet<Identifier>();
for (const block of fn.body.blocks.values()) {
for (const phi of block.phis) {
captures.union([
phi.place.identifier,
...[...phi.operands.values()].map(place => place.identifier),
]);
}
for (const instr of block.instructions) {
for (const lvalue of eachInstructionLValue(instr)) {
lvalue.identifier.mutableRange.start = instr.id;
lvalue.identifier.mutableRange.end = makeInstructionId(
Math.max(instr.id + 1, lvalue.identifier.mutableRange.end),
);
}
if (instr.effects == null) continue;
for (const effect of instr.effects) {
if (
effect.kind === 'Alias' ||
effect.kind === 'CreateFrom' ||
effect.kind === 'Capture'
) {
captures.union([effect.from.identifier, effect.into.identifier]);
} else if (
effect.kind === 'MutateTransitive' ||
effect.kind === 'MutateTransitiveConditionally'
) {
const value = effect.value;
value.identifier.mutableRange.end = makeInstructionId(instr.id + 1);
}
}
}
}
/**
* TODO: this will incorrectly mark values as mutated in the following case:
* 1. Create value x
* 2. Create value y
* 3. Transitively mutate y
* 4. Capture x -> y
*
* We will put these all into one alias set in captures, and then InferMutableRangesForAlias
* will look at all identifiers in the set that start before the mutation. Thus it will see
* that x is created before the mutation, and consider it mutated. But the capture doesn't
* occur until after the mutation!
*
* The fix is to use a graph to precisely describe what is captured where at each instruction,
* so that on a transitive mutation we can iterate all the captured values and mark them.
*
* This then needs a fixpoint: although we would track captures for phi operands, the operands'
* own capture values won't be populated until we do a fixpoint.
*/
inferMutableRangesForAlias(fn, captures);
/**
* Part 2
* Infer ranges for local (non-transitive) mutations. We build a disjoint set
* that only tracks direct value aliasing, and look only at local mutations
* to extend ranges
*
* TODO: similar design to the above todo for captures, use a directed graph instead of disjoint union,
* with fixpoint.
*/
const aliases = new DisjointSet<Identifier>();
for (const block of fn.body.blocks.values()) {
for (const phi of block.phis) {
aliases.union([
phi.place.identifier,
...[...phi.operands.values()].map(place => place.identifier),
]);
}
for (const instr of block.instructions) {
if (instr.effects == null) continue;
for (const effect of instr.effects) {
if (effect.kind === 'Alias' || effect.kind === 'CreateFrom') {
aliases.union([effect.from.identifier, effect.into.identifier]);
} else if (
effect.kind === 'Mutate' ||
effect.kind === 'MutateConditionally'
) {
const value = effect.value;
value.identifier.mutableRange.end = makeInstructionId(instr.id + 1);
}
}
}
}
inferMutableRangesForAlias(fn, aliases);
/**
* Part 3
* Add legacy operand-specific effects based on instruction effects and mutable ranges.
* Also fixes up operand mutable ranges, making sure that start is non-zero if the value
* is mutated (depended on by later passes like InferReactiveScopeVariables which uses this
* to filter spurious mutations of globals, which we now guard against more precisely)
*/
for (const block of fn.body.blocks.values()) {
for (const phi of block.phis) {
// TODO: we don't actually set these effects today!
phi.place.effect = Effect.Store;
const isPhiMutatedAfterCreation: boolean =
phi.place.identifier.mutableRange.end >
(block.instructions.at(0)?.id ?? block.terminal.id);
for (const operand of phi.operands.values()) {
operand.effect = isPhiMutatedAfterCreation
? Effect.Capture
: Effect.Read;
}
}
for (const instr of block.instructions) {
if (instr.effects == null) {
for (const lvalue of eachInstructionLValue(instr)) {
lvalue.effect = Effect.ConditionallyMutate;
}
for (const operand of eachInstructionValueOperand(instr.value)) {
operand.effect = Effect.Read;
}
continue;
}
const operandEffects = new Map<IdentifierId, Effect>();
for (const effect of instr.effects) {
switch (effect.kind) {
case 'Alias':
case 'Capture':
case 'CreateFrom': {
const isMutatedOrReassigned =
effect.into.identifier.mutableRange.end > instr.id;
if (isMutatedOrReassigned) {
operandEffects.set(effect.from.identifier.id, Effect.Capture);
operandEffects.set(effect.into.identifier.id, Effect.Store);
} else {
operandEffects.set(effect.from.identifier.id, Effect.Read);
operandEffects.set(effect.into.identifier.id, Effect.Store);
}
break;
}
case 'ImmutableCapture': {
operandEffects.set(effect.from.identifier.id, Effect.Read);
break;
}
case 'Create': {
break;
}
case 'Mutate': {
operandEffects.set(effect.value.identifier.id, Effect.Store);
break;
}
case 'Apply': {
operandEffects.set(
effect.function.place.identifier.id,
Effect.ConditionallyMutate,
);
break;
}
case 'MutateTransitive':
case 'MutateConditionally':
case 'MutateTransitiveConditionally': {
operandEffects.set(
effect.value.identifier.id,
Effect.ConditionallyMutate,
);
break;
}
case 'Freeze': {
operandEffects.set(effect.value.identifier.id, Effect.Freeze);
break;
}
default: {
assertExhaustive(
effect,
`Unexpected effect kind ${(effect as any).kind}`,
);
}
}
}
for (const lvalue of eachInstructionLValue(instr)) {
const effect =
operandEffects.get(lvalue.identifier.id) ??
Effect.ConditionallyMutate;
lvalue.effect = effect;
}
for (const operand of eachInstructionValueOperand(instr.value)) {
if (
operand.identifier.mutableRange.end > instr.id &&
operand.identifier.mutableRange.start === 0
) {
operand.identifier.mutableRange.start = instr.id;
}
const effect = operandEffects.get(operand.identifier.id) ?? Effect.Read;
operand.effect = effect;
}
}
for (const operand of eachTerminalOperand(block.terminal)) {
operand.effect = Effect.Read;
}
}
}

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, Set_isSuperset} 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) {
@@ -779,7 +790,7 @@ function inferParam(
* │ Mutable │───┘
* └──────────────────────────┘
*/
function mergeValues(a: ValueKind, b: ValueKind): ValueKind {
export function mergeValueKinds(a: ValueKind, b: ValueKind): ValueKind {
if (a === b) {
return a;
} else if (a === ValueKind.MaybeFrozen || b === ValueKind.MaybeFrozen) {
@@ -821,28 +832,16 @@ function mergeValues(a: ValueKind, b: ValueKind): ValueKind {
}
}
/**
* @returns `true` if `a` is a superset of `b`.
*/
function isSuperset<T>(a: ReadonlySet<T>, b: ReadonlySet<T>): boolean {
for (const v of b) {
if (!a.has(v)) {
return false;
}
}
return true;
}
function mergeAbstractValues(
a: AbstractValue,
b: AbstractValue,
): AbstractValue {
const kind = mergeValues(a.kind, b.kind);
const kind = mergeValueKinds(a.kind, b.kind);
if (
kind === a.kind &&
kind === b.kind &&
isSuperset(a.reason, b.reason) &&
isSuperset(a.context, b.context)
Set_isSuperset(a.reason, b.reason) &&
Set_isSuperset(a.context, b.context)
) {
return a;
}
@@ -902,19 +901,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 +926,7 @@ function inferBlock(
let _: 'Hole' = element.kind;
}
}
state.initialize(instrValue, valueKind);
state.define(instr.lvalue, instrValue);
instr.lvalue.effect = Effect.Store;
@@ -954,19 +946,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 +1174,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 +1293,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 +1331,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 +1357,9 @@ function inferBlock(
state.referenceAndRecordEffects(
freezeActions,
instrValue.object,
Effect.Store,
isObjectType(instrValue.object.identifier)
? Effect.Store
: Effect.Mutate,
ValueReason.Other,
);
@@ -1387,7 +1396,7 @@ function inferBlock(
state.referenceAndRecordEffects(
freezeActions,
instrValue.object,
Effect.Read,
Effect.Capture,
ValueReason.Other,
);
state.referenceAndRecordEffects(
@@ -1397,7 +1406,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 +1820,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 +1849,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

@@ -235,6 +235,7 @@ function rewriteBlock(
type: null,
loc: terminal.loc,
},
effects: null,
});
block.terminal = {
kind: 'goto',
@@ -263,5 +264,6 @@ function declareTemporary(
type: null,
loc: result.loc,
},
effects: null,
});
}

View File

@@ -27,6 +27,7 @@ import {
Place,
promoteTemporary,
SpreadPattern,
todoPopulateAliasingEffects,
} from '../HIR';
import {
createTemporaryPlace,
@@ -151,6 +152,7 @@ export function inlineJsxTransform(
type: null,
loc: instr.value.loc,
},
effects: todoPopulateAliasingEffects(),
loc: instr.loc,
};
currentBlockInstructions.push(varInstruction);
@@ -167,6 +169,7 @@ export function inlineJsxTransform(
},
loc: instr.value.loc,
},
effects: todoPopulateAliasingEffects(),
loc: instr.loc,
};
currentBlockInstructions.push(devGlobalInstruction);
@@ -220,6 +223,7 @@ export function inlineJsxTransform(
type: null,
loc: instr.value.loc,
},
effects: todoPopulateAliasingEffects(),
loc: instr.loc,
};
thenBlockInstructions.push(reassignElseInstruction);
@@ -292,6 +296,7 @@ export function inlineJsxTransform(
],
loc: instr.value.loc,
},
effects: todoPopulateAliasingEffects(),
loc: instr.loc,
};
elseBlockInstructions.push(reactElementInstruction);
@@ -309,6 +314,7 @@ export function inlineJsxTransform(
type: null,
loc: instr.value.loc,
},
effects: todoPopulateAliasingEffects(),
loc: instr.loc,
};
elseBlockInstructions.push(reassignConditionalInstruction);
@@ -436,6 +442,7 @@ function createSymbolProperty(
binding: {kind: 'Global', name: 'Symbol'},
loc: instr.value.loc,
},
effects: todoPopulateAliasingEffects(),
loc: instr.loc,
};
nextInstructions.push(symbolInstruction);
@@ -450,6 +457,7 @@ function createSymbolProperty(
property: makePropertyLiteral('for'),
loc: instr.value.loc,
},
effects: todoPopulateAliasingEffects(),
loc: instr.loc,
};
nextInstructions.push(symbolForInstruction);
@@ -463,6 +471,7 @@ function createSymbolProperty(
value: symbolName,
loc: instr.value.loc,
},
effects: todoPopulateAliasingEffects(),
loc: instr.loc,
};
nextInstructions.push(symbolValueInstruction);
@@ -478,6 +487,7 @@ function createSymbolProperty(
args: [symbolValueInstruction.lvalue],
loc: instr.value.loc,
},
effects: todoPopulateAliasingEffects(),
loc: instr.loc,
};
const $$typeofProperty: ObjectProperty = {
@@ -508,6 +518,7 @@ function createTagProperty(
value: componentTag.name,
loc: instr.value.loc,
},
effects: todoPopulateAliasingEffects(),
loc: instr.loc,
};
tagProperty = {
@@ -634,6 +645,7 @@ function createPropsProperties(
elements: [...children],
loc: instr.value.loc,
},
effects: todoPopulateAliasingEffects(),
loc: instr.loc,
};
nextInstructions.push(childrenPropInstruction);
@@ -657,6 +669,7 @@ function createPropsProperties(
value: null,
loc: instr.value.loc,
},
effects: todoPopulateAliasingEffects(),
loc: instr.loc,
};
refProperty = {
@@ -678,6 +691,7 @@ function createPropsProperties(
value: null,
loc: instr.value.loc,
},
effects: todoPopulateAliasingEffects(),
loc: instr.loc,
};
keyProperty = {
@@ -711,6 +725,7 @@ function createPropsProperties(
properties: props,
loc: instr.value.loc,
},
effects: todoPopulateAliasingEffects(),
loc: instr.loc,
};
propsProperty = {

View File

@@ -29,6 +29,7 @@ import {
markInstructionIds,
promoteTemporary,
reversePostorderBlocks,
todoPopulateAliasingEffects,
} from '../HIR';
import {createTemporaryPlace} from '../HIR/HIRBuilder';
import {enterSSA} from '../SSA';
@@ -146,6 +147,7 @@ function emitLoadLoweredContextCallee(
id: makeInstructionId(0),
loc: GeneratedSource,
lvalue: createTemporaryPlace(env, GeneratedSource),
effects: todoPopulateAliasingEffects(),
value: loadGlobal,
};
}
@@ -192,6 +194,7 @@ function emitPropertyLoad(
lvalue: object,
value: loadObj,
id: makeInstructionId(0),
effects: todoPopulateAliasingEffects(),
loc: GeneratedSource,
};
@@ -206,6 +209,7 @@ function emitPropertyLoad(
lvalue: element,
value: loadProp,
id: makeInstructionId(0),
effects: todoPopulateAliasingEffects(),
loc: GeneratedSource,
};
return {
@@ -250,6 +254,7 @@ function emitSelectorFn(env: Environment, keys: Array<string>): Instruction {
params: [obj],
returnTypeAnnotation: null,
returnType: makeType(),
returns: createTemporaryPlace(env, GeneratedSource),
context: [],
effects: null,
body: {
@@ -278,6 +283,7 @@ function emitSelectorFn(env: Environment, keys: Array<string>): Instruction {
loc: GeneratedSource,
},
lvalue: createTemporaryPlace(env, GeneratedSource),
effects: todoPopulateAliasingEffects(),
loc: GeneratedSource,
};
return fnInstr;
@@ -294,6 +300,7 @@ function emitArrayInstr(elements: Array<Place>, env: Environment): Instruction {
id: makeInstructionId(0),
value: array,
lvalue: arrayLvalue,
effects: todoPopulateAliasingEffects(),
loc: GeneratedSource,
};
return arrayInstr;

View File

@@ -26,6 +26,7 @@ import {
Place,
promoteTemporary,
promoteTemporaryJsxTag,
todoPopulateAliasingEffects,
} from '../HIR/HIR';
import {createTemporaryPlace} from '../HIR/HIRBuilder';
import {printIdentifier} from '../HIR/PrintHIR';
@@ -297,6 +298,7 @@ function emitOutlinedJsx(
},
loc: GeneratedSource,
},
effects: null,
};
promoteTemporaryJsxTag(loadJsx.lvalue.identifier);
const jsxExpr: Instruction = {
@@ -312,6 +314,7 @@ function emitOutlinedJsx(
openingLoc: GeneratedSource,
closingLoc: GeneratedSource,
},
effects: todoPopulateAliasingEffects(),
};
return [loadJsx, jsxExpr];
@@ -366,6 +369,7 @@ function emitOutlinedFn(
params: [propsObj],
returnTypeAnnotation: null,
returnType: makeType(),
returns: createTemporaryPlace(env, GeneratedSource),
context: [],
effects: null,
body: {
@@ -517,6 +521,7 @@ function emitDestructureProps(
loc: GeneratedSource,
value: propsObj,
},
effects: todoPopulateAliasingEffects(),
};
return destructurePropsInstr;
}

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

@@ -31,6 +31,7 @@ import {
NonLocalImportSpecifier,
Place,
promoteTemporary,
todoPopulateAliasingEffects,
} from '../HIR';
import {createTemporaryPlace, markInstructionIds} from '../HIR/HIRBuilder';
import {getOrInsertWith} from '../Utils/utils';
@@ -436,6 +437,7 @@ function makeLoadUseFireInstruction(
value: instrValue,
lvalue: {...useFirePlace},
loc: GeneratedSource,
effects: todoPopulateAliasingEffects(),
};
}
@@ -460,6 +462,7 @@ function makeLoadFireCalleeInstruction(
},
lvalue: {...loadedFireCallee},
loc: GeneratedSource,
effects: todoPopulateAliasingEffects(),
};
}
@@ -483,6 +486,7 @@ function makeCallUseFireInstruction(
value: useFireCall,
lvalue: {...useFireCallResultPlace},
loc: GeneratedSource,
effects: todoPopulateAliasingEffects(),
};
}
@@ -511,6 +515,7 @@ function makeStoreUseFireInstruction(
},
lvalue: fireFunctionBindingLValuePlace,
loc: GeneratedSource,
effects: todoPopulateAliasingEffects(),
};
}

View File

@@ -121,6 +121,21 @@ export function Set_intersect<T>(sets: Array<ReadonlySet<T>>): Set<T> {
return result;
}
/**
* @returns `true` if `a` is a superset of `b`.
*/
export function Set_isSuperset<T>(
a: ReadonlySet<T>,
b: ReadonlySet<T>,
): boolean {
for (const v of b) {
if (!a.has(v)) {
return false;
}
}
return true;
}
export function Iterable_some<T>(
iter: Iterable<T>,
pred: (item: T) => boolean,
@@ -133,6 +148,19 @@ export function Iterable_some<T>(
return false;
}
export function Iterable_filter<T>(
iter: Iterable<T>,
pred: (item: T) => boolean,
): Array<T> {
const result: Array<T> = [];
for (const item of iter) {
if (pred(item)) {
result.push(item);
}
}
return result;
}
export function nonNull<T extends NonNullable<U>, U>(
value: T | null | undefined,
): value is T {

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

@@ -22,7 +22,7 @@ function Component(props) {
7 | return hasErrors;
8 | }
> 9 | return hasErrors();
| ^^^^^^^^^ Invariant: [hoisting] Expected value for identifier to be initialized. hasErrors_0$14 (9:9)
| ^^^^^^^^^ Invariant: [hoisting] Expected value for identifier to be initialized. hasErrors_0$15 (9:9)
10 | }
11 |
```

View File

@@ -0,0 +1,57 @@
## Input
```javascript
// @enableNewMutationAliasingModel
function Component({a, b, c}) {
const x = [];
x.push(a);
const merged = {b}; // could be mutated by mutate(x) below
x.push(merged);
mutate(x);
const independent = {c}; // can't be later mutated
x.push(independent);
return <Foo value={x} />;
}
```
## Code
```javascript
import { c as _c } from "react/compiler-runtime"; // @enableNewMutationAliasingModel
function Component(t0) {
const $ = _c(6);
const { a, b, c } = t0;
let t1;
if ($[0] !== a || $[1] !== b || $[2] !== c) {
const x = [];
x.push(a);
const merged = { b };
x.push(merged);
mutate(x);
let t2;
if ($[4] !== c) {
t2 = { c };
$[4] = c;
$[5] = t2;
} else {
t2 = $[5];
}
const independent = t2;
x.push(independent);
t1 = <Foo value={x} />;
$[0] = a;
$[1] = b;
$[2] = c;
$[3] = t1;
} else {
t1 = $[3];
}
return t1;
}
```
### Eval output
(kind: exception) Fixture not implemented

View File

@@ -0,0 +1,11 @@
// @enableNewMutationAliasingModel
function Component({a, b, c}) {
const x = [];
x.push(a);
const merged = {b}; // could be mutated by mutate(x) below
x.push(merged);
mutate(x);
const independent = {c}; // can't be later mutated
x.push(independent);
return <Foo value={x} />;
}

View File

@@ -0,0 +1,49 @@
## Input
```javascript
// @enableNewMutationAliasingModel
function Component({a, b}) {
const x = {a};
const y = [b];
const f = () => {
y.x = x;
mutate(y);
};
f();
return <div>{x}</div>;
}
```
## Code
```javascript
import { c as _c } from "react/compiler-runtime"; // @enableNewMutationAliasingModel
function Component(t0) {
const $ = _c(3);
const { a, b } = t0;
let t1;
if ($[0] !== a || $[1] !== b) {
const x = { a };
const y = [b];
const f = () => {
y.x = x;
mutate(y);
};
f();
t1 = <div>{x}</div>;
$[0] = a;
$[1] = b;
$[2] = t1;
} else {
t1 = $[2];
}
return t1;
}
```
### Eval output
(kind: exception) Fixture not implemented

View File

@@ -0,0 +1,11 @@
// @enableNewMutationAliasingModel
function Component({a, b}) {
const x = {a};
const y = [b];
const f = () => {
y.x = x;
mutate(y);
};
f();
return <div>{x}</div>;
}

View File

@@ -0,0 +1,42 @@
## Input
```javascript
// @enableNewMutationAliasingModel
function Component({a, b}) {
const x = {a};
const y = [b];
y.x = x;
mutate(y);
return <div>{x}</div>;
}
```
## Code
```javascript
import { c as _c } from "react/compiler-runtime"; // @enableNewMutationAliasingModel
function Component(t0) {
const $ = _c(3);
const { a, b } = t0;
let t1;
if ($[0] !== a || $[1] !== b) {
const x = { a };
const y = [b];
y.x = x;
mutate(y);
t1 = <div>{x}</div>;
$[0] = a;
$[1] = b;
$[2] = t1;
} else {
t1 = $[2];
}
return t1;
}
```
### Eval output
(kind: exception) Fixture not implemented

View File

@@ -0,0 +1,8 @@
// @enableNewMutationAliasingModel
function Component({a, b}) {
const x = {a};
const y = [b];
y.x = x;
mutate(y);
return <div>{x}</div>;
}

View File

@@ -0,0 +1,28 @@
## Input
```javascript
// @enableNewMutationAliasingModel
function Component({a, b}) {
const x = {a};
useFreeze(x);
x.y = true;
return <div>error</div>;
}
```
## Error
```
3 | const x = {a};
4 | useFreeze(x);
> 5 | x.y = true;
| ^ InvalidReact: This mutates a variable that React considers immutable (5:5)
6 | return <div>error</div>;
7 | }
8 |
```

View File

@@ -0,0 +1,7 @@
// @enableNewMutationAliasingModel
function Component({a, b}) {
const x = {a};
useFreeze(x);
x.y = true;
return <div>error</div>;
}

View File

@@ -0,0 +1,75 @@
## Input
```javascript
// @enableNewMutationAliasingModel
import {useState} from 'react';
import {useIdentity} from 'shared-runtime';
function useMakeCallback({obj}: {obj: {value: number}}) {
const [state, setState] = useState(0);
const cb = () => {
if (obj.value !== state) setState(obj.value);
};
useIdentity();
cb();
return [cb];
}
export const FIXTURE_ENTRYPOINT = {
fn: useMakeCallback,
params: [{obj: {value: 1}}],
sequentialRenders: [{obj: {value: 1}}, {obj: {value: 2}}],
};
```
## Code
```javascript
import { c as _c } from "react/compiler-runtime"; // @enableNewMutationAliasingModel
import { useState } from "react";
import { useIdentity } from "shared-runtime";
function useMakeCallback(t0) {
const $ = _c(5);
const { obj } = t0;
const [state, setState] = useState(0);
let t1;
if ($[0] !== obj.value || $[1] !== state) {
t1 = () => {
if (obj.value !== state) {
setState(obj.value);
}
};
$[0] = obj.value;
$[1] = state;
$[2] = t1;
} else {
t1 = $[2];
}
const cb = t1;
useIdentity();
cb();
let t2;
if ($[3] !== cb) {
t2 = [cb];
$[3] = cb;
$[4] = t2;
} else {
t2 = $[4];
}
return t2;
}
export const FIXTURE_ENTRYPOINT = {
fn: useMakeCallback,
params: [{ obj: { value: 1 } }],
sequentialRenders: [{ obj: { value: 1 } }, { obj: { value: 2 } }],
};
```
### Eval output
(kind: ok) ["[[ function params=0 ]]"]
["[[ function params=0 ]]"]

View File

@@ -0,0 +1,18 @@
// @enableNewMutationAliasingModel
import {useState} from 'react';
import {useIdentity} from 'shared-runtime';
function useMakeCallback({obj}: {obj: {value: number}}) {
const [state, setState] = useState(0);
const cb = () => {
if (obj.value !== state) setState(obj.value);
};
useIdentity();
cb();
return [cb];
}
export const FIXTURE_ENTRYPOINT = {
fn: useMakeCallback,
params: [{obj: {value: 1}}],
sequentialRenders: [{obj: {value: 1}}, {obj: {value: 2}}],
};

View File

@@ -0,0 +1,64 @@
## Input
```javascript
// @enableNewMutationAliasingModel
function Component({a, b, c}) {
const x = [a, b];
const f = () => {
maybeMutate(x);
// different dependency to force this not to merge with x's scope
console.log(c);
};
return <Foo onClick={f} value={x} />;
}
```
## Code
```javascript
import { c as _c } from "react/compiler-runtime"; // @enableNewMutationAliasingModel
function Component(t0) {
const $ = _c(9);
const { a, b, c } = t0;
let t1;
if ($[0] !== a || $[1] !== b) {
t1 = [a, b];
$[0] = a;
$[1] = b;
$[2] = t1;
} else {
t1 = $[2];
}
const x = t1;
let t2;
if ($[3] !== c || $[4] !== x) {
t2 = () => {
maybeMutate(x);
console.log(c);
};
$[3] = c;
$[4] = x;
$[5] = t2;
} else {
t2 = $[5];
}
const f = t2;
let t3;
if ($[6] !== f || $[7] !== x) {
t3 = <Foo onClick={f} value={x} />;
$[6] = f;
$[7] = x;
$[8] = t3;
} else {
t3 = $[8];
}
return t3;
}
```
### Eval output
(kind: exception) Fixture not implemented

View File

@@ -0,0 +1,10 @@
// @enableNewMutationAliasingModel
function Component({a, b, c}) {
const x = [a, b];
const f = () => {
maybeMutate(x);
// different dependency to force this not to merge with x's scope
console.log(c);
};
return <Foo onClick={f} value={x} />;
}

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;

View File

@@ -42,6 +42,7 @@ export function runSprout(
(globalThis as any).__SNAP_EVALUATOR_MODE = undefined;
}
if (forgetResult.kind === 'UnexpectedError') {
console.log(forgetCode);
return makeError('Unexpected error in Forget runner', forgetResult.value);
}
if (originalCode.indexOf('@disableNonForgetInSprout') === -1) {