[babel][contextvar] Patch context identifier babel logic; only use referenced

identifiers 

--- 

A few fixes for finding context identifiers: 

Previously, we counted every babel identifier as a reference. This is 
problematic because babel counts every string symbol as an identifier. 

```js 

print(x);  // x is an identifier as expected 

obj.x      // x is.. also an identifier here 

{x: 2}     // x is also an identifier here 

``` 

This PR adds a check for `isReferencedIdentifier`. Note that only non-lval 
references pass this check 

```js 

print(x);  // isReferencedIdentifier(x) -> true 

obj.x      // isReferencedIdentifier(x) -> false 

{x: 2}     // isReferencedIdentifier(x) -> false 

x = 2      // isReferencedIdentifier(x) -> false 

``` 

Which brings us to change #2. 

Previously, we counted assignments as references due to the identifier visiting 
+ checking logic. The logic was roughly the following (from #1691) 

```js 

contextVars = intersection(reassigned, referencedByInnerFn); 

``` 

Now that assignments (lvals) and references (rvals) are tracked separately, the 
equivalent logic is this. Note that assignment to a context variable does not 
need to be modeled as a read (`console.log(x = 5)` always will evaluates and 
prints 5, regardless of the previous value of x). 

``` 

contextVars = union(reassignedByInnerFn, intersection(reassigned, 
referencedByInnerFn)) 

``` 

--- 

Note that variables that are never read do not need to be modeled as context 
variables, but this is unlikely to be a common pattern. 

```js 

function fn() { 

let x = 2; 

const inner = () => { 

x = 3; 

} 

} 

```
This commit is contained in:
Mofei Zhang
2024-01-18 18:29:18 -05:00
parent 8f18b8233f
commit 241a615732
26 changed files with 656 additions and 73 deletions
@@ -8,9 +8,20 @@
import type { NodePath } from "@babel/traverse";
import type * as t from "@babel/types";
import { CompilerError } from "../CompilerError";
import { Set_union } from "../Utils/utils";
import { getOrInsertDefault } from "../Utils/utils";
import { GeneratedSource } from "./HIR";
type IdentifierInfo = {
reassigned: boolean;
reassignedByInnerFn: boolean;
referencedByInnerFn: boolean;
};
const DEFAULT_IDENTIFIER_INFO: IdentifierInfo = {
reassigned: false,
reassignedByInnerFn: false,
referencedByInnerFn: false,
};
type BabelFunction =
| NodePath<t.FunctionDeclaration>
| NodePath<t.FunctionExpression>
@@ -18,8 +29,7 @@ type BabelFunction =
| NodePath<t.ObjectMethod>;
type FindContextIdentifierState = {
currentFn: Array<BabelFunction>;
reassigned: Set<t.Identifier>;
referenced: Set<t.Identifier>;
identifiers: Map<t.Identifier, IdentifierInfo>;
};
const withFunctionScope = {
@@ -39,8 +49,7 @@ export function findContextIdentifiers(
): Set<t.Identifier> {
const state: FindContextIdentifierState = {
currentFn: [],
reassigned: new Set(),
referenced: new Set(),
identifiers: new Map(),
};
func.traverse<FindContextIdentifierState>(
@@ -54,38 +63,59 @@ export function findContextIdentifiers(
state: FindContextIdentifierState
): void {
const left = path.get("left");
handleAssignment(state.reassigned, left);
const currentFn = state.currentFn.at(-1) ?? null;
handleAssignment(currentFn, state.identifiers, left);
},
Identifier(
path: NodePath<t.Identifier>,
state: FindContextIdentifierState
): void {
const currentFn = state.currentFn.at(-1);
if (currentFn !== undefined)
handleIdentifier(currentFn, state.referenced, path);
const currentFn = state.currentFn.at(-1) ?? null;
if (path.isReferencedIdentifier()) {
handleIdentifier(currentFn, state.identifiers, path);
}
},
},
state
);
return Set_union(state.reassigned, state.referenced);
const result = new Set<t.Identifier>();
for (const [id, info] of state.identifiers.entries()) {
if (info.reassignedByInnerFn) {
result.add(id);
} else if (info.reassigned && info.referencedByInnerFn) {
result.add(id);
}
}
return result;
}
function handleIdentifier(
currentFn: BabelFunction,
referenced: Set<t.Identifier>,
currentFn: BabelFunction | null,
identifiers: Map<t.Identifier, IdentifierInfo>,
path: NodePath<t.Identifier>
): void {
const name = path.node.name;
const binding = path.scope.getBinding(name);
const bindingAboveLambdaScope = currentFn.scope.parent.getBinding(name);
if (binding == null) {
return;
}
const identifier = getOrInsertDefault(identifiers, binding.identifier, {
...DEFAULT_IDENTIFIER_INFO,
});
if (binding != null && binding === bindingAboveLambdaScope) {
referenced.add(binding.identifier);
if (currentFn != null) {
const bindingAboveLambdaScope = currentFn.scope.parent.getBinding(name);
if (binding === bindingAboveLambdaScope) {
identifier.referencedByInnerFn = true;
}
}
}
function handleAssignment(
reassigned: Set<t.Identifier>,
currentFn: BabelFunction | null,
identifiers: Map<t.Identifier, IdentifierInfo>,
lvalPath: NodePath<t.LVal>
): void {
/*
@@ -98,8 +128,20 @@ function handleAssignment(
const path = lvalPath as NodePath<t.Identifier>;
const name = path.node.name;
const binding = path.scope.getBinding(name);
if (binding != null) {
reassigned.add(binding.identifier);
if (binding == null) {
break;
}
const state = getOrInsertDefault(identifiers, binding.identifier, {
...DEFAULT_IDENTIFIER_INFO,
});
state.reassigned = true;
if (currentFn != null) {
const bindingAboveLambdaScope = currentFn.scope.parent.getBinding(name);
if (binding === bindingAboveLambdaScope) {
state.reassignedByInnerFn = true;
}
}
break;
}
@@ -107,7 +149,7 @@ function handleAssignment(
const path = lvalPath as NodePath<t.ArrayPattern>;
for (const element of path.get("elements")) {
if (nonNull(element)) {
handleAssignment(reassigned, element);
handleAssignment(currentFn, identifiers, element);
}
}
break;
@@ -123,7 +165,7 @@ function handleAssignment(
loc: valuePath.node.loc ?? GeneratedSource,
suggestions: null,
});
handleAssignment(reassigned, valuePath);
handleAssignment(currentFn, identifiers, valuePath);
} else {
CompilerError.invariant(property.isRestElement(), {
reason: `[FindContextIdentifiers] Invalid assumptions for babel types.`,
@@ -131,7 +173,7 @@ function handleAssignment(
loc: property.node.loc ?? GeneratedSource,
suggestions: null,
});
handleAssignment(reassigned, property);
handleAssignment(currentFn, identifiers, property);
}
}
break;
@@ -139,12 +181,12 @@ function handleAssignment(
case "AssignmentPattern": {
const path = lvalPath as NodePath<t.AssignmentPattern>;
const left = path.get("left");
handleAssignment(reassigned, left);
handleAssignment(currentFn, identifiers, left);
break;
}
case "RestElement": {
const path = lvalPath as NodePath<t.RestElement>;
handleAssignment(reassigned, path.get("argument"));
handleAssignment(currentFn, identifiers, path.get("argument"));
break;
}
case "MemberExpression": {
@@ -17,8 +17,10 @@ function bar(a, b) {
export const FIXTURE_ENTRYPOINT = {
fn: bar,
params: ["TodoAdd"],
isComponent: "TodoAdd",
params: [
[1, 2],
[2, 3],
],
};
```
@@ -52,9 +54,13 @@ function bar(a, b) {
export const FIXTURE_ENTRYPOINT = {
fn: bar,
params: ["TodoAdd"],
isComponent: "TodoAdd",
params: [
[1, 2],
[2, 3],
],
};
```
### Eval output
(kind: ok) 2
@@ -13,6 +13,8 @@ function bar(a, b) {
export const FIXTURE_ENTRYPOINT = {
fn: bar,
params: ["TodoAdd"],
isComponent: "TodoAdd",
params: [
[1, 2],
[2, 3],
],
};
@@ -2,22 +2,31 @@
## Input
```javascript
import { makeArray } from "shared-runtime";
function Component() {
let x,
y = (x = {});
const foo = () => {
x = getObject();
x = makeArray();
};
foo();
return [y, x];
}
export const FIXTURE_ENTRYPOINT = {
fn: Component,
params: [{}],
};
```
## Code
```javascript
import { unstable_useMemoCache as useMemoCache } from "react";
import { makeArray } from "shared-runtime";
function Component() {
const $ = useMemoCache(3);
let x;
@@ -26,7 +35,7 @@ function Component() {
y = x = {};
const foo = () => {
x = getObject();
x = makeArray();
};
foo();
@@ -46,5 +55,12 @@ function Component() {
return t0;
}
export const FIXTURE_ENTRYPOINT = {
fn: Component,
params: [{}],
};
```
### Eval output
(kind: ok) [{},[]]
@@ -1,9 +1,16 @@
import { makeArray } from "shared-runtime";
function Component() {
let x,
y = (x = {});
const foo = () => {
x = getObject();
x = makeArray();
};
foo();
return [y, x];
}
export const FIXTURE_ENTRYPOINT = {
fn: Component,
params: [{}],
};
@@ -0,0 +1,44 @@
## Input
```javascript
import { invoke } from "shared-runtime";
function Component() {
let x = 2;
const fn = () => {
return { x: "value" };
};
invoke(fn);
x = 3;
return x;
}
export const FIXTURE_ENTRYPOINT = {
fn: Component,
params: [{}],
};
```
## Code
```javascript
import { invoke } from "shared-runtime";
function Component() {
const fn = () => ({ x: "value" });
invoke(fn);
return 3;
}
export const FIXTURE_ENTRYPOINT = {
fn: Component,
params: [{}],
};
```
### Eval output
(kind: ok) 3
@@ -0,0 +1,16 @@
import { invoke } from "shared-runtime";
function Component() {
let x = 2;
const fn = () => {
return { x: "value" };
};
invoke(fn);
x = 3;
return x;
}
export const FIXTURE_ENTRYPOINT = {
fn: Component,
params: [{}],
};
@@ -0,0 +1,65 @@
## Input
```javascript
import { identity, invoke } from "shared-runtime";
function foo() {
let x = 2;
const fn1 = () => {
const copy1 = (x = 3);
return identity(copy1);
};
const fn2 = () => {
const copy2 = (x = 4);
return [invoke(fn1), copy2, identity(copy2)];
};
return invoke(fn2);
}
export const FIXTURE_ENTRYPOINT = {
fn: foo,
params: [],
};
```
## Code
```javascript
import { unstable_useMemoCache as useMemoCache } from "react";
import { identity, invoke } from "shared-runtime";
function foo() {
const $ = useMemoCache(1);
let t0;
if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
let x;
x = 2;
const fn1 = () => {
const copy1 = (x = 3);
return identity(copy1);
};
const fn2 = () => {
const copy2 = (x = 4);
return [invoke(fn1), copy2, identity(copy2)];
};
t0 = invoke(fn2);
$[0] = t0;
} else {
t0 = $[0];
}
return t0;
}
export const FIXTURE_ENTRYPOINT = {
fn: foo,
params: [],
};
```
### Eval output
(kind: ok) [3,4,4]
@@ -0,0 +1,19 @@
import { identity, invoke } from "shared-runtime";
function foo() {
let x = 2;
const fn1 = () => {
const copy1 = (x = 3);
return identity(copy1);
};
const fn2 = () => {
const copy2 = (x = 4);
return [invoke(fn1), copy2, identity(copy2)];
};
return invoke(fn2);
}
export const FIXTURE_ENTRYPOINT = {
fn: foo,
params: [],
};
@@ -0,0 +1,63 @@
## Input
```javascript
import { invoke } from "shared-runtime";
function Component({ shouldReassign }) {
let x = null;
const reassign = () => {
if (shouldReassign) {
x = 2;
}
};
invoke(reassign);
return x;
}
export const FIXTURE_ENTRYPOINT = {
fn: Component,
params: [{ shouldReassign: true }],
sequentialRenders: [{ shouldReassign: false }, { shouldReassign: true }],
};
```
## Code
```javascript
import { unstable_useMemoCache as useMemoCache } from "react";
import { invoke } from "shared-runtime";
function Component(t21) {
const $ = useMemoCache(2);
const { shouldReassign } = t21;
let x;
if ($[0] !== shouldReassign) {
x = null;
const reassign = () => {
if (shouldReassign) {
x = 2;
}
};
invoke(reassign);
$[0] = shouldReassign;
$[1] = x;
} else {
x = $[1];
}
return x;
}
export const FIXTURE_ENTRYPOINT = {
fn: Component,
params: [{ shouldReassign: true }],
sequentialRenders: [{ shouldReassign: false }, { shouldReassign: true }],
};
```
### Eval output
(kind: ok) null
2
@@ -0,0 +1,18 @@
import { invoke } from "shared-runtime";
function Component({ shouldReassign }) {
let x = null;
const reassign = () => {
if (shouldReassign) {
x = 2;
}
};
invoke(reassign);
return x;
}
export const FIXTURE_ENTRYPOINT = {
fn: Component,
params: [{ shouldReassign: true }],
sequentialRenders: [{ shouldReassign: false }, { shouldReassign: true }],
};
@@ -0,0 +1,65 @@
## Input
```javascript
import { conditionalInvoke } from "shared-runtime";
// same as context-variable-reactive-explicit-control-flow.js, but make
// the control flow implicit
function Component({ shouldReassign }) {
let x = null;
const reassign = () => {
x = 2;
};
conditionalInvoke(shouldReassign, reassign);
return x;
}
export const FIXTURE_ENTRYPOINT = {
fn: Component,
params: [{ shouldReassign: true }],
sequentialRenders: [{ shouldReassign: false }, { shouldReassign: true }],
};
```
## Code
```javascript
import { unstable_useMemoCache as useMemoCache } from "react";
import { conditionalInvoke } from "shared-runtime";
// same as context-variable-reactive-explicit-control-flow.js, but make
// the control flow implicit
function Component(t20) {
const $ = useMemoCache(2);
const { shouldReassign } = t20;
let x;
if ($[0] !== shouldReassign) {
x = null;
const reassign = () => {
x = 2;
};
conditionalInvoke(shouldReassign, reassign);
$[0] = shouldReassign;
$[1] = x;
} else {
x = $[1];
}
return x;
}
export const FIXTURE_ENTRYPOINT = {
fn: Component,
params: [{ shouldReassign: true }],
sequentialRenders: [{ shouldReassign: false }, { shouldReassign: true }],
};
```
### Eval output
(kind: ok) null
2
@@ -0,0 +1,19 @@
import { conditionalInvoke } from "shared-runtime";
// same as context-variable-reactive-explicit-control-flow.js, but make
// the control flow implicit
function Component({ shouldReassign }) {
let x = null;
const reassign = () => {
x = 2;
};
conditionalInvoke(shouldReassign, reassign);
return x;
}
export const FIXTURE_ENTRYPOINT = {
fn: Component,
params: [{ shouldReassign: true }],
sequentialRenders: [{ shouldReassign: false }, { shouldReassign: true }],
};
@@ -2,40 +2,48 @@
## Input
```javascript
// @debug
import { Stringify } from "shared-runtime";
function Component(props) {
let x = null;
const onChange = (e) => {
const callback = () => {
console.log(x);
};
x = {};
return <Foo onChange={onChange} />;
return <Stringify callback={callback} shouldInvokeFns={true} />;
}
export const FIXTURE_ENTRYPOINT = {
fn: Component,
params: [{}],
};
```
## Code
```javascript
import { unstable_useMemoCache as useMemoCache } from "react"; // @debug
import { unstable_useMemoCache as useMemoCache } from "react";
import { Stringify } from "shared-runtime";
function Component(props) {
const $ = useMemoCache(2);
let onChange;
let callback;
if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
let x;
x = null;
onChange = (e) => {
callback = () => {
console.log(x);
};
x = {};
$[0] = onChange;
$[0] = callback;
} else {
onChange = $[0];
callback = $[0];
}
let t0;
if ($[1] === Symbol.for("react.memo_cache_sentinel")) {
t0 = <Foo onChange={onChange} />;
t0 = <Stringify callback={callback} shouldInvokeFns={true} />;
$[1] = t0;
} else {
t0 = $[1];
@@ -43,5 +51,13 @@ function Component(props) {
return t0;
}
export const FIXTURE_ENTRYPOINT = {
fn: Component,
params: [{}],
};
```
### Eval output
(kind: ok) <div>{"callback":{"kind":"Function"},"shouldInvokeFns":true}</div>
logs: [{}]
@@ -1,9 +1,15 @@
// @debug
import { Stringify } from "shared-runtime";
function Component(props) {
let x = null;
const onChange = (e) => {
const callback = () => {
console.log(x);
};
x = {};
return <Foo onChange={onChange} />;
return <Stringify callback={callback} shouldInvokeFns={true} />;
}
export const FIXTURE_ENTRYPOINT = {
fn: Component,
params: [{}],
};
@@ -0,0 +1,59 @@
## Input
```javascript
import { invoke } from "shared-runtime";
function Component({ value }) {
let x = null;
const reassign = () => {
x = value;
};
invoke(reassign);
return x;
}
export const FIXTURE_ENTRYPOINT = {
fn: Component,
params: [{ value: 2 }],
sequentialRenders: [{ value: 2 }, { value: 4 }],
};
```
## Code
```javascript
import { unstable_useMemoCache as useMemoCache } from "react";
import { invoke } from "shared-runtime";
function Component(t20) {
const $ = useMemoCache(2);
const { value } = t20;
let x;
if ($[0] !== value) {
x = null;
const reassign = () => {
x = value;
};
invoke(reassign);
$[0] = value;
$[1] = x;
} else {
x = $[1];
}
return x;
}
export const FIXTURE_ENTRYPOINT = {
fn: Component,
params: [{ value: 2 }],
sequentialRenders: [{ value: 2 }, { value: 4 }],
};
```
### Eval output
(kind: ok) 2
4
@@ -0,0 +1,16 @@
import { invoke } from "shared-runtime";
function Component({ value }) {
let x = null;
const reassign = () => {
x = value;
};
invoke(reassign);
return x;
}
export const FIXTURE_ENTRYPOINT = {
fn: Component,
params: [{ value: 2 }],
sequentialRenders: [{ value: 2 }, { value: 4 }],
};
@@ -0,0 +1,78 @@
## Input
```javascript
import { conditionalInvoke } from "shared-runtime";
function Component({ doReassign1, doReassign2 }) {
let x = {};
const reassign1 = () => {
x = 2;
};
const reassign2 = () => {
x = 3;
};
conditionalInvoke(doReassign1, reassign1);
conditionalInvoke(doReassign2, reassign2);
return x;
}
export const FIXTURE_ENTRYPOINT = {
fn: Component,
params: [{ doReassign1: true, doReassign2: true }],
sequentialRenders: [
{ doReassign1: true, doReassign2: true },
{ doReassign1: true, doReassign2: false },
{ doReassign1: false, doReassign2: false },
],
};
```
## Code
```javascript
import { unstable_useMemoCache as useMemoCache } from "react";
import { conditionalInvoke } from "shared-runtime";
function Component(t32) {
const $ = useMemoCache(3);
const { doReassign1, doReassign2 } = t32;
let x;
if ($[0] !== doReassign1 || $[1] !== doReassign2) {
x = {};
const reassign1 = () => {
x = 2;
};
const reassign2 = () => {
x = 3;
};
conditionalInvoke(doReassign1, reassign1);
conditionalInvoke(doReassign2, reassign2);
$[0] = doReassign1;
$[1] = doReassign2;
$[2] = x;
} else {
x = $[2];
}
return x;
}
export const FIXTURE_ENTRYPOINT = {
fn: Component,
params: [{ doReassign1: true, doReassign2: true }],
sequentialRenders: [
{ doReassign1: true, doReassign2: true },
{ doReassign1: true, doReassign2: false },
{ doReassign1: false, doReassign2: false },
],
};
```
### Eval output
(kind: ok) 3
2
{}
@@ -0,0 +1,24 @@
import { conditionalInvoke } from "shared-runtime";
function Component({ doReassign1, doReassign2 }) {
let x = {};
const reassign1 = () => {
x = 2;
};
const reassign2 = () => {
x = 3;
};
conditionalInvoke(doReassign1, reassign1);
conditionalInvoke(doReassign2, reassign2);
return x;
}
export const FIXTURE_ENTRYPOINT = {
fn: Component,
params: [{ doReassign1: true, doReassign2: true }],
sequentialRenders: [
{ doReassign1: true, doReassign2: true },
{ doReassign1: true, doReassign2: false },
{ doReassign1: false, doReassign2: false },
],
};
@@ -25,16 +25,14 @@ import { unstable_useMemoCache as useMemoCache } from "react";
function component() {
const $ = useMemoCache(1);
let x;
let t0;
if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
x = function x(a) {
a.foo();
};
x = {};
$[0] = x;
t0 = {};
$[0] = t0;
} else {
x = $[0];
t0 = $[0];
}
x = t0;
return x;
}
@@ -13,8 +13,7 @@ function Component(props) {
export const FIXTURE_ENTRYPOINT = {
fn: Component,
params: ["TodoAdd"],
isComponent: "TodoAdd",
params: [{}],
};
```
@@ -42,8 +41,7 @@ function Component(props) {
export const FIXTURE_ENTRYPOINT = {
fn: Component,
params: ["TodoAdd"],
isComponent: "TodoAdd",
params: [{}],
};
```
@@ -9,6 +9,5 @@ function Component(props) {
export const FIXTURE_ENTRYPOINT = {
fn: Component,
params: ["TodoAdd"],
isComponent: "TodoAdd",
params: [{}],
};
@@ -13,8 +13,7 @@ function Component(props) {
export const FIXTURE_ENTRYPOINT = {
fn: Component,
params: ["TodoAdd"],
isComponent: "TodoAdd",
params: [{}],
};
```
@@ -42,9 +41,10 @@ function Component(props) {
export const FIXTURE_ENTRYPOINT = {
fn: Component,
params: ["TodoAdd"],
isComponent: "TodoAdd",
params: [{}],
};
```
### Eval output
(kind: ok) {}
@@ -9,6 +9,5 @@ function Component(props) {
export const FIXTURE_ENTRYPOINT = {
fn: Component,
params: ["TodoAdd"],
isComponent: "TodoAdd",
params: [{}],
};
@@ -32,7 +32,6 @@ const skipFilter = new Set([
"capturing-func-mutate-nested",
"capturing-func-mutate",
"capturing-function-1",
"capturing-function-alias-computed-load-3",
"capturing-function-alias-computed-load",
"capturing-function-decl",
"capturing-function-skip-computed-path",
@@ -123,7 +122,6 @@ const skipFilter = new Set([
"reactive-scopes",
"reactivity-analysis-interleaved-reactivity",
"reassign-object-in-context",
"reassign-primitive-in-context",
"reassignment-separate-scopes",
"reduce-reactive-cond-memberexpr-join",
"reduce-reactive-uncond-deps-nonoverlap-descendant",
@@ -191,7 +189,6 @@ const skipFilter = new Set([
*/
"alias-capture-in-method-receiver",
"alias-nested-member-path-mutate",
"chained-assignment-context-variable",
"concise-arrow-expr",
"const-propagation-into-function-expression-global",
"declare-reassign-variable-in-function-declaration",
@@ -268,7 +265,6 @@ const skipFilter = new Set([
"computed-load-primitive-as-dependency",
"computed-store-alias",
"constant-propagation-into-function-expressions",
"context-variable-reassigned-outside-of-lambda",
"destructuring-mixed-scope-declarations-and-locals",
"destructuring-property-inference",
"do-while-conditional-break",
+14 -2
View File
@@ -127,8 +127,8 @@ export function makeObject_Primitives(): StringKeyedObject {
return { a: 0, b: "value1", c: true };
}
export function makeArray<T>(value: T): Array<T> {
return [value];
export function makeArray<T>(...values: Array<T>): Array<T> {
return [...values];
}
export function addOne(value: number): number {
@@ -173,6 +173,18 @@ export function invoke<T extends Array<any>, ReturnType>(
return fn(...params);
}
export function conditionalInvoke<T extends Array<any>, ReturnType>(
shouldInvoke: boolean,
fn: (...input: T) => ReturnType,
...params: T
) {
if (shouldInvoke) {
return fn(...params);
} else {
return null;
}
}
/**
* React Components
*/