@samitouri / QOS-React-2 / commits / 35ba4149ec

Support assignment in for statement's update clause

Per the title, this PR adds support for assignment expressions in update clauses. This was mostly fixed by the previous diff to improve value block handling, and there's only a bit more to do here to allow a "value block" that doesn't produce a value (we need a better name).

Joseph Savona committed Dec 12, 2022 at 15:40 UTC 35ba4149ec2fb3831e7f82c00f68725c7897124f
11 files changed +221 -63
compiler/forget/src/HIR/BuildHIR.ts
+11 -9
@@ -340,6 +340,7 @@ function lowerStatement(
340
341 const updateBlock = builder.enter((blockId) => {
342 const update = stmt.get("update");
343 + todoInvariant(update.hasNode(), "Handle empty for updater");
344 if (update.hasNode()) {
345 lowerExpressionToVoid(builder, update);
346 }
@@ -1275,20 +1276,21 @@ function lowerExpressionToPlace(
1276 return place;
1277 }
1278
1279 +/**
1280 + * Lowers an expression to an instruction with no lvalue
1281 + */
1282 function lowerExpressionToVoid(
1283 builder: HIRBuilder,
1284 exprPath: NodePath<t.Expression>
1285 ): void {
1286 const instr = lowerExpression(builder, exprPath);
1283 - if (instr.kind !== "Identifier") {
1284 - const exprLoc = exprPath.node.loc ?? GeneratedSource;
1285 - builder.push({
1286 - id: makeInstructionId(0),
1287 - value: instr,
1288 - loc: exprLoc,
1289 - lvalue: null,
1290 - });
1291 - }
1287 + const exprLoc = exprPath.node.loc ?? GeneratedSource;
1288 + builder.push({
1289 + id: makeInstructionId(0),
1290 + value: instr,
1291 + loc: exprLoc,
1292 + lvalue: null,
1293 + });
1294 }
1295
1296 function lowerLVal(builder: HIRBuilder, exprPath: NodePath<t.LVal>): Place {
compiler/forget/src/HIR/Codegen.ts
+8 -2
@@ -137,9 +137,13 @@ class CodegenVisitor
137 appendValueBlock(block: t.Statement[], item: t.Statement): void {
138 this.appendBlock(block, item);
139 }
140 - leaveValueBlock(block: t.Statement[], place: t.Expression): t.Expression {
140 + leaveValueBlock(
141 + block: t.Statement[],
142 + place: t.Expression | null
143 + ): t.Expression {
144 this.depth--;
145 if (block.length === 0) {
146 + invariant(place !== null, "Unexpected empty value block");
147 return place;
148 }
149 const expressions = block.map((stmt) => {
@@ -153,7 +157,9 @@ class CodegenVisitor
157 );
158 }
159 });
156 - expressions.push(place);
160 + if (place !== null) {
161 + expressions.push(place);
162 + }
163 return t.sequenceExpression(expressions);
164 }
165
compiler/forget/src/HIR/HIRTreeVisitor.ts
+13 -10
@@ -451,24 +451,27 @@ class Driver<TBlock, TInit, TValueBlock, TValue, TStatement, TCase> {
451 ): TValue {
452 const valueBlock = this.visitor.enterValueBlock(parent);
453 const instructions = [...block.instructions];
454 - let lastValue: { value: InstructionValue; id: InstructionId };
454 + let lastValue: { value: InstructionValue; id: InstructionId } | null = null;
455 if (terminalValue != null) {
456 lastValue = terminalValue;
457 } else {
458 - invariant(instructions.length > 0, "Value block may not be empty");
459 - const last = instructions.pop()!;
460 - invariant(
461 - last.lvalue === null,
462 - "Expected value block to end in a value, not an assignment"
463 - );
464 - lastValue = { value: last.value, id: last.id };
458 + if (
459 + instructions.length &&
460 + instructions[instructions.length - 1].lvalue === null
461 + ) {
462 + const last = instructions.pop()!;
463 + lastValue = { value: last.value, id: last.id };
464 + }
465 }
466 for (const instr of instructions) {
467 const value = this.visitor.visitValue(instr.value, instr.id);
468 const item = this.visitor.visitInstruction(instr, value);
469 this.visitor.appendValueBlock(valueBlock, item);
470 }
471 - const value = this.visitor.visitValue(lastValue.value, lastValue.id);
471 + const value =
472 + lastValue !== null
473 + ? this.visitor.visitValue(lastValue.value, lastValue.id)
474 + : null;
475 return this.visitor.leaveValueBlock(valueBlock, value);
476 }
477
@@ -817,7 +820,7 @@ export interface Visitor<
820 * Converts the visitor's value block (and final value) to the visitor's
821 * value representation.
822 */
820 - leaveValueBlock(block: TValueBlock, value: TValue): TValue;
823 + leaveValueBlock(block: TValueBlock, value: TValue | null): TValue;
824
825 enterInitBlock(block: TBlock): TValueBlock;
826
compiler/forget/src/HIR/InferReactiveScopes.ts
+27 -10
@@ -336,7 +336,10 @@ class AlignReactiveScopesToBlockScopeRangeVisitor
336 {
337 // For each block scope (outer array) stores a list of ReactiveScopes that start
338 // in that block scope.
339 - blockScopes: Array<Array<PendingReactiveScope>> = [];
339 + blockScopes: Array<{
340 + kind: "block" | "value";
341 + scopes: Array<PendingReactiveScope>;
342 + }> = [];
343
344 // ReactiveScopes whose declaring block scope has ended but may still need to
345 // be "closed" (ie have their range.end be updated). A given scope can be in
@@ -349,7 +352,10 @@ class AlignReactiveScopesToBlockScopeRangeVisitor
352
353 visitId(id: InstructionId) {
354 const currentScopes = this.blockScopes[this.blockScopes.length - 1]!;
352 - const scopes = [...currentScopes, ...this.unclosedScopes];
355 + if (currentScopes.kind === "value") {
356 + return;
357 + }
358 + const scopes = [...currentScopes.scopes, ...this.unclosedScopes];
359 for (const pending of scopes) {
360 if (!pending.active) {
361 continue;
@@ -362,7 +368,7 @@ class AlignReactiveScopesToBlockScopeRangeVisitor
368 }
369
370 enterBlock(): void {
365 - this.blockScopes.push([]);
371 + this.blockScopes.push({ kind: "block", scopes: [] });
372 }
373
374 appendBlock(block: void, item: void, label?: BlockId | undefined): void {}
@@ -370,10 +376,10 @@ class AlignReactiveScopesToBlockScopeRangeVisitor
376 leaveBlock(block: void): void {
377 const lastScope = this.blockScopes.pop();
378 invariant(
373 - lastScope !== undefined,
379 + lastScope !== undefined && lastScope.kind === "block",
380 "Expected enterBlock/leaveBlock to be called 1:1"
381 );
376 - for (const scope of lastScope) {
382 + for (const scope of lastScope.scopes) {
383 if (scope.active) {
384 this.unclosedScopes.push(scope);
385 }
@@ -381,19 +387,30 @@ class AlignReactiveScopesToBlockScopeRangeVisitor
387 }
388
389 enterValueBlock(): void {
384 - this.enterBlock();
390 + this.blockScopes.push({ kind: "value", scopes: [] });
391 }
392 appendValueBlock(block: void, item: void): void {}
393 leaveValueBlock(block: void, value: void): void {
388 - this.leaveBlock(block);
394 + const lastScope = this.blockScopes.pop();
395 + invariant(
396 + lastScope !== undefined && lastScope.kind === "value",
397 + "Expected enterValueBlock/leaveValueBlock to be called 1:1"
398 + );
399 + for (const scope of lastScope.scopes) {
400 + invariant(
401 + scope.active,
402 + "Value scopes cannot be closed separately from the parent block"
403 + );
404 + this.unclosedScopes.push(scope);
405 + }
406 }
407
408 enterInitBlock(block: void): void {
392 - this.enterBlock();
409 + this.enterValueBlock();
410 }
411 appendInitBlock(block: void, item: void): void {}
412 leaveInitBlock(block: void): void {
396 - this.leaveBlock(block);
413 + this.leaveValueBlock(block);
414 }
415
416 visitInstruction(instruction: Instruction, value: void): void {
@@ -403,7 +420,7 @@ class AlignReactiveScopesToBlockScopeRangeVisitor
420 if (!this.seenScopes.has(scope.id)) {
421 const currentScopes = this.blockScopes[this.blockScopes.length - 1]!;
422 this.seenScopes.add(scope.id);
406 - currentScopes.push({
423 + currentScopes.scopes.push({
424 active: true,
425 scope,
426 });
compiler/forget/src/HIR/LeaveSSA.ts
+9
@@ -62,8 +62,17 @@ export function leaveSSA(fn: HIRFunction) {
62 phis.push(...loop.phis);
63 }
64 if (terminal.kind === "for") {
65 + const init = fn.body.blocks.get(terminal.init)!;
66 + phis.push(...init.phis);
67 const update = fn.body.blocks.get(terminal.update)!;
68 phis.push(...update.phis);
69 +
70 + // find declarations in the for init
71 + for (const instr of init.instructions) {
72 + if (instr.lvalue !== null && instr.lvalue.place.memberPath === null) {
73 + hasDeclaration.add(instr.lvalue.place.identifier);
74 + }
75 + }
76 }
77
78 // For each phi, determine a canonical identifier to use for versions of the variable
compiler/forget/src/__tests__/fixtures/hir/ssa-for-trivial-update.expect.md new
+113
@@ -0,0 +1,113 @@
1 +
2 +## Input
3 +
4 +```javascript
5 +function foo() {
6 + let x = 1;
7 + for (let i = 0; i < 10; /* update is intentally a single identifier */ i) {
8 + x += 1;
9 + }
10 + return x;
11 +}
12 +
13 +```
14 +
15 +## HIR
16 +
17 +```
18 +bb0:
19 + [1] Let mutate x$6_@1[1:13] = 1
20 + [2] For init=bb3 test=bb1 loop=bb5 update=bb4 fallthrough=bb2
21 +bb3:
22 + predecessor blocks: bb0
23 + [3] Const mutate i$7_@1[1:13] = 0
24 + [4] Goto bb1
25 +bb1:
26 + predecessor blocks: bb3 bb4
27 + [5] Const mutate $8_@1[1:13] = 10
28 + [6] Const mutate $10_@3[6:8] = Binary read i$7_@1 < read $8_@1
29 + [7] If (read $10_@3) then:bb5 else:bb2 fallthrough=bb2
30 +bb5:
31 + predecessor blocks: bb1
32 + [8] Const mutate $11_@4 = 1
33 + [9] Reassign mutate x$6_@1[1:13] = Binary read x$6_@1 + read $11_@4
34 + [10] Goto(Continue) bb4
35 +bb4:
36 + predecessor blocks: bb5
37 + [11] read i$7_@1
38 + [12] Goto bb1
39 +bb2:
40 + predecessor blocks: bb1
41 + [13] Return read x$6_@1
42 +
43 +```
44 +
45 +### CFG
46 +
47 +```mermaid
48 +flowchart TB
49 + %% Basic Blocks
50 + subgraph bb0
51 + bb0_instrs["
52 + [1] Let mutate x$6_@1[1:13] = 1
53 + "]
54 + bb0_instrs --> bb0_terminal(["For"])
55 + end
56 + subgraph bb3
57 + bb3_instrs["
58 + [3] Const mutate i$7_@1[1:13] = 0
59 + "]
60 + bb3_instrs --> bb3_terminal(["Goto"])
61 + end
62 + subgraph bb1
63 + bb1_instrs["
64 + [5] Const mutate $8_@1[1:13] = 10
65 + [6] Const mutate $10_@3[6:8] = Binary read i$7_@1 < read $8_@1
66 + "]
67 + bb1_instrs --> bb1_terminal(["If (read $10_@3)"])
68 + end
69 + subgraph bb5
70 + bb5_instrs["
71 + [8] Const mutate $11_@4 = 1
72 + [9] Reassign mutate x$6_@1[1:13] = Binary read x$6_@1 + read $11_@4
73 + "]
74 + bb5_instrs --> bb5_terminal(["Goto"])
75 + end
76 + subgraph bb4
77 + bb4_instrs["
78 + [11] read i$7_@1
79 + "]
80 + bb4_instrs --> bb4_terminal(["Goto"])
81 + end
82 + subgraph bb2
83 + bb2_terminal(["Return read x$6_@1"])
84 + end
85 +
86 + %% Jumps
87 + bb0_terminal -- "init" --> bb3
88 + bb0_terminal -- "test" --> bb1
89 + bb0_terminal -- "update" --> bb4
90 + bb0_terminal -- "loop" --> bb5
91 + bb0_terminal -- "fallthrough" --> bb2
92 + bb3_terminal --> bb1
93 + bb1_terminal -- "then" --> bb5
94 + bb1_terminal -- "else" --> bb2
95 + bb5_terminal --> bb4
96 + bb4_terminal --> bb1
97 +
98 +```
99 +
100 +## Code
101 +
102 +```javascript
103 +function foo$0() {
104 + let x$6 = 1;
105 + bb2: for (const i$7 = 0; i$7 < 10; i$7) {
106 + x$6 = x$6 + 1;
107 + }
108 +
109 + return x$6;
110 +}
111 +
112 +```
113 +
\ No newline at end of file
compiler/forget/src/__tests__/fixtures/hir/ssa-for-trivial-update.js new
+7
@@ -0,0 +1,7 @@
1 +function foo() {
2 + let x = 1;
3 + for (let i = 0; i < 10; /* update is intentally a single identifier */ i) {
4 + x += 1;
5 + }
6 + return x;
7 +}
compiler/forget/src/__tests__/fixtures/hir/ssa-for.expect.md
+26 -24
@@ -4,7 +4,7 @@
4 ```javascript
5 function foo() {
6 let x = 1;
7 - for (let i = 0; i < 10; update()) {
7 + for (let i = 0; i < 10; i += 1) {
8 x += 1;
9 }
10 return x;
@@ -16,32 +16,32 @@ function foo() {
16
17 ```
18 bb0:
19 - [1] Let mutate x$7_@0[1:13] = 1
19 + [1] Let mutate x$7_@1[1:15] = 1
20 [2] For init=bb3 test=bb1 loop=bb5 update=bb4 fallthrough=bb2
21 bb3:
22 predecessor blocks: bb0
23 - [3] Const mutate i$8_@1[3:5] = 0
23 + [3] Let mutate i$8_@1[1:15] = 0
24 [4] Goto bb1
25 bb1:
26 predecessor blocks: bb3 bb4
27 - [5] Const mutate $9_@2 = 10
28 - [6] Const mutate $11_@3[6:8] = Binary read i$8_@1 < read $9_@2
29 - [7] If (read $11_@3) then:bb5 else:bb2 fallthrough=bb2
27 + [5] Const mutate $9_@1[1:15] = 10
28 + [6] Const mutate $11_@1[1:15] = Binary read i$8_@1 < read $9_@1
29 + [7] If (read $11_@1) then:bb5 else:bb2 fallthrough=bb2
30 bb5:
31 predecessor blocks: bb1
32 - [8] Const mutate $12_@4 = 1
33 - [9] Reassign mutate x$7_@0[1:13] = Binary read x$7_@0 + read $12_@4
32 + [8] Const mutate $12_@3 = 1
33 + [9] Reassign mutate x$7_@1[1:15] = Binary read x$7_@1 + read $12_@3
34 [10] Goto(Continue) bb4
35 bb4:
36 predecessor blocks: bb5
37 - [11] Call mutate update$3_@5()
38 - [12] Goto bb1
37 + [11] Const mutate $15_@1[1:15] = 1
38 + [12] Reassign mutate i$8_@1[1:15] = Binary read i$8_@1 + read $15_@1
39 + [13] read i$8_@1
40 + [14] Goto bb1
41 bb2:
42 predecessor blocks: bb1
41 - [13] Return read x$7_@0
42 -scope3 [6:8]:
43 - - dependency: read i$8_@1
44 - - dependency: read $9_@2
43 + [15] Return read x$7_@1
44 +
45 ```
46
47 ### CFG
@@ -51,38 +51,40 @@ flowchart TB
51 %% Basic Blocks
52 subgraph bb0
53 bb0_instrs["
54 - [1] Let mutate x$7_@0[1:13] = 1
54 + [1] Let mutate x$7_@1[1:15] = 1
55 "]
56 bb0_instrs --> bb0_terminal(["For"])
57 end
58 subgraph bb3
59 bb3_instrs["
60 - [3] Const mutate i$8_@1[3:5] = 0
60 + [3] Let mutate i$8_@1[1:15] = 0
61 "]
62 bb3_instrs --> bb3_terminal(["Goto"])
63 end
64 subgraph bb1
65 bb1_instrs["
66 - [5] Const mutate $9_@2 = 10
67 - [6] Const mutate $11_@3[6:8] = Binary read i$8_@1 < read $9_@2
66 + [5] Const mutate $9_@1[1:15] = 10
67 + [6] Const mutate $11_@1[1:15] = Binary read i$8_@1 < read $9_@1
68 "]
69 - bb1_instrs --> bb1_terminal(["If (read $11_@3)"])
69 + bb1_instrs --> bb1_terminal(["If (read $11_@1)"])
70 end
71 subgraph bb5
72 bb5_instrs["
73 - [8] Const mutate $12_@4 = 1
74 - [9] Reassign mutate x$7_@0[1:13] = Binary read x$7_@0 + read $12_@4
73 + [8] Const mutate $12_@3 = 1
74 + [9] Reassign mutate x$7_@1[1:15] = Binary read x$7_@1 + read $12_@3
75 "]
76 bb5_instrs --> bb5_terminal(["Goto"])
77 end
78 subgraph bb4
79 bb4_instrs["
80 - [11] Call mutate update$3_@5()
80 + [11] Const mutate $15_@1[1:15] = 1
81 + [12] Reassign mutate i$8_@1[1:15] = Binary read i$8_@1 + read $15_@1
82 + [13] read i$8_@1
83 "]
84 bb4_instrs --> bb4_terminal(["Goto"])
85 end
86 subgraph bb2
85 - bb2_terminal(["Return read x$7_@0"])
87 + bb2_terminal(["Return read x$7_@1"])
88 end
89
90 %% Jumps
@@ -104,7 +106,7 @@ flowchart TB
106 ```javascript
107 function foo$0() {
108 let x$7 = 1;
107 - bb2: for (const i$8 = 0; i$8 < 10; update$3()) {
109 + bb2: for (let i$8 = 0; i$8 < 10; i$8 = i$8 + 1, i$8) {
110 x$7 = x$7 + 1;
111 }
112
compiler/forget/src/__tests__/fixtures/hir/ssa-for.js
+1 -1
@@ -1,6 +1,6 @@
1 function foo() {
2 let x = 1;
3 - for (let i = 0; i < 10; update()) {
3 + for (let i = 0; i < 10; i += 1) {
4 x += 1;
5 }
6 return x;
compiler/forget/src/__tests__/fixtures/hir/ssa-while-no-reassign.expect.md
+2 -3
@@ -21,7 +21,7 @@ bb0:
21 [2] While test=bb1 loop=bb3 fallthrough=bb2
22 bb1:
23 predecessor blocks: bb0 bb3
24 - [3] Const mutate $6_@1 = 10
24 + [3] Const mutate $6_@1[3:6] = 10
25 [4] Const mutate $8_@2[4:6] = Binary read x$5_@0 < read $6_@1
26 [5] If (read $8_@2) then:bb3 else:bb2 fallthrough=bb2
27 bb3:
@@ -34,7 +34,6 @@ bb2:
34 [9] Return read x$5_@0
35 scope2 [4:6]:
36 - dependency: read x$5_@0
37 - - dependency: read $6_@1
37 scope3 [6:7]:
38 - dependency: read x$5_@0
39 ```
@@ -52,7 +51,7 @@ flowchart TB
51 end
52 subgraph bb1
53 bb1_instrs["
55 - [3] Const mutate $6_@1 = 10
54 + [3] Const mutate $6_@1[3:6] = 10
55 [4] Const mutate $8_@2[4:6] = Binary read x$5_@0 < read $6_@1
56 "]
57 bb1_instrs --> bb1_terminal(["If (read $8_@2)"])
compiler/forget/src/__tests__/fixtures/hir/ssa-while.expect.md
+4 -4
@@ -21,8 +21,8 @@ bb0:
21 [2] While test=bb1 loop=bb3 fallthrough=bb2
22 bb1:
23 predecessor blocks: bb0 bb3
24 - [3] Const mutate $6_@1 = 10
25 - [4] Const mutate $8_@0[1:9] = Binary read x$5_@0 < read $6_@1
24 + [3] Const mutate $6_@0[1:9] = 10
25 + [4] Const mutate $8_@0[1:9] = Binary read x$5_@0 < read $6_@0
26 [5] If (read $8_@0) then:bb3 else:bb2 fallthrough=bb2
27 bb3:
28 predecessor blocks: bb1
@@ -48,8 +48,8 @@ flowchart TB
48 end
49 subgraph bb1
50 bb1_instrs["
51 - [3] Const mutate $6_@1 = 10
52 - [4] Const mutate $8_@0[1:9] = Binary read x$5_@0 < read $6_@1
51 + [3] Const mutate $6_@0[1:9] = 10
52 + [4] Const mutate $8_@0[1:9] = Binary read x$5_@0 < read $6_@0
53 "]
54 bb1_instrs --> bb1_terminal(["If (read $8_@0)"])
55 end