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