Bailout on assigning to module-scope variables
Joe Savona committed
Feb 17, 2023 at 12:18 UTC
2b3902f043813a23c5f5e2ebd22786689abcdf04
7 files changed
+92
-22
compiler/forget/src/HIR/BuildHIR.ts
+15
-11
@@ -20,6 +20,7 @@ import {
20
GeneratedSource,
21
GotoVariant,
22
HIRFunction,
23
+ Identifier,
24
IfTerminal,
25
InstructionKind,
26
InstructionValue,
@@ -52,10 +53,12 @@ import HIRBuilder from "./HIRBuilder";
53
export function lower(
54
func: NodePath<t.Function>,
55
options: EnvironmentOptions | null,
55
- capturedRefs: t.Identifier[] = []
56
+ capturedRefs: t.Identifier[] = [],
57
+ // the outermost function being compiled, in case lower() is called recursively (for lambdas)
58
+ parent: NodePath<t.Function> | null = null
59
): Result<HIRFunction, CompilerError> {
60
const env = new Environment(options);
58
- const builder = new HIRBuilder(env, capturedRefs);
61
+ const builder = new HIRBuilder(env, parent ?? func, capturedRefs);
62
const context: Place[] = [];
63
64
for (const ref of capturedRefs ?? []) {
@@ -70,11 +73,10 @@ export function lower(
73
// Internal babel is on an older version that does not have hasNode (v7.17)
74
// See https://github.com/babel/babel/pull/13940/files for impl
75
// TODO: write helper function for NodePath.node != null
73
- const id =
74
- func.isFunctionDeclaration() && func.get("id").node != null
75
- ? builder.resolveIdentifier(func.get("id") as NodePath<t.Identifier>)
76
- : null;
77
-
76
+ let id: Identifier | null = null;
77
+ if (func.isFunctionDeclaration() && func.get("id").node != null) {
78
+ id = builder.resolveIdentifier(func.get("id") as NodePath<t.Identifier>);
79
+ }
80
const params: Array<Place> = [];
81
func.get("params").forEach((param) => {
82
if (param.isIdentifier()) {
@@ -1343,10 +1345,12 @@ function lowerExpression(
1345
//
1346
// This isn't a problem in practice because use Babel's scope analysis to
1347
// identify the correct references.
1346
- const lowering = lower(expr, builder.environment.options, [
1347
- ...builder.context,
1348
- ...captured.identifiers,
1349
- ]);
1348
+ const lowering = lower(
1349
+ expr,
1350
+ builder.environment.options,
1351
+ [...builder.context, ...captured.identifiers],
1352
+ builder.parentFunction
1353
+ );
1354
let loweredFunc: HIRFunction;
1355
if (lowering.isErr()) {
1356
lowering
compiler/forget/src/HIR/HIRBuilder.ts
+29
-4
@@ -83,6 +83,7 @@ export default class HIRBuilder {
83
#bindings: Map<string, { node: t.Identifier; identifier: Identifier }> =
84
new Map();
85
#env: Environment;
86
+ parentFunction: NodePath<t.Function>;
87
errors: CompilerError = new CompilerError();
88
89
get nextIdentifierId() {
@@ -97,8 +98,13 @@ export default class HIRBuilder {
98
return this.#env;
99
}
100
100
- constructor(env: Environment, context: t.Identifier[]) {
101
+ constructor(
102
+ env: Environment,
103
+ parentFunction: NodePath<t.Function>, // the outermost function being compiled
104
+ context: t.Identifier[]
105
+ ) {
106
this.#env = env;
107
+ this.parentFunction = parentFunction;
108
this.#context = context;
109
}
110
@@ -174,11 +180,30 @@ export default class HIRBuilder {
180
path: NodePath<t.Identifier | t.JSXIdentifier>
181
): Identifier | null {
182
const originalName = path.node.name;
177
- const node = path.scope.getBindingIdentifier(originalName);
178
- if (node == null) {
183
+ const binding = path.scope.getBinding(originalName);
184
+ if (binding == null) {
185
return null;
186
}
181
- return this.resolveBinding(node);
187
+ // If the binding is from the parent function's outer scope, then
188
+ // we treat it equivalently to a global.
189
+ //
190
+ // TODO: remove the exception that resolves references to the
191
+ // parent function itself. We don't need to support self-recursion,
192
+ // so we can treat such references as globals.
193
+ const outerBinding =
194
+ this.parentFunction.scope.parent.getBinding(originalName);
195
+ if (binding === outerBinding) {
196
+ const func = this.parentFunction;
197
+ const isParentFunctionReference =
198
+ func.isFunctionDeclaration() &&
199
+ func.get("id").node != null &&
200
+ func.get("id").node!.name === originalName;
201
+ if (!isParentFunctionReference) {
202
+ return null;
203
+ }
204
+ }
205
+
206
+ return this.resolveBinding(binding.identifier);
207
}
208
209
resolveBinding(node: t.Identifier): Identifier {
compiler/forget/src/__tests__/fixtures/hir/destructure-capture-global.expect.md
+8
-5
@@ -15,15 +15,18 @@ function component(a) {
15
```javascript
16
let someGlobal = {};
17
function component(a) {
18
- const $ = React.unstable_useMemoCache(2);
18
+ const $ = React.unstable_useMemoCache(3);
19
+ const t0 = someGlobal;
20
const c_0 = $[0] !== a;
21
+ const c_1 = $[1] !== t0;
22
let x;
21
- if (c_0) {
22
- x = { a: a, someGlobal: someGlobal };
23
+ if (c_0 || c_1) {
24
+ x = { a: a, someGlobal: t0 };
25
$[0] = a;
24
- $[1] = x;
26
+ $[1] = t0;
27
+ $[2] = x;
28
} else {
26
- x = $[1];
29
+ x = $[2];
30
}
31
return x;
32
}
compiler/forget/src/__tests__/fixtures/hir/error.todo-kitchensink.expect.md
+15
-2
@@ -71,8 +71,11 @@ function foo([a, b], { c, d, e = "e" }, f = "f", ...args) {
71
72
// Cannot assign to globals
73
someUnknownGlobal = true;
74
+ moduleLocal = true;
75
}
76
77
+let moduleLocal = false;
78
+
79
```
80
81
@@ -409,8 +412,18 @@ function foo([a, b], { c, d, e = "e" }, f = "f", ...args) {
412
68 | // Cannot assign to globals
413
> 69 | someUnknownGlobal = true;
414
| ^^^^^^^^^^^^^^^^^
412
- 70 | }
413
- 71 |
415
+ 70 | moduleLocal = true;
416
+ 71 | }
417
+ 72 |
418
+
419
+[ReactForget] InvalidInputError: (BuildHIR::lowerAssignment) Assigning to an identifier defined outside the function scope is not supported.
420
+ 68 | // Cannot assign to globals
421
+ 69 | someUnknownGlobal = true;
422
+> 70 | moduleLocal = true;
423
+ | ^^^^^^^^^^^
424
+ 71 | }
425
+ 72 |
426
+ 73 | let moduleLocal = false;
427
```
428
429
\ No newline at end of file
compiler/forget/src/__tests__/fixtures/hir/error.todo-kitchensink.js
+3
@@ -67,4 +67,7 @@ function foo([a, b], { c, d, e = "e" }, f = "f", ...args) {
67
68
// Cannot assign to globals
69
someUnknownGlobal = true;
70
+ moduleLocal = true;
71
}
72
+
73
+let moduleLocal = false;
compiler/forget/src/__tests__/fixtures/hir/trivial.expect.md
new
+19
@@ -0,0 +1,19 @@
1
+
2
+## Input
3
+
4
+```javascript
5
+function foo(x) {
6
+ return x;
7
+}
8
+
9
+```
10
+
11
+## Code
12
+
13
+```javascript
14
+function foo(x) {
15
+ return x;
16
+}
17
+
18
+```
19
+
\ No newline at end of file
compiler/forget/src/__tests__/fixtures/hir/trivial.js
new
+3
@@ -0,0 +1,3 @@
1
+function foo(x) {
2
+ return x;
3
+}