Compare commits

...

19 Commits

Author SHA1 Message Date
Mofei Zhang
53afbf0fb6 Update on "[compiler][rewrite] PropagateScopeDeps hir rewrite"
\### Quick background:
\#### Rvalues / temporaries:
In the compiler, unnamed temporaries that represents the evaluation of an expression
In the code snippet below, $1, $2, $3, and $4 are temporaries.
```js
// input
function Component({ bar} ) {
  const x = {a: foo(bar), b: {}};
}
// gets lowered to
[1] $2 = LoadGlobal(global) foo
[2] $3 = LoadLocal bar$1
[3] $4 = Call $2(<unknown> $3)
[4] $5 = Object {  }
[5] $6 = Object { a: $4, b: $5 }
[6] $8 = StoreLocal Const x$7 = $6
```
The compiler currently treats temporaries and named variables (e.g. `x`) differently in this pass.
- named variables may be reassigned (in fact, since we're running after LeaveSSA, a single named identifier's IdentifierId may map to multiple `Identifier` instances -- each with its own scope and mutable range)
- temporaries are replaced with their represented expressions during codegen. This is correct (mostly correct, see #29878) as we're careful to always lower the correct evaluation semantics. However, since we rewrite reactive scopes entirely (to if/else blocks), we need to track temporaries that a scope produces in `ReactiveScope.declarations` and later promote them to named variables.
In the same example, $4, $5, and $6 need to be promoted: $2 ->`t0`,  $5 ->`t1`, and $6 ->`t2`.
```js
[1] $2 = LoadGlobal(global) foo
[2] $3 = LoadLocal bar$1
scope 0:
  [3] $4 = Call $2(<unknown> $3)
scope 1:
  [4] $5 = Object {  }
scope 2:
  [5] $6 = Object { a: $4, b: $5 }
[6] $8 = StoreLocal Const x$7 = $6
```

\#### Dependencies
`ReactiveScope.dependencies` records the set of (read-only) values that a reactive scope is dependent on. This is currently limited to just variables (named variables from source and promoted temporaries) and property-loads.
All dependencies we record need to be hoistable -- i.e. reordered to just before the ReactiveScope begins. Not all PropertyLoads are hoistable.

In this example, we should not evaluate `obj.a.b` without before creating x and checking `objIsNull`.
```js
// reduce-reactive-deps/no-uncond.js
function useFoo({ obj, objIsNull }) {
  const x = [];
  if (isFalse(objIsNull)) {
    x.push(obj.a.b);
  }
  return x;
}
```

While other memoization strategies with different constraints exist, the current compiler requires that `ReactiveScope.dependencies` be re-orderable to the beginning of the reactive scope. But.. `PropertyLoad`s from null values will throw `TypeError`. This means that evaluating hoisted dependencies should throw if and only if the source program throws. (It is also a bug if source throws and compiler output does not throw. See https://github.com/facebook/react-forget/pull/2709)

---
\### Rough high level overview
1. Pass 1
Walk over instructions to gather every temporary used outside of its defining scope (same as ReactiveFunction version). These determine the sidemaps we produce, as temporaries used outside of their declaring scopes get promoted to named variables later (and are not considered hoistable rvals).
2. Pass 2 (collectHoistablePropertyLoads)
Walk over instructions to generate sidemaps:
  a. temporary identifier -> named variable and property path (e.g. `$3 -> {obj: props, path: ["a", "b"]}`)
  b. block -> accessed variables and properties (e.g. `bb0 -> [ {obj: props, path: ["a", "b"]} ]`)
  c. Walk over control flow graph to understand the set of object and property paths that can be read by each basic block. This analysis:
    - relies on post-dominator trees
    - traverses the CFG from entry (producing the set of variables/paths unconditionally evaluated *before* a block).
    - traverses the CFG from exit (producing the set of variables/paths unconditionally evaluated *after* a block).
4. Pass 3: (collectDependencies)
Walks over instructions again to record dependencies and declarations, using the previously produced sidemaps

Will add more fixture tests (although most cases should be covered in `reduce-reactive-deps`).
Tested by syncing internally and checking compilation output differences ([internal link](https://fburl.com/wiki_markdown/nazsiszd))

---
\### Followups:
1. Rewrite function expression deps
This change produces much more optimal output as the compiler now uses the function CFG to understand which variables / paths are assumed to be non-null. However, it may exacerbate [this function-expr hoisting bug](https://github.com/facebook/react/blob/main/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/bug-invalid-hoisting-functionexpr.ts). A short term fix here is to simply call some form of `collectNonNullObjects` on every function expression to find hoistable variable / paths. In the longer term, we should refactor out `FunctionExpression.deps`.

2. Enable optional paths
(a) don't count optional load temporaries as dependencies (e.g. `collectOptionalLoadRValues(...)`).
(b) add optional paths back. This is a bit tricky as we'll want to implement some merging logic for `ConditionalAccess | OptionalChain | UnconditionalAccess`. In addition, our current optional chain lowering is slightly incorrect / imprecise

[ghstack-poisoned]
2024-07-10 19:22:02 -04:00
Mofei Zhang
c088ad09f1 Update base for Update on "[compiler][rewrite] PropagateScopeDeps hir rewrite"
\### Quick background:
\#### Rvalues / temporaries:
In the compiler, unnamed temporaries that represents the evaluation of an expression
In the code snippet below, $1, $2, $3, and $4 are temporaries.
```js
// input
function Component({ bar} ) {
  const x = {a: foo(bar), b: {}};
}
// gets lowered to
[1] $2 = LoadGlobal(global) foo
[2] $3 = LoadLocal bar$1
[3] $4 = Call $2(<unknown> $3)
[4] $5 = Object {  }
[5] $6 = Object { a: $4, b: $5 }
[6] $8 = StoreLocal Const x$7 = $6
```
The compiler currently treats temporaries and named variables (e.g. `x`) differently in this pass.
- named variables may be reassigned (in fact, since we're running after LeaveSSA, a single named identifier's IdentifierId may map to multiple `Identifier` instances -- each with its own scope and mutable range)
- temporaries are replaced with their represented expressions during codegen. This is correct (mostly correct, see #29878) as we're careful to always lower the correct evaluation semantics. However, since we rewrite reactive scopes entirely (to if/else blocks), we need to track temporaries that a scope produces in `ReactiveScope.declarations` and later promote them to named variables.
In the same example, $4, $5, and $6 need to be promoted: $2 ->`t0`,  $5 ->`t1`, and $6 ->`t2`.
```js
[1] $2 = LoadGlobal(global) foo
[2] $3 = LoadLocal bar$1
scope 0:
  [3] $4 = Call $2(<unknown> $3)
scope 1:
  [4] $5 = Object {  }
scope 2:
  [5] $6 = Object { a: $4, b: $5 }
[6] $8 = StoreLocal Const x$7 = $6
```

\#### Dependencies
`ReactiveScope.dependencies` records the set of (read-only) values that a reactive scope is dependent on. This is currently limited to just variables (named variables from source and promoted temporaries) and property-loads.
All dependencies we record need to be hoistable -- i.e. reordered to just before the ReactiveScope begins. Not all PropertyLoads are hoistable.

In this example, we should not evaluate `obj.a.b` without before creating x and checking `objIsNull`.
```js
// reduce-reactive-deps/no-uncond.js
function useFoo({ obj, objIsNull }) {
  const x = [];
  if (isFalse(objIsNull)) {
    x.push(obj.a.b);
  }
  return x;
}
```

While other memoization strategies with different constraints exist, the current compiler requires that `ReactiveScope.dependencies` be re-orderable to the beginning of the reactive scope. But.. `PropertyLoad`s from null values will throw `TypeError`. This means that evaluating hoisted dependencies should throw if and only if the source program throws. (It is also a bug if source throws and compiler output does not throw. See https://github.com/facebook/react-forget/pull/2709)

---
\### Rough high level overview
1. Pass 1
Walk over instructions to gather every temporary used outside of its defining scope (same as ReactiveFunction version). These determine the sidemaps we produce, as temporaries used outside of their declaring scopes get promoted to named variables later (and are not considered hoistable rvals).
2. Pass 2 (collectHoistablePropertyLoads)
Walk over instructions to generate sidemaps:
  a. temporary identifier -> named variable and property path (e.g. `$3 -> {obj: props, path: ["a", "b"]}`)
  b. block -> accessed variables and properties (e.g. `bb0 -> [ {obj: props, path: ["a", "b"]} ]`)
  c. Walk over control flow graph to understand the set of object and property paths that can be read by each basic block. This analysis:
    - relies on post-dominator trees
    - traverses the CFG from entry (producing the set of variables/paths unconditionally evaluated *before* a block).
    - traverses the CFG from exit (producing the set of variables/paths unconditionally evaluated *after* a block).
4. Pass 3: (collectDependencies)
Walks over instructions again to record dependencies and declarations, using the previously produced sidemaps

Will add more fixture tests (although most cases should be covered in `reduce-reactive-deps`).
Tested by syncing internally and checking compilation output differences ([internal link](https://fburl.com/wiki_markdown/nazsiszd))

---
\### Followups:
1. Rewrite function expression deps
This change produces much more optimal output as the compiler now uses the function CFG to understand which variables / paths are assumed to be non-null. However, it may exacerbate [this function-expr hoisting bug](https://github.com/facebook/react/blob/main/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/bug-invalid-hoisting-functionexpr.ts). A short term fix here is to simply call some form of `collectNonNullObjects` on every function expression to find hoistable variable / paths. In the longer term, we should refactor out `FunctionExpression.deps`.

2. Enable optional paths
(a) don't count optional load temporaries as dependencies (e.g. `collectOptionalLoadRValues(...)`).
(b) add optional paths back. This is a bit tricky as we'll want to implement some merging logic for `ConditionalAccess | OptionalChain | UnconditionalAccess`. In addition, our current optional chain lowering is slightly incorrect / imprecise

[ghstack-poisoned]
2024-07-10 19:22:02 -04:00
Mofei Zhang
e0acdd81ed [wip][will rewrite] Working draft of PropagateScopeDeps hir rewrite
[ghstack-poisoned]
2024-06-24 19:19:48 -04:00
Mofei Zhang
2c9fdc4838 [compiler][ez] PrintHIR prints optional flag for debugging
[ghstack-poisoned]
2024-06-24 19:19:46 -04:00
Mofei Zhang
79198b0cd7 [compiler][patch] Patch O(n^2) traversal in validatePreserveMemo
[ghstack-poisoned]
2024-06-24 19:19:44 -04:00
Mofei Zhang
f8026d06f5 [compiler][hir] Correctly remove non-existent terminal preds when pruning labels
[ghstack-poisoned]
2024-06-24 19:19:42 -04:00
Mofei Zhang
f399ed896b [compiler][ez] Add more Array.prototype methods
[ghstack-poisoned]
2024-06-24 19:19:40 -04:00
Mofei Zhang
1c55d8810f [compiler][ez] Patch Array.concat object shape to capture callee
[ghstack-poisoned]
2024-06-24 19:19:38 -04:00
Mofei Zhang
a2f6bbe6a4 Update on "[compiler][rewrite] Patch logic for aligning scopes to non-value blocks"
Our previous logic for aligning scopes to block scopes constructs a tree of block and scope nodes. We ensured that blocks always mapped to the same node as their fallthroughs. e.g.
```js
// source
a();
if (...) {
  b();
}
c();

// HIR
bb0:
a()
if test=... consequent=bb1 fallthrough=bb2

bb1:
b()
goto bb2

bb2:
c()

// AlignReactiveScopesToBlockScopesHIR nodes
Root node (maps to both bb0 and bb2)
  |- bb1
  |- ...
```

There are two issues with the existing implementation:
1. Only scopes that overlap with the beginning of a block are aligned correctly. This is because the traversal does not store information about the block-fallthrough pair for scopes that begin *within* the block-fallthrough range.
```
\# This case gets handled correctly
         ┌──────────────┐
         │              │
         block start    block end

scope start     scope end
│               │
└───────────────┘

\# But not this one!
┌──────────────┐
│              │
block start    block end

          scope start     scope end
          │               │
          └───────────────┘
```
2. Only scopes that are directly used by a block is considered. See the `align-scopes-nested-block-structure` fixture for details.

[ghstack-poisoned]
2024-06-24 19:19:37 -04:00
Mofei Zhang
f8855cfd1e Update base for Update on "[compiler][rewrite] Patch logic for aligning scopes to non-value blocks"
Our previous logic for aligning scopes to block scopes constructs a tree of block and scope nodes. We ensured that blocks always mapped to the same node as their fallthroughs. e.g.
```js
// source
a();
if (...) {
  b();
}
c();

// HIR
bb0:
a()
if test=... consequent=bb1 fallthrough=bb2

bb1:
b()
goto bb2

bb2:
c()

// AlignReactiveScopesToBlockScopesHIR nodes
Root node (maps to both bb0 and bb2)
  |- bb1
  |- ...
```

There are two issues with the existing implementation:
1. Only scopes that overlap with the beginning of a block are aligned correctly. This is because the traversal does not store information about the block-fallthrough pair for scopes that begin *within* the block-fallthrough range.
```
\# This case gets handled correctly
         ┌──────────────┐
         │              │
         block start    block end

scope start     scope end
│               │
└───────────────┘

\# But not this one!
┌──────────────┐
│              │
block start    block end

          scope start     scope end
          │               │
          └───────────────┘
```
2. Only scopes that are directly used by a block is considered. See the `align-scopes-nested-block-structure` fixture for details.

[ghstack-poisoned]
2024-06-24 19:19:37 -04:00
Mofei Zhang
b718a77516 Update on "[compiler][rewrite] Patch logic for aligning scopes to non-value blocks"
Our previous logic for aligning scopes to block scopes constructs a tree of block and scope nodes. We ensured that blocks always mapped to the same node as their fallthroughs. e.g.
```js
// source
a();
if (...) {
  b();
}
c();

// HIR
bb0:
a()
if test=... consequent=bb1 fallthrough=bb2

bb1:
b()
goto bb2

bb2:
c()

// AlignReactiveScopesToBlockScopesHIR nodes
Root node (maps to both bb0 and bb2)
  |- bb1
  |- ...
```

There are two issues with the existing implementation:
1. Only scopes that overlap with the beginning of a block are aligned correctly. This is because the traversal does not store information about the block-fallthrough pair for scopes that begin *within* the block-fallthrough range.
```
\# This case gets handled correctly
         ┌──────────────┐
         │              │
         block start    block end

scope start     scope end
│               │
└───────────────┘

\# But not this one!
┌──────────────┐
│              │
block start    block end

          scope start     scope end
          │               │
          └───────────────┘
```
2. Only scopes that are directly used by a block is considered. See the `align-scopes-nested-block-structure` fixture for details.

[ghstack-poisoned]
2024-06-13 20:33:40 -04:00
Mofei Zhang
3fa1cceed8 Update base for Update on "[compiler][rewrite] Patch logic for aligning scopes to non-value blocks"
Our previous logic for aligning scopes to block scopes constructs a tree of block and scope nodes. We ensured that blocks always mapped to the same node as their fallthroughs. e.g.
```js
// source
a();
if (...) {
  b();
}
c();

// HIR
bb0:
a()
if test=... consequent=bb1 fallthrough=bb2

bb1:
b()
goto bb2

bb2:
c()

// AlignReactiveScopesToBlockScopesHIR nodes
Root node (maps to both bb0 and bb2)
  |- bb1
  |- ...
```

There are two issues with the existing implementation:
1. Only scopes that overlap with the beginning of a block are aligned correctly. This is because the traversal does not store information about the block-fallthrough pair for scopes that begin *within* the block-fallthrough range.
```
\# This case gets handled correctly
         ┌──────────────┐
         │              │
         block start    block end

scope start     scope end
│               │
└───────────────┘

\# But not this one!
┌──────────────┐
│              │
block start    block end

          scope start     scope end
          │               │
          └───────────────┘
```
2. Only scopes that are directly used by a block is considered. See the `align-scopes-nested-block-structure` fixture for details.

[ghstack-poisoned]
2024-06-13 20:33:39 -04:00
Mofei Zhang
9194615949 Update on "[compiler][rewrite] Patch logic for aligning scopes to non-value blocks"
Our previous logic for aligning scopes to block scopes constructs a tree of block and scope nodes. We ensured that blocks always mapped to the same node as their fallthroughs. e.g.
```js
// source
a();
if (...) {
  b();
}
c();

// HIR
bb0:
a()
if test=... consequent=bb1 fallthrough=bb2

bb1:
b()
goto bb2

bb2:
c()

// AlignReactiveScopesToBlockScopesHIR nodes
Root node (maps to both bb0 and bb2)
  |- bb1
  |- ...
```

There are two issues with the existing implementation:
1. Only scopes that overlap with the beginning of a block are aligned correctly. This is because the traversal does not store information about the block-fallthrough pair for scopes that begin *within* the block-fallthrough range.
```
         ┌──────────────┐
         │              │
         block start    block end

scope start     scope end
│               │
└───────────────┘
┌──────────────┐
│              │
block start    block end

          scope start     scope end
          │               │
          └───────────────┘
```
2. Only scopes that are directly used by a block is considered. See the `align-scopes-nested-block-structure` fixture for details.

[ghstack-poisoned]
2024-06-13 20:32:38 -04:00
Mofei Zhang
d720486b12 Update base for Update on "[compiler][rewrite] Patch logic for aligning scopes to non-value blocks"
Our previous logic for aligning scopes to block scopes constructs a tree of block and scope nodes. We ensured that blocks always mapped to the same node as their fallthroughs. e.g.
```js
// source
a();
if (...) {
  b();
}
c();

// HIR
bb0:
a()
if test=... consequent=bb1 fallthrough=bb2

bb1:
b()
goto bb2

bb2:
c()

// AlignReactiveScopesToBlockScopesHIR nodes
Root node (maps to both bb0 and bb2)
  |- bb1
  |- ...
```

There are two issues with the existing implementation:
1. Only scopes that overlap with the beginning of a block are aligned correctly. This is because the traversal does not store information about the block-fallthrough pair for scopes that begin *within* the block-fallthrough range.
```
         ┌──────────────┐
         │              │
         block start    block end

scope start     scope end
│               │
└───────────────┘
┌──────────────┐
│              │
block start    block end

          scope start     scope end
          │               │
          └───────────────┘
```
2. Only scopes that are directly used by a block is considered. See the `align-scopes-nested-block-structure` fixture for details.

[ghstack-poisoned]
2024-06-13 20:32:38 -04:00
Mofei Zhang
1ba72191a5 Update on "[compiler][draft] Patch logic for aligning scopes to non-value blocks"
[ghstack-poisoned]
2024-06-13 20:31:02 -04:00
Mofei Zhang
2d6a434df5 [compiler][draft] Patch logic for aligning scopes to non-value blocks
[ghstack-poisoned]
2024-06-13 16:10:23 -04:00
Mofei Zhang
bec373555a Update on "[compiler][fixtures] test repros: codegen, alignScope, phis"
The AlignReactiveScope bug should be simplest to fix, but it's also caught by an invariant assertion. I think a fix could be either keeping track of "active" block-fallthrough pairs (`retainWhere(pair => pair.range.end > current.instr[0].id)`) or following the approach in `assertValidBlockNesting`.
I'm tempted to pull the value-block aligning logic out into its own pass (using the current `node` tree traversal), then align to non-value blocks with the `assertValidBlockNesting` approach. Happy to hear feedback on this though!

The other two are likely bigger issues, as they're not caught by static invariants.

Update:
- removed bug-phi-reference-effect as it's been patched by josephsavona
- added bug-array-concat-should-capture

[ghstack-poisoned]
2024-06-13 12:47:42 -04:00
Mofei Zhang
0bceafbe97 Update base for Update on "[compiler][fixtures] test repros: codegen, alignScope, phis"
The AlignReactiveScope bug should be simplest to fix, but it's also caught by an invariant assertion. I think a fix could be either keeping track of "active" block-fallthrough pairs (`retainWhere(pair => pair.range.end > current.instr[0].id)`) or following the approach in `assertValidBlockNesting`.
I'm tempted to pull the value-block aligning logic out into its own pass (using the current `node` tree traversal), then align to non-value blocks with the `assertValidBlockNesting` approach. Happy to hear feedback on this though!

The other two are likely bigger issues, as they're not caught by static invariants.

Update:
- removed bug-phi-reference-effect as it's been patched by josephsavona
- added bug-array-concat-should-capture

[ghstack-poisoned]
2024-06-13 12:47:42 -04:00
Mofei Zhang
2827cbc594 [compiler][fixtures] Bug repros: codegen, alignScope, phis
[ghstack-poisoned]
2024-06-12 15:51:01 -04:00
50 changed files with 2331 additions and 250 deletions

View File

@@ -96,6 +96,7 @@ import {
validatePreservedManualMemoization,
validateUseMemo,
} from "../Validation";
import { propagateScopeDependenciesHIR } from "../HIR/PropagateScopeDependenciesHIR";
export type CompilerPipelineValue =
| { kind: "ast"; name: string; value: CodegenFunction }
@@ -306,6 +307,13 @@ function* runWithEnvironment(
});
assertTerminalSuccessorsExist(hir);
assertTerminalPredsExist(hir);
propagateScopeDependenciesHIR(hir);
yield log({
kind: "hir",
name: "PropagateScopeDependenciesHIR",
value: hir,
});
}
const reactiveFunction = buildReactiveFunction(hir);
@@ -359,17 +367,16 @@ function* runWithEnvironment(
name: "FlattenScopesWithHooks",
value: reactiveFunction,
});
assertScopeInstructionsWithinScopes(reactiveFunction);
propagateScopeDependencies(reactiveFunction);
yield log({
kind: "reactive",
name: "PropagateScopeDependencies",
value: reactiveFunction,
});
}
assertScopeInstructionsWithinScopes(reactiveFunction);
propagateScopeDependencies(reactiveFunction);
yield log({
kind: "reactive",
name: "PropagateScopeDependencies",
value: reactiveFunction,
});
pruneNonEscapingScopes(reactiveFunction);
yield log({
kind: "reactive",

View File

@@ -0,0 +1,436 @@
import { CompilerError } from "../CompilerError";
import { isMutable } from "../ReactiveScopes/InferReactiveScopeVariables";
import { Set_intersect, Set_union, getOrInsertDefault } from "../Utils/utils";
import {
BlockId,
GeneratedSource,
HIRFunction,
Identifier,
IdentifierId,
InstructionId,
Place,
ReactiveScopeDependency,
ScopeId,
} from "./HIR";
type CollectHoistablePropertyLoadsResult = {
nodes: Map<BlockId, BlockInfo>;
temporaries: Map<Identifier, Identifier>;
properties: Map<Identifier, ReactiveScopeDependency>;
};
export function collectHoistablePropertyLoads(
fn: HIRFunction,
usedOutsideDeclaringScope: Set<IdentifierId>
): CollectHoistablePropertyLoadsResult {
const result = new TemporariesSideMap();
const functionExprRvals = fn.env.config.enableTreatFunctionDepsAsConditional
? collectFunctionExpressionRValues(fn)
: new Set<IdentifierId>();
const nodes = collectNodes(
fn,
functionExprRvals,
usedOutsideDeclaringScope,
result
);
deriveNonNull(fn, nodes);
return {
nodes,
temporaries: result.temporaries,
properties: result.properties,
};
}
export type BlockInfo = {
blockId: BlockId;
scope: ScopeId | null;
preds: Set<BlockId>;
assumedNonNullObjects: Set<PropertyLoadNode>;
};
/**
* Tree data structure to dedupe property loads (e.g. a.b.c)
* and make computing sets and intersections simpler.
*/
type RootNode = {
properties: Map<string, PropertyLoadNode>;
parent: null;
// Recorded to make later computations simpler
fullPath: ReactiveScopeDependency;
root: Identifier;
};
type PropertyLoadNode =
| {
properties: Map<string, PropertyLoadNode>;
parent: PropertyLoadNode;
fullPath: ReactiveScopeDependency;
}
| RootNode;
class Tree {
roots: Map<Identifier, RootNode> = new Map();
#getOrCreateRoot(identifier: Identifier): PropertyLoadNode {
// roots can always be accessed unconditionally in JS
let rootNode = this.roots.get(identifier);
if (rootNode === undefined) {
rootNode = {
root: identifier,
properties: new Map(),
fullPath: {
identifier,
path: [],
},
parent: null,
};
this.roots.set(identifier, rootNode);
}
return rootNode;
}
static #getOrMakeProperty(
node: PropertyLoadNode,
property: string
): PropertyLoadNode {
let child = node.properties.get(property);
if (child == null) {
child = {
properties: new Map(),
parent: node,
fullPath: {
identifier: node.fullPath.identifier,
path: node.fullPath.path.concat([property]),
},
};
node.properties.set(property, child);
}
return child;
}
add(n: ReactiveScopeDependency): PropertyLoadNode {
let currNode = this.#getOrCreateRoot(n.identifier);
// We add ReactiveScopeDependencies sequentially (e.g. a.b before a.b.c),
// so subpaths should already exist.
for (let i = 0; i < n.path.length - 1; i++) {
currNode = assertNonNull(currNode.properties.get(n.path[i]));
}
currNode = Tree.#getOrMakeProperty(currNode, n.path.at(-1)!);
return currNode;
}
}
/**
* We currently lower function expression dependencies inline before the
* function expression instruction. This causes our HIR to deviate from
* JS specs.
*
* For example, note that instructions 0-2 in the below HIR are incorrectly
* hoisted.
* ```js
* // Input
* function Component(props) {
* const fn = () => cond && read(props.a.b);
* // ...
* }
*
* // HIR:
* [0] $0 = LoadLocal "props"
* [1] $1 = PropertyLoad $0, "a"
* [2] $2 = PropertyLoad $1, "b"
* [3] $3 = FunctionExpression deps=[$2] context=[$0] {
* ...
* }
*
* TODO: rewrite function expression deps
*/
function collectFunctionExpressionRValues(fn: HIRFunction): Set<IdentifierId> {
const result = new Set<IdentifierId>();
const loads = new Map<IdentifierId, IdentifierId>();
for (const [_, block] of fn.body.blocks) {
for (const instr of block.instructions) {
if (instr.value.kind === "LoadLocal") {
loads.set(instr.lvalue.identifier.id, instr.value.place.identifier.id);
} else if (instr.value.kind === "PropertyLoad") {
loads.set(instr.lvalue.identifier.id, instr.value.object.identifier.id);
} else if (instr.value.kind === "FunctionExpression") {
for (const dep of instr.value.loweredFunc.dependencies) {
result.add(dep.identifier.id);
}
}
}
}
// don't iterate newly added objects as optimization
for (const res of result) {
let curr = loads.get(res);
while (curr != null) {
result.add(curr);
curr = loads.get(curr);
}
}
return result;
}
class TemporariesSideMap {
temporaries: Map<Identifier, Identifier> = new Map();
properties: Map<Identifier, ReactiveScopeDependency> = new Map();
tree: Tree = new Tree();
declareTemporary(from: Identifier, to: Identifier): void {
this.temporaries.set(from, to);
}
declareProperty(
lvalue: Place,
object: Place,
propertyName: string,
shouldDeclare: boolean
): PropertyLoadNode {
// temporaries contains object if this is a property load chain from a named variable
// Otherwise, there is a non-trivial expression
const resolvedObject =
this.temporaries.get(object.identifier) ?? object.identifier;
const resolvedDependency = this.properties.get(resolvedObject);
let property: ReactiveScopeDependency;
if (resolvedDependency == null) {
property = {
identifier: resolvedObject,
path: [propertyName],
};
} else {
property = {
identifier: resolvedDependency.identifier,
path: [...resolvedDependency.path, propertyName],
};
}
if (shouldDeclare) {
this.properties.set(lvalue.identifier, property);
}
return this.tree.add(property);
}
}
function collectNodes(
fn: HIRFunction,
functionExprRvals: Set<IdentifierId>,
usedOutsideDeclaringScope: Set<IdentifierId>,
c: TemporariesSideMap
): Map<BlockId, BlockInfo> {
const nodes = new Map<BlockId, BlockInfo>();
const scopeStartBlocks = new Map<BlockId, ScopeId>();
for (const [blockId, block] of fn.body.blocks) {
const assumedNonNullObjects = new Set<PropertyLoadNode>();
for (const instr of block.instructions) {
const { value, lvalue } = instr;
const usedOutside = usedOutsideDeclaringScope.has(lvalue.identifier.id);
if (value.kind === "PropertyLoad") {
const propertyNode = c.declareProperty(
lvalue,
value.object,
value.property,
!usedOutside
);
if (
!functionExprRvals.has(lvalue.identifier.id) &&
!isMutable(instr, value.object)
) {
let curr = propertyNode.parent;
while (curr != null) {
assumedNonNullObjects.add(curr);
curr = curr.parent;
}
}
} else if (value.kind === "LoadLocal") {
if (
lvalue.identifier.name == null &&
value.place.identifier.name !== null &&
!usedOutside
) {
c.declareTemporary(lvalue.identifier, value.place.identifier);
}
}
/**
* Note that we do not record StoreLocals as this runs after ExitSSA.
* As a result, an expression like `(a ?? b).c` is represented as two
* StoreLocals to the same identifier id.
*/
}
if (
block.terminal.kind === "scope" ||
block.terminal.kind === "pruned-scope"
) {
scopeStartBlocks.set(block.terminal.block, block.terminal.scope.id);
}
nodes.set(blockId, {
blockId,
scope: scopeStartBlocks.get(blockId) ?? null,
preds: block.preds,
assumedNonNullObjects,
});
}
return nodes;
}
function deriveNonNull(fn: HIRFunction, nodes: Map<BlockId, BlockInfo>): void {
// block -> successors sidemap
const succ = new Map<BlockId, Set<BlockId>>();
const terminalPreds = new Set<BlockId>();
for (const [blockId, block] of fn.body.blocks) {
for (const pred of block.preds) {
const predVal = getOrInsertDefault(succ, pred, new Set());
predVal.add(blockId);
}
if (block.terminal.kind === "throw" || block.terminal.kind === "return") {
terminalPreds.add(blockId);
}
}
function recursivelyDeriveNonNull(
nodeId: BlockId,
kind: "succ" | "pred",
traversalState: Map<BlockId, "active" | "done">,
result: Map<BlockId, Set<PropertyLoadNode>>
): boolean {
if (traversalState.has(nodeId)) {
return false;
}
traversalState.set(nodeId, "active");
const node = nodes.get(nodeId);
if (node == null) {
CompilerError.invariant(false, {
reason: `Bad node ${nodeId}, kind: ${kind}`,
loc: GeneratedSource,
});
}
const neighbors = Array.from(
kind === "succ" ? succ.get(nodeId) ?? [] : node.preds
);
let changed = false;
for (const pred of neighbors) {
if (!traversalState.has(pred)) {
const neighborChanged = recursivelyDeriveNonNull(
pred,
kind,
traversalState,
result
);
changed ||= neighborChanged;
}
}
// active neighbors can be filtered out as we're solving for the following
// relation.
// X = Intersect(X_neighbors, X)
// non-active neighbors with no recorded results can occur due to backedges.
// it's not safe to assume they can be filtered out (e.g. not intersected)
const neighborAccesses = Set_intersect([
...(Array.from(neighbors)
.filter((n) => traversalState.get(n) !== "active")
.map((n) => result.get(n) ?? new Set()) as Array<
Set<PropertyLoadNode>
>),
]);
const prevSize = result.get(nodeId)?.size;
// const prevPrinted = [...(result.get(nodeId) ?? [])].map(
// printDependencyNode
// );
result.set(nodeId, Set_union(node.assumedNonNullObjects, neighborAccesses));
traversalState.set(nodeId, "done");
// const newPrinted = [...(result.get(nodeId) ?? [])].map(printDependencyNode);
// llog(" - ", nodeId, prevPrinted, newPrinted);
changed ||= prevSize !== result.get(nodeId)!.size;
CompilerError.invariant(
prevSize == null || prevSize <= result.get(nodeId)!.size,
{
reason: "[CollectHoistablePropertyLoads] Nodes shrank!",
description: `${nodeId} ${kind} ${prevSize} ${
result.get(nodeId)!.size
}`,
loc: GeneratedSource,
}
);
return changed;
}
const fromEntry = new Map<BlockId, Set<PropertyLoadNode>>();
const fromExit = new Map<BlockId, Set<PropertyLoadNode>>();
let changed = true;
const traversalState = new Map<BlockId, "done" | "active">();
const reversedBlocks = [...fn.body.blocks];
reversedBlocks.reverse();
let i = 0;
while (changed) {
i++;
changed = false;
for (const [blockId] of fn.body.blocks) {
const changed_ = recursivelyDeriveNonNull(
blockId,
"pred",
traversalState,
fromEntry
);
changed ||= changed_;
}
traversalState.clear();
for (const [blockId] of reversedBlocks) {
const changed_ = recursivelyDeriveNonNull(
blockId,
"succ",
traversalState,
fromExit
);
changed ||= changed_;
}
traversalState.clear();
}
// TODO: I can't come up with a case that requires fixed-point iteration
CompilerError.invariant(i === 2, {
reason: "require fixed-point iteration",
loc: GeneratedSource,
});
CompilerError.invariant(
fromEntry.size === fromExit.size && fromEntry.size === nodes.size,
{
reason:
"bad sizes after calculating fromEntry + fromExit " +
`${fromEntry.size} ${fromExit.size} ${nodes.size}`,
loc: GeneratedSource,
}
);
for (const [id, node] of nodes) {
node.assumedNonNullObjects = Set_union(
assertNonNull(fromEntry.get(id)),
assertNonNull(fromExit.get(id))
);
}
}
export function assertNonNull<T extends NonNullable<U>, U>(
value: T | null | undefined,
source?: string
): T {
CompilerError.invariant(value != null, {
reason: "Unexpected null",
description: source != null ? `(from ${source})` : null,
loc: GeneratedSource,
});
return value;
}

View File

@@ -0,0 +1,352 @@
/**
* 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 { CompilerError } from "../CompilerError";
import { GeneratedSource, Identifier, ReactiveScopeDependency } from "../HIR";
import { printIdentifier } from "../HIR/PrintHIR";
import { ReactiveScopePropertyDependency } from "../ReactiveScopes/DeriveMinimalDependencies";
const ENABLE_DEBUG_INVARIANTS = true;
/*
* Finalizes a set of ReactiveScopeDependencies to produce a set of minimal unconditional
* dependencies, preserving granular accesses when possible.
*
* Correctness properties:
* - All dependencies to a ReactiveBlock must be tracked.
* We can always truncate a dependency's path to a subpath, due to Forget assuming
* deep immutability. If the value produced by a subpath has not changed, then
* dependency must have not changed.
* i.e. props.a === $[..] implies props.a.b === $[..]
*
* Note the inverse is not true, but this only means a false positive (we run the
* reactive block more than needed).
* i.e. props.a !== $[..] does not imply props.a.b !== $[..]
*
* - The dependencies of a finalized ReactiveBlock must be all safe to access
* unconditionally (i.e. preserve program semantics with respect to nullthrows).
* If a dependency is only accessed within a conditional, we must track the nearest
* unconditionally accessed subpath instead.
* @param initialDeps
* @returns
*/
export class ReactiveScopeDependencyTreeHIR {
#roots: Map<Identifier, DependencyNode> = new Map();
#getOrCreateRoot(identifier: Identifier, isNonNull: boolean): DependencyNode {
// roots can always be accessed unconditionally in JS
let rootNode = this.#roots.get(identifier);
if (rootNode === undefined) {
rootNode = {
properties: new Map(),
accessType: isNonNull
? PropertyAccessType.NonNullAccess
: PropertyAccessType.MaybeNullAccess,
};
this.#roots.set(identifier, rootNode);
}
return rootNode;
}
addDependency(dep: ReactiveScopePropertyDependency): void {
const { path, optionalPath } = dep;
let currNode = this.#getOrCreateRoot(dep.identifier, false);
const accessType = PropertyAccessType.MaybeNullAccess;
currNode.accessType = merge(currNode.accessType, accessType);
for (const property of path) {
// all properties read 'on the way' to a dependency are marked as 'access'
let currChild = getOrMakeProperty(currNode, property);
currChild.accessType = merge(currChild.accessType, accessType);
currNode = currChild;
}
if (optionalPath.length === 0) {
/*
* If this property does not have a conditional path (i.e. a.b.c), the
* final property node should be marked as an conditional/unconditional
* `dependency` as based on control flow.
*/
currNode.accessType = merge(
currNode.accessType,
PropertyAccessType.MaybeNullDependency
);
} else {
/*
* Technically, we only depend on whether unconditional path `dep.path`
* is nullish (not its actual value). As long as we preserve the nullthrows
* behavior of `dep.path`, we can keep it as an access (and not promote
* to a dependency).
* See test `reduce-reactive-cond-memberexpr-join` for example.
*/
/*
* If this property has an optional path (i.e. a?.b.c), all optional
* nodes should be marked accordingly.
*/
for (const property of optionalPath) {
let currChild = getOrMakeProperty(currNode, property);
currChild.accessType = merge(
currChild.accessType,
PropertyAccessType.MaybeNullAccess
);
currNode = currChild;
}
// The final node should be marked as a conditional dependency.
currNode.accessType = merge(
currNode.accessType,
PropertyAccessType.MaybeNullDependency
);
}
}
markNodesNonNull(dep: ReactiveScopePropertyDependency): void {
const accessType = PropertyAccessType.NonNullAccess;
let currNode = this.#roots.get(dep.identifier);
let cursor = 0;
while (currNode != null && cursor < dep.path.length) {
currNode.accessType = merge(currNode.accessType, accessType);
currNode = currNode.properties.get(dep.path[cursor++]);
}
if (currNode != null) {
currNode.accessType = merge(currNode.accessType, accessType);
}
}
/**
* Derive a set of minimal dependencies that are safe to
* access unconditionally (with respect to nullthrows behavior)
*/
deriveMinimalDependencies(): Set<ReactiveScopeDependency> {
const results = new Set<ReactiveScopeDependency>();
for (const [rootId, rootNode] of this.#roots.entries()) {
if (ENABLE_DEBUG_INVARIANTS) {
assertWellFormedTree(rootNode);
}
const deps = deriveMinimalDependenciesInSubtree(rootNode, []);
for (const dep of deps) {
results.add({
identifier: rootId,
path: dep.path,
});
}
}
return results;
}
/*
* Prints dependency tree to string for debugging.
* @param includeAccesses
* @returns string representation of DependencyTree
*/
printDeps(includeAccesses: boolean): string {
let res: Array<Array<string>> = [];
for (const [rootId, rootNode] of this.#roots.entries()) {
const rootResults = printSubtree(rootNode, includeAccesses).map(
(result) => `${printIdentifier(rootId)}.${result}`
);
res.push(rootResults);
}
return res.flat().join("\n");
}
}
enum PropertyAccessType {
MaybeNullAccess = "MaybeNullAccess",
NonNullAccess = "NonNullAccess",
MaybeNullDependency = "MaybeNullDependency",
NonNullDependency = "NonNullDependency",
}
const MIN_ACCESS_TYPE = PropertyAccessType.MaybeNullAccess;
function isNonNull(access: PropertyAccessType): boolean {
return (
access === PropertyAccessType.NonNullAccess ||
access === PropertyAccessType.NonNullDependency
);
}
function isDependency(access: PropertyAccessType): boolean {
return (
access === PropertyAccessType.MaybeNullDependency ||
access === PropertyAccessType.NonNullDependency
);
}
function merge(
access1: PropertyAccessType,
access2: PropertyAccessType
): PropertyAccessType {
const resultIsNonNull = isNonNull(access1) || isNonNull(access2);
const resultIsDependency = isDependency(access1) || isDependency(access2);
/*
* Straightforward merge.
* This can be represented as bitwise OR, but is written out for readability
*
* Observe that `NonNullAccess | MaybeNullDependency` produces an
* unconditionally accessed conditional dependency. We currently use these
* as we use unconditional dependencies. (i.e. to codegen change variables)
*/
if (resultIsNonNull) {
if (resultIsDependency) {
return PropertyAccessType.NonNullDependency;
} else {
return PropertyAccessType.NonNullAccess;
}
} else {
if (resultIsDependency) {
return PropertyAccessType.MaybeNullDependency;
} else {
return PropertyAccessType.MaybeNullAccess;
}
}
}
type DependencyNode = {
properties: Map<string, DependencyNode>;
accessType: PropertyAccessType;
};
type ReduceResultNode = {
path: Array<string>;
};
function assertWellFormedTree(node: DependencyNode): void {
let nonNullInChildren = false;
for (const childNode of node.properties.values()) {
assertWellFormedTree(childNode);
nonNullInChildren ||= isNonNull(childNode.accessType);
}
if (nonNullInChildren) {
CompilerError.invariant(isNonNull(node.accessType), {
reason:
"[DeriveMinimialDependencies] Not well formed tree, unexpected nonnull node",
description: node.accessType,
loc: GeneratedSource,
});
}
}
function deriveMinimalDependenciesInSubtree(
node: DependencyNode,
path: Array<string>
): Array<ReduceResultNode> {
if (isDependency(node.accessType)) {
/**
* If this node is a dependency, we truncate the subtree
* and return this node. e.g. deps=[`obj.a`, `obj.a.b`]
* reduces to deps=[`obj.a`]
*/
return [{ path }];
} else {
if (isNonNull(node.accessType)) {
/*
* Only recurse into subtree dependencies if this node
* is known to be non-null.
*/
const result: Array<ReduceResultNode> = [];
for (const [childName, childNode] of node.properties) {
result.push(
...deriveMinimalDependenciesInSubtree(childNode, [...path, childName])
);
}
return result;
} else {
/*
* This only occurs when this subtree contains a dependency,
* but this node is potentially nullish. As we currently
* don't record optional property paths as scope dependencies,
* we truncate and record this node as a dependency.
*/
return [{ path }];
}
}
}
/*
* Demote all unconditional accesses + dependencies in subtree to the
* conditional equivalent, mutating subtree in place.
* @param subtree unconditional node representing a subtree of dependencies
*/
function _demoteSubtreeToConditional(subtree: DependencyNode): void {
const stack: Array<DependencyNode> = [subtree];
let node;
while ((node = stack.pop()) !== undefined) {
const { accessType, properties } = node;
if (!isNonNull(accessType)) {
// A conditionally accessed node should not have unconditional children
continue;
}
node.accessType = isDependency(accessType)
? PropertyAccessType.MaybeNullDependency
: PropertyAccessType.MaybeNullAccess;
for (const childNode of properties.values()) {
if (isNonNull(accessType)) {
/*
* No conditional node can have an unconditional node as a child, so
* we only process childNode if it is unconditional
*/
stack.push(childNode);
}
}
}
}
function printSubtree(
node: DependencyNode,
includeAccesses: boolean
): Array<string> {
const results: Array<string> = [];
for (const [propertyName, propertyNode] of node.properties) {
if (includeAccesses || isDependency(propertyNode.accessType)) {
results.push(`${propertyName} (${propertyNode.accessType})`);
}
const propertyResults = printSubtree(propertyNode, includeAccesses);
results.push(
...propertyResults.map((result) => `${propertyName}.${result}`)
);
}
return results;
}
function getOrMakeProperty(
node: DependencyNode,
property: string
): DependencyNode {
let child = node.properties.get(property);
if (child == null) {
child = {
properties: new Map(),
accessType: MIN_ACCESS_TYPE,
};
node.properties.set(property, child);
}
return child;
}
function mapNonNull<T extends NonNullable<V>, V, U>(
arr: Array<U>,
fn: (arg0: U) => T | undefined | null
): Array<T> | null {
const result = [];
for (let i = 0; i < arr.length; i++) {
const element = fn(arr[i]);
if (element) {
result.push(element);
} else {
return null;
}
}
return result;
}

View File

@@ -0,0 +1,578 @@
import {
IdentifierId,
ScopeId,
HIRFunction,
Place,
Instruction,
ReactiveScopeDependency,
BlockId,
Identifier,
ReactiveScope,
isObjectMethodType,
isRefValueType,
isUseRefType,
makeInstructionId,
InstructionId,
InstructionKind,
GeneratedSource,
} from "./HIR";
import {
BlockInfo,
collectHoistablePropertyLoads,
} from "./CollectHoistablePropertyLoads";
import {
NO_OP,
ScopeBlockTraversal,
eachInstructionOperand,
eachInstructionValueOperand,
eachPatternOperand,
eachTerminalOperand,
} from "./visitors";
import { ReactiveScopeDependencyTreeHIR } from "./DeriveMinimalDependenciesHIR";
import { Stack, empty } from "../Utils/Stack";
import { CompilerError } from "../CompilerError";
export function llog(..._args: any): void {
console.log(..._args);
}
type TemporariesUsedOutsideDefiningScope = {
/*
* tracks all relevant temporary declarations (currently LoadLocal and PropertyLoad)
* and the scope where they are defined
*/
declarations: Map<IdentifierId, ScopeId>;
// temporaries used outside of their defining scope
usedOutsideDeclaringScope: Set<IdentifierId>;
};
function findPromotedTemporaries(
fn: HIRFunction,
{
declarations,
usedOutsideDeclaringScope,
}: TemporariesUsedOutsideDefiningScope
): void {
const prunedScopes = new Set<ScopeId>();
const scopeTraversal = new ScopeBlockTraversal<null, null>(
null,
(scope, pruned) => {
if (pruned) {
prunedScopes.add(scope.id);
}
return null;
},
NO_OP
);
function handlePlace(place: Place): void {
const declaringScope = declarations.get(place.identifier.id);
if (
declaringScope != null &&
scopeTraversal.activeScopes.indexOf(declaringScope) === -1 &&
!prunedScopes.has(declaringScope)
) {
// Declaring scope is not active === used outside declaring scope
usedOutsideDeclaringScope.add(place.identifier.id);
}
}
function handleInstruction(instr: Instruction): void {
const scope = scopeTraversal.activeScopes.at(-1);
if (scope === undefined || prunedScopes.has(scope)) {
return;
}
switch (instr.value.kind) {
case "LoadLocal":
case "LoadContext":
case "PropertyLoad": {
declarations.set(instr.lvalue.identifier.id, scope);
break;
}
default: {
break;
}
}
}
for (const [_, block] of fn.body.blocks) {
scopeTraversal.handleBlock(block);
for (const instr of block.instructions) {
for (const place of eachInstructionOperand(instr)) {
handlePlace(place);
}
handleInstruction(instr);
}
for (const place of eachTerminalOperand(block.terminal)) {
handlePlace(place);
}
}
}
type Decl = {
id: InstructionId;
scope: Stack<ReactiveScope>;
};
class Context {
#temporariesUsedOutsideScope: Set<IdentifierId>;
#declarations: Map<IdentifierId, Decl> = new Map();
#reassignments: Map<Identifier, Decl> = new Map();
// Reactive dependencies used in the current reactive scope.
#dependencies: Array<ReactiveScopeDependency> = [];
/*
* We keep a sidemap for temporaries created by PropertyLoads, and do
* not store any control flow (i.e. #inConditionalWithinScope) here.
* - a ReactiveScope (A) containing a PropertyLoad may differ from the
* ReactiveScope (B) that uses the produced temporary.
* - codegen will inline these PropertyLoads back into scope (B)
*/
#properties: Map<Identifier, ReactiveScopeDependency>;
#temporaries: Map<Identifier, Identifier>;
#scopes: Stack<ReactiveScope> = empty();
deps: Map<ReactiveScope, Array<ReactiveScopeDependency>> = new Map();
get properties(): Map<Identifier, ReactiveScopeDependency> {
return this.#properties;
}
constructor(
temporariesUsedOutsideScope: Set<IdentifierId>,
temporaries: Map<Identifier, Identifier>,
properties: Map<Identifier, ReactiveScopeDependency>
) {
this.#temporariesUsedOutsideScope = temporariesUsedOutsideScope;
this.#temporaries = temporaries;
this.#properties = properties;
}
static enterScope(
scope: ReactiveScope,
_pruned: boolean,
context: Context
): { previousDeps: Array<ReactiveScopeDependency> } {
// Save context of previous scope
const previousDeps = context.#dependencies;
/*
* Set context for new scope
*/
context.#dependencies = [];
context.#scopes = context.#scopes.push(scope);
return { previousDeps };
}
static exitScope(
scope: ReactiveScope,
pruned: boolean,
context: Context,
state: { previousDeps: Array<ReactiveScopeDependency> }
): void {
const scopedDependencies = context.#dependencies;
// Restore context of previous scope
context.#scopes = context.#scopes.pop();
context.#dependencies = state.previousDeps;
/*
* Collect dependencies we recorded for the exiting scope and propagate
* them upward using the same rules as normal dependency collection.
* Child scopes may have dependencies on values created within the outer
* scope, which necessarily cannot be dependencies of the outer scope.
*/
for (const dep of scopedDependencies) {
if (context.#checkValidDependency(dep)) {
context.#dependencies.push(dep);
}
}
if (!pruned) {
context.deps.set(scope, scopedDependencies);
}
}
isUsedOutsideDeclaringScope(place: Place): boolean {
return this.#temporariesUsedOutsideScope.has(place.identifier.id);
}
/*
* Records where a value was declared, and optionally, the scope where the value originated from.
* This is later used to determine if a dependency should be added to a scope; if the current
* scope we are visiting is the same scope where the value originates, it can't be a dependency
* on itself.
*/
declare(identifier: Identifier, decl: Decl): void {
if (!this.#declarations.has(identifier.id)) {
this.#declarations.set(identifier.id, decl);
}
this.#reassignments.set(identifier, decl);
}
resolveTemporary(place: Place): Identifier {
return this.#temporaries.get(place.identifier) ?? place.identifier;
}
getProperty(object: Place, property: string): ReactiveScopeDependency {
return this.#getProperty(object, property, false);
}
#getProperty(
object: Place,
property: string,
_isConditional: boolean
): ReactiveScopeDependency {
/**
Example 1:
$0 = LoadLocal x
$1 = PropertyLoad $0.y
resolvedObject = x, resolvedDependency = null
Example 2:
$0 = LoadLocal x
$1 = PropertyLoad $0.y
$2 = PropertyLoad $1.z
resolvedObject = null, resolvedDependency = x.y
Example 3:
$0 = Call(...)
$1 = PropertyLoad $0.y
resolvedObject = null, resolvedDependency = null
*/
const resolvedObject = this.resolveTemporary(object);
const resolvedDependency = this.#properties.get(resolvedObject);
let objectDependency: ReactiveScopeDependency;
/*
* (1) Create the base property dependency as either a LoadLocal (from a temporary)
* or a deep copy of an existing property dependency.
*/
if (resolvedDependency === undefined) {
objectDependency = {
identifier: resolvedObject,
path: [],
};
} else {
objectDependency = {
identifier: resolvedDependency.identifier,
path: [...resolvedDependency.path],
};
}
objectDependency.path.push(property);
return objectDependency;
}
// Checks if identifier is a valid dependency in the current scope
#checkValidDependency(maybeDependency: ReactiveScopeDependency): boolean {
// ref.current access is not a valid dep
if (
isUseRefType(maybeDependency.identifier) &&
maybeDependency.path.at(0) === "current"
) {
return false;
}
// ref value is not a valid dep
if (isRefValueType(maybeDependency.identifier)) {
return false;
}
/*
* object methods are not deps because they will be codegen'ed back in to
* the object literal.
*/
if (isObjectMethodType(maybeDependency.identifier)) {
return false;
}
const identifier = maybeDependency.identifier;
/*
* If this operand is used in a scope, has a dynamic value, and was defined
* before this scope, then its a dependency of the scope.
*/
const currentDeclaration =
this.#reassignments.get(identifier) ??
this.#declarations.get(identifier.id);
const currentScope = this.currentScope.value;
return (
currentScope != null &&
currentDeclaration !== undefined &&
currentDeclaration.id < currentScope.range.start
);
}
#isScopeActive(scope: ReactiveScope): boolean {
if (this.#scopes === null) {
return false;
}
return this.#scopes.find((state) => state === scope);
}
get currentScope(): Stack<ReactiveScope> {
return this.#scopes;
}
visitOperand(place: Place): void {
const resolved = this.resolveTemporary(place);
/*
* if this operand is a temporary created for a property load, try to resolve it to
* the expanded Place. Fall back to using the operand as-is.
*/
let dependency: ReactiveScopeDependency | null = null;
if (resolved.name === null) {
const propertyDependency = this.#properties.get(resolved);
if (propertyDependency !== undefined) {
dependency = { ...propertyDependency };
}
}
// console.log(
// `resolving ${place.identifier.id} -> ${dependency ? printDependency(dependency) : ""}`
// );
this.visitDependency(
dependency ?? {
identifier: resolved,
path: [],
}
);
}
visitProperty(object: Place, property: string): void {
const nextDependency = this.#getProperty(object, property, false);
// if (object.identifier.id === 32) {
// console.log(printDependency(nextDependency));
// }
this.visitDependency(nextDependency);
}
visitDependency(maybeDependency: ReactiveScopeDependency): void {
/*
* Any value used after its originally defining scope has concluded must be added as an
* output of its defining scope. Regardless of whether its a const or not,
* some later code needs access to the value. If the current
* scope we are visiting is the same scope where the value originates, it can't be a dependency
* on itself.
*/
/*
* if originalDeclaration is undefined here, then this is not a local var
* (all decls e.g. `let x;` should be initialized in BuildHIR)
*/
const originalDeclaration = this.#declarations.get(
maybeDependency.identifier.id
);
if (
originalDeclaration !== undefined &&
originalDeclaration.scope.value !== null
) {
originalDeclaration.scope.each((scope) => {
if (!this.#isScopeActive(scope)) {
scope.declarations.set(maybeDependency.identifier.id, {
identifier: maybeDependency.identifier,
scope: originalDeclaration.scope.value!,
});
}
});
}
if (this.#checkValidDependency(maybeDependency)) {
this.#dependencies.push(maybeDependency);
}
}
/*
* Record a variable that is declared in some other scope and that is being reassigned in the
* current one as a {@link ReactiveScope.reassignments}
*/
visitReassignment(place: Place): void {
const currentScope = this.currentScope.value;
if (
currentScope != null &&
!Array.from(currentScope.reassignments).some(
(identifier) => identifier.id === place.identifier.id
) &&
this.#checkValidDependency({ identifier: place.identifier, path: [] })
) {
currentScope.reassignments.add(place.identifier);
}
}
}
function handleInstruction(instr: Instruction, context: Context) {
const { id, value, lvalue } = instr;
if (value.kind === "LoadLocal") {
if (
value.place.identifier.name === null ||
lvalue.identifier.name !== null ||
context.isUsedOutsideDeclaringScope(lvalue)
) {
context.visitOperand(value.place);
}
} else if (value.kind === "PropertyLoad") {
if (context.isUsedOutsideDeclaringScope(lvalue)) {
context.visitProperty(value.object, value.property);
} else {
const nextDependency = context.getProperty(value.object, value.property);
context.properties.set(lvalue.identifier, nextDependency);
}
} else if (value.kind === "StoreLocal") {
context.visitOperand(value.value);
if (value.lvalue.kind === InstructionKind.Reassign) {
context.visitReassignment(value.lvalue.place);
}
context.declare(value.lvalue.place.identifier, {
id,
scope: context.currentScope,
});
} else if (value.kind === "DeclareLocal" || value.kind === "DeclareContext") {
/*
* Some variables may be declared and never initialized. We need
* to retain (and hoist) these declarations if they are included
* in a reactive scope. One approach is to simply add all `DeclareLocal`s
* as scope declarations.
*/
/*
* We add context variable declarations here, not at `StoreContext`, since
* context Store / Loads are modeled as reads and mutates to the underlying
* variable reference (instead of through intermediate / inlined temporaries)
*/
context.declare(value.lvalue.place.identifier, {
id,
scope: context.currentScope,
});
} else if (value.kind === "Destructure") {
context.visitOperand(value.value);
for (const place of eachPatternOperand(value.lvalue.pattern)) {
if (value.lvalue.kind === InstructionKind.Reassign) {
context.visitReassignment(place);
}
context.declare(place.identifier, {
id,
scope: context.currentScope,
});
}
} else {
for (const operand of eachInstructionValueOperand(value)) {
context.visitOperand(operand);
}
}
context.declare(lvalue.identifier, {
id,
scope: context.currentScope,
});
}
function collectDependencies(
fn: HIRFunction,
usedOutsideDeclaringScope: Set<IdentifierId>,
temporaries: Map<Identifier, Identifier>,
properties: Map<Identifier, ReactiveScopeDependency>
) {
const context = new Context(
usedOutsideDeclaringScope,
temporaries,
properties
);
for (const param of fn.params) {
if (param.kind === "Identifier") {
context.declare(param.identifier, {
id: makeInstructionId(0),
scope: empty(),
});
} else {
context.declare(param.place.identifier, {
id: makeInstructionId(0),
scope: empty(),
});
}
}
type ScopeTraversalContext = { previousDeps: Array<ReactiveScopeDependency> };
const scopeTraversal = new ScopeBlockTraversal<
Context,
ScopeTraversalContext
>(context, Context.enterScope, Context.exitScope);
// TODO don't count optional load rvals as dep (e.g. collectOptionalLoadRValues(...))
for (const [_, block] of fn.body.blocks) {
// Handle scopes that begin or end at this block
scopeTraversal.handleBlock(block);
for (const instr of block.instructions) {
handleInstruction(instr, context);
}
for (const place of eachTerminalOperand(block.terminal)) {
context.visitOperand(place);
}
}
return context.deps;
}
/**
* Compute the set of hoistable property reads.
*/
function recordHoistablePropertyReads(
nodes: Map<BlockId, BlockInfo>,
scopeId: ScopeId,
tree: ReactiveScopeDependencyTreeHIR
): void {
let nonNullObjects: Array<ReactiveScopeDependency> | null = null;
for (const [_blockId, node] of nodes) {
if (node.scope === scopeId) {
nonNullObjects = [...node.assumedNonNullObjects].map((n) => n.fullPath);
break;
}
}
CompilerError.invariant(nonNullObjects != null, {
reason: "[PropagateScopeDependencies] Scope not found in tracked blocks",
loc: GeneratedSource,
});
for (const node of nonNullObjects) {
tree.markNodesNonNull({
...node,
optionalPath: [],
});
}
}
export function propagateScopeDependenciesHIR(fn: HIRFunction): void {
const escapingTemporaries: TemporariesUsedOutsideDefiningScope = {
declarations: new Map(),
usedOutsideDeclaringScope: new Set(),
};
findPromotedTemporaries(fn, escapingTemporaries);
const { nodes, temporaries, properties } = collectHoistablePropertyLoads(
fn,
escapingTemporaries.usedOutsideDeclaringScope
);
const scopeDeps = collectDependencies(
fn,
escapingTemporaries.usedOutsideDeclaringScope,
temporaries,
properties
);
/**
* Derive the minimal set of hoistable dependencies for each scope.
*/
for (const [scope, deps] of scopeDeps) {
const tree = new ReactiveScopeDependencyTreeHIR();
/**
* Step 1: Add every dependency used by this scope (e.g. `a.b.c`)
*/
for (const dep of deps) {
tree.addDependency({ ...dep, optionalPath: [] });
}
/**
* Step 2: Mark "hoistable" property reads, i.e. ones that will
* unconditionally run, given the basic block in which the scope
* begins.
*/
recordHoistablePropertyReads(nodes, scope.id, tree);
scope.dependencies = tree.deriveMinimalDependencies();
}
}

View File

@@ -5,8 +5,10 @@
* LICENSE file in the root directory of this source tree.
*/
import { CompilerError } from "..";
import { assertExhaustive } from "../Utils/utils";
import {
BasicBlock,
BlockId,
Instruction,
InstructionValue,
@@ -14,7 +16,9 @@ import {
Pattern,
Place,
ReactiveInstruction,
ReactiveScope,
ReactiveValue,
ScopeId,
SpreadPattern,
Terminal,
} from "./HIR";
@@ -1147,3 +1151,112 @@ export function* eachTerminalOperand(terminal: Terminal): Iterable<Place> {
}
}
}
export const NO_OP = () => null;
/**
* Helper class for traversing scope blocks in HIR-form.
*/
export class ScopeBlockTraversal<TContext, TState> {
#blockInfos: Map<
BlockId,
| {
kind: "end";
scope: ReactiveScope;
pruned: boolean;
state: TState;
}
| {
kind: "begin";
scope: ReactiveScope;
pruned: boolean;
fallthrough: BlockId;
}
> = new Map();
#context: TContext;
#enterCallback: (
scope: ReactiveScope,
pruned: boolean,
context: TContext
) => TState;
#exitCallback: (
scope: ReactiveScope,
pruned: boolean,
context: TContext,
state: TState
) => void;
activeScopes: Array<ScopeId> = [];
constructor(
context: TContext,
enter: (scope: ReactiveScope, pruned: boolean, context: TContext) => TState,
exit: (
scope: ReactiveScope,
pruned: boolean,
context: TContext,
state: TState
) => void
) {
this.#context = context;
this.#enterCallback = enter ?? null;
this.#exitCallback = exit ?? null;
}
// Handle scopes that begin or end at the start of the given block,
// invoking scope enter/exit callbacks as applicable
handleBlock(block: BasicBlock): void {
const blockInfo = this.#blockInfos.get(block.id);
if (blockInfo?.kind === "begin") {
this.#blockInfos.delete(block.id);
this.activeScopes.push(blockInfo.scope.id);
const state = this.#enterCallback(
blockInfo.scope,
blockInfo.pruned,
this.#context
);
CompilerError.invariant(!this.#blockInfos.has(blockInfo.fallthrough), {
reason: "Expected unique scope blocks and fallthroughs",
loc: blockInfo.scope.loc,
});
this.#blockInfos.set(blockInfo.fallthrough, {
kind: "end",
scope: blockInfo.scope,
pruned: blockInfo.pruned,
state,
});
} else if (blockInfo?.kind === "end") {
this.#blockInfos.delete(block.id);
const top = this.activeScopes.at(-1);
CompilerError.invariant(blockInfo.scope.id === top, {
reason:
"Expected traversed block fallthrough to match top-most active scope",
loc: block.instructions[0]?.loc ?? block.terminal.id,
});
this.activeScopes.pop();
this.#exitCallback?.(
blockInfo.scope,
blockInfo.pruned,
this.#context,
blockInfo.state
);
}
if (
block.terminal.kind === "scope" ||
block.terminal.kind === "pruned-scope"
) {
CompilerError.invariant(!this.#blockInfos.has(block.terminal.block), {
reason: "Expected unique scope blocks and fallthroughs",
loc: block.terminal.loc,
});
this.#blockInfos.set(block.terminal.block, {
kind: "begin",
scope: block.terminal.scope,
pruned: block.terminal.kind === "pruned-scope",
fallthrough: block.terminal.fallthrough,
});
}
}
}

