@samitouri / QOS-React / commits / acd227440e

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