Compare commits

...

34 Commits

Author SHA1 Message Date
Joe Savona
07cb0433b1 [compiler][newinference] Error handling and related fixes
Lots of small fixes related to error handling. InferMutationAliasRanges now tracks transitive calls that may mutate frozen or global values. We properly populate and track the reason each value has the kind it has, to use when throwing errors for invalid mutations (can't mutate state vs can't mutate a captured jsx value, etc). When we infer mutation effects for inner functions, we populate the location of mutations as the location where the mutation occurred, not the declaration of the captured value (aside: this was quite involved to do in the old inference, it's trivial here). A bunch of other small fixes that make sense in context.

And some of our "bug-*" fixtures output changes...becasue the new inference fixes the bugs. One example included here.

ghstack-source-id: 2c11291351
Pull Request resolved: https://github.com/facebook/react/pull/33458
2025-06-09 11:11:45 -07:00
Joe Savona
76091ea3ac [compiler][newinference] ensure fixpoint converges for loops w backedges
The fixpoint converges based on the abstract values being equal, but we synthesize new values cached based on the effects and new effects can be synthesized on the fly. So here we intern the effect objects by "value" (cache key computed from the value) to ensure that effects are stable, values cached based on them are stable, and that the fixpoint can converge.

ghstack-source-id: 9ca53d7e83
Pull Request resolved: https://github.com/facebook/react/pull/33449
2025-06-09 11:11:42 -07:00
Joe Savona
3267ce8c14 [compiler][newinference] Fix for phi types, extracting primitives from objects
First a quick fix: if we have a known type for the lvalue of CreateFrom, we can drop the effect. This is a bit awkward generally because the types and abstract values overlap a bit, and i'd prefer to only look at types during one phase. So maybe move all the type-checking to where we generate effects, and then after that applyEffect() doesn't have to consider types.

The larger fix is for InferMutationAliasingRanges. When processing a mutation we have to walk the graph in both forward and backwards directions. Consider `alias a -> b, mutate(b)`. We have to walk back the alias chain from b to a, and mark a as mutated too. But for `alias a -> b, mutate(a)`, we also have to mark b as mutated — walking forwards along the alias chain.

But phis are a bit different. You can have a mutation of one of the phi operands, such that you have `a, b -> phi c, mutate(a)`. Here, we do need to mark c as mutated, but we should not walk back to b — it's a different operand! a and b don't alias together.

There are now about 150 fixtures failing, but they're in a few categories and all of them are addressable:
* Infinite loops. `applyEffect()` creates new un-memoized effect values which means that any input with a backedge (loop) will spin until my infinite loop detection throws. This is somewhat tedious to address but it's a pragmatic concern and not a flaw in the model. I also need to convince myself that the approach in InferMutationAliasingRanges is safe for loops, but first i have to get inputs w loops to even reach that phase.
* LoadContext/StoreContext - i currently treat these too similarly to regular load/store, ie assuming the mutations happen in order. One idea is to treat LoadContext as a mutate instead of an alias, just to make sure all instances get grouped together.
* InvalidReact detection. We already synthesize MutateFrozen/MutateGlobal effects but we don't throw these as errors yet. This is probably the largest category of failing tests, which means overall this is actually "pretty close" (so, 50% of the way there).

ghstack-source-id: 7d6374c46e
Pull Request resolved: https://github.com/facebook/react/pull/33440
2025-06-09 11:11:39 -07:00
Joe Savona
78332a25b5 [compiler][newinference] Fixes for transitive function capturing, mutation via property loads
ghstack-source-id: 02d13be909
Pull Request resolved: https://github.com/facebook/react/pull/33430
2025-06-09 11:11:36 -07:00
Joe Savona
cef8f40c95 [compiler] Effects for Return/MaybeThrow terminals
ghstack-source-id: a0411c70fa
Pull Request resolved: https://github.com/facebook/react/pull/33429
2025-06-09 11:11:33 -07:00
Joe Savona
dbd6d754af [compiler] Post/Pre-FixUpdate, try/catch, ConditionallyMutateIterator support
ghstack-source-id: 62efaf761a
Pull Request resolved: https://github.com/facebook/react/pull/33427
2025-06-09 11:11:30 -07:00
Joe Savona
bc780793f7 [compiler] Further improve new range inference
Further refine the abstract interpretation for InferMutationAliasingRanges to account for forward data flow: Alias/Capture a -> b, mutate(a) => mutate(b) ie mutation affects aliases of the place being mutated, not just things that place aliased.

Fixes inference of which context vars of a function mutate, using the precise inference from the Range pass.

Adds MutateFrozen and MutateGlobal effects but they're not fully hooked up yet.

ghstack-source-id: 0d83b1afa9
Pull Request resolved: https://github.com/facebook/react/pull/33411
2025-06-09 11:11:27 -07:00
Joe Savona
996b3be844 [compiler] InferMutationAliasingRanges precisely models which values mutate when
It turns out that InferMutationAliasingRanges does need a fixpoint loop, but the approach is arguably simpler overall and more precise than the previous implementation. Like InferMutationAliasingEffects (which is the new InferReferenceEffects), we build an abstract model of the heap. But here we know what the effects are, so we can do abstract interpretation of the effects. Each abstract value stores a set of values that it has captured (for transitive mutation), while each variable keeps a set of values it may directly mutate (for assign/alias/capturefrom).

This means that at each mutation, we can mark _exactly_ the set of variables/values that are affected by that specific instruction. This means we can correctly infer that `mutate(b)` can't impact `a` here:

```
a = make();
b = make();
mutate(b); // when we interpret the mutation here, a isn't captured yet
b.a = a;
```

We will need to make this a fixpoint, but only if there are backedges in the CFG.

ghstack-source-id: 6fd1662469
Pull Request resolved: https://github.com/facebook/react/pull/33401
2025-06-09 11:11:23 -07:00
Joe Savona
9dbf05f570 [compiler] Fix mutable ranges for StoreContext
ghstack-source-id: 1d78b11730
Pull Request resolved: https://github.com/facebook/react/pull/33386
2025-06-09 11:11:20 -07:00
Joe Savona
277ee3e9e0 [compiler] Distinguish Alias/Assign effects
Alias was akward becuase it had a specific data flow / mutability relationship — direct mutations affect both sides - but also populated the output. We need the ability to express the aliasing dataflwo/mutability relationship w/o expressing actual assignment.

An example of this is that for an unknown method call `const z = x.y()`, we want to express the Alias-style mutability relationship btw z and x (Muate(z) => Mutate(x)), but we want to assume that `z !== x`.

This ends up being really clean, the places that should use Assign are very obvious (LoadLocal, StoreLocal) and most places keep using Alias.

ghstack-source-id: ae2b35b3f4
Pull Request resolved: https://github.com/facebook/react/pull/33385
2025-06-09 11:11:17 -07:00
Joe Savona
765e7f1151 [compiler] Effect inference across signatures and user-provided callbacks
This ties all the ideas together, showing the promise of the new inference. When applying effects, when we encounter an Apply effect we now check to see if the function we're calling is a locally defined FunctionExpression. If so, we construct a signature for it on the fly (we already have created the effects in AnalyzeFunctions), substitute in the args to get a set of effects we can apply, and then recursively apply those effects.

This required adding an ability for signatures to declare additional temporary places that they can reference. For example, Array.prototype.map needs a temporary to represent the items it extracts from the receiver array, and another temporary for the result of calling the user-provided function.

This also meant adding a `CreateFunction` effect which a) allows us to preserve the FunctionExpression value in the inference state (the previous Create effect meant we just created a dummy ObjectExpression) and b) allows dynamically constructing the ValueKind of the function based on whether it actually captures any mutable values.

Lots of other little fixes as well, such as changing function related effects (and PropertyLoad) to use Alias instead of Capture so that subsequent mutations of the output count as mutations of the input.

ghstack-source-id: 93c3bc3a0c
Pull Request resolved: https://github.com/facebook/react/pull/33384
2025-06-09 11:11:14 -07:00
Joe Savona
887c4f1839 [compiler] Receiver is mutate? for functions wo signatures
ghstack-source-id: 71bc9297af
Pull Request resolved: https://github.com/facebook/react/pull/33380
2025-06-09 11:11:10 -07:00
Joe Savona
029d4eac14 [compiler] Handle legacy mutableIfOperandsMutable signatures
ghstack-source-id: 3478c26d27
Pull Request resolved: https://github.com/facebook/react/pull/33379
2025-06-09 11:11:07 -07:00
Joe Savona
04abd518ee [compiler] Prep for making new/call/etc use Apply effects
ghstack-source-id: d7f94f3623
Pull Request resolved: https://github.com/facebook/react/pull/33378
2025-06-09 11:11:04 -07:00
Joe Savona
8ca284f58d [compiler] Bailout on mutations of frozen/global values
ghstack-source-id: ea86f072a1
Pull Request resolved: https://github.com/facebook/react/pull/33377
2025-06-09 11:11:01 -07:00
Joe Savona
61ef0959de [compiler] comments and todos
ghstack-source-id: 851743f0d6
Pull Request resolved: https://github.com/facebook/react/pull/33376
2025-06-09 11:10:58 -07:00
Joe Savona
6e8b6c98e7 [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-source-id: ba2fec0e25
Pull Request resolved: https://github.com/facebook/react/pull/33371
2025-06-09 11:10:55 -07:00
Joe Savona
7265729d8d [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-source-id: 204e369686
Pull Request resolved: https://github.com/facebook/react/pull/33370
2025-06-09 11:10:53 -07:00
Joe Savona
84f8a0d65a [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-source-id: 355895578e
Pull Request resolved: https://github.com/facebook/react/pull/33369
2025-06-09 11:10:50 -07:00
Joe Savona
04d0aa795a [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-source-id: fef144d7fb
Pull Request resolved: https://github.com/facebook/react/pull/33367
2025-06-09 11:10:47 -07:00
Joe Savona
d291f98573 [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-source-id: c6d6d897a1
Pull Request resolved: https://github.com/facebook/react/pull/33365
2025-06-09 11:10:44 -07:00
Joe Savona
d8eccc8f40 [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-source-id: 03427439ec
Pull Request resolved: https://github.com/facebook/react/pull/33364
2025-06-09 11:10:40 -07:00
Joe Savona
7cfe507be8 [compiler] Add HIRFunction.returns: Place
ghstack-source-id: 274d5ff328
Pull Request resolved: https://github.com/facebook/react/pull/33363
2025-06-09 11:10:36 -07:00
Joe Savona
0688114941 [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-source-id: 5b9c88a5f1
Pull Request resolved: https://github.com/facebook/react/pull/33346
2025-06-09 11:10:33 -07:00
Joe Savona
77d33803e2 [compiler] Add Instruction.effects property
ghstack-source-id: ffdbd73de5
Pull Request resolved: https://github.com/facebook/react/pull/33350
2025-06-09 11:10:31 -07:00
Joe Savona
631644075f [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-source-id: 68cecb869f
Pull Request resolved: https://github.com/facebook/react/pull/33180
2025-06-09 11:10:28 -07:00
Joe Savona
114b4c1001 [compiler] avoid empty switch case bodies
ghstack-source-id: bec0ccd794
Pull Request resolved: https://github.com/facebook/react/pull/33179
2025-06-09 11:10:25 -07:00
Joe Savona
5fc694a282 [compiler] allow local fixtures to be excluded from git w "nocommit" prefix
ghstack-source-id: acd64f7eaf
Pull Request resolved: https://github.com/facebook/react/pull/33178
2025-06-09 11:10:21 -07:00
Joe Savona
741854820f [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-source-id: fef41edcc5
Pull Request resolved: https://github.com/facebook/react/pull/33164
2025-06-09 11:10:18 -07:00
Joe Savona
ac2c71e44d [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-source-id: 4491c86970
Pull Request resolved: https://github.com/facebook/react/pull/33163
2025-06-09 11:10:15 -07:00
Joe Savona
17ffa32fe9 [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-source-id: 829d114d12
Pull Request resolved: https://github.com/facebook/react/pull/33157
2025-06-09 11:10:11 -07:00
Joe Savona
01f0449b1e [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.

As part of this, I realized that our handling of PropertyStore's effect wasn't quite right. We used a store effect for the object, but only if it was a known object type — otherwise we recorded it as a mutation. But a PropertyStore really always is a store — it only mutates direct aliases of a value, not any interior objects that are captured. So I updated to always use store for known properties, and use mutate for computed properties. The latter is still also wrong, but i want to debug the change there separately.

ghstack-source-id: 32978ceda6
Pull Request resolved: https://github.com/facebook/react/pull/33151
2025-06-09 11:10:08 -07:00
Joe Savona
ff77c0abe9 [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-source-id: 37bda1509b
Pull Request resolved: https://github.com/facebook/react/pull/33114
2025-06-09 11:10:05 -07:00
Joe Savona
34dfede6ed [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-source-id: f22345952a
Pull Request resolved: https://github.com/facebook/react/pull/33113
2025-06-09 11:10:02 -07:00
94 changed files with 6522 additions and 433 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

@@ -115,6 +115,14 @@ export class CompilerErrorDetail {
export class CompilerError extends Error {
details: Array<CompilerErrorDetail> = [];
static from(details: Array<CompilerErrorDetailOptions>): CompilerError {
const error = new CompilerError();
for (const detail of details) {
error.push(detail);
}
return error;
}
static invariant(
condition: unknown,
options: Omit<CompilerErrorDetailOptions, 'severity'>,

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,20 @@ 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 {
const mutabilityAliasingErrors = inferMutationAliasingRanges(hir, {
isFunctionExpression: false,
});
log({kind: 'hir', name: 'InferMutationAliasingRanges', value: hir});
if (env.isInferredMemoEnabled) {
if (mutabilityAliasingErrors.isErr()) {
throw mutabilityAliasingErrors.unwrapErr();
}
}
}
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';
/*
@@ -179,6 +179,7 @@ export function lower(
loc: GeneratedSource,
value: lowerExpressionToTemporary(builder, body),
id: makeInstructionId(0),
effects: null,
};
builder.terminateWithContinuation(terminal, fallthrough);
} else if (body.isBlockStatement()) {
@@ -208,6 +209,7 @@ export function lower(
loc: GeneratedSource,
}),
id: makeInstructionId(0),
effects: null,
},
null,
);
@@ -218,6 +220,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 +228,7 @@ export function lower(
loc: func.node.loc ?? GeneratedSource,
env,
effects: null,
aliasingEffects: null,
directives,
});
}
@@ -285,6 +289,7 @@ function lowerStatement(
loc: stmt.node.loc ?? GeneratedSource,
value,
id: makeInstructionId(0),
effects: null,
};
builder.terminate(terminal, 'block');
return;
@@ -1235,6 +1240,7 @@ function lowerStatement(
kind: 'Debugger',
loc,
},
effects: null,
loc,
});
return;
@@ -1892,6 +1898,7 @@ function lowerExpression(
place: leftValue,
loc: exprLoc,
},
effects: null,
loc: exprLoc,
});
builder.terminateWithContinuation(
@@ -2827,6 +2834,7 @@ function lowerOptionalCallExpression(
args,
loc,
},
effects: null,
loc,
});
} else {
@@ -2840,6 +2848,7 @@ function lowerOptionalCallExpression(
args,
loc,
},
effects: null,
loc,
});
}
@@ -3466,9 +3475,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(true),
/**
* 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>;
};
/*
@@ -449,6 +457,7 @@ export type ReturnTerminal = {
value: Place;
id: InstructionId;
fallthrough?: never;
effects: Array<AliasingEffect> | null;
};
export type GotoTerminal = {
@@ -609,6 +618,7 @@ export type MaybeThrowTerminal = {
id: InstructionId;
loc: SourceLocation;
fallthrough?: never;
effects: Array<AliasingEffect> | null;
};
export type ReactiveScopeTerminal = {
@@ -645,12 +655,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

@@ -165,6 +165,7 @@ export default class HIRBuilder {
handler: exceptionHandler,
id: makeInstructionId(0),
loc: instruction.loc,
effects: null,
},
continuationBlock,
);

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,9 @@ export type FunctionSignature = {
impure?: boolean;
canonicalName?: string;
aliasing?: AliasingSignature | null;
todo_aliasing?: AliasingSignature | null;
};
/*
@@ -302,6 +316,30 @@ addObject(BUILTIN_SHAPES, BuiltInArrayId, [
returnType: PRIMITIVE_TYPE,
calleeEffect: Effect.Store,
returnValueKind: ValueKind.Primitive,
aliasing: {
receiver: makeIdentifierId(0),
params: [],
rest: makeIdentifierId(1),
returns: makeIdentifierId(2),
temporaries: [],
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,
reason: ValueReason.KnownReturnSignature,
},
],
},
}),
],
[
@@ -332,6 +370,61 @@ addObject(BUILTIN_SHAPES, BuiltInArrayId, [
returnValueKind: ValueKind.Mutable,
noAlias: true,
mutableOnlyIfOperandsAreMutable: true,
aliasing: {
receiver: makeIdentifierId(0),
params: [makeIdentifierId(1)],
rest: null,
returns: makeIdentifierId(2),
temporaries: [
// Temporary representing captured items of the receiver
signatureArgument(3),
// Temporary representing the result of the callback
signatureArgument(4),
/*
* Undefined `this` arg to the callback. Note the signature does not
* support passing an explicit thisArg second param
*/
signatureArgument(5),
],
effects: [
// Map creates a new mutable array
{
kind: 'Create',
into: signatureArgument(2),
value: ValueKind.Mutable,
reason: ValueReason.KnownReturnSignature,
},
// The first arg to the callback is an item extracted from the receiver array
{
kind: 'CreateFrom',
from: signatureArgument(0),
into: signatureArgument(3),
},
// The undefined this for the callback
{
kind: 'Create',
into: signatureArgument(5),
value: ValueKind.Primitive,
reason: ValueReason.KnownReturnSignature,
},
// calls the callback, returning the result into a temporary
{
kind: 'Apply',
receiver: signatureArgument(5),
args: [signatureArgument(3), {kind: 'Hole'}, signatureArgument(0)],
function: signatureArgument(1),
into: signatureArgument(4),
signature: null,
mutatesFunction: false,
},
// captures the result of the callback into the return array
{
kind: 'Capture',
from: signatureArgument(4),
into: signatureArgument(2),
},
],
},
}),
],
[
@@ -479,6 +572,32 @@ addObject(BUILTIN_SHAPES, BuiltInSetId, [
calleeEffect: Effect.Store,
// returnValueKind is technically dependent on the ValueKind of the set itself
returnValueKind: ValueKind.Mutable,
aliasing: {
receiver: makeIdentifierId(0),
params: [],
rest: makeIdentifierId(1),
returns: makeIdentifierId(2),
temporaries: [],
effects: [
// Set.add returns the receiver Set
{
kind: 'Assign',
from: signatureArgument(0),
into: signatureArgument(2),
},
// Set.add mutates the set itself
{
kind: 'Mutate',
value: signatureArgument(0),
},
// Captures the rest params into the set
{
kind: 'Capture',
from: signatureArgument(1),
into: signatureArgument(0),
},
],
},
}),
],
[
@@ -1169,3 +1288,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,10 @@ import type {
Type,
} from './HIR';
import {GotoVariant, InstructionKind} from './HIR';
import {
AliasingEffect,
AliasingSignature,
} from '../Inference/InferMutationAliasingEffects';
export type Options = {
indent: number;
@@ -67,13 +71,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 +157,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}`;
@@ -213,6 +222,9 @@ export function printTerminal(terminal: Terminal): Array<string> | string {
value = `[${terminal.id}] Return${
terminal.value != null ? ' ' + printPlace(terminal.value) : ''
}`;
if (terminal.effects != null) {
value += `\n ${terminal.effects.map(printAliasingEffect).join('\n ')}`;
}
break;
}
case 'goto': {
@@ -281,6 +293,9 @@ export function printTerminal(terminal: Terminal): Array<string> | string {
}
case 'maybe-throw': {
value = `[${terminal.id}] MaybeThrow continuation=bb${terminal.continuation} handler=bb${terminal.handler}`;
if (terminal.effects != null) {
value += `\n ${terminal.effects.map(printAliasingEffect).join('\n ')}`;
}
break;
}
case 'scope': {
@@ -546,17 +561,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 +749,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 +951,101 @@ function getFunctionName(
return defaultValue;
}
}
export function printAliasingEffect(effect: AliasingEffect): string {
switch (effect.kind) {
case 'Assign': {
return `Assign ${printPlaceForAliasEffect(effect.into)} = ${printPlaceForAliasEffect(effect.from)}`;
}
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 'CreateFunction': {
return `Function ${printPlaceForAliasEffect(effect.into)} = Function captures=[${effect.captures.map(printPlaceForAliasEffect).join(', ')}]`;
}
case 'Apply': {
const receiverCallee =
effect.receiver.identifier.id === effect.function.identifier.id
? printPlaceForAliasEffect(effect.receiver)
: `${printPlaceForAliasEffect(effect.receiver)}.${printPlaceForAliasEffect(effect.function)}`;
const args = effect.args
.map(arg => {
if (arg.kind === 'Identifier') {
return printPlaceForAliasEffect(arg);
} else if (arg.kind === 'Hole') {
return ' ';
}
return `...${printPlaceForAliasEffect(arg.place)}`;
})
.join(', ');
let signature = '';
if (effect.signature != null) {
if (effect.signature.aliasing != null) {
signature = printAliasingSignature(effect.signature.aliasing);
} else {
signature = JSON.stringify(effect.signature, null, 2);
}
}
return `Apply ${printPlaceForAliasEffect(effect.into)} = ${receiverCallee}(${args})${signature != '' ? '\n ' : ''}${signature}`;
}
case 'Freeze': {
return `Freeze ${printPlaceForAliasEffect(effect.value)} ${effect.reason}`;
}
case 'Mutate':
case 'MutateConditionally':
case 'MutateTransitive':
case 'MutateTransitiveConditionally': {
return `${effect.kind} ${printPlaceForAliasEffect(effect.value)}`;
}
case 'MutateFrozen': {
return `MutateFrozen ${printPlaceForAliasEffect(effect.place)} reason=${JSON.stringify(effect.error.reason)}`;
}
case 'MutateGlobal': {
return `MutateGlobal ${printPlaceForAliasEffect(effect.place)} reason=${JSON.stringify(effect.error.reason)}`;
}
default: {
assertExhaustive(effect, `Unexpected kind '${(effect as any).kind}'`);
}
}
}
function printPlaceForAliasEffect(place: Place): string {
return printIdentifier(place.identifier);
}
export function printAliasingSignature(signature: AliasingSignature): string {
const tokens: Array<string> = ['function '];
if (signature.temporaries.length !== 0) {
tokens.push('<');
tokens.push(
signature.temporaries.map(temp => `$${temp.identifier.id}`).join(', '),
);
tokens.push('>');
}
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

@@ -735,6 +735,7 @@ export function mapTerminalSuccessors(
loc: terminal.loc,
value: terminal.value,
id: makeInstructionId(0),
effects: terminal.effects,
};
}
case 'throw': {
@@ -842,6 +843,7 @@ export function mapTerminalSuccessors(
handler,
id: makeInstructionId(0),
loc: terminal.loc,
effects: terminal.effects,
};
}
case 'try': {

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,82 @@ 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, {isFunctionExpression: true});
rewriteInstructionKindsBasedOnReassignment(fn);
inferReactiveScopeVariables(fn);
const effects = inferMutationAliasingFunctionEffects(fn);
fn.env.logger?.debugLogIRs?.({
kind: 'hir',
name: 'AnalyseFunction (inner)',
value: fn,
});
if (effects != null) {
fn.aliasingEffects ??= [];
fn.aliasingEffects?.push(...effects);
}
const capturedOrMutated = new Set<IdentifierId>();
for (const effect of effects ?? []) {
switch (effect.kind) {
case 'Assign':
case 'Alias':
case 'Capture':
case 'CreateFrom': {
capturedOrMutated.add(effect.from.identifier.id);
break;
}
case 'Apply': {
CompilerError.invariant(false, {
reason: `[AnalyzeFunctions] Expected Apply effects to be replaced with more precise effects`,
loc: effect.function.loc,
});
}
case 'Mutate':
case 'MutateConditionally':
case 'MutateTransitive':
case 'MutateTransitiveConditionally': {
capturedOrMutated.add(effect.value.identifier.id);
break;
}
case 'MutateFrozen':
case 'MutateGlobal':
case 'CreateFunction':
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
) {
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 +142,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 +223,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,185 @@
/**
* 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, ValueKind, ValueReason} 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 (const operand of [...fn.context, ...fn.params]) {
const place = operand.kind === 'Identifier' ? operand : operand.place;
tracked.set(place.identifier.id, place);
}
/**
* 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: AliasingKind;
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 === 'Assign' ||
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 === 'Create' ||
effect.kind === 'CreateFunction'
) {
getOrInsertDefault(dataFlow, effect.into.identifier.id, [
{kind: 'Alias', place: effect.into},
]);
} else if (
effect.kind === 'MutateFrozen' ||
effect.kind === 'MutateGlobal'
) {
effects.push(effect);
}
}
}
if (block.terminal.kind === 'return') {
const from = lookup(block.terminal.value, 'Alias');
if (from != null) {
getOrInsertDefault(dataFlow, fn.returns.identifier.id, []).push(
...from,
);
}
}
}
// Create aliasing effects based on observed data flow
let hasReturn = false;
for (const [into, from] of dataFlow) {
const input = tracked.get(into);
if (input == null) {
continue;
}
for (const aliased of from) {
if (
aliased.place.identifier.id === input.identifier.id ||
!tracked.has(aliased.place.identifier.id)
) {
continue;
}
const effect = {kind: aliased.kind, from: aliased.place, into: input};
effects.push(effect);
if (
into === fn.returns.identifier.id &&
(aliased.kind === 'Assign' || aliased.kind === 'CreateFrom')
) {
hasReturn = true;
}
}
}
// TODO: more precise return effect inference
if (!hasReturn) {
effects.unshift({
kind: 'Create',
into: fn.returns,
value:
fn.returnType.kind === 'Primitive'
? ValueKind.Primitive
: ValueKind.Mutable,
reason: ValueReason.KnownReturnSignature,
});
}
return effects;
}
export enum MutationKind {
None = 0,
Conditional = 1,
Definite = 2,
}
type AliasingKind = 'Alias' | 'Capture' | 'CreateFrom' | 'Assign';
function joinEffects(
effect1: AliasingKind,
effect2: AliasingKind,
): AliasingKind {
if (effect1 === 'Capture' || effect2 === 'Capture') {
return 'Capture';
} else if (effect1 === 'Assign' || effect2 === 'Assign') {
return 'Assign';
} else {
return 'Alias';
}
}

View File

@@ -0,0 +1,629 @@
/**
* 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 prettyFormat from 'pretty-format';
import {CompilerError, SourceLocation} from '..';
import {
BlockId,
Effect,
HIRFunction,
Identifier,
IdentifierId,
InstructionId,
makeInstructionId,
Place,
} from '../HIR/HIR';
import {
eachInstructionLValue,
eachInstructionValueOperand,
eachTerminalOperand,
} from '../HIR/visitors';
import {assertExhaustive, getOrInsertWith} from '../Utils/utils';
import {printFunction} from '../HIR';
import {printIdentifier, printPlace} from '../HIR/PrintHIR';
import {MutationKind} from './InferMutationAliasingFunctionEffects';
import {Result} from '../Utils/Result';
const DEBUG = false;
const VERBOSE = false;
/**
* Infers mutable ranges for all values.
*/
export function inferMutationAliasingRanges(
fn: HIRFunction,
{isFunctionExpression}: {isFunctionExpression: boolean},
): Result<void, CompilerError> {
if (VERBOSE) {
console.log();
console.log(printFunction(fn));
}
/**
* Part 1: Infer mutable ranges for values. We build an abstract model of
* values, the alias/capture edges between them, and the set of mutations.
* Edges and mutations are ordered, with mutations processed against the
* abstract model only after it is fully constructed by visiting all blocks
* _and_ connecting phis. Phis are considered ordered at the time of the
* phi node.
*
* This should (may?) mean that mutations are able to see the full state
* of the graph and mark all the appropriate identifiers as mutated at
* the correct point, accounting for both backward and forward edges.
* Ie a mutation of x accounts for both values that flowed into x,
* and values that x flowed into.
*/
const state = new AliasingState();
type PendingPhiOperand = {from: Place; into: Place; index: number};
const pendingPhis = new Map<BlockId, Array<PendingPhiOperand>>();
const mutations: Array<{
index: number;
id: InstructionId;
transitive: boolean;
kind: MutationKind;
place: Place;
}> = [];
let index = 0;
const errors = new CompilerError();
for (const param of [...fn.params, ...fn.context, fn.returns]) {
const place = param.kind === 'Identifier' ? param : param.place;
state.create(place, {kind: 'Object'});
}
const seenBlocks = new Set<BlockId>();
for (const block of fn.body.blocks.values()) {
for (const phi of block.phis) {
state.create(phi.place, {kind: 'Phi'});
for (const [pred, operand] of phi.operands) {
if (!seenBlocks.has(pred)) {
// NOTE: annotation required to actually typecheck and not silently infer `any`
const blockPhis = getOrInsertWith<BlockId, Array<PendingPhiOperand>>(
pendingPhis,
pred,
() => [],
);
blockPhis.push({from: operand, into: phi.place, index: index++});
} else {
state.assign(index++, operand, phi.place);
}
}
}
seenBlocks.add(block.id);
for (const instr of block.instructions) {
if (
instr.value.kind === 'FunctionExpression' ||
instr.value.kind === 'ObjectMethod'
) {
state.create(instr.lvalue, {
kind: 'Function',
function: instr.value.loweredFunc.func,
});
} else {
for (const lvalue of eachInstructionLValue(instr)) {
state.create(lvalue, {kind: 'Object'});
}
}
if (instr.effects == null) continue;
for (const effect of instr.effects) {
if (effect.kind === 'Create') {
state.create(effect.into, {kind: 'Object'});
} else if (effect.kind === 'CreateFunction') {
state.create(effect.into, {
kind: 'Function',
function: effect.function.loweredFunc.func,
});
} else if (effect.kind === 'CreateFrom') {
state.createFrom(index++, effect.from, effect.into);
} else if (effect.kind === 'Assign' || effect.kind === 'Alias') {
state.assign(index++, effect.from, effect.into);
} else if (effect.kind === 'Capture') {
state.capture(index++, effect.from, effect.into);
} else if (
effect.kind === 'MutateTransitive' ||
effect.kind === 'MutateTransitiveConditionally'
) {
mutations.push({
index: index++,
id: instr.id,
transitive: true,
kind:
effect.kind === 'MutateTransitive'
? MutationKind.Definite
: MutationKind.Conditional,
place: effect.value,
});
} else if (
effect.kind === 'Mutate' ||
effect.kind === 'MutateConditionally'
) {
mutations.push({
index: index++,
id: instr.id,
transitive: false,
kind:
effect.kind === 'Mutate'
? MutationKind.Definite
: MutationKind.Conditional,
place: effect.value,
});
} else if (
effect.kind === 'MutateFrozen' ||
effect.kind === 'MutateGlobal'
) {
errors.push(effect.error);
}
}
}
const blockPhis = pendingPhis.get(block.id);
if (blockPhis != null) {
for (const {from, into, index} of blockPhis) {
state.assign(index, from, into);
}
}
if (block.terminal.kind === 'return') {
state.assign(index++, block.terminal.value, fn.returns);
}
if (
(block.terminal.kind === 'maybe-throw' ||
block.terminal.kind === 'return') &&
block.terminal.effects != null
) {
for (const effect of block.terminal.effects) {
if (effect.kind === 'Alias') {
state.assign(index++, effect.from, effect.into);
} else {
CompilerError.invariant(effect.kind === 'Freeze', {
reason: `Unexpected '${effect.kind}' effect for MaybeThrow terminal`,
loc: block.terminal.loc,
});
}
}
}
}
if (VERBOSE) {
console.log(state.debug());
console.log(pretty(mutations));
}
for (const mutation of mutations) {
state.mutate(
mutation.index,
mutation.place.identifier,
makeInstructionId(mutation.id + 1),
mutation.transitive,
mutation.kind,
mutation.place.loc,
errors,
);
}
if (VERBOSE) {
console.log(state.debug());
}
fn.aliasingEffects ??= [];
for (const param of [...fn.context, ...fn.params]) {
const place = param.kind === 'Identifier' ? param : param.place;
const node = state.nodes.get(place.identifier);
if (node == null) {
continue;
}
let mutated = false;
if (node.local != null) {
if (node.local.kind === MutationKind.Conditional) {
mutated = true;
fn.aliasingEffects.push({
kind: 'MutateConditionally',
value: {...place, loc: node.local.loc},
});
} else if (node.local.kind === MutationKind.Definite) {
mutated = true;
fn.aliasingEffects.push({
kind: 'Mutate',
value: {...place, loc: node.local.loc},
});
}
}
if (node.transitive != null) {
if (node.transitive.kind === MutationKind.Conditional) {
mutated = true;
fn.aliasingEffects.push({
kind: 'MutateTransitiveConditionally',
value: {...place, loc: node.transitive.loc},
});
} else if (node.transitive.kind === MutationKind.Definite) {
mutated = true;
fn.aliasingEffects.push({
kind: 'MutateTransitive',
value: {...place, loc: node.transitive.loc},
});
}
}
if (mutated) {
place.effect = Effect.Capture;
}
}
/**
* Part 2
* 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;
}
if (
isPhiMutatedAfterCreation &&
phi.place.identifier.mutableRange.start === 0
) {
/*
* TODO: ideally we'd construct a precise start range, but what really
* matters is that the phi's range appears mutable (end > start + 1)
* so we just set the start to the previous instruction before this block
*/
const firstInstructionIdOfBlock =
block.instructions.at(0)?.id ?? block.terminal.id;
phi.place.identifier.mutableRange.start = makeInstructionId(
firstInstructionIdOfBlock - 1,
);
}
}
for (const instr of block.instructions) {
for (const lvalue of eachInstructionLValue(instr)) {
lvalue.effect = Effect.ConditionallyMutate;
if (lvalue.identifier.mutableRange.start === 0) {
lvalue.identifier.mutableRange.start = instr.id;
}
if (lvalue.identifier.mutableRange.end === 0) {
lvalue.identifier.mutableRange.end = makeInstructionId(
Math.max(instr.id + 1, lvalue.identifier.mutableRange.end),
);
}
}
for (const operand of eachInstructionValueOperand(instr.value)) {
operand.effect = Effect.Read;
}
if (instr.effects == null) {
continue;
}
const operandEffects = new Map<IdentifierId, Effect>();
for (const effect of instr.effects) {
switch (effect.kind) {
case 'Assign':
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 'CreateFunction':
case 'Create': {
break;
}
case 'Mutate': {
operandEffects.set(effect.value.identifier.id, Effect.Store);
break;
}
case 'Apply': {
CompilerError.invariant(false, {
reason: `[AnalyzeFunctions] Expected Apply effects to be replaced with more precise effects`,
loc: effect.function.loc,
});
}
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;
}
case 'MutateFrozen':
case 'MutateGlobal': {
// no-op
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;
}
}
if (block.terminal.kind === 'return') {
block.terminal.value.effect = isFunctionExpression
? Effect.Read
: Effect.Freeze;
} else {
for (const operand of eachTerminalOperand(block.terminal)) {
operand.effect = Effect.Read;
}
}
}
if (VERBOSE) {
console.log(printFunction(fn));
}
return errors.asResult();
}
function appendFunctionErrors(errors: CompilerError, fn: HIRFunction): void {
for (const effect of fn.aliasingEffects ?? []) {
switch (effect.kind) {
case 'MutateFrozen':
case 'MutateGlobal': {
errors.push(effect.error);
break;
}
}
}
}
type Node = {
id: Identifier;
createdFrom: Map<Identifier, number>;
captures: Map<Identifier, number>;
aliases: Map<Identifier, number>;
edges: Array<{index: number; node: Identifier; kind: 'capture' | 'alias'}>;
transitive: {kind: MutationKind; loc: SourceLocation} | null;
local: {kind: MutationKind; loc: SourceLocation} | null;
value:
| {kind: 'Object'}
| {kind: 'Phi'}
| {kind: 'Function'; function: HIRFunction};
};
class AliasingState {
nodes: Map<Identifier, Node> = new Map();
create(place: Place, value: Node['value']): void {
this.nodes.set(place.identifier, {
id: place.identifier,
createdFrom: new Map(),
captures: new Map(),
aliases: new Map(),
edges: [],
transitive: null,
local: null,
value,
});
}
createFrom(index: number, from: Place, into: Place): void {
this.create(into, {kind: 'Object'});
const fromNode = this.nodes.get(from.identifier);
const toNode = this.nodes.get(into.identifier);
if (fromNode == null || toNode == null) {
if (VERBOSE) {
console.log(
`skip: createFrom ${printPlace(from)}${!!fromNode} -> ${printPlace(into)}${!!toNode}`,
);
}
return;
}
fromNode.edges.push({index, node: into.identifier, kind: 'alias'});
if (!toNode.createdFrom.has(from.identifier)) {
toNode.createdFrom.set(from.identifier, index);
}
}
capture(index: number, from: Place, into: Place): void {
const fromNode = this.nodes.get(from.identifier);
const toNode = this.nodes.get(into.identifier);
if (fromNode == null || toNode == null) {
if (VERBOSE) {
console.log(
`skip: capture ${printPlace(from)}${!!fromNode} -> ${printPlace(into)}${!!toNode}`,
);
}
return;
}
fromNode.edges.push({index, node: into.identifier, kind: 'capture'});
if (!toNode.captures.has(from.identifier)) {
toNode.captures.set(from.identifier, index);
}
}
assign(index: number, from: Place, into: Place): void {
const fromNode = this.nodes.get(from.identifier);
const toNode = this.nodes.get(into.identifier);
if (fromNode == null || toNode == null) {
if (VERBOSE) {
console.log(
`skip: assign ${printPlace(from)}${!!fromNode} -> ${printPlace(into)}${!!toNode}`,
);
}
return;
}
fromNode.edges.push({index, node: into.identifier, kind: 'alias'});
if (!toNode.aliases.has(from.identifier)) {
toNode.aliases.set(from.identifier, index);
}
}
mutate(
index: number,
start: Identifier,
end: InstructionId,
transitive: boolean,
kind: MutationKind,
loc: SourceLocation,
errors: CompilerError,
): void {
const seen = new Set<Identifier>();
const queue: Array<{
place: Identifier;
transitive: boolean;
direction: 'backwards' | 'forwards';
}> = [{place: start, transitive, direction: 'backwards'}];
while (queue.length !== 0) {
const {place: current, transitive, direction} = queue.pop()!;
if (seen.has(current)) {
continue;
}
seen.add(current);
const node = this.nodes.get(current);
if (node == null) {
if (DEBUG) {
console.log(
`no node! ${printIdentifier(start)} for identifier ${printIdentifier(current)}`,
);
}
continue;
}
if (DEBUG) {
console.log(
`[${end}] mutate index=${index} ${printIdentifier(start)}: ${printIdentifier(node.id)}`,
);
}
node.id.mutableRange.end = makeInstructionId(
Math.max(node.id.mutableRange.end, end),
);
if (
node.value.kind === 'Function' &&
node.transitive == null &&
node.local == null
) {
appendFunctionErrors(errors, node.value.function);
}
if (transitive) {
if (node.transitive == null || node.transitive.kind < kind) {
node.transitive = {kind, loc};
}
} else {
if (node.local == null || node.local.kind < kind) {
node.local = {kind, loc};
}
}
/**
* all mutations affect "forward" edges by the rules:
* - Capture a -> b, mutate(a) => mutate(b)
* - Alias a -> b, mutate(a) => mutate(b)
*/
for (const edge of node.edges) {
if (edge.index >= index) {
break;
}
queue.push({place: edge.node, transitive, direction: 'forwards'});
}
for (const [alias, when] of node.createdFrom) {
if (when >= index) {
continue;
}
queue.push({place: alias, transitive: true, direction: 'backwards'});
}
if (direction === 'backwards' || node.value.kind !== 'Phi') {
/**
* all mutations affect backward alias edges by the rules:
* - Alias a -> b, mutate(b) => mutate(a)
* - Alias a -> b, mutateTransitive(b) => mutate(a)
*
* However, if we reached a phi because one of its inputs was mutated
* (and we're advancing "forwards" through that node's edges), then
* we know we've already processed the mutation at its source. The
* phi's other inputs can't be affected.
*/
for (const [alias, when] of node.aliases) {
if (when >= index) {
continue;
}
queue.push({place: alias, transitive, direction: 'backwards'});
}
}
/**
* but only transitive mutations affect captures
*/
if (transitive) {
for (const [capture, when] of node.captures) {
if (when >= index) {
continue;
}
queue.push({place: capture, transitive, direction: 'backwards'});
}
}
}
if (DEBUG) {
const nodes = new Map();
for (const id of seen) {
const node = this.nodes.get(id);
nodes.set(id.id, node);
}
console.log(pretty(nodes));
}
}
debug(): string {
return pretty(this.nodes);
}
}
export function pretty(v: any): string {
return prettyFormat(v, {
plugins: [
{
test: v =>
v !== null && typeof v === 'object' && v.kind === 'Identifier',
serialize: v => printPlace(v),
},
{
test: v =>
v !== null &&
typeof v === 'object' &&
typeof v.declarationId === 'number',
serialize: v =>
`${printIdentifier(v)}:${v.mutableRange.start}:${v.mutableRange.end}`,
},
],
});
}

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 {
@@ -237,6 +241,7 @@ function emitSelectorFn(env: Environment, keys: Array<string>): Instruction {
kind: 'return',
loc: GeneratedSource,
value: arrayInstr.lvalue,
effects: null,
},
preds: new Set(),
phis: new Set(),
@@ -250,6 +255,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 +284,7 @@ function emitSelectorFn(env: Environment, keys: Array<string>): Instruction {
loc: GeneratedSource,
},
lvalue: createTemporaryPlace(env, GeneratedSource),
effects: todoPopulateAliasingEffects(),
loc: GeneratedSource,
};
return fnInstr;
@@ -294,6 +301,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];
@@ -353,6 +356,7 @@ function emitOutlinedFn(
kind: 'return',
loc: GeneratedSource,
value: instructions.at(-1)!.lvalue,
effects: null,
},
preds: new Set(),
phis: new Set(),
@@ -366,6 +370,7 @@ function emitOutlinedFn(
params: [propsObj],
returnTypeAnnotation: null,
returnType: makeType(),
returns: createTemporaryPlace(env, GeneratedSource),
context: [],
effects: null,
body: {
@@ -517,6 +522,7 @@ function emitDestructureProps(
loc: GeneratedSource,
value: propsObj,
},
effects: todoPopulateAliasingEffects(),
};
return destructurePropsInstr;
}

View File

@@ -44,7 +44,7 @@ import {
getHookKind,
makeIdentifierName,
} from '../HIR/HIR';
import {printIdentifier, printPlace} from '../HIR/PrintHIR';
import {printIdentifier, printInstruction, printPlace} from '../HIR/PrintHIR';
import {eachPatternOperand} from '../HIR/visitors';
import {Err, Ok, Result} from '../Utils/Result';
import {GuardKind} from '../Utils/RuntimeDiagnosticConstants';
@@ -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]);
}),
);
}
@@ -1310,7 +1310,7 @@ function codegenInstructionNullable(
});
CompilerError.invariant(value?.type === 'FunctionExpression', {
reason: 'Expected a function as a function declaration value',
description: null,
description: `Got ${value == null ? String(value) : value.type} at ${printInstruction(instr)}`,
loc: instr.value.loc,
suggestions: null,
});

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

@@ -112,6 +112,31 @@ export function validateNoFreezingKnownMutableFunctions(
);
if (knownMutation && knownMutation.kind === 'ContextMutation') {
contextMutationEffects.set(lvalue.identifier.id, knownMutation);
} else if (
fn.env.config.enableNewMutationAliasingModel &&
value.loweredFunc.func.aliasingEffects != null
) {
const context = new Set(
value.loweredFunc.func.context.map(p => p.identifier.id),
);
effects: for (const effect of value.loweredFunc.func
.aliasingEffects) {
switch (effect.kind) {
case 'Mutate':
case 'MutateTransitive': {
if (context.has(effect.value.identifier.id)) {
contextMutationEffects.set(lvalue.identifier.id, {
kind: 'ContextMutation',
effect: Effect.Mutate,
loc: effect.value.loc,
places: new Set([effect.value]),
});
break effects;
}
break;
}
}
}
}
break;
}

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,93 @@
## Input
```javascript
// @enableNewMutationAliasingModel
function Component({value}) {
const arr = [{value: 'foo'}, {value: 'bar'}, {value}];
useIdentity(null);
const derived = arr.filter(Boolean);
return (
<Stringify>
{derived.at(0)}
{derived.at(-1)}
</Stringify>
);
}
```
## Code
```javascript
import { c as _c } from "react/compiler-runtime"; // @enableNewMutationAliasingModel
function Component(t0) {
const $ = _c(13);
const { value } = t0;
let t1;
let t2;
if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
t1 = { value: "foo" };
t2 = { value: "bar" };
$[0] = t1;
$[1] = t2;
} else {
t1 = $[0];
t2 = $[1];
}
let t3;
if ($[2] !== value) {
t3 = [t1, t2, { value }];
$[2] = value;
$[3] = t3;
} else {
t3 = $[3];
}
const arr = t3;
useIdentity(null);
let t4;
if ($[4] !== arr) {
t4 = arr.filter(Boolean);
$[4] = arr;
$[5] = t4;
} else {
t4 = $[5];
}
const derived = t4;
let t5;
if ($[6] !== derived) {
t5 = derived.at(0);
$[6] = derived;
$[7] = t5;
} else {
t5 = $[7];
}
let t6;
if ($[8] !== derived) {
t6 = derived.at(-1);
$[8] = derived;
$[9] = t6;
} else {
t6 = $[9];
}
let t7;
if ($[10] !== t5 || $[11] !== t6) {
t7 = (
<Stringify>
{t5}
{t6}
</Stringify>
);
$[10] = t5;
$[11] = t6;
$[12] = t7;
} else {
t7 = $[12];
}
return t7;
}
```
### Eval output
(kind: exception) Fixture not implemented

View File

@@ -0,0 +1,12 @@
// @enableNewMutationAliasingModel
function Component({value}) {
const arr = [{value: 'foo'}, {value: 'bar'}, {value}];
useIdentity(null);
const derived = arr.filter(Boolean);
return (
<Stringify>
{derived.at(0)}
{derived.at(-1)}
</Stringify>
);
}

View File

@@ -0,0 +1,71 @@
## Input
```javascript
// @enableNewMutationAliasingModel
function Component(props) {
// This item is part of the receiver, should be memoized
const item = {a: props.a};
const items = [item];
const mapped = items.map(item => item);
// mapped[0].a = null;
return mapped;
}
export const FIXTURE_ENTRYPOINT = {
fn: Component,
params: [{a: {id: 42}}],
isComponent: false,
};
```
## Code
```javascript
import { c as _c } from "react/compiler-runtime"; // @enableNewMutationAliasingModel
function Component(props) {
const $ = _c(6);
let t0;
if ($[0] !== props.a) {
t0 = { a: props.a };
$[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;
return mapped;
}
function _temp(item_0) {
return item_0;
}
export const FIXTURE_ENTRYPOINT = {
fn: Component,
params: [{ a: { id: 42 } }],
isComponent: false,
};
```
### Eval output
(kind: ok) [{"a":{"id":42}}]

View File

@@ -0,0 +1,15 @@
// @enableNewMutationAliasingModel
function Component(props) {
// This item is part of the receiver, should be memoized
const item = {a: props.a};
const items = [item];
const mapped = items.map(item => item);
// mapped[0].a = null;
return mapped;
}
export const FIXTURE_ENTRYPOINT = {
fn: Component,
params: [{a: {id: 42}}],
isComponent: false,
};

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,87 @@
## Input
```javascript
// @enableNewMutationAliasingModel
import {identity, mutate} from 'shared-runtime';
/**
* Bug: copy of error.todo-object-expression-computed-key-modified-during-after-construction-sequence-expr
* with the mutation hoisted to a named variable instead of being directly
* inlined into the Object key.
*
* Found differences in evaluator results
* Non-forget (expected):
* (kind: ok) [{"[object Object]":[42]},{"wat0":"joe","wat1":"joe"}]
* [{"[object Object]":[42]},{"wat0":"joe","wat1":"joe"}]
* Forget:
* (kind: ok) [{"[object Object]":[42]},{"wat0":"joe","wat1":"joe"}]
* [{"[object Object]":[42]},{"wat0":"joe","wat1":"joe","wat2":"joe"}]
*/
function Component(props) {
const key = {};
const tmp = (mutate(key), key);
const context = {
// Here, `tmp` is frozen (as it's inferred to be a primitive/string)
[tmp]: identity([props.value]),
};
mutate(key);
return [context, key];
}
export const FIXTURE_ENTRYPOINT = {
fn: Component,
params: [{value: 42}],
sequentialRenders: [{value: 42}, {value: 42}],
};
```
## Code
```javascript
import { c as _c } from "react/compiler-runtime"; // @enableNewMutationAliasingModel
import { identity, mutate } from "shared-runtime";
/**
* Bug: copy of error.todo-object-expression-computed-key-modified-during-after-construction-sequence-expr
* with the mutation hoisted to a named variable instead of being directly
* inlined into the Object key.
*
* Found differences in evaluator results
* Non-forget (expected):
* (kind: ok) [{"[object Object]":[42]},{"wat0":"joe","wat1":"joe"}]
* [{"[object Object]":[42]},{"wat0":"joe","wat1":"joe"}]
* Forget:
* (kind: ok) [{"[object Object]":[42]},{"wat0":"joe","wat1":"joe"}]
* [{"[object Object]":[42]},{"wat0":"joe","wat1":"joe","wat2":"joe"}]
*/
function Component(props) {
const $ = _c(2);
let t0;
if ($[0] !== props.value) {
const key = {};
const tmp = (mutate(key), key);
const context = { [tmp]: identity([props.value]) };
mutate(key);
t0 = [context, key];
$[0] = props.value;
$[1] = t0;
} else {
t0 = $[1];
}
return t0;
}
export const FIXTURE_ENTRYPOINT = {
fn: Component,
params: [{ value: 42 }],
sequentialRenders: [{ value: 42 }, { value: 42 }],
};
```
### Eval output
(kind: ok) [{"[object Object]":[42]},{"wat0":"joe","wat1":"joe"}]
[{"[object Object]":[42]},{"wat0":"joe","wat1":"joe"}]

View File

@@ -0,0 +1,32 @@
// @enableNewMutationAliasingModel
import {identity, mutate} from 'shared-runtime';
/**
* Bug: copy of error.todo-object-expression-computed-key-modified-during-after-construction-sequence-expr
* with the mutation hoisted to a named variable instead of being directly
* inlined into the Object key.
*
* Found differences in evaluator results
* Non-forget (expected):
* (kind: ok) [{"[object Object]":[42]},{"wat0":"joe","wat1":"joe"}]
* [{"[object Object]":[42]},{"wat0":"joe","wat1":"joe"}]
* Forget:
* (kind: ok) [{"[object Object]":[42]},{"wat0":"joe","wat1":"joe"}]
* [{"[object Object]":[42]},{"wat0":"joe","wat1":"joe","wat2":"joe"}]
*/
function Component(props) {
const key = {};
const tmp = (mutate(key), key);
const context = {
// Here, `tmp` is frozen (as it's inferred to be a primitive/string)
[tmp]: identity([props.value]),
};
mutate(key);
return [context, key];
}
export const FIXTURE_ENTRYPOINT = {
fn: Component,
params: [{value: 42}],
sequentialRenders: [{value: 42}, {value: 42}],
};

View File

@@ -0,0 +1,105 @@
## Input
```javascript
// @enableNewMutationAliasingModel
import {arrayPush, Stringify} from 'shared-runtime';
function Component({prop1, prop2}) {
'use memo';
// we'll ultimately extract the item from this array as z, and mutate later
let x = [{value: prop1}];
let z;
while (x.length < 2) {
// there's a phi here for x (value before the loop and the reassignment later)
// this mutation occurs before the reassigned value
arrayPush(x, {value: prop2});
// this condition will never be true, so x doesn't get reassigned
if (x[0].value === null) {
x = [{value: prop2}];
const y = x;
z = y[0];
}
}
// the code is set up so that z will always be the value from the original x
z.other = true;
return <Stringify z={z} />;
}
export const FIXTURE_ENTRYPOINT = {
fn: Component,
params: [{prop1: 0, prop2: 0}],
sequentialRenders: [
{prop1: 0, prop2: 0},
{prop1: 1, prop2: 0},
{prop1: 1, prop2: 1},
{prop1: 0, prop2: 1},
{prop1: 0, prop2: 0},
],
};
```
## Code
```javascript
import { c as _c } from "react/compiler-runtime"; // @enableNewMutationAliasingModel
import { arrayPush, Stringify } from "shared-runtime";
function Component(t0) {
"use memo";
const $ = _c(5);
const { prop1, prop2 } = t0;
let z;
if ($[0] !== prop1 || $[1] !== prop2) {
let x = [{ value: prop1 }];
while (x.length < 2) {
arrayPush(x, { value: prop2 });
if (x[0].value === null) {
x = [{ value: prop2 }];
const y = x;
z = y[0];
}
}
z.other = true;
$[0] = prop1;
$[1] = prop2;
$[2] = z;
} else {
z = $[2];
}
let t1;
if ($[3] !== z) {
t1 = <Stringify z={z} />;
$[3] = z;
$[4] = t1;
} else {
t1 = $[4];
}
return t1;
}
export const FIXTURE_ENTRYPOINT = {
fn: Component,
params: [{ prop1: 0, prop2: 0 }],
sequentialRenders: [
{ prop1: 0, prop2: 0 },
{ prop1: 1, prop2: 0 },
{ prop1: 1, prop2: 1 },
{ prop1: 0, prop2: 1 },
{ prop1: 0, prop2: 0 },
],
};
```
### Eval output
(kind: ok) [[ (exception in render) TypeError: Cannot set properties of undefined (setting 'other') ]]
[[ (exception in render) TypeError: Cannot set properties of undefined (setting 'other') ]]
[[ (exception in render) TypeError: Cannot set properties of undefined (setting 'other') ]]
[[ (exception in render) TypeError: Cannot set properties of undefined (setting 'other') ]]
[[ (exception in render) TypeError: Cannot set properties of undefined (setting 'other') ]]

View File

@@ -0,0 +1,35 @@
// @enableNewMutationAliasingModel
import {arrayPush, Stringify} from 'shared-runtime';
function Component({prop1, prop2}) {
'use memo';
let x = [{value: prop1}];
let z;
while (x.length < 2) {
// there's a phi here for x (value before the loop and the reassignment later)
// this mutation occurs before the reassigned value
arrayPush(x, {value: prop2});
if (x[0].value === prop1) {
x = [{value: prop2}];
const y = x;
z = y[0];
}
}
z.other = true;
return <Stringify z={z} />;
}
// export const FIXTURE_ENTRYPOINT = {
// fn: Component,
// params: [{prop1: 0, prop2: 0}],
// sequentialRenders: [
// {prop1: 0, prop2: 0},
// {prop1: 1, prop2: 0},
// {prop1: 1, prop2: 1},
// {prop1: 0, prop2: 1},
// {prop1: 0, prop2: 0},
// ],
// };

View File

@@ -0,0 +1,43 @@
## Input
```javascript
// @validatePreserveExistingMemoizationGuarantees @enableNewMutationAliasingModel
import {useCallback} from 'react';
import {makeArray} from 'shared-runtime';
// This case is already unsound in source, so we can safely bailout
function Foo(props) {
let x = [];
x.push(props);
// makeArray() is captured, but depsList contains [props]
const cb = useCallback(() => [x], [x]);
x = makeArray();
return cb;
}
export const FIXTURE_ENTRYPOINT = {
fn: Foo,
params: [{}],
};
```
## Error
```
9 |
10 | // makeArray() is captured, but depsList contains [props]
> 11 | const cb = useCallback(() => [x], [x]);
| ^ CannotPreserveMemoization: React Compiler has skipped optimizing this component because the existing manual memoization could not be preserved. This dependency may be mutated later, which could cause the value to change unexpectedly (11:11)
CannotPreserveMemoization: React Compiler has skipped optimizing this component because the existing manual memoization could not be preserved. This value was memoized in source but not in compilation output. (11:11)
12 |
13 | x = makeArray();
14 |
```

View File

@@ -0,0 +1,20 @@
// @validatePreserveExistingMemoizationGuarantees @enableNewMutationAliasingModel
import {useCallback} from 'react';
import {makeArray} from 'shared-runtime';
// This case is already unsound in source, so we can safely bailout
function Foo(props) {
let x = [];
x.push(props);
// makeArray() is captured, but depsList contains [props]
const cb = useCallback(() => [x], [x]);
x = makeArray();
return cb;
}
export const FIXTURE_ENTRYPOINT = {
fn: Foo,
params: [{}],
};

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,58 @@
## Input
```javascript
function Component(props) {
const items = (() => {
if (props.cond) {
return [];
} else {
return null;
}
})();
items?.push(props.a);
return items;
}
export const FIXTURE_ENTRYPOINT = {
fn: Component,
params: [{a: {}}],
};
```
## Code
```javascript
import { c as _c } from "react/compiler-runtime";
function Component(props) {
const $ = _c(3);
let items;
if ($[0] !== props.a || $[1] !== props.cond) {
let t0;
if (props.cond) {
t0 = [];
} else {
t0 = null;
}
items = t0;
items?.push(props.a);
$[0] = props.a;
$[1] = props.cond;
$[2] = items;
} else {
items = $[2];
}
return items;
}
export const FIXTURE_ENTRYPOINT = {
fn: Component,
params: [{ a: {} }],
};
```
### Eval output
(kind: ok) null

View File

@@ -0,0 +1,16 @@
function Component(props) {
const items = (() => {
if (props.cond) {
return [];
} else {
return null;
}
})();
items?.push(props.a);
return items;
}
export const FIXTURE_ENTRYPOINT = {
fn: Component,
params: [{a: {}}],
};

View File

@@ -0,0 +1,67 @@
## Input
```javascript
// @enableNewMutationAliasingModel
import {Stringify} from 'shared-runtime';
function Component({a, b}) {
const x = {a, b};
const f = () => {
const y = [x];
return y[0];
};
const x0 = f();
const z = [x0];
const x1 = z[0];
x1.key = 'value';
return <Stringify x={x} />;
}
export const FIXTURE_ENTRYPOINT = {
fn: Component,
params: [{a: 0, b: 1}],
};
```
## Code
```javascript
import { c as _c } from "react/compiler-runtime"; // @enableNewMutationAliasingModel
import { Stringify } from "shared-runtime";
function Component(t0) {
const $ = _c(3);
const { a, b } = t0;
let t1;
if ($[0] !== a || $[1] !== b) {
const x = { a, b };
const f = () => {
const y = [x];
return y[0];
};
const x0 = f();
const z = [x0];
const x1 = z[0];
x1.key = "value";
t1 = <Stringify x={x} />;
$[0] = a;
$[1] = b;
$[2] = t1;
} else {
t1 = $[2];
}
return t1;
}
export const FIXTURE_ENTRYPOINT = {
fn: Component,
params: [{ a: 0, b: 1 }],
};
```
### Eval output
(kind: ok) <div>{"x":{"a":0,"b":1,"key":"value"}}</div>

View File

@@ -0,0 +1,20 @@
// @enableNewMutationAliasingModel
import {Stringify} from 'shared-runtime';
function Component({a, b}) {
const x = {a, b};
const f = () => {
const y = [x];
return y[0];
};
const x0 = f();
const z = [x0];
const x1 = z[0];
x1.key = 'value';
return <Stringify x={x} />;
}
export const FIXTURE_ENTRYPOINT = {
fn: Component,
params: [{a: 0, b: 1}],
};

View File

@@ -0,0 +1,67 @@
## Input
```javascript
// @enableNewMutationAliasingModel
import {Stringify} from 'shared-runtime';
function Component({a, b}) {
const x = {a, b};
const y = [x];
const f = () => {
const x0 = y[0];
return [x0];
};
const z = f();
const x1 = z[0];
x1.key = 'value';
return <Stringify x={x} />;
}
export const FIXTURE_ENTRYPOINT = {
fn: Component,
params: [{a: 0, b: 1}],
};
```
## Code
```javascript
import { c as _c } from "react/compiler-runtime"; // @enableNewMutationAliasingModel
import { Stringify } from "shared-runtime";
function Component(t0) {
const $ = _c(3);
const { a, b } = t0;
let t1;
if ($[0] !== a || $[1] !== b) {
const x = { a, b };
const y = [x];
const f = () => {
const x0 = y[0];
return [x0];
};
const z = f();
const x1 = z[0];
x1.key = "value";
t1 = <Stringify x={x} />;
$[0] = a;
$[1] = b;
$[2] = t1;
} else {
t1 = $[2];
}
return t1;
}
export const FIXTURE_ENTRYPOINT = {
fn: Component,
params: [{ a: 0, b: 1 }],
};
```
### Eval output
(kind: ok) <div>{"x":{"a":0,"b":1,"key":"value"}}</div>

View File

@@ -0,0 +1,20 @@
// @enableNewMutationAliasingModel
import {Stringify} from 'shared-runtime';
function Component({a, b}) {
const x = {a, b};
const y = [x];
const f = () => {
const x0 = y[0];
return [x0];
};
const z = f();
const x1 = z[0];
x1.key = 'value';
return <Stringify x={x} />;
}
export const FIXTURE_ENTRYPOINT = {
fn: Component,
params: [{a: 0, b: 1}],
};

View File

@@ -0,0 +1,60 @@
## Input
```javascript
// @enableNewMutationAliasingModel
import {Stringify} from 'shared-runtime';
function Component({a, b}) {
const x = {a, b};
const y = [x];
const x0 = y[0];
const z = [x0];
const x1 = z[0];
x1.key = 'value';
return <Stringify x={x} />;
}
export const FIXTURE_ENTRYPOINT = {
fn: Component,
params: [{a: 0, b: 1}],
};
```
## Code
```javascript
import { c as _c } from "react/compiler-runtime"; // @enableNewMutationAliasingModel
import { Stringify } from "shared-runtime";
function Component(t0) {
const $ = _c(3);
const { a, b } = t0;
let t1;
if ($[0] !== a || $[1] !== b) {
const x = { a, b };
const y = [x];
const x0 = y[0];
const z = [x0];
const x1 = z[0];
x1.key = "value";
t1 = <Stringify x={x} />;
$[0] = a;
$[1] = b;
$[2] = t1;
} else {
t1 = $[2];
}
return t1;
}
export const FIXTURE_ENTRYPOINT = {
fn: Component,
params: [{ a: 0, b: 1 }],
};
```
### Eval output
(kind: ok) <div>{"x":{"a":0,"b":1,"key":"value"}}</div>

View File

@@ -0,0 +1,17 @@
// @enableNewMutationAliasingModel
import {Stringify} from 'shared-runtime';
function Component({a, b}) {
const x = {a, b};
const y = [x];
const x0 = y[0];
const z = [x0];
const x1 = z[0];
x1.key = 'value';
return <Stringify x={x} />;
}
export const FIXTURE_ENTRYPOINT = {
fn: Component,
params: [{a: 0, b: 1}],
};

View File

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

View File

@@ -0,0 +1,8 @@
// @enableNewMutationAliasingModel
function Component({a, b}) {
const x = {};
const y = {x};
const z = y.x;
z.true = false;
return <div>{z}</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,54 @@
## Input
```javascript
// @enableNewMutationAliasingModel
function ReactiveRefInEffect(props) {
const ref1 = useRef('initial value');
const ref2 = useRef('initial value');
let ref;
if (props.foo) {
ref = ref1;
} else {
ref = ref2;
}
useEffect(() => print(ref));
}
```
## Code
```javascript
import { c as _c } from "react/compiler-runtime"; // @enableNewMutationAliasingModel
function ReactiveRefInEffect(props) {
const $ = _c(4);
const ref1 = useRef("initial value");
const ref2 = useRef("initial value");
let ref;
if ($[0] !== props.foo) {
if (props.foo) {
ref = ref1;
} else {
ref = ref2;
}
$[0] = props.foo;
$[1] = ref;
} else {
ref = $[1];
}
let t0;
if ($[2] !== ref) {
t0 = () => print(ref);
$[2] = ref;
$[3] = t0;
} else {
t0 = $[3];
}
useEffect(t0);
}
```
### Eval output
(kind: exception) Fixture not implemented

View File

@@ -0,0 +1,12 @@
// @enableNewMutationAliasingModel
function ReactiveRefInEffect(props) {
const ref1 = useRef('initial value');
const ref2 = useRef('initial value');
let ref;
if (props.foo) {
ref = ref1;
} else {
ref = ref2;
}
useEffect(() => print(ref));
}

View File

@@ -0,0 +1,125 @@
## Input
```javascript
// @enableNewMutationAliasingModel
import {makeArray, mutate} from 'shared-runtime';
/**
* Bug repro:
* Found differences in evaluator results
* Non-forget (expected):
* (kind: ok)
* {"bar":4,"x":{"foo":3,"wat0":"joe"}}
* {"bar":5,"x":{"foo":3,"wat0":"joe"}}
* Forget:
* (kind: ok)
* {"bar":4,"x":{"foo":3,"wat0":"joe"}}
* {"bar":5,"x":{"foo":3,"wat0":"joe","wat1":"joe"}}
*
* Fork of `capturing-func-alias-captured-mutate`, but instead of directly
* aliasing `y` via `[y]`, we make an opaque call.
*
* Note that the bug here is that we don't infer that `a = makeArray(y)`
* potentially captures a context variable into a local variable. As a result,
* we don't understand that `a[0].x = b` captures `x` into `y` -- instead, we're
* currently inferring that this lambda captures `y` (for a potential later
* mutation) and simply reads `x`.
*
* Concretely `InferReferenceEffects.hasContextRefOperand` is incorrectly not
* used when we analyze CallExpressions.
*/
function Component({foo, bar}: {foo: number; bar: number}) {
let x = {foo};
let y: {bar: number; x?: {foo: number}} = {bar};
const f0 = function () {
let a = makeArray(y); // a = [y]
let b = x;
// this writes y.x = x
a[0].x = b;
};
f0();
mutate(y.x);
return y;
}
export const FIXTURE_ENTRYPOINT = {
fn: Component,
params: [{foo: 3, bar: 4}],
sequentialRenders: [
{foo: 3, bar: 4},
{foo: 3, bar: 5},
],
};
```
## Code
```javascript
import { c as _c } from "react/compiler-runtime"; // @enableNewMutationAliasingModel
import { makeArray, mutate } from "shared-runtime";
/**
* Bug repro:
* Found differences in evaluator results
* Non-forget (expected):
* (kind: ok)
* {"bar":4,"x":{"foo":3,"wat0":"joe"}}
* {"bar":5,"x":{"foo":3,"wat0":"joe"}}
* Forget:
* (kind: ok)
* {"bar":4,"x":{"foo":3,"wat0":"joe"}}
* {"bar":5,"x":{"foo":3,"wat0":"joe","wat1":"joe"}}
*
* Fork of `capturing-func-alias-captured-mutate`, but instead of directly
* aliasing `y` via `[y]`, we make an opaque call.
*
* Note that the bug here is that we don't infer that `a = makeArray(y)`
* potentially captures a context variable into a local variable. As a result,
* we don't understand that `a[0].x = b` captures `x` into `y` -- instead, we're
* currently inferring that this lambda captures `y` (for a potential later
* mutation) and simply reads `x`.
*
* Concretely `InferReferenceEffects.hasContextRefOperand` is incorrectly not
* used when we analyze CallExpressions.
*/
function Component(t0) {
const $ = _c(3);
const { foo, bar } = t0;
let y;
if ($[0] !== bar || $[1] !== foo) {
const x = { foo };
y = { bar };
const f0 = function () {
const a = makeArray(y);
const b = x;
a[0].x = b;
};
f0();
mutate(y.x);
$[0] = bar;
$[1] = foo;
$[2] = y;
} else {
y = $[2];
}
return y;
}
export const FIXTURE_ENTRYPOINT = {
fn: Component,
params: [{ foo: 3, bar: 4 }],
sequentialRenders: [
{ foo: 3, bar: 4 },
{ foo: 3, bar: 5 },
],
};
```
### Eval output
(kind: ok) {"bar":4,"x":{"foo":3,"wat0":"joe"}}
{"bar":5,"x":{"foo":3,"wat0":"joe"}}

View File

@@ -0,0 +1,49 @@
// @enableNewMutationAliasingModel
import {makeArray, mutate} from 'shared-runtime';
/**
* Bug repro:
* Found differences in evaluator results
* Non-forget (expected):
* (kind: ok)
* {"bar":4,"x":{"foo":3,"wat0":"joe"}}
* {"bar":5,"x":{"foo":3,"wat0":"joe"}}
* Forget:
* (kind: ok)
* {"bar":4,"x":{"foo":3,"wat0":"joe"}}
* {"bar":5,"x":{"foo":3,"wat0":"joe","wat1":"joe"}}
*
* Fork of `capturing-func-alias-captured-mutate`, but instead of directly
* aliasing `y` via `[y]`, we make an opaque call.
*
* Note that the bug here is that we don't infer that `a = makeArray(y)`
* potentially captures a context variable into a local variable. As a result,
* we don't understand that `a[0].x = b` captures `x` into `y` -- instead, we're
* currently inferring that this lambda captures `y` (for a potential later
* mutation) and simply reads `x`.
*
* Concretely `InferReferenceEffects.hasContextRefOperand` is incorrectly not
* used when we analyze CallExpressions.
*/
function Component({foo, bar}: {foo: number; bar: number}) {
let x = {foo};
let y: {bar: number; x?: {foo: number}} = {bar};
const f0 = function () {
let a = makeArray(y); // a = [y]
let b = x;
// this writes y.x = x
a[0].x = b;
};
f0();
mutate(y.x);
return y;
}
export const FIXTURE_ENTRYPOINT = {
fn: Component,
params: [{foo: 3, bar: 4}],
sequentialRenders: [
{foo: 3, bar: 4},
{foo: 3, bar: 5},
],
};

View File

@@ -0,0 +1,54 @@
## Input
```javascript
// @enableNewMutationAliasingModel
function useHook({el1, el2}) {
const s = new Set();
const arr = makeArray(el1);
s.add(arr);
// Mutate after store
arr.push(el2);
s.add(makeArray(el2));
return s.size;
}
```
## Code
```javascript
import { c as _c } from "react/compiler-runtime"; // @enableNewMutationAliasingModel
function useHook(t0) {
const $ = _c(5);
const { el1, el2 } = t0;
let s;
if ($[0] !== el1 || $[1] !== el2) {
s = new Set();
const arr = makeArray(el1);
s.add(arr);
arr.push(el2);
let t1;
if ($[3] !== el2) {
t1 = makeArray(el2);
$[3] = el2;
$[4] = t1;
} else {
t1 = $[4];
}
s.add(t1);
$[0] = el1;
$[1] = el2;
$[2] = s;
} else {
s = $[2];
}
return s.size;
}
```
### Eval output
(kind: exception) Fixture not implemented

View File

@@ -0,0 +1,11 @@
// @enableNewMutationAliasingModel
function useHook({el1, el2}) {
const s = new Set();
const arr = makeArray(el1);
s.add(arr);
// Mutate after store
arr.push(el2);
s.add(makeArray(el2));
return s.size;
}

View File

@@ -0,0 +1,70 @@
## Input
```javascript
// @enablePropagateDepsInHIR @enableNewMutationAliasingModel
function useFoo(props) {
let x = [];
x.push(props.bar);
// todo: the below should memoize separately from the above
// my guess is that the phi causes the different `x` identifiers
// to get added to an alias group. this is where we need to track
// the actual state of the alias groups at the time of the mutation
props.cond ? (({x} = {x: {}}), ([x] = [[]]), x.push(props.foo)) : null;
return x;
}
export const FIXTURE_ENTRYPOINT = {
fn: useFoo,
params: [{cond: false, foo: 2, bar: 55}],
sequentialRenders: [
{cond: false, foo: 2, bar: 55},
{cond: false, foo: 3, bar: 55},
{cond: true, foo: 3, bar: 55},
],
};
```
## Code
```javascript
import { c as _c } from "react/compiler-runtime"; // @enablePropagateDepsInHIR @enableNewMutationAliasingModel
function useFoo(props) {
const $ = _c(5);
let x;
if ($[0] !== props.bar) {
x = [];
x.push(props.bar);
$[0] = props.bar;
$[1] = x;
} else {
x = $[1];
}
if ($[2] !== props.cond || $[3] !== props.foo) {
props.cond ? (([x] = [[]]), x.push(props.foo)) : null;
$[2] = props.cond;
$[3] = props.foo;
$[4] = x;
} else {
x = $[4];
}
return x;
}
export const FIXTURE_ENTRYPOINT = {
fn: useFoo,
params: [{ cond: false, foo: 2, bar: 55 }],
sequentialRenders: [
{ cond: false, foo: 2, bar: 55 },
{ cond: false, foo: 3, bar: 55 },
{ cond: true, foo: 3, bar: 55 },
],
};
```
### Eval output
(kind: ok) [55]
[55]
[3]

View File

@@ -0,0 +1,21 @@
// @enablePropagateDepsInHIR @enableNewMutationAliasingModel
function useFoo(props) {
let x = [];
x.push(props.bar);
// todo: the below should memoize separately from the above
// my guess is that the phi causes the different `x` identifiers
// to get added to an alias group. this is where we need to track
// the actual state of the alias groups at the time of the mutation
props.cond ? (({x} = {x: {}}), ([x] = [[]]), x.push(props.foo)) : null;
return x;
}
export const FIXTURE_ENTRYPOINT = {
fn: useFoo,
params: [{cond: false, foo: 2, bar: 55}],
sequentialRenders: [
{cond: false, foo: 2, bar: 55},
{cond: false, foo: 3, bar: 55},
{cond: true, foo: 3, bar: 55},
],
};

View File

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

View File

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

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) {