Use single name resolver for nested functions

This is a prerequisite to inlining `useMemo()` lambdas so that we can better 
optimize them. Nested functions are evaluated with a fresh HIRBuilder, which 
means that they currently have their own `bindings` object for mapping 
identifier instances to IdentifierIds. This means that identifier ids in a 
closure are _always_ different that those outside the closure, even when they 
refer to the same identifier: 

``` 

function Component(props) { 

props; // becomes e.g. props$1 

const onClick = () => { 

props // becomes e.g. props$2 

}; 

} 

``` 

For useMemo inlining this is problematic because we've lost the association that 
these identifiers actually refer to the same thing. This PR changes that, 
sharing the name resolution data structure between the top-level function and 
any nested function expressions.
This commit is contained in:
Joe Savona
2023-04-06 15:25:30 -07:00
parent fcfb66914a
commit 5622c0ee91
10 changed files with 91 additions and 16 deletions
+2 -1
View File
@@ -56,7 +56,8 @@ export function* run(
func: NodePath<t.FunctionDeclaration>,
config?: EnvironmentConfig | null
): Generator<CompilerPipelineValue, t.FunctionDeclaration> {
const hir = lower(func, new Environment(config ?? null)).unwrap();
const env = new Environment(config ?? null);
const hir = lower(func, env).unwrap();
yield log({ kind: "hir", name: "HIR", value: hir });
mergeConsecutiveBlocks(hir);
+4 -2
View File
@@ -36,7 +36,7 @@ import {
SpreadPattern,
ThrowTerminal,
} from "./HIR";
import HIRBuilder from "./HIRBuilder";
import HIRBuilder, { Bindings } from "./HIRBuilder";
// *******************************************************************************************
// *******************************************************************************************
@@ -58,11 +58,12 @@ import HIRBuilder from "./HIRBuilder";
export function lower(
func: NodePath<t.Function>,
env: Environment,
bindings: Bindings | null = null,
capturedRefs: t.Identifier[] = [],
// the outermost function being compiled, in case lower() is called recursively (for lambdas)
parent: NodePath<t.Function> | null = null
): Result<HIRFunction, CompilerError> {
const builder = new HIRBuilder(env, parent ?? func, capturedRefs);
const builder = new HIRBuilder(env, parent ?? func, bindings, capturedRefs);
const context: Place[] = [];
for (const ref of capturedRefs ?? []) {
@@ -2134,6 +2135,7 @@ function lowerFunctionExpression(
const lowering = lower(
expr,
builder.environment,
builder.bindings,
[...builder.context, ...captured.identifiers],
builder.parentFunction
);
+14 -4
View File
@@ -71,6 +71,11 @@ function newBlock(id: BlockId, kind: BlockKind): WipBlock {
return { id, kind, instructions: [] };
}
export type Bindings = Map<
string,
{ node: t.Identifier; identifier: Identifier }
>;
/**
* Helper class for constructing a CFG
*/
@@ -80,8 +85,7 @@ export default class HIRBuilder {
#entry: BlockId;
#scopes: Array<Scope> = [];
#context: t.Identifier[];
#bindings: Map<string, { node: t.Identifier; identifier: Identifier }> =
new Map();
#bindings: Bindings;
#env: Environment;
parentFunction: NodePath<t.Function>;
errors: CompilerError = new CompilerError();
@@ -94,6 +98,10 @@ export default class HIRBuilder {
return this.#context;
}
get bindings(): Bindings {
return this.#bindings;
}
get environment(): Environment {
return this.#env;
}
@@ -101,11 +109,13 @@ export default class HIRBuilder {
constructor(
env: Environment,
parentFunction: NodePath<t.Function>, // the outermost function being compiled
context: t.Identifier[]
bindings: Bindings | null = null,
context: t.Identifier[] | null = null
) {
this.#env = env;
this.#bindings = bindings ?? new Map();
this.parentFunction = parentFunction;
this.#context = context;
this.#context = context ?? [];
this.#entry = makeBlockId(env.nextBlockId);
this.#current = newBlock(this.#entry, "block");
}
@@ -26,8 +26,8 @@ function component(a) {
const x = { a };
y = {};
(function () {
let a = y;
a["x"] = x;
let a_0 = y;
a_0["x"] = x;
})();
mutate(y);
$[0] = a;
@@ -26,8 +26,8 @@ function component(a) {
const x = { a };
y = {};
(function () {
let a = y;
a.x = x;
let a_0 = y;
a_0.x = x;
})();
mutate(y);
$[0] = a;
@@ -19,9 +19,9 @@ function component({ mutator }) {
## Code
```javascript
function component(t26) {
function component(t24) {
const $ = React.unstable_useMemoCache(7);
const { mutator } = t26;
const { mutator } = t24;
const c_0 = $[0] !== mutator;
let t0;
if (c_0) {
@@ -21,8 +21,8 @@ function component(a) {
let t0;
if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
t0 = function () {
let z;
mutate(z);
let z_0;
mutate(z_0);
};
$[0] = t0;
} else {
@@ -27,7 +27,7 @@ function Component(props) {
const c_0 = $[0] !== setValue;
let t0;
if (c_0) {
t0 = (e) => setValue((value) => value + e.target.value);
t0 = (e) => setValue((value_0) => value_0 + e.target.value);
$[0] = setValue;
$[1] = t0;
} else {
@@ -0,0 +1,52 @@
## Input
```javascript
function Component(props) {
const [x, setX] = useState(null);
const onChange = (e) => {
let x = null; // intentionally shadow the original x
setX((currentX) => currentX + x); // intentionally refer to shadowed x
};
return <input value={x} onChange={onChange} />;
}
```
## Code
```javascript
function Component(props) {
const $ = React.unstable_useMemoCache(5);
const [x, setX] = useState(null);
const c_0 = $[0] !== setX;
let t0;
if (c_0) {
t0 = (e) => {
let x_0 = null; // intentionally shadow the original x
setX((currentX) => currentX + x_0); // intentionally refer to shadowed x
};
$[0] = setX;
$[1] = t0;
} else {
t0 = $[1];
}
const onChange = t0;
const c_2 = $[2] !== x;
const c_3 = $[3] !== onChange;
let t1;
if (c_2 || c_3) {
t1 = <input value={x} onChange={onChange} />;
$[2] = x;
$[3] = onChange;
$[4] = t1;
} else {
t1 = $[4];
}
return t1;
}
```
@@ -0,0 +1,10 @@
function Component(props) {
const [x, setX] = useState(null);
const onChange = (e) => {
let x = null; // intentionally shadow the original x
setX((currentX) => currentX + x); // intentionally refer to shadowed x
};
return <input value={x} onChange={onChange} />;
}