@samitouri / QOS-React / commits / 2b3902f043

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 +}