@samitouri / QOS-React / commits / d4acc7efa6

[valueblocks] Convert logical terminal to ReactiveValue

Implements the conversion from LogicalTerminal into a ReactiveLogicalValue (and ReactiveSequenveValue if necessary). The implementation is a bit rough, i clean it up in subsequent PRs which revealed parts of the logic that could be shared w ternaries.

Joe Savona committed Jan 31, 2023 at 13:39 UTC d4acc7efa6aa10d03cc2eda97345da12a60c6cbe
8 files changed +261 -73
compiler/forget/src/HIR/HIR.ts
+11 -1
@@ -93,7 +93,10 @@ export type ReactiveInstruction = {
93 loc: SourceLocation;
94 };
95
96 -export type ReactiveValue = InstructionValue | ReactiveLogicalValue;
96 +export type ReactiveValue =
97 + | InstructionValue
98 + | ReactiveLogicalValue
99 + | ReactiveSequenceValue;
100
101 export type ReactiveLogicalValue = {
102 kind: "LogicalExpression";
@@ -103,6 +106,13 @@ export type ReactiveLogicalValue = {
106 loc: SourceLocation;
107 };
108
109 +export type ReactiveSequenceValue = {
110 + kind: "SequenceExpression";
111 + instructions: Array<ReactiveInstruction>;
112 + value: ReactiveValue;
113 + loc: SourceLocation;
114 +};
115 +
116 export type ReactiveTerminal =
117 | ReactiveBreakTerminal
118 | ReactiveContinueTerminal
compiler/forget/src/HIR/PrintHIR.ts
+10
@@ -339,6 +339,16 @@ export function printInstructionValue(instrValue: ReactiveValue): string {
339 } ${printInstructionValue(instrValue.right)}`;
340 break;
341 }
342 + case "SequenceExpression": {
343 + value = [
344 + `Sequence`,
345 + ...instrValue.instructions.map(
346 + (instr) => ` ${printInstruction(instr)}`
347 + ),
348 + ` ${printInstructionValue(instrValue.value)}`,
349 + ].join("\n");
350 + break;
351 + }
352 default: {
353 assertExhaustive(
354 instrValue,
compiler/forget/src/ReactiveScopes/BuildReactiveFunction.ts
+136 -1
@@ -19,11 +19,24 @@ import {
19 } from "../HIR";
20 import {
21 HIRFunction,
22 + Instruction,
23 + InstructionKind,
24 ReactiveBreakTerminal,
25 ReactiveContinueTerminal,
26 ReactiveFunction,
27 + ReactiveInstruction,
28 + ReactiveLogicalValue,
29 + ReactiveSequenceValue,
30 ReactiveTerminalStatement,
31 + ReactiveValue,
32 + Terminal,
33 } from "../HIR/HIR";
34 +import {
35 + printInstructionValue,
36 + printPlace,
37 + printTerminal,
38 +} from "../HIR/PrintHIR";
39 +import { mapInstructionOperands } from "../HIR/visitors";
40 import { assertExhaustive } from "../Utils/utils";
41
42 /**
@@ -407,7 +420,16 @@ class Driver {
420 const scheduleId = this.cx.schedule(fallthroughId, "if");
421 scheduleIds.push(scheduleId);
422
410 - this.visitBlock(this.cx.ir.blocks.get(terminal.test)!, blockValue);
423 + const { place, value } = this.visitValueTerminal(terminal);
424 + blockValue.push({
425 + kind: "instruction",
426 + instruction: {
427 + id: terminal.id,
428 + lvalue: { kind: InstructionKind.Const, place },
429 + value,
430 + loc: terminal.loc,
431 + },
432 + });
433
434 this.cx.unschedule(scheduleId);
435 this.visitBlock(this.cx.ir.blocks.get(fallthroughId)!, blockValue);
@@ -447,6 +469,119 @@ class Driver {
469 }
470 }
471
472 + visitValueTerminal(terminal: Terminal): {
473 + value: ReactiveValue;
474 + place: Place;
475 + fallthrough: BlockId;
476 + } {
477 + switch (terminal.kind) {
478 + case "logical": {
479 + let testBlock: BasicBlock;
480 + let leftValue: ReactiveValue | null = null;
481 + let leftPlace: Place | null = null;
482 + const defaultTestBlock = this.cx.ir.blocks.get(terminal.test)!;
483 + if (defaultTestBlock.terminal.kind === "branch") {
484 + testBlock = defaultTestBlock;
485 + } else {
486 + const leftResult = this.visitValueTerminal(defaultTestBlock.terminal);
487 + testBlock = this.cx.ir.blocks.get(leftResult.fallthrough)!;
488 + leftValue = leftResult.value;
489 + leftPlace = leftResult.place;
490 + }
491 +
492 + invariant(
493 + testBlock.terminal.kind === "branch",
494 + "Unexpected terminal kind '%s' for logical test block",
495 + testBlock.terminal.kind
496 + );
497 + const leftInstructions: Array<ReactiveInstruction> =
498 + testBlock.instructions;
499 + const leftBlock = this.cx.ir.blocks.get(testBlock.terminal.consequent)!;
500 + leftInstructions.push(...leftBlock.instructions);
501 + // TODO: If right block ends in a value terminal, recursively process with visitValueTerminal
502 + // similar to handling for the compound lhs case.
503 + const rightBlock = this.cx.ir.blocks.get(testBlock.terminal.alternate)!;
504 + const rightInstructions: Array<ReactiveInstruction> =
505 + rightBlock.instructions;
506 + const place = leftInstructions.at(-1)!.lvalue!.place;
507 + invariant(
508 + place.identifier ===
509 + rightInstructions.at(-1)!.lvalue!.place.identifier,
510 + "Expected both branches of a logical expression to store to the same temporary"
511 + );
512 + if (leftPlace !== null) {
513 + leftInstructions.forEach((instr) =>
514 + mapInstructionOperands(instr as Instruction, (place) => {
515 + return place.identifier === leftPlace!.identifier
516 + ? (leftValue! as Place)
517 + : place;
518 + })
519 + );
520 + rightInstructions.forEach((instr) =>
521 + mapInstructionOperands(instr as Instruction, (place) => {
522 + return place.identifier === leftPlace!.identifier
523 + ? (leftValue! as Place)
524 + : place;
525 + })
526 + );
527 + }
528 +
529 + let left: ReactiveValue;
530 + if (leftInstructions.length === 1) {
531 + left = leftInstructions[0]!.value;
532 + } else {
533 + const sequence: ReactiveSequenceValue = {
534 + kind: "SequenceExpression",
535 + instructions: leftInstructions.slice(0, -1),
536 + value: leftInstructions.at(-1)!.value,
537 + loc: terminal.loc,
538 + };
539 + left = sequence;
540 + }
541 + let right: ReactiveValue;
542 + if (rightInstructions.length === 1) {
543 + right = rightInstructions[0]!.value;
544 + } else {
545 + const sequence: ReactiveSequenceValue = {
546 + kind: "SequenceExpression",
547 + instructions: rightInstructions.slice(0, -1),
548 + value: rightInstructions.at(-1)!.value,
549 + loc: terminal.loc,
550 + };
551 + right = sequence;
552 + }
553 + const value: ReactiveLogicalValue = {
554 + kind: "LogicalExpression",
555 + operator: terminal.operator,
556 + left,
557 + right,
558 + loc: terminal.loc,
559 + };
560 + console.log(
561 + printTerminal(terminal) +
562 + " testBlock=" +
563 + testBlock.id +
564 + " " +
565 + printPlace(place) +
566 + "=" +
567 + printInstructionValue(value)
568 + );
569 + return {
570 + place: { ...place },
571 + value,
572 + fallthrough: terminal.fallthrough,
573 + };
574 + }
575 + default: {
576 + invariant(
577 + false,
578 + "Unexpected value block terminal kind '%s'",
579 + terminal.kind
580 + );
581 + }
582 + }
583 + }
584 +
585 visitInitBlock(parent: ReactiveBlock, block: BasicBlock): ReactiveValueBlock {
586 const initBlock: ReactiveValueBlock = {
587 kind: "value-block",
compiler/forget/src/ReactiveScopes/CodegenReactiveFunction.ts
+79 -53
@@ -93,7 +93,7 @@ function codegenBlock(cx: Context, block: ReactiveBlock): t.BlockStatement {
93 const statement = codegenInstructionNullable(
94 cx,
95 item.instruction,
96 - codegenInstructionValue(cx.temp, item.instruction.value)
96 + codegenInstructionValue(cx, item.instruction.value)
97 );
98 if (statement !== null) {
99 statements.push(statement);
@@ -272,7 +272,7 @@ function codegenTerminal(
272 }
273 case "if": {
274 return t.ifStatement(
275 - codegenPlace(cx.temp, terminal.test),
275 + codegenPlace(cx, terminal.test),
276 codegenBlock(cx, terminal.consequent),
277 terminal.alternate !== null
278 ? codegenBlock(cx, terminal.alternate)
@@ -281,22 +281,22 @@ function codegenTerminal(
281 }
282 case "return": {
283 return t.returnStatement(
284 - terminal.value !== null ? codegenPlace(cx.temp, terminal.value) : null
284 + terminal.value !== null ? codegenPlace(cx, terminal.value) : null
285 );
286 }
287 case "switch": {
288 return t.switchStatement(
289 - codegenPlace(cx.temp, terminal.test),
289 + codegenPlace(cx, terminal.test),
290 terminal.cases.map((case_) => {
291 const test =
292 - case_.test !== null ? codegenPlace(cx.temp, case_.test) : null;
292 + case_.test !== null ? codegenPlace(cx, case_.test) : null;
293 const block = codegenBlock(cx, case_.block!);
294 return t.switchCase(test, [block]);
295 })
296 );
297 }
298 case "throw": {
299 - return t.throwStatement(codegenPlace(cx.temp, terminal.value));
299 + return t.throwStatement(codegenPlace(cx, terminal.value));
300 }
301 case "while": {
302 const test = codegenValueBlock(cx, terminal.test);
@@ -319,7 +319,7 @@ function codegenInstructionNullable(
319 let statement;
320 if (instr.lvalue !== null && cx.declared(instr.lvalue.place.identifier)) {
321 statement = codegenInstruction(
322 - cx.temp,
322 + cx,
323 {
324 ...instr,
325 lvalue: {
@@ -330,7 +330,7 @@ function codegenInstructionNullable(
330 value
331 );
332 } else {
333 - statement = codegenInstruction(cx.temp, instr, value);
333 + statement = codegenInstruction(cx, instr, value);
334 }
335 if (statement.type === "EmptyStatement") {
336 return null;
@@ -348,7 +348,7 @@ function codegenForInit(
348 body.length === 0,
349 "Expected for init block to produce only temporaries"
350 );
351 - return codegenInstructionValue(cx.temp, init.last.value);
351 + return codegenInstructionValue(cx, init.last.value);
352 } else {
353 invariant(
354 body.length === 1,
@@ -376,7 +376,7 @@ function codegenValueBlock(
376 }
377 });
378 if (block.last !== null) {
379 - const value = codegenInstructionValue(cx.temp, block.last.value);
379 + const value = codegenInstructionValue(cx, block.last.value);
380 expressions.push(value);
381 }
382 invariant(
@@ -431,6 +431,7 @@ const createVariableDeclaration = withLoc(t.variableDeclaration);
431 const createWhileStatement = withLoc(t.whileStatement);
432 const createTaggedTemplateExpression = withLoc(t.taggedTemplateExpression);
433 const createLogicalExpression = withLoc(t.logicalExpression);
434 +const createSequenceExpression = withLoc(t.sequenceExpression);
435
436 type Temporaries = Map<IdentifierId, t.Expression>;
437
@@ -439,7 +440,7 @@ function codegenLabel(id: BlockId): string {
440 }
441
442 function codegenInstruction(
442 - temp: Temporaries,
443 + cx: Context,
444 instr: ReactiveInstruction,
445 value: t.Expression
446 ): t.Statement {
@@ -451,7 +452,7 @@ function codegenInstruction(
452 }
453 if (instr.lvalue.place.identifier.name === null) {
454 // temporary
454 - temp.set(instr.lvalue.place.identifier.id, value);
455 + cx.temp.set(instr.lvalue.place.identifier.id, value);
456 return t.emptyStatement();
457 } else {
458 switch (instr.lvalue.kind) {
@@ -482,21 +483,21 @@ function codegenInstruction(
483 }
484
485 function codegenInstructionValue(
485 - temp: Temporaries,
486 + cx: Context,
487 instrValue: ReactiveValue
488 ): t.Expression {
489 let value: t.Expression;
490 switch (instrValue.kind) {
491 case "ArrayExpression": {
492 const elements = instrValue.elements.map((element) =>
492 - codegenPlace(temp, element)
493 + codegenPlace(cx, element)
494 );
495 value = t.arrayExpression(elements);
496 break;
497 }
498 case "BinaryExpression": {
498 - const left = codegenPlace(temp, instrValue.left);
499 - const right = codegenPlace(temp, instrValue.right);
499 + const left = codegenPlace(cx, instrValue.left);
500 + const right = codegenPlace(cx, instrValue.right);
501 value = createBinaryExpression(
502 instrValue.loc,
503 instrValue.operator,
@@ -508,41 +509,41 @@ function codegenInstructionValue(
509 case "UnaryExpression": {
510 value = t.unaryExpression(
511 instrValue.operator as "throw", // todo
511 - codegenPlace(temp, instrValue.value)
512 + codegenPlace(cx, instrValue.value)
513 );
514 break;
515 }
516 case "Primitive": {
516 - value = codegenValue(temp, instrValue.value);
517 + value = codegenValue(cx, instrValue.value);
518 break;
519 }
520 case "CallExpression": {
520 - const callee = codegenPlace(temp, instrValue.callee);
521 - const args = instrValue.args.map((arg) => codegenPlace(temp, arg));
521 + const callee = codegenPlace(cx, instrValue.callee);
522 + const args = instrValue.args.map((arg) => codegenPlace(cx, arg));
523 value = createCallExpression(instrValue.loc, callee, args);
524 break;
525 }
526 case "PropertyCall": {
526 - const receiver = codegenPlace(temp, instrValue.receiver);
527 + const receiver = codegenPlace(cx, instrValue.receiver);
528 const callee = t.memberExpression(
529 receiver,
530 t.identifier(instrValue.property)
531 );
531 - const args = instrValue.args.map((arg) => codegenPlace(temp, arg));
532 + const args = instrValue.args.map((arg) => codegenPlace(cx, arg));
533 value = createCallExpression(instrValue.loc, callee, args);
534 break;
535 }
536 case "ComputedCall": {
536 - const receiver = codegenPlace(temp, instrValue.receiver);
537 - const property = codegenPlace(temp, instrValue.property);
537 + const receiver = codegenPlace(cx, instrValue.receiver);
538 + const property = codegenPlace(cx, instrValue.property);
539 const callee = t.memberExpression(receiver, property, true);
539 - const args = instrValue.args.map((arg) => codegenPlace(temp, arg));
540 + const args = instrValue.args.map((arg) => codegenPlace(cx, arg));
541 value = createCallExpression(instrValue.loc, callee, args);
542 break;
543 }
544 case "NewExpression": {
544 - const callee = codegenPlace(temp, instrValue.callee);
545 - const args = instrValue.args.map((arg) => codegenPlace(temp, arg));
545 + const callee = codegenPlace(cx, instrValue.callee);
546 + const args = instrValue.args.map((arg) => codegenPlace(cx, arg));
547 value = t.newExpression(callee, args);
548 break;
549 }
@@ -551,10 +552,7 @@ function codegenInstructionValue(
552 if (instrValue.properties !== null) {
553 for (const [property, value] of instrValue.properties) {
554 properties.push(
554 - t.objectProperty(
555 - t.stringLiteral(property),
556 - codegenPlace(temp, value)
557 - )
555 + t.objectProperty(t.stringLiteral(property), codegenPlace(cx, value))
556 );
557 }
558 }
@@ -571,11 +569,11 @@ function codegenInstructionValue(
569 attributes.push(
570 t.jsxAttribute(
571 t.jsxIdentifier(prop),
574 - t.jsxExpressionContainer(codegenPlace(temp, value))
572 + t.jsxExpressionContainer(codegenPlace(cx, value))
573 )
574 );
575 }
578 - let tagValue = codegenPlace(temp, instrValue.tag);
576 + let tagValue = codegenPlace(cx, instrValue.tag);
577 let tag: string;
578 if (tagValue.type === "Identifier") {
579 tag = tagValue.name;
@@ -588,7 +586,7 @@ function codegenInstructionValue(
586 }
587 const children =
588 instrValue.children !== null
591 - ? instrValue.children.map((child) => codegenJsxElement(temp, child))
589 + ? instrValue.children.map((child) => codegenJsxElement(cx, child))
590 : [];
591 value = t.jsxElement(
592 t.jsxOpeningElement(
@@ -608,7 +606,7 @@ function codegenInstructionValue(
606 value = t.jsxFragment(
607 t.jsxOpeningFragment(),
608 t.jsxClosingFragment(),
611 - instrValue.children.map((child) => codegenJsxElement(temp, child))
609 + instrValue.children.map((child) => codegenJsxElement(cx, child))
610 );
611 break;
612 }
@@ -624,24 +622,24 @@ function codegenInstructionValue(
622 value = t.assignmentExpression(
623 "=",
624 t.memberExpression(
627 - codegenPlace(temp, instrValue.object),
625 + codegenPlace(cx, instrValue.object),
626 t.identifier(instrValue.property)
627 ),
630 - codegenPlace(temp, instrValue.value)
628 + codegenPlace(cx, instrValue.value)
629 );
630 break;
631 }
632 case "PropertyLoad": {
633 if (instrValue.optional) {
634 value = t.optionalMemberExpression(
637 - codegenPlace(temp, instrValue.object),
635 + codegenPlace(cx, instrValue.object),
636 t.identifier(instrValue.property),
637 undefined,
638 true
639 );
640 } else {
641 value = t.memberExpression(
644 - codegenPlace(temp, instrValue.object),
642 + codegenPlace(cx, instrValue.object),
643 t.identifier(instrValue.property)
644 );
645 }
@@ -651,24 +649,24 @@ function codegenInstructionValue(
649 value = t.assignmentExpression(
650 "=",
651 t.memberExpression(
654 - codegenPlace(temp, instrValue.object),
655 - codegenPlace(temp, instrValue.property),
652 + codegenPlace(cx, instrValue.object),
653 + codegenPlace(cx, instrValue.property),
654 true
655 ),
658 - codegenPlace(temp, instrValue.value)
656 + codegenPlace(cx, instrValue.value)
657 );
658 break;
659 }
660 case "ComputedLoad": {
661 value = t.memberExpression(
664 - codegenPlace(temp, instrValue.object),
665 - codegenPlace(temp, instrValue.property),
662 + codegenPlace(cx, instrValue.object),
663 + codegenPlace(cx, instrValue.property),
664 true
665 );
666 break;
667 }
668 case "Identifier": {
671 - value = codegenPlace(temp, instrValue);
669 + value = codegenPlace(cx, instrValue);
670 break;
671 }
672 case "FunctionExpression": {
@@ -681,7 +679,7 @@ function codegenInstructionValue(
679 case "TaggedTemplateExpression": {
680 value = createTaggedTemplateExpression(
681 instrValue.loc,
684 - codegenPlace(temp, instrValue.tag),
682 + codegenPlace(cx, instrValue.tag),
683 t.templateLiteral([t.templateElement(instrValue.value)], [])
684 );
685 break;
@@ -690,11 +688,39 @@ function codegenInstructionValue(
688 value = createLogicalExpression(
689 instrValue.loc,
690 instrValue.operator,
693 - codegenInstructionValue(temp, instrValue.left),
694 - codegenInstructionValue(temp, instrValue.right)
691 + codegenInstructionValue(cx, instrValue.left),
692 + codegenInstructionValue(cx, instrValue.right)
693 );
694 break;
695 }
696 + case "SequenceExpression": {
697 + const body = codegenBlock(
698 + cx,
699 + instrValue.instructions.map((instruction) => ({
700 + kind: "instruction",
701 + instruction,
702 + }))
703 + ).body;
704 + const expressions = body.map((stmt) => {
705 + if (stmt.type === "ExpressionStatement") {
706 + return stmt.expression;
707 + } else {
708 + todoInvariant(
709 + false,
710 + `Handle conversion of ${stmt.type} to expression`
711 + );
712 + }
713 + });
714 + if (expressions.length === 0) {
715 + value = codegenInstructionValue(cx, instrValue.value);
716 + } else {
717 + value = createSequenceExpression(instrValue.loc, [
718 + ...expressions,
719 + codegenInstructionValue(cx, instrValue.value),
720 + ]);
721 + }
722 + break;
723 + }
724 default: {
725 assertExhaustive(
726 instrValue,
@@ -706,7 +732,7 @@ function codegenInstructionValue(
732 }
733
734 function codegenJsxElement(
709 - temp: Temporaries,
735 + cx: Context,
736 place: Place
737 ):
738 | t.JSXText
@@ -714,7 +740,7 @@ function codegenJsxElement(
740 | t.JSXSpreadChild
741 | t.JSXElement
742 | t.JSXFragment {
717 - const value = codegenPlace(temp, place);
743 + const value = codegenPlace(cx, place);
744 switch (value.type) {
745 case "StringLiteral": {
746 return t.jsxText(value.value);
@@ -730,7 +756,7 @@ function codegenLVal(lval: LValue): t.LVal {
756 }
757
758 function codegenValue(
733 - temp: Temporaries,
759 + cx: Context,
760 value: boolean | number | string | null | undefined
761 ): t.Expression {
762 if (typeof value === "number") {
@@ -748,9 +774,9 @@ function codegenValue(
774 }
775 }
776
751 -function codegenPlace(temp: Temporaries, place: Place): t.Expression {
777 +function codegenPlace(cx: Context, place: Place): t.Expression {
778 todoInvariant(place.kind === "Identifier", "support scope values");
753 - let tmp = temp.get(place.identifier.id);
779 + let tmp = cx.temp.get(place.identifier.id);
780 if (tmp != null) {
781 return tmp;
782 }
compiler/forget/src/ReactiveScopes/visitors.ts
+7
@@ -91,6 +91,13 @@ export function* eachReactiveValueOperand(
91 yield* eachReactiveValueOperand(instrValue.right);
92 break;
93 }
94 + case "SequenceExpression": {
95 + for (const instr of instrValue.instructions) {
96 + yield* eachReactiveValueOperand(instr.value);
97 + }
98 + yield* eachReactiveValueOperand(instrValue.value);
99 + break;
100 + }
101 default: {
102 yield* eachInstructionValueOperand(instrValue);
103 }
compiler/forget/src/SSA/LeaveSSA.ts
+5 -1
@@ -123,7 +123,6 @@ export function leaveSSA(fn: HIRFunction) {
123 (terminal.kind === "if" ||
124 terminal.kind === "switch" ||
125 terminal.kind === "while" ||
126 - terminal.kind === "logical" ||
126 terminal.kind === "for") &&
127 terminal.fallthrough !== null
128 ) {
@@ -149,6 +148,11 @@ export function leaveSSA(fn: HIRFunction) {
148 pushPhis(rewritePhis, update);
149 update.phis.clear();
150 }
151 + if (terminal.kind === "logical") {
152 + const fallthrough = fn.body.blocks.get(terminal.fallthrough)!;
153 + pushPhis(rewritePhis, fallthrough);
154 + fallthrough.phis.clear();
155 + }
156
157 for (const { phi, block: phiBlock } of reassignmentPhis) {
158 // In some cases one of the phi operands can be defined *before* the let binding
compiler/forget/src/__tests__/fixtures/hir/logical-expression.expect.md
+11 -16
@@ -2,8 +2,9 @@
2 ## Input
3
4 ```javascript
5 +// @only
6 function component(props) {
6 - let a = props.a && props.b;
7 + let a = (props.a && props.b && props.c) || props.d;
8 return a;
9 // let b = props.c || props.d;
10 // let c = props.e ?? props.f;
@@ -15,25 +16,19 @@ function component(props) {
16 ## Code
17
18 ```javascript
19 +// @only
20 function component(props) {
21 const $ = React.useMemoCache();
20 - const c_0 = $[0] !== props.a;
21 - const c_1 = $[1] !== props.b;
22 - let t2;
23 - if (c_0 || c_1) {
24 - t2 = undefined;
25 - if (props.a) {
26 - t2 = props.a;
27 - } else {
28 - t2 = props.b;
29 - }
30 - $[0] = props.a;
31 - $[1] = props.b;
32 - $[2] = t2;
22 + const c_0 = $[0] !== props;
23 + let t1;
24 + if (c_0) {
25 + t1 = (props.a && props.b && props.c) || props.d;
26 + $[0] = props;
27 + $[1] = t1;
28 } else {
34 - t2 = $[2];
29 + t1 = $[1];
30 }
36 - const a = t2;
31 + const a = t1;
32 return a;
33 }
34
compiler/forget/src/__tests__/fixtures/hir/logical-expression.js
+2 -1
@@ -1,5 +1,6 @@
1 +// @only
2 function component(props) {
2 - let a = props.a && props.b;
3 + let a = (props.a && props.b && props.c) || props.d;
4 return a;
5 // let b = props.c || props.d;
6 // let c = props.e ?? props.f;