Skip to content

Commit

Permalink
compiler: treat pruned scope outputs as reactive
Browse files Browse the repository at this point in the history
Mostly addresses the issue with non-reactive pruned scopes. Before, values from pruned scopes would not be memoized, but could still be depended upon by downstream scopes. However, those downstream scopes would assume the value could never change. This could allow the developer to observe two different versions of a value - the freshly created one (if observed outside a scope) or a cached one (if observed inside, or through) a scope which used the value but didn't depend on it.

The fix here is to consider the outputs of pruned reactive scopes as reactive. Note that this is a partial fix because of things like aliasing and control variables — the full solution would be to mark these values as reactive, and then re-run InferReactivePlaces. We can do this once we've fully converted our pipeline to use HIR everywhere. For now, this should fix most issues in practice.

ghstack-source-id: ce19c5c26f33653765970cfd546be47943f7aad2
Pull Request resolved: #29790
  • Loading branch information
josephsavona committed Jun 6, 2024
1 parent 942c9b4 commit 67ef114
Show file tree
Hide file tree
Showing 11 changed files with 217 additions and 222 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,9 @@ import {
IdentifierId,
InstructionId,
Place,
PrunedReactiveScopeBlock,
ReactiveFunction,
isPrimitiveType,
} from "../HIR/HIR";
import { ReactiveFunctionVisitor, visitReactiveFunction } from "./visitors";

Expand Down Expand Up @@ -40,6 +42,19 @@ class Visitor extends ReactiveFunctionVisitor<Set<IdentifierId>> {
state.add(place.identifier.id);
}
}

override visitPrunedScope(
scopeBlock: PrunedReactiveScopeBlock,
state: Set<IdentifierId>
): void {
this.traversePrunedScope(scopeBlock, state);

for (const [id, decl] of scopeBlock.scope.declarations) {
if (!isPrimitiveType(decl.identifier)) {
state.add(id);
}
}
}
}

/*
Expand Down

This file was deleted.

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ export const FIXTURE_ENTRYPOINT = {
```javascript
import { c as _c } from "react/compiler-runtime";
function Component(props) {
const $ = _c(1);
const $ = _c(2);

const a = [];
const b = [];
Expand All @@ -53,11 +53,12 @@ function Component(props) {
x = 1;
}
let t0;
if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
if ($[0] !== x) {
t0 = [x];
$[0] = t0;
$[0] = x;
$[1] = t0;
} else {
t0 = $[0];
t0 = $[1];
}
return t0;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ import { useEffect, useState } from "react";
import { mutate } from "shared-runtime";

function Component(props) {
const $ = _c(6);
const $ = _c(8);
const x = [{ ...props.value }];
let t0;
let t1;
Expand All @@ -64,25 +64,27 @@ function Component(props) {
return <span key={item.id}>{item.text}</span>;
});
let t3;
if ($[2] === Symbol.for("react.memo_cache_sentinel")) {
if ($[2] !== y) {
t3 = mutate(y);
$[2] = t3;
$[2] = y;
$[3] = t3;
} else {
t3 = $[2];
t3 = $[3];
}
let t4;
if ($[3] !== onClick || $[4] !== t2) {
if ($[4] !== onClick || $[5] !== t2 || $[6] !== t3) {
t4 = (
<div onClick={onClick}>
{t2}
{t3}
</div>
);
$[3] = onClick;
$[4] = t2;
$[5] = t4;
$[4] = onClick;
$[5] = t2;
$[6] = t3;
$[7] = t4;
} else {
t4 = $[5];
t4 = $[7];
}
return t4;
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@

## Input

```javascript
import invariant from "invariant";
import {
makeObject_Primitives,
mutate,
sum,
useIdentity,
} from "shared-runtime";

/**
* Here, `z`'s original memo block is removed due to the inner hook call.
* However, we also infer that `z` is non-reactive, so by default we would create
* the memo block for `thing = [y, z]` as only depending on `y`.
*
* This could then mean that `thing[1]` and `z` may not refer to the same value,
* since z recreates every time but `thing` doesn't correspondingly invalidate.
*
* The fix is to consider pruned memo block outputs as reactive, since they will
* recreate on every render. This means `thing` depends on both y and z.
*/
function MyApp({ count }) {
const z = makeObject_Primitives();
const x = useIdentity(2);
const y = sum(x, count);
mutate(z);
const thing = [y, z];
if (thing[1] !== z) {
invariant(false, "oh no!");
}
return thing;
}

export const FIXTURE_ENTRYPOINT = {
fn: MyApp,
params: [{ count: 2 }],
sequentialRenders: [{ count: 2 }, { count: 2 }, { count: 3 }],
};

```

## Code

```javascript
import { c as _c } from "react/compiler-runtime";
import invariant from "invariant";
import {
makeObject_Primitives,
mutate,
sum,
useIdentity,
} from "shared-runtime";

/**
* Here, `z`'s original memo block is removed due to the inner hook call.
* However, we also infer that `z` is non-reactive, so by default we would create
* the memo block for `thing = [y, z]` as only depending on `y`.
*
* This could then mean that `thing[1]` and `z` may not refer to the same value,
* since z recreates every time but `thing` doesn't correspondingly invalidate.
*
* The fix is to consider pruned memo block outputs as reactive, since they will
* recreate on every render. This means `thing` depends on both y and z.
*/
function MyApp(t0) {
const $ = _c(6);
const { count } = t0;
const z = makeObject_Primitives();
const x = useIdentity(2);
let t1;
if ($[0] !== x || $[1] !== count) {
t1 = sum(x, count);
$[0] = x;
$[1] = count;
$[2] = t1;
} else {
t1 = $[2];
}
const y = t1;
mutate(z);
let t2;
if ($[3] !== y || $[4] !== z) {
t2 = [y, z];
$[3] = y;
$[4] = z;
$[5] = t2;
} else {
t2 = $[5];
}
const thing = t2;
if (thing[1] !== z) {
invariant(false, "oh no!");
}
return thing;
}

export const FIXTURE_ENTRYPOINT = {
fn: MyApp,
params: [{ count: 2 }],
sequentialRenders: [{ count: 2 }, { count: 2 }, { count: 3 }],
};

```
### Eval output
(kind: ok) [4,{"a":0,"b":"value1","c":true,"wat0":"joe"}]
[4,{"a":0,"b":"value1","c":true,"wat0":"joe"}]
[5,{"a":0,"b":"value1","c":true,"wat0":"joe"}]
Loading

0 comments on commit 67ef114

Please sign in to comment.