[valueblocks] Support conditional expressions (ternary)
Support conditional expressions from AST -> HIR -> ReactiveFunction -> AST. This also helps make the patterns for value block handling more clear, so i was able to extract some reusable logic in the HIR -> ReactiveFunction conversion phase.
Joe Savona committed
Jan 31, 2023 at 13:39 UTC
acd227440e7dc17655c56d9cf4a9d1b73eb4bdda
12 files changed
+300
-56
compiler/forget/src/HIR/BuildHIR.ts
+63
@@ -889,6 +889,69 @@ function lowerExpression(
889
loc: exprLoc,
890
};
891
}
892
+ case "ConditionalExpression": {
893
+ const expr = exprPath as NodePath<t.ConditionalExpression>;
894
+ const exprLoc = expr.node.loc ?? GeneratedSource;
895
+
896
+ // Block for code following the if
897
+ const continuationBlock = builder.reserve(builder.currentBlockKind());
898
+ const testBlock = builder.reserve("value");
899
+ const place = buildTemporaryPlace(builder, exprLoc);
900
+
901
+ // Block for the consequent (if the test is truthy)
902
+ const consequentBlock = builder.enter("block", (blockId) => {
903
+ builder.push({
904
+ id: makeInstructionId(0),
905
+ lvalue: { kind: InstructionKind.Reassign, place: { ...place } },
906
+ value: lowerExpressionToPlace(builder, expr.get("consequent")),
907
+ loc: exprLoc,
908
+ });
909
+ return {
910
+ kind: "goto",
911
+ block: continuationBlock.id,
912
+ variant: GotoVariant.Break,
913
+ id: makeInstructionId(0),
914
+ };
915
+ });
916
+ // Block for the alternate (if the test is not truthy)
917
+ const alternateBlock = builder.enter("block", (blockId) => {
918
+ builder.push({
919
+ id: makeInstructionId(0),
920
+ lvalue: { kind: InstructionKind.Reassign, place: { ...place } },
921
+ value: lowerExpressionToPlace(builder, expr.get("alternate")),
922
+ loc: exprLoc,
923
+ });
924
+ return {
925
+ kind: "goto",
926
+ block: continuationBlock.id,
927
+ variant: GotoVariant.Break,
928
+ id: makeInstructionId(0),
929
+ };
930
+ });
931
+
932
+ builder.terminateWithContinuation(
933
+ {
934
+ kind: "ternary",
935
+ fallthrough: continuationBlock.id,
936
+ id: makeInstructionId(0),
937
+ test: testBlock.id,
938
+ loc: exprLoc,
939
+ },
940
+ testBlock
941
+ );
942
+ const testPlace = lowerExpressionToPlace(builder, expr.get("test"));
943
+ builder.terminateWithContinuation(
944
+ {
945
+ kind: "branch",
946
+ test: { ...testPlace },
947
+ consequent: consequentBlock,
948
+ alternate: alternateBlock,
949
+ id: makeInstructionId(0),
950
+ },
951
+ continuationBlock
952
+ );
953
+ return place;
954
+ }
955
case "LogicalExpression": {
956
const expr = exprPath as NodePath<t.LogicalExpression>;
957
const exprLoc = expr.node.loc ?? GeneratedSource;
compiler/forget/src/HIR/HIR.ts
+10
-1
@@ -96,7 +96,8 @@ export type ReactiveInstruction = {
96
export type ReactiveValue =
97
| InstructionValue
98
| ReactiveLogicalValue
99
- | ReactiveSequenceValue;
99
+ | ReactiveSequenceValue
100
+ | ReactiveTernaryValue;
101
102
export type ReactiveLogicalValue = {
103
kind: "LogicalExpression";
@@ -106,6 +107,14 @@ export type ReactiveLogicalValue = {
107
loc: SourceLocation;
108
};
109
110
+export type ReactiveTernaryValue = {
111
+ kind: "ConditionalExpression";
112
+ test: ReactiveValue;
113
+ consequent: ReactiveValue;
114
+ alternate: ReactiveValue;
115
+ loc: SourceLocation;
116
+};
117
+
118
export type ReactiveSequenceValue = {
119
kind: "SequenceExpression";
120
instructions: Array<ReactiveInstruction>;
compiler/forget/src/HIR/PrintHIR.ts
+8
@@ -354,6 +354,14 @@ export function printInstructionValue(instrValue: ReactiveValue): string {
354
].join("\n");
355
break;
356
}
357
+ case "ConditionalExpression": {
358
+ value = `Ternary ${printInstructionValue(
359
+ instrValue.test
360
+ )} ? ${printInstructionValue(
361
+ instrValue.consequent
362
+ )} : ${printInstructionValue(instrValue.alternate)}`;
363
+ break;
364
+ }
365
default: {
366
assertExhaustive(
367
instrValue,
compiler/forget/src/ReactiveScopes/BuildReactiveFunction.ts
+106
-46
@@ -16,6 +16,7 @@ import {
16
Place,
17
ReactiveBlock,
18
ReactiveValueBlock,
19
+ SourceLocation,
20
} from "../HIR";
21
import {
22
HIRFunction,
@@ -28,6 +29,7 @@ import {
29
ReactiveLogicalValue,
30
ReactiveSequenceValue,
31
ReactiveTerminalStatement,
32
+ ReactiveTernaryValue,
33
ReactiveValue,
34
Terminal,
35
} from "../HIR/HIR";
@@ -465,6 +467,68 @@ class Driver {
467
}
468
}
469
470
+ visitValueBlockNew(
471
+ id: BlockId,
472
+ loc: SourceLocation
473
+ ): { block: BlockId; value: ReactiveValue; place: Place } {
474
+ let block: BasicBlock;
475
+ let value: ReactiveValue | null = null;
476
+ let place: Place | null = null;
477
+ const defaultBlock = this.cx.ir.blocks.get(id)!;
478
+ if (
479
+ defaultBlock.terminal.kind === "goto" ||
480
+ defaultBlock.terminal.kind === "branch"
481
+ ) {
482
+ block = defaultBlock;
483
+ } else {
484
+ const result = this.visitValueTerminal(defaultBlock.terminal);
485
+ block = this.cx.ir.blocks.get(result.fallthrough)!;
486
+ place = result.place;
487
+ value = result.value;
488
+ }
489
+ const instructions: Array<ReactiveInstruction> = block.instructions;
490
+ if (place !== null && value !== null) {
491
+ instructions.forEach((instr) =>
492
+ mapInstructionOperands(instr as Instruction, (place) => {
493
+ return place.identifier === place.identifier
494
+ ? (value as Place)
495
+ : place;
496
+ })
497
+ );
498
+ }
499
+ if (instructions.length === 0) {
500
+ invariant(
501
+ block.terminal.kind === "branch",
502
+ "Expected instructions for non-branch terminal"
503
+ );
504
+ return {
505
+ block: block.id,
506
+ place: block.terminal.test,
507
+ value: value ?? block.terminal.test,
508
+ };
509
+ } else if (instructions.length === 1) {
510
+ const instr = instructions[0]!;
511
+ return {
512
+ block: block.id,
513
+ place: instr.lvalue!.place,
514
+ value: instr.value,
515
+ };
516
+ } else {
517
+ const instr = instructions.at(-1)!;
518
+ const sequence: ReactiveSequenceValue = {
519
+ kind: "SequenceExpression",
520
+ instructions: instructions.slice(0, -1),
521
+ value: instr.value,
522
+ loc: loc,
523
+ };
524
+ return {
525
+ block: block.id,
526
+ place: instr.lvalue!.place,
527
+ value: sequence,
528
+ };
529
+ }
530
+ }
531
+
532
visitValueTerminal(terminal: Terminal): {
533
value: ReactiveValue;
534
place: Place;
@@ -503,40 +567,7 @@ class Driver {
567
})
568
);
569
}
506
-
507
- let rightBlock: BasicBlock;
508
- let rightValue: ReactiveValue | null = null;
509
- let rightPlace: Place | null = null;
510
- const defaultRightBlock = this.cx.ir.blocks.get(
511
- testBlock.terminal.alternate
512
- )!;
513
- if (defaultRightBlock.terminal.kind === "goto") {
514
- rightBlock = defaultRightBlock;
515
- } else {
516
- const rightResult = this.visitValueTerminal(
517
- defaultRightBlock.terminal
518
- );
519
- rightBlock = this.cx.ir.blocks.get(rightResult.fallthrough)!;
520
- rightPlace = rightResult.place;
521
- rightValue = rightResult.value;
522
- }
523
- const rightInstructions: Array<ReactiveInstruction> =
524
- rightBlock.instructions;
570
const place = leftInstructions.at(-1)!.lvalue!.place;
526
- invariant(
527
- place.identifier ===
528
- rightInstructions.at(-1)!.lvalue!.place.identifier,
529
- "Expected both branches of a logical expression to store to the same temporary"
530
- );
531
- if (rightPlace !== null && rightValue !== null) {
532
- rightInstructions.forEach((instr) =>
533
- mapInstructionOperands(instr as Instruction, (place) => {
534
- return place.identifier === rightPlace!.identifier
535
- ? (rightValue as Place)
536
- : place;
537
- })
538
- );
539
- }
571
572
let left: ReactiveValue;
573
if (leftInstructions.length === 1) {
@@ -550,23 +581,19 @@ class Driver {
581
};
582
left = sequence;
583
}
553
- let right: ReactiveValue;
554
- if (rightInstructions.length === 1) {
555
- right = rightInstructions[0]!.value;
556
- } else {
557
- const sequence: ReactiveSequenceValue = {
558
- kind: "SequenceExpression",
559
- instructions: rightInstructions.slice(0, -1),
560
- value: rightInstructions.at(-1)!.value,
561
- loc: terminal.loc,
562
- };
563
- right = sequence;
564
- }
584
+ const right = this.visitValueBlockNew(
585
+ testBlock.terminal.alternate,
586
+ terminal.loc
587
+ );
588
+ invariant(
589
+ place.identifier === right.place.identifier,
590
+ "Expected the left and right side of a logical expression to store a value to the same place"
591
+ );
592
const value: ReactiveLogicalValue = {
593
kind: "LogicalExpression",
594
operator: terminal.operator,
595
left,
569
- right,
596
+ right: right.value,
597
loc: terminal.loc,
598
};
599
return {
@@ -575,6 +602,39 @@ class Driver {
602
fallthrough: terminal.fallthrough,
603
};
604
}
605
+ case "ternary": {
606
+ const test = this.visitValueBlockNew(terminal.test, terminal.loc);
607
+ const testBlock = this.cx.ir.blocks.get(test.block)!;
608
+ invariant(
609
+ testBlock.terminal.kind === "branch",
610
+ "Unexpected terminal kind '%s' for ternary test block",
611
+ testBlock.terminal.kind
612
+ );
613
+ const consequent = this.visitValueBlockNew(
614
+ testBlock.terminal.consequent,
615
+ terminal.loc
616
+ );
617
+ const alternate = this.visitValueBlockNew(
618
+ testBlock.terminal.alternate,
619
+ terminal.loc
620
+ );
621
+ const value: ReactiveTernaryValue = {
622
+ kind: "ConditionalExpression",
623
+ test: test.value,
624
+ consequent: consequent.value,
625
+ alternate: alternate.value,
626
+ loc: terminal.loc,
627
+ };
628
+ invariant(
629
+ consequent.place.identifier === alternate.place.identifier,
630
+ "Expected the consquent and alternate of a ternary to store a value to the same place"
631
+ );
632
+ return {
633
+ place: { ...consequent.place },
634
+ value,
635
+ fallthrough: terminal.fallthrough,
636
+ };
637
+ }
638
default: {
639
invariant(
640
false,
compiler/forget/src/ReactiveScopes/CodegenReactiveFunction.ts
+19
@@ -432,6 +432,7 @@ const createWhileStatement = withLoc(t.whileStatement);
432
const createTaggedTemplateExpression = withLoc(t.taggedTemplateExpression);
433
const createLogicalExpression = withLoc(t.logicalExpression);
434
const createSequenceExpression = withLoc(t.sequenceExpression);
435
+const createConditionalExpression = withLoc(t.conditionalExpression);
436
437
type Temporaries = Map<IdentifierId, t.Expression>;
438
@@ -693,6 +694,15 @@ function codegenInstructionValue(
694
);
695
break;
696
}
697
+ case "ConditionalExpression": {
698
+ value = createConditionalExpression(
699
+ instrValue.loc,
700
+ codegenInstructionValue(cx, instrValue.test),
701
+ codegenInstructionValue(cx, instrValue.consequent),
702
+ codegenInstructionValue(cx, instrValue.alternate)
703
+ );
704
+ break;
705
+ }
706
case "SequenceExpression": {
707
const body = codegenBlock(
708
cx,
@@ -705,6 +715,15 @@ function codegenInstructionValue(
715
if (stmt.type === "ExpressionStatement") {
716
return stmt.expression;
717
} else {
718
+ if (t.isVariableDeclaration(stmt)) {
719
+ const declarator = stmt.declarations[0];
720
+ todoInvariant(
721
+ false,
722
+ `Cannot declare variables in a value block, tried to declare '${
723
+ (declarator.id as t.Identifier).name
724
+ }'`
725
+ );
726
+ }
727
todoInvariant(
728
false,
729
`Handle conversion of ${stmt.type} to expression`
compiler/forget/src/ReactiveScopes/PrintReactiveFunction.ts
+14
@@ -124,6 +124,20 @@ function printValueBlock(writer: Writer, block: ReactiveValueBlock): void {
124
125
function printReactiveValue(writer: Writer, value: ReactiveValue): void {
126
switch (value.kind) {
127
+ case "ConditionalExpression": {
128
+ writer.append(`Ternary `);
129
+ printReactiveValue(writer, value.test);
130
+ writer.newline();
131
+ writer.indented(() => {
132
+ writer.write(`? `);
133
+ printReactiveValue(writer, value.consequent);
134
+ writer.newline();
135
+ writer.write(`: `);
136
+ printReactiveValue(writer, value.alternate);
137
+ writer.newline();
138
+ });
139
+ break;
140
+ }
141
case "LogicalExpression": {
142
writer.append(`Logical ${value.operator} `);
143
printReactiveValue(writer, value.left);
compiler/forget/src/ReactiveScopes/visitors.ts
+6
@@ -98,6 +98,12 @@ export function* eachReactiveValueOperand(
98
yield* eachReactiveValueOperand(instrValue.value);
99
break;
100
}
101
+ case "ConditionalExpression": {
102
+ yield* eachReactiveValueOperand(instrValue.test);
103
+ yield* eachReactiveValueOperand(instrValue.consequent);
104
+ yield* eachReactiveValueOperand(instrValue.alternate);
105
+ break;
106
+ }
107
default: {
108
yield* eachInstructionValueOperand(instrValue);
109
}
compiler/forget/src/SSA/LeaveSSA.ts
+1
-1
@@ -148,7 +148,7 @@ export function leaveSSA(fn: HIRFunction) {
148
pushPhis(rewritePhis, update);
149
update.phis.clear();
150
}
151
- if (terminal.kind === "logical") {
151
+ if (terminal.kind === "logical" || terminal.kind === "ternary") {
152
const fallthrough = fn.body.blocks.get(terminal.fallthrough)!;
153
pushPhis(rewritePhis, fallthrough);
154
fallthrough.phis.clear();
compiler/forget/src/__tests__/fixtures/hir/logical-expression.expect.md
+9
-7
@@ -5,7 +5,7 @@
5
function component(props) {
6
let a = props.a || (props.b && props.c && props.d);
7
let b = (props.a && props.b && props.c) || props.d;
8
- return a ?? b;
8
+ return a ? b : props.c;
9
}
10
11
```
@@ -37,16 +37,18 @@ function component(props) {
37
const b = t3;
38
const c_4 = $[4] !== a;
39
const c_5 = $[5] !== b;
40
- let t6;
41
- if (c_4 || c_5) {
42
- t6 = a ?? b;
40
+ const c_6 = $[6] !== props;
41
+ let t7;
42
+ if (c_4 || c_5 || c_6) {
43
+ t7 = a ? b : props.c;
44
$[4] = a;
45
$[5] = b;
45
- $[6] = t6;
46
+ $[6] = props;
47
+ $[7] = t7;
48
} else {
47
- t6 = $[6];
49
+ t7 = $[7];
50
}
49
- return t6;
51
+ return t7;
52
}
53
54
```
compiler/forget/src/__tests__/fixtures/hir/logical-expression.js
+1
-1
@@ -1,5 +1,5 @@
1
function component(props) {
2
let a = props.a || (props.b && props.c && props.d);
3
let b = (props.a && props.b && props.c) || props.d;
4
- return a ?? b;
4
+ return a ? b : props.c;
5
}
compiler/forget/src/__tests__/fixtures/hir/sequence-expression.expect.md
new
+51
@@ -0,0 +1,51 @@
1
+
2
+## Input
3
+
4
+```javascript
5
+// @only
6
+function sequence(props) {
7
+ let x = (null, Math.max(1, 2), sequence({}));
8
+ if (((x = x + 1), x < 10)) {
9
+ x = 10;
10
+ }
11
+ // while (((x = x * 2), x < 20)) {
12
+ // x = ((x = x + 1), x + 1);
13
+ // }
14
+ return x;
15
+}
16
+
17
+```
18
+
19
+## Code
20
+
21
+```javascript
22
+// @only
23
+function sequence(props) {
24
+ const $ = React.useMemoCache();
25
+ null;
26
+ Math.max(1, 2);
27
+ let t0;
28
+ if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
29
+ t0 = sequence({});
30
+ $[0] = t0;
31
+ } else {
32
+ t0 = $[0];
33
+ }
34
+ const x = t0;
35
+ const x$0 = x + 1;
36
+ let x$1;
37
+ if ($[1] === Symbol.for("react.memo_cache_sentinel")) {
38
+ x$1 = x$0;
39
+ if (x$0 < 10) {
40
+ const x$2 = 10;
41
+ x$1 = x$2;
42
+ }
43
+ $[1] = x$1;
44
+ } else {
45
+ x$1 = $[1];
46
+ }
47
+ return x$1;
48
+}
49
+
50
+```
51
+
\ No newline at end of file
compiler/forget/src/__tests__/fixtures/hir/sequence-expression.js
new
+12
@@ -0,0 +1,12 @@
1
+// @skip
2
+function sequence(props) {
3
+ let x = (null, Math.max(1, 2), sequence({}));
4
+ if (((x = x + 1), x < 10)) {
5
+ x = 10;
6
+ }
7
+ x = ((x = x + 1), x > 15) && x < 20 ? ((x = x + 1), x) : 42;
8
+ while (((x = x * 2), x < 20)) {
9
+ x = ((x = x + 1), x + 1);
10
+ }
11
+ return x;
12
+}