View File

@@ -24,6 +24,9 @@ import { assertExhaustive } from "../Utils/utils";
* path: ['b', 'c'],
* optionalPath: ['d', 'e', 'f'].
* }
* TODO remove optionalPath or rewrite our PropagateScopeDeps logic to understand
* optional load expressions
*
*/
export type ReactiveScopePropertyDependency = ReactiveScopeDependency & {
optionalPath: Array<string>;

View File

@@ -84,15 +84,32 @@ export function getOrInsertDefault<U, V>(
}
export function Set_union<T>(a: Set<T>, b: Set<T>): Set<T> {
const union = new Set<T>();
for (const item of a) {
if (b.has(item)) {
union.add(item);
}
const union = new Set<T>(a);
for (const item of b) {
union.add(item);
}
return union;
}
export function Set_intersect<T>(sets: Array<Set<T>>): Set<T> {
if (sets.length === 0 || sets.some((s) => s.size === 0)) {
return new Set();
} else if (sets.length === 1) {
return new Set(sets[0]);
}
const result: Set<T> = new Set();
const first = sets[0];
outer: for (const e of first) {
for (let i = 1; i < sets.length; i++) {
if (!sets[i].has(e)) {
continue outer;
}
}
result.add(e);
}
return result;
}
export function nonNull<T extends NonNullable<U>, U>(
value: T | null | undefined
): value is T {

View File

@@ -33,9 +33,14 @@ import { c as _c } from "react/compiler-runtime"; /**
* props.b *does* influence `a`
*/
function Component(props) {
const $ = _c(2);
const $ = _c(5);
let a;
if ($[0] !== props) {
if (
$[0] !== props.a ||
$[1] !== props.b ||
$[2] !== props.c ||
$[3] !== props.d
) {
a = [];
a.push(props.a);
bb0: {
@@ -47,10 +52,13 @@ function Component(props) {
}
a.push(props.d);
$[0] = props;
$[1] = a;
$[0] = props.a;
$[1] = props.b;
$[2] = props.c;
$[3] = props.d;
$[4] = a;
} else {
a = $[1];
a = $[4];
}
return a;
}

View File

@@ -70,10 +70,10 @@ import { c as _c } from "react/compiler-runtime"; /**
* props.b does *not* influence `a`
*/
function ComponentA(props) {
const $ = _c(3);
const $ = _c(5);
let a_DEBUG;
let t0;
if ($[0] !== props) {
if ($[0] !== props.a || $[1] !== props.b || $[2] !== props.d) {
t0 = Symbol.for("react.early_return_sentinel");
bb0: {
a_DEBUG = [];
@@ -85,12 +85,14 @@ function ComponentA(props) {
a_DEBUG.push(props.d);
}
$[0] = props;
$[1] = a_DEBUG;
$[2] = t0;
$[0] = props.a;
$[1] = props.b;
$[2] = props.d;
$[3] = a_DEBUG;
$[4] = t0;
} else {
a_DEBUG = $[1];
t0 = $[2];
a_DEBUG = $[3];
t0 = $[4];
}
if (t0 !== Symbol.for("react.early_return_sentinel")) {
return t0;
@@ -102,9 +104,14 @@ function ComponentA(props) {
* props.b *does* influence `a`
*/
function ComponentB(props) {
const $ = _c(2);
const $ = _c(5);
let a;
if ($[0] !== props) {
if (
$[0] !== props.a ||
$[1] !== props.b ||
$[2] !== props.c ||
$[3] !== props.d
) {
a = [];
a.push(props.a);
if (props.b) {
@@ -112,10 +119,13 @@ function ComponentB(props) {
}
a.push(props.d);
$[0] = props;
$[1] = a;
$[0] = props.a;
$[1] = props.b;
$[2] = props.c;
$[3] = props.d;
$[4] = a;
} else {
a = $[1];
a = $[4];
}
return a;
}
@@ -124,10 +134,15 @@ function ComponentB(props) {
* props.b *does* influence `a`, but only in a way that is never observable
*/
function ComponentC(props) {
const $ = _c(3);
const $ = _c(6);
let a;
let t0;
if ($[0] !== props) {
if (
$[0] !== props.a ||
$[1] !== props.b ||
$[2] !== props.c ||
$[3] !== props.d
) {
t0 = Symbol.for("react.early_return_sentinel");
bb0: {
a = [];
@@ -140,12 +155,15 @@ function ComponentC(props) {
a.push(props.d);
}
$[0] = props;
$[1] = a;
$[2] = t0;
$[0] = props.a;
$[1] = props.b;
$[2] = props.c;
$[3] = props.d;
$[4] = a;
$[5] = t0;
} else {
a = $[1];
t0 = $[2];
a = $[4];
t0 = $[5];
}
if (t0 !== Symbol.for("react.early_return_sentinel")) {
return t0;
@@ -157,10 +175,15 @@ function ComponentC(props) {
* props.b *does* influence `a`
*/
function ComponentD(props) {
const $ = _c(3);
const $ = _c(6);
let a;
let t0;
if ($[0] !== props) {
if (
$[0] !== props.a ||
$[1] !== props.b ||
$[2] !== props.c ||
$[3] !== props.d
) {
t0 = Symbol.for("react.early_return_sentinel");
bb0: {
a = [];
@@ -173,12 +196,15 @@ function ComponentD(props) {
a.push(props.d);
}
$[0] = props;
$[1] = a;
$[2] = t0;
$[0] = props.a;
$[1] = props.b;
$[2] = props.c;
$[3] = props.d;
$[4] = a;
$[5] = t0;
} else {
a = $[1];
t0 = $[2];
a = $[4];
t0 = $[5];
}
if (t0 !== Symbol.for("react.early_return_sentinel")) {
return t0;

View File

@@ -36,9 +36,9 @@ function mayMutate() {}
```javascript
import { c as _c } from "react/compiler-runtime";
function ComponentA(props) {
const $ = _c(2);
const $ = _c(4);
let t0;
if ($[0] !== props) {
if ($[0] !== props.p0 || $[1] !== props.p1 || $[2] !== props.p2) {
const a = [];
const b = [];
if (b) {
@@ -49,18 +49,20 @@ function ComponentA(props) {
}
t0 = <Foo a={a} b={b} />;
$[0] = props;
$[1] = t0;
$[0] = props.p0;
$[1] = props.p1;
$[2] = props.p2;
$[3] = t0;
} else {
t0 = $[1];
t0 = $[3];
}
return t0;
}
function ComponentB(props) {
const $ = _c(2);
const $ = _c(4);
let t0;
if ($[0] !== props) {
if ($[0] !== props.p0 || $[1] !== props.p1 || $[2] !== props.p2) {
const a = [];
const b = [];
if (mayMutate(b)) {
@@ -71,10 +73,12 @@ function ComponentB(props) {
}
t0 = <Foo a={a} b={b} />;
$[0] = props;
$[1] = t0;
$[0] = props.p0;
$[1] = props.p1;
$[2] = props.p2;
$[3] = t0;
} else {
t0 = $[1];
t0 = $[3];
}
return t0;
}

View File

@@ -30,7 +30,7 @@ import { identity } from "shared-runtime";
function Component(props) {
const $ = _c(4);
let x;
if ($[0] !== props.value) {
if ($[0] !== props) {
const [t0] = props.value;
x = t0;
const foo = () => {
@@ -38,7 +38,7 @@ function Component(props) {
};
foo();
$[0] = props.value;
$[0] = props;
$[1] = x;
} else {
x = $[1];

View File

@@ -29,7 +29,7 @@ import { identity } from "shared-runtime";
function Component(props) {
const $ = _c(4);
let x;
if ($[0] !== props.value) {
if ($[0] !== props) {
const [t0] = props.value;
x = t0;
const foo = () => {
@@ -37,7 +37,7 @@ function Component(props) {
};
foo();
$[0] = props.value;
$[0] = props;
$[1] = x;
} else {
x = $[1];

View File

@@ -31,9 +31,9 @@ export const FIXTURE_ENTRYPOINT = {
```javascript
import { c as _c } from "react/compiler-runtime";
function Component(props) {
const $ = _c(5);
const $ = _c(7);
let t0;
if ($[0] !== props) {
if ($[0] !== props.cond || $[1] !== props.a || $[2] !== props.b) {
t0 = Symbol.for("react.early_return_sentinel");
bb0: {
const x = [];
@@ -41,12 +41,12 @@ function Component(props) {
x.push(props.a);
if (props.b) {
let t1;
if ($[2] !== props.b) {
if ($[4] !== props.b) {
t1 = [props.b];
$[2] = props.b;
$[3] = t1;
$[4] = props.b;
$[5] = t1;
} else {
t1 = $[3];
t1 = $[5];
}
const y = t1;
x.push(y);
@@ -58,20 +58,22 @@ function Component(props) {
break bb0;
} else {
let t1;
if ($[4] === Symbol.for("react.memo_cache_sentinel")) {
if ($[6] === Symbol.for("react.memo_cache_sentinel")) {
t1 = foo();
$[4] = t1;
$[6] = t1;
} else {
t1 = $[4];
t1 = $[6];
}
t0 = t1;
break bb0;
}
}
$[0] = props;
$[1] = t0;
$[0] = props.cond;
$[1] = props.a;
$[2] = props.b;
$[3] = t0;
} else {
t0 = $[1];
t0 = $[3];
}
if (t0 !== Symbol.for("react.early_return_sentinel")) {
return t0;

View File

@@ -45,9 +45,9 @@ import { c as _c } from "react/compiler-runtime";
import { makeArray } from "shared-runtime";
function Component(props) {
const $ = _c(4);
const $ = _c(6);
let t0;
if ($[0] !== props) {
if ($[0] !== props.cond || $[1] !== props.a || $[2] !== props.b) {
t0 = Symbol.for("react.early_return_sentinel");
bb0: {
const x = [];
@@ -57,21 +57,23 @@ function Component(props) {
break bb0;
} else {
let t1;
if ($[2] !== props.b) {
if ($[4] !== props.b) {
t1 = makeArray(props.b);
$[2] = props.b;
$[3] = t1;
$[4] = props.b;
$[5] = t1;
} else {
t1 = $[3];
t1 = $[5];
}
t0 = t1;
break bb0;
}
}
$[0] = props;
$[1] = t0;
$[0] = props.cond;
$[1] = props.a;
$[2] = props.b;
$[3] = t0;
} else {
t0 = $[1];
t0 = $[3];
}
if (t0 !== Symbol.for("react.early_return_sentinel")) {
return t0;

View File

@@ -0,0 +1,58 @@
## Input
```javascript
// @enableTreatFunctionDepsAsConditional
import { Stringify } from "shared-runtime";
function Component({ props }) {
const f = () => props.a.b;
return <Stringify f={props == null ? () => {} : f} />;
}
export const FIXTURE_ENTRYPOINT = {
fn: Component,
params: [{ props: null }],
};
```
## Code
```javascript
import { c as _c } from "react/compiler-runtime"; // @enableTreatFunctionDepsAsConditional
import { Stringify } from "shared-runtime";
function Component(t0) {
const $ = _c(5);
const { props } = t0;
let t1;
if ($[0] !== props) {
t1 = () => props.a.b;
$[0] = props;
$[1] = t1;
} else {
t1 = $[1];
}
const f = t1;
let t2;
if ($[2] !== props || $[3] !== f) {
t2 = <Stringify f={props == null ? () => {} : f} />;
$[2] = props;
$[3] = f;
$[4] = t2;
} else {
t2 = $[4];
}
return t2;
}
export const FIXTURE_ENTRYPOINT = {
fn: Component,
params: [{ props: null }],
};
```
### Eval output
(kind: ok) <div>{"f":"[[ function params=0 ]]"}</div>

View File

@@ -0,0 +1,12 @@
// @enableTreatFunctionDepsAsConditional
import { Stringify } from "shared-runtime";
function Component({ props }) {
const f = () => props.a.b;
return <Stringify f={props == null ? () => {} : f} />;
}
export const FIXTURE_ENTRYPOINT = {
fn: Component,
params: [{ props: null }],
};

View File

@@ -25,11 +25,11 @@ import { c as _c } from "react/compiler-runtime"; // @enableTreatFunctionDepsAsC
function Component(props) {
const $ = _c(5);
let t0;
if ($[0] !== props) {
if ($[0] !== props.bar) {
t0 = function getLength() {
return props.bar.length;
};
$[0] = props;
$[0] = props.bar;
$[1] = t0;
} else {
t0 = $[1];

View File

@@ -26,9 +26,9 @@ export const FIXTURE_ENTRYPOINT = {
```javascript
import { c as _c } from "react/compiler-runtime";
function Component(props) {
const $ = _c(2);
const $ = _c(3);
let items;
if ($[0] !== props) {
if ($[0] !== props.cond || $[1] !== props.a) {
let t0;
if (props.cond) {
t0 = [];
@@ -38,10 +38,11 @@ function Component(props) {
items = t0;
items?.push(props.a);
$[0] = props;
$[1] = items;
$[0] = props.cond;
$[1] = props.a;
$[2] = items;
} else {
items = $[1];
items = $[2];
}
return items;
}

View File

@@ -29,9 +29,9 @@ import { c as _c } from "react/compiler-runtime";
import { makeObject_Primitives } from "shared-runtime";
function Component(props) {
const $ = _c(2);
const $ = _c(3);
let t0;
if ($[0] !== props) {
if ($[0] !== props.cond || $[1] !== props.value) {
t0 = Symbol.for("react.early_return_sentinel");
bb0: {
const object = makeObject_Primitives();
@@ -45,10 +45,11 @@ function Component(props) {
break bb0;
}
}
$[0] = props;
$[1] = t0;
$[0] = props.cond;
$[1] = props.value;
$[2] = t0;
} else {
t0 = $[1];
t0 = $[2];
}
if (t0 !== Symbol.for("react.early_return_sentinel")) {
return t0;

View File

@@ -30,10 +30,10 @@ export const FIXTURE_ENTRYPOINT = {
```javascript
import { c as _c } from "react/compiler-runtime";
function Component(props) {
const $ = _c(4);
const $ = _c(6);
let y;
let t0;
if ($[0] !== props) {
if ($[0] !== props.cond || $[1] !== props.a || $[2] !== props.b) {
t0 = Symbol.for("react.early_return_sentinel");
bb0: {
const x = [];
@@ -43,11 +43,11 @@ function Component(props) {
break bb0;
} else {
let t1;
if ($[3] === Symbol.for("react.memo_cache_sentinel")) {
if ($[5] === Symbol.for("react.memo_cache_sentinel")) {
t1 = foo();
$[3] = t1;
$[5] = t1;
} else {
t1 = $[3];
t1 = $[5];
}
y = t1;
if (props.b) {
@@ -56,12 +56,14 @@ function Component(props) {
}
}
}
$[0] = props;
$[1] = y;
$[2] = t0;
$[0] = props.cond;
$[1] = props.a;
$[2] = props.b;
$[3] = y;
$[4] = t0;
} else {
y = $[1];
t0 = $[2];
y = $[3];
t0 = $[4];
}
if (t0 !== Symbol.for("react.early_return_sentinel")) {
return t0;

View File

@@ -36,9 +36,9 @@ export const FIXTURE_ENTRYPOINT = {
```javascript
import { c as _c } from "react/compiler-runtime";
function Component(props) {
const $ = _c(2);
const $ = _c(3);
let t0;
if ($[0] !== props) {
if ($[0] !== props.cond || $[1] !== props.value) {
const x = {};
let y;
if (props.cond) {
@@ -50,10 +50,11 @@ function Component(props) {
y.push(x);
t0 = [x, y];
$[0] = props;
$[1] = t0;
$[0] = props.cond;
$[1] = props.value;
$[2] = t0;
} else {
t0 = $[1];
t0 = $[2];
}
return t0;
}

View File

@@ -32,7 +32,7 @@ export const FIXTURE_ENTRYPOINT = {
```javascript
import { c as _c } from "react/compiler-runtime"; // @debug
function Component(props) {
const $ = _c(5);
const $ = _c(6);
let t0;
if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
t0 = {};
@@ -42,7 +42,7 @@ function Component(props) {
}
const x = t0;
let y;
if ($[1] !== props) {
if ($[1] !== props.cond || $[2] !== props.a) {
if (props.cond) {
y = {};
} else {
@@ -50,18 +50,19 @@ function Component(props) {
}
y.x = x;
$[1] = props;
$[2] = y;
$[1] = props.cond;
$[2] = props.a;
$[3] = y;
} else {
y = $[2];
y = $[3];
}
let t1;
if ($[3] !== y) {
if ($[4] !== y) {
t1 = [x, y];
$[3] = y;
$[4] = t1;
$[4] = y;
$[5] = t1;
} else {
t1 = $[4];
t1 = $[5];
}
return t1;
}

View File

@@ -61,20 +61,13 @@ import { c as _c } from "react/compiler-runtime"; // This tests an optimization,
import { CONST_TRUE, setProperty } from "shared-runtime";
function useJoinCondDepsInUncondScopes(props) {
const $ = _c(4);
const $ = _c(2);
let t0;
if ($[0] !== props.a.b) {
const y = {};
let x;
if ($[2] !== props) {
x = {};
if (CONST_TRUE) {
setProperty(x, props.a.b);
}
$[2] = props;
$[3] = x;
} else {
x = $[3];
const x = {};
if (CONST_TRUE) {
setProperty(x, props.a.b);
}
setProperty(y, props.a.b);

View File

@@ -34,19 +34,20 @@ import { identity } from "shared-runtime";
// and promote it to an unconditional dependency.
function usePromoteUnconditionalAccessToDependency(props, other) {
const $ = _c(3);
const $ = _c(4);
let x;
if ($[0] !== props.a || $[1] !== other) {
if ($[0] !== props.a.a.a || $[1] !== props.a.b || $[2] !== other) {
x = {};
x.a = props.a.a.a;
if (identity(other)) {
x.c = props.a.b.c;
}
$[0] = props.a;
$[1] = other;
$[2] = x;
$[0] = props.a.a.a;
$[1] = props.a.b;
$[2] = other;
$[3] = x;
} else {
x = $[2];
x = $[3];
}
return x;
}

View File

@@ -0,0 +1,74 @@
## Input
```javascript
function useFoo(a, b, c) {
let x = {};
write(x, a);
const y = [];
if (x.a != null) {
y.push(x.a.b);
}
y.push(b);
x = makeThing();
write(x.a.b);
return [y, x.a.b];
}
```
## Code
```javascript
import { c as _c } from "react/compiler-runtime";
function useFoo(a, b, c) {
const $ = _c(9);
let x;
if ($[0] !== a) {
x = {};
write(x, a);
$[0] = a;
$[1] = x;
} else {
x = $[1];
}
let y;
if ($[2] !== x.a || $[3] !== b) {
y = [];
if (x.a != null) {
y.push(x.a.b);
}
y.push(b);
$[2] = x.a;
$[3] = b;
$[4] = y;
} else {
y = $[4];
}
if ($[5] === Symbol.for("react.memo_cache_sentinel")) {
x = makeThing();
write(x.a.b);
$[5] = x;
} else {
x = $[5];
}
let t0;
if ($[6] !== y || $[7] !== x.a.b) {
t0 = [y, x.a.b];
$[6] = y;
$[7] = x.a.b;
$[8] = t0;
} else {
t0 = $[8];
}
return t0;
}
```
### Eval output
(kind: exception) Fixture not implemented

View File

@@ -0,0 +1,15 @@
function useFoo(a, b, c) {
let x = {};
write(x, a);
const y = [];
if (x.a != null) {
y.push(x.a.b);
}
y.push(b);
x = makeThing();
write(x.a.b);
return [y, x.a.b];
}

View File

@@ -0,0 +1,105 @@
## Input
```javascript
// x.a.b was accessed unconditionally within the mutable range of x.
// As a result, we cannot infer anything about whether `x` or `x.a`
// may be null. This means that it's not safe to hoist reads from x
// (e.g. take `x.a` or `x.a.b` as a dependency).
import { identity, makeObject_Primitives, setProperty } from "shared-runtime";
function Component({ cond, other }) {
const x = makeObject_Primitives();
setProperty(x, { b: 3, other }, "a");
identity(x.a.b);
if (!cond) {
x.a = null;
}
const y = [identity(cond) && x.a.b];
return y;
}
export const FIXTURE_ENTRYPOINT = {
fn: Component,
params: [{ cond: false }],
sequentialRenders: [
{ cond: false },
{ cond: false },
{ cond: false, other: 8 },
{ cond: true },
{ cond: true },
],
};
```
## Code
```javascript
import { c as _c } from "react/compiler-runtime"; // x.a.b was accessed unconditionally within the mutable range of x.
// As a result, we cannot infer anything about whether `x` or `x.a`
// may be null. This means that it's not safe to hoist reads from x
// (e.g. take `x.a` or `x.a.b` as a dependency).
import { identity, makeObject_Primitives, setProperty } from "shared-runtime";
function Component(t0) {
const $ = _c(8);
const { cond, other } = t0;
let x;
if ($[0] !== other || $[1] !== cond) {
x = makeObject_Primitives();
setProperty(x, { b: 3, other }, "a");
identity(x.a.b);
if (!cond) {
x.a = null;
}
$[0] = other;
$[1] = cond;
$[2] = x;
} else {
x = $[2];
}
let t1;
if ($[3] !== cond || $[4] !== x) {
t1 = identity(cond) && x.a.b;
$[3] = cond;
$[4] = x;
$[5] = t1;
} else {
t1 = $[5];
}
let t2;
if ($[6] !== t1) {
t2 = [t1];
$[6] = t1;
$[7] = t2;
} else {
t2 = $[7];
}
const y = t2;
return y;
}
export const FIXTURE_ENTRYPOINT = {
fn: Component,
params: [{ cond: false }],
sequentialRenders: [
{ cond: false },
{ cond: false },
{ cond: false, other: 8 },
{ cond: true },
{ cond: true },
],
};
```
### Eval output
(kind: ok) [false]
[false]
[false]
[3]
[3]

View File

@@ -0,0 +1,30 @@
// x.a.b was accessed unconditionally within the mutable range of x.
// As a result, we cannot infer anything about whether `x` or `x.a`
// may be null. This means that it's not safe to hoist reads from x
// (e.g. take `x.a` or `x.a.b` as a dependency).
import { identity, makeObject_Primitives, setProperty } from "shared-runtime";
function Component({ cond, other }) {
const x = makeObject_Primitives();
setProperty(x, { b: 3, other }, "a");
identity(x.a.b);
if (!cond) {
x.a = null;
}
const y = [identity(cond) && x.a.b];
return y;
}
export const FIXTURE_ENTRYPOINT = {
fn: Component,
params: [{ cond: false }],
sequentialRenders: [
{ cond: false },
{ cond: false },
{ cond: false, other: 8 },
{ cond: true },
{ cond: true },
],
};

View File

@@ -31,10 +31,16 @@ export const FIXTURE_ENTRYPOINT = {
```javascript
import { c as _c } from "react/compiler-runtime";
function Component(props) {
const $ = _c(4);
const $ = _c(7);
let x = 0;
let values;
if ($[0] !== props || $[1] !== x) {
if (
$[0] !== props.a ||
$[1] !== props.b ||
$[2] !== props.c ||
$[3] !== props.d ||
$[4] !== x
) {
values = [];
const y = props.a || props.b;
values.push(y);
@@ -48,13 +54,16 @@ function Component(props) {
}
values.push(x);
$[0] = props;
$[1] = x;
$[2] = values;
$[3] = x;
$[0] = props.a;
$[1] = props.b;
$[2] = props.c;
$[3] = props.d;
$[4] = x;
$[5] = values;
$[6] = x;
} else {
values = $[2];
x = $[3];
values = $[5];
x = $[6];
}
return values;
}

View File

@@ -24,9 +24,9 @@ function Component(props) {
```javascript
import { c as _c } from "react/compiler-runtime";
function Component(props) {
const $ = _c(2);
const $ = _c(3);
let t0;
if ($[0] !== props) {
if ($[0] !== props.p0 || $[1] !== props.p1) {
const x = [];
let y;
if (props.p0) {
@@ -40,10 +40,11 @@ function Component(props) {
{y}
</Component>
);
$[0] = props;
$[1] = t0;
$[0] = props.p0;
$[1] = props.p1;
$[2] = t0;
} else {
t0 = $[1];
t0 = $[2];
}
return t0;
}

View File

@@ -17,17 +17,19 @@ function foo(props) {
```javascript
import { c as _c } from "react/compiler-runtime";
function foo(props) {
const $ = _c(2);
const $ = _c(4);
let x;
if ($[0] !== props) {
if ($[0] !== props.bar || $[1] !== props.cond || $[2] !== props.foo) {
x = [];
x.push(props.bar);
props.cond ? (([x] = [[]]), x.push(props.foo)) : null;
mut(x);
$[0] = props;
$[1] = x;
$[0] = props.bar;
$[1] = props.cond;
$[2] = props.foo;
$[3] = x;
} else {
x = $[1];
x = $[3];
}
return x;
}

View File

@@ -22,7 +22,7 @@ export const FIXTURE_ENTRYPOINT = {
```javascript
import { c as _c } from "react/compiler-runtime";
function foo(props) {
const $ = _c(4);
const $ = _c(5);
let x;
if ($[0] !== props.bar) {
x = [];
@@ -32,12 +32,13 @@ function foo(props) {
} else {
x = $[1];
}
if ($[2] !== props) {
if ($[2] !== props.cond || $[3] !== props.foo) {
props.cond ? (([x] = [[]]), x.push(props.foo)) : null;
$[2] = props;
$[3] = x;
$[2] = props.cond;
$[3] = props.foo;
$[4] = x;
} else {
x = $[3];
x = $[4];
}
return x;
}

View File

@@ -17,17 +17,19 @@ function foo(props) {
```javascript
import { c as _c } from "react/compiler-runtime";
function foo(props) {
const $ = _c(2);
const $ = _c(4);
let x;
if ($[0] !== props) {
if ($[0] !== props.bar || $[1] !== props.cond || $[2] !== props.foo) {
x = [];
x.push(props.bar);
props.cond ? ((x = []), x.push(props.foo)) : null;
mut(x);
$[0] = props;
$[1] = x;
$[0] = props.bar;
$[1] = props.cond;
$[2] = props.foo;
$[3] = x;
} else {
x = $[1];
x = $[3];
}
return x;
}

View File

@@ -22,7 +22,7 @@ export const FIXTURE_ENTRYPOINT = {
```javascript
import { c as _c } from "react/compiler-runtime";
function foo(props) {
const $ = _c(4);
const $ = _c(5);
let x;
if ($[0] !== props.bar) {
x = [];
@@ -32,12 +32,13 @@ function foo(props) {
} else {
x = $[1];
}
if ($[2] !== props) {
if ($[2] !== props.cond || $[3] !== props.foo) {
props.cond ? ((x = []), x.push(props.foo)) : null;
$[2] = props;
$[3] = x;
$[2] = props.cond;
$[3] = props.foo;
$[4] = x;
} else {
x = $[3];
x = $[4];
}
return x;
}

View File

@@ -31,17 +31,19 @@ export const FIXTURE_ENTRYPOINT = {
import { c as _c } from "react/compiler-runtime";
import { arrayPush } from "shared-runtime";
function foo(props) {
const $ = _c(2);
const $ = _c(4);
let x;
if ($[0] !== props) {
if ($[0] !== props.bar || $[1] !== props.cond || $[2] !== props.foo) {
x = [];
x.push(props.bar);
props.cond ? ((x = []), x.push(props.foo)) : ((x = []), x.push(props.bar));
arrayPush(x, 4);
$[0] = props;
$[1] = x;
$[0] = props.bar;
$[1] = props.cond;
$[2] = props.foo;
$[3] = x;
} else {
x = $[1];
x = $[3];
}
return x;
}

View File

@@ -24,7 +24,7 @@ export const FIXTURE_ENTRYPOINT = {
```javascript
import { c as _c } from "react/compiler-runtime";
function foo(props) {
const $ = _c(4);
const $ = _c(6);
let x;
if ($[0] !== props.bar) {
x = [];
@@ -34,12 +34,14 @@ function foo(props) {
} else {
x = $[1];
}
if ($[2] !== props) {
if ($[2] !== props.cond || $[3] !== props.foo || $[4] !== props.bar) {
props.cond ? ((x = []), x.push(props.foo)) : ((x = []), x.push(props.bar));
$[2] = props;
$[3] = x;
$[2] = props.cond;
$[3] = props.foo;
$[4] = props.bar;
$[5] = x;
} else {
x = $[3];
x = $[5];
}
return x;
}

View File

@@ -25,9 +25,9 @@ function foo(props) {
```javascript
import { c as _c } from "react/compiler-runtime";
function foo(props) {
const $ = _c(2);
const $ = _c(4);
let x;
if ($[0] !== props) {
if ($[0] !== props.bar || $[1] !== props.cond || $[2] !== props.foo) {
x = [];
x.push(props.bar);
if (props.cond) {
@@ -39,10 +39,12 @@ function foo(props) {
}
mut(x);
$[0] = props;
$[1] = x;
$[0] = props.bar;
$[1] = props.cond;
$[2] = props.foo;
$[3] = x;
} else {
x = $[1];
x = $[3];
}
return x;
}

View File

@@ -21,9 +21,9 @@ function foo(props) {
```javascript
import { c as _c } from "react/compiler-runtime";
function foo(props) {
const $ = _c(2);
const $ = _c(4);
let x;
if ($[0] !== props) {
if ($[0] !== props.bar || $[1] !== props.cond || $[2] !== props.foo) {
({ x } = { x: [] });
x.push(props.bar);
if (props.cond) {
@@ -32,10 +32,12 @@ function foo(props) {
}
mut(x);
$[0] = props;
$[1] = x;
$[0] = props.bar;
$[1] = props.cond;
$[2] = props.foo;
$[3] = x;
} else {
x = $[1];
x = $[3];
}
return x;
}

View File

@@ -21,9 +21,9 @@ function foo(props) {
```javascript
import { c as _c } from "react/compiler-runtime";
function foo(props) {
const $ = _c(2);
const $ = _c(4);
let x;
if ($[0] !== props) {
if ($[0] !== props.bar || $[1] !== props.cond || $[2] !== props.foo) {
x = [];
x.push(props.bar);
if (props.cond) {
@@ -32,10 +32,12 @@ function foo(props) {
}
mut(x);
$[0] = props;
$[1] = x;
$[0] = props.bar;
$[1] = props.cond;
$[2] = props.foo;
$[3] = x;
} else {
x = $[1];
x = $[3];
}
return x;
}

View File

@@ -33,10 +33,10 @@ function Component(props) {
```javascript
import { c as _c } from "react/compiler-runtime";
function Component(props) {
const $ = _c(7);
const $ = _c(8);
let y;
let t0;
if ($[0] !== props) {
if ($[0] !== props.p0 || $[1] !== props.p2) {
const x = [];
bb0: switch (props.p0) {
case 1: {
@@ -45,11 +45,11 @@ function Component(props) {
case true: {
x.push(props.p2);
let t1;
if ($[3] === Symbol.for("react.memo_cache_sentinel")) {
if ($[4] === Symbol.for("react.memo_cache_sentinel")) {
t1 = [];
$[3] = t1;
$[4] = t1;
} else {
t1 = $[3];
t1 = $[4];
}
y = t1;
}
@@ -62,23 +62,24 @@ function Component(props) {
}
t0 = <Component data={x} />;
$[0] = props;
$[1] = y;
$[2] = t0;
$[0] = props.p0;
$[1] = props.p2;
$[2] = y;
$[3] = t0;
} else {
y = $[1];
t0 = $[2];
y = $[2];
t0 = $[3];
}
const child = t0;
y.push(props.p4);
let t1;
if ($[4] !== y || $[5] !== child) {
if ($[5] !== y || $[6] !== child) {
t1 = <Component data={y}>{child}</Component>;
$[4] = y;
$[5] = child;
$[6] = t1;
$[5] = y;
$[6] = child;
$[7] = t1;
} else {
t1 = $[6];
t1 = $[7];
}
return t1;
}

View File

@@ -28,10 +28,10 @@ function Component(props) {
```javascript
import { c as _c } from "react/compiler-runtime";
function Component(props) {
const $ = _c(6);
const $ = _c(8);
let y;
let t0;
if ($[0] !== props) {
if ($[0] !== props.p0 || $[1] !== props.p2 || $[2] !== props.p3) {
const x = [];
switch (props.p0) {
case true: {
@@ -44,23 +44,25 @@ function Component(props) {
}
t0 = <Component data={x} />;
$[0] = props;
$[1] = y;
$[2] = t0;
$[0] = props.p0;
$[1] = props.p2;
$[2] = props.p3;
$[3] = y;
$[4] = t0;
} else {
y = $[1];
t0 = $[2];
y = $[3];
t0 = $[4];
}
const child = t0;
y.push(props.p4);
let t1;
if ($[3] !== y || $[4] !== child) {
if ($[5] !== y || $[6] !== child) {
t1 = <Component data={y}>{child}</Component>;
$[3] = y;
$[4] = child;
$[5] = t1;
$[5] = y;
$[6] = child;
$[7] = t1;
} else {
t1 = $[5];
t1 = $[7];
}
return t1;
}

View File

@@ -0,0 +1,73 @@
## Input
```javascript
import { Stringify } from "shared-runtime";
import { makeArray } from "shared-runtime";
/**
* Here, we don't need to memoize Stringify as it is a read off of a global.
* TODO: in PropagateScopeDeps (hir), we should produce a sidemap of global rvals
* and avoid adding them to `temporariesUsedOutsideDefiningScope`.
*/
function Component({ num }: { num: number }) {
const arr = makeArray(num);
return <Stringify value={arr.push(num)}></Stringify>;
}
export const FIXTURE_ENTRYPOINT = {
fn: Component,
params: [{ num: 2 }],
};
```
## Code
```javascript
import { c as _c } from "react/compiler-runtime";
import { Stringify } from "shared-runtime";
import { makeArray } from "shared-runtime";
/**
* Here, we don't need to memoize Stringify as it is a read off of a global.
* TODO: in PropagateScopeDeps (hir), we should produce a sidemap of global rvals
* and avoid adding them to `temporariesUsedOutsideDefiningScope`.
*/
function Component(t0) {
const $ = _c(6);
const { num } = t0;
let T0;
let t1;
if ($[0] !== num) {
const arr = makeArray(num);
T0 = Stringify;
t1 = arr.push(num);
$[0] = num;
$[1] = T0;
$[2] = t1;
} else {
T0 = $[1];
t1 = $[2];
}
let t2;
if ($[3] !== T0 || $[4] !== t1) {
t2 = <T0 value={t1} />;
$[3] = T0;
$[4] = t1;
$[5] = t2;
} else {
t2 = $[5];
}
return t2;
}
export const FIXTURE_ENTRYPOINT = {
fn: Component,
params: [{ num: 2 }],
};
```
### Eval output
(kind: ok) <div>{"value":2}</div>

View File

@@ -0,0 +1,17 @@
import { Stringify } from "shared-runtime";
import { makeArray } from "shared-runtime";
/**
* Here, we don't need to memoize Stringify as it is a read off of a global.
* TODO: in PropagateScopeDeps (hir), we should produce a sidemap of global rvals
* and avoid adding them to `temporariesUsedOutsideDefiningScope`.
*/
function Component({ num }: { num: number }) {
const arr = makeArray(num);
return <Stringify value={arr.push(num)}></Stringify>;
}
export const FIXTURE_ENTRYPOINT = {
fn: Component,
params: [{ num: 2 }],
};

View File

@@ -0,0 +1,78 @@
## Input
```javascript
import * as SharedRuntime from "shared-runtime";
import { makeArray } from "shared-runtime";
/**
* Here, we don't need to memoize SharedRuntime.Stringify as it is a PropertyLoad
* off of a global.
* TODO: in PropagateScopeDeps (hir), we should produce a sidemap of global rvals
* and avoid adding them to `temporariesUsedOutsideDefiningScope`.
*/
function Component({ num }: { num: number }) {
const arr = makeArray(num);
return (
<SharedRuntime.Stringify value={arr.push(num)}></SharedRuntime.Stringify>
);
}
export const FIXTURE_ENTRYPOINT = {
fn: Component,
params: [{ num: 2 }],
};
```
## Code
```javascript
import { c as _c } from "react/compiler-runtime";
import * as SharedRuntime from "shared-runtime";
import { makeArray } from "shared-runtime";
/**
* Here, we don't need to memoize SharedRuntime.Stringify as it is a PropertyLoad
* off of a global.
* TODO: in PropagateScopeDeps (hir), we should produce a sidemap of global rvals
* and avoid adding them to `temporariesUsedOutsideDefiningScope`.
*/
function Component(t0) {
const $ = _c(6);
const { num } = t0;
let T0;
let t1;
if ($[0] !== num) {
const arr = makeArray(num);
T0 = SharedRuntime.Stringify;
t1 = arr.push(num);
$[0] = num;
$[1] = T0;
$[2] = t1;
} else {
T0 = $[1];
t1 = $[2];
}
let t2;
if ($[3] !== T0 || $[4] !== t1) {
t2 = <T0 value={t1} />;
$[3] = T0;
$[4] = t1;
$[5] = t2;
} else {
t2 = $[5];
}
return t2;
}
export const FIXTURE_ENTRYPOINT = {
fn: Component,
params: [{ num: 2 }],
};
```
### Eval output
(kind: ok) <div>{"value":2}</div>

View File

@@ -0,0 +1,20 @@
import * as SharedRuntime from "shared-runtime";
import { makeArray } from "shared-runtime";
/**
* Here, we don't need to memoize SharedRuntime.Stringify as it is a PropertyLoad
* off of a global.
* TODO: in PropagateScopeDeps (hir), we should produce a sidemap of global rvals
* and avoid adding them to `temporariesUsedOutsideDefiningScope`.
*/
function Component({ num }: { num: number }) {
const arr = makeArray(num);
return (
<SharedRuntime.Stringify value={arr.push(num)}></SharedRuntime.Stringify>
);
}
export const FIXTURE_ENTRYPOINT = {
fn: Component,
params: [{ num: 2 }],
};

View File

@@ -28,9 +28,9 @@ import { c as _c } from "react/compiler-runtime";
const { shallowCopy, throwErrorWithMessage } = require("shared-runtime");
function Component(props) {
const $ = _c(3);
const $ = _c(5);
let x;
if ($[0] !== props.a) {
if ($[0] !== props) {
x = [];
try {
let t0;
@@ -42,9 +42,17 @@ function Component(props) {
}
x.push(t0);
} catch {
x.push(shallowCopy({ a: props.a }));
let t0;
if ($[3] !== props.a) {
t0 = shallowCopy({ a: props.a });
$[3] = props.a;
$[4] = t0;
} else {
t0 = $[4];
}
x.push(t0);
}
$[0] = props.a;
$[0] = props;
$[1] = x;
} else {
x = $[1];

View File

@@ -31,9 +31,9 @@ import { c as _c } from "react/compiler-runtime";
const { throwInput } = require("shared-runtime");
function Component(props) {
const $ = _c(3);
const $ = _c(2);
let x;
if ($[0] !== props.y || $[1] !== props.e) {
if ($[0] !== props) {
try {
const y = [];
y.push(props.y);
@@ -43,11 +43,10 @@ function Component(props) {
e.push(props.e);
x = e;
}
$[0] = props.y;
$[1] = props.e;
$[2] = x;
$[0] = props;
$[1] = x;
} else {
x = $[2];
x = $[1];
}
return x;
}

View File

@@ -30,9 +30,9 @@ import { c as _c } from "react/compiler-runtime";
const { throwInput } = require("shared-runtime");
function Component(props) {
const $ = _c(3);
const $ = _c(2);
let t0;
if ($[0] !== props.y || $[1] !== props.e) {
if ($[0] !== props) {
t0 = Symbol.for("react.early_return_sentinel");
bb0: {
try {
@@ -46,11 +46,10 @@ function Component(props) {
break bb0;
}
}
$[0] = props.y;
$[1] = props.e;
$[2] = t0;
$[0] = props;
$[1] = t0;
} else {
t0 = $[2];
t0 = $[1];
}
if (t0 !== Symbol.for("react.early_return_sentinel")) {
return t0;

View File

@@ -33,11 +33,16 @@ import { c as _c } from "react/compiler-runtime";
import { useMemo } from "react";
function Component(props) {
const $ = _c(3);
const $ = _c(6);
let t0;
bb0: {
let y;
if ($[0] !== props) {
if (
$[0] !== props.cond ||
$[1] !== props.a ||
$[2] !== props.cond2 ||
$[3] !== props.b
) {
y = [];
if (props.cond) {
y.push(props.a);
@@ -48,12 +53,15 @@ function Component(props) {
}
y.push(props.b);
$[0] = props;
$[1] = y;
$[2] = t0;
$[0] = props.cond;
$[1] = props.a;
$[2] = props.cond2;
$[3] = props.b;
$[4] = y;
$[5] = t0;
} else {
y = $[1];
t0 = $[2];
y = $[4];
t0 = $[5];
}
t0 = y;
}

View File

@@ -78,21 +78,29 @@ export function mutateAndReturnNewValue<T>(arg: T): string {
return "hello!";
}
export function setProperty(arg: any, property: any): void {
export function setProperty(
arg: any,
property: any,
propertyName?: string
): void {
// don't mutate primitive
if (arg == null || typeof arg !== "object") {
return arg;
}
let count: number = 0;
let key;
while (true) {
key = "wat" + count;
if (!Object.hasOwn(arg, key)) {
arg[key] = property;
return arg;
if (propertyName != null && typeof propertyName === "string") {
arg[propertyName] = property;
} else {
let count: number = 0;
let key;
while (true) {
key = "wat" + count;
if (!Object.hasOwn(arg, key)) {
arg[key] = property;
return arg;
}
count++;
}
count++;
}
}