diff --git a/compiler/forget/src/HIR/HIR.ts b/compiler/forget/src/HIR/HIR.ts index 67e96323c5..5d9cde3984 100644 --- a/compiler/forget/src/HIR/HIR.ts +++ b/compiler/forget/src/HIR/HIR.ts @@ -93,7 +93,10 @@ export type ReactiveInstruction = { loc: SourceLocation; }; -export type ReactiveValue = InstructionValue | ReactiveLogicalValue; +export type ReactiveValue = + | InstructionValue + | ReactiveLogicalValue + | ReactiveSequenceValue; export type ReactiveLogicalValue = { kind: "LogicalExpression"; @@ -103,6 +106,13 @@ export type ReactiveLogicalValue = { loc: SourceLocation; }; +export type ReactiveSequenceValue = { + kind: "SequenceExpression"; + instructions: Array; + value: ReactiveValue; + loc: SourceLocation; +}; + export type ReactiveTerminal = | ReactiveBreakTerminal | ReactiveContinueTerminal diff --git a/compiler/forget/src/HIR/PrintHIR.ts b/compiler/forget/src/HIR/PrintHIR.ts index 11db411664..da8b46c4f0 100644 --- a/compiler/forget/src/HIR/PrintHIR.ts +++ b/compiler/forget/src/HIR/PrintHIR.ts @@ -339,6 +339,16 @@ export function printInstructionValue(instrValue: ReactiveValue): string { } ${printInstructionValue(instrValue.right)}`; break; } + case "SequenceExpression": { + value = [ + `Sequence`, + ...instrValue.instructions.map( + (instr) => ` ${printInstruction(instr)}` + ), + ` ${printInstructionValue(instrValue.value)}`, + ].join("\n"); + break; + } default: { assertExhaustive( instrValue, diff --git a/compiler/forget/src/ReactiveScopes/BuildReactiveFunction.ts b/compiler/forget/src/ReactiveScopes/BuildReactiveFunction.ts index 9e5ea3a52e..90fe00e8f0 100644 --- a/compiler/forget/src/ReactiveScopes/BuildReactiveFunction.ts +++ b/compiler/forget/src/ReactiveScopes/BuildReactiveFunction.ts @@ -19,11 +19,24 @@ import { } from "../HIR"; import { HIRFunction, + Instruction, + InstructionKind, ReactiveBreakTerminal, ReactiveContinueTerminal, ReactiveFunction, + ReactiveInstruction, + ReactiveLogicalValue, + ReactiveSequenceValue, ReactiveTerminalStatement, + ReactiveValue, + Terminal, } from "../HIR/HIR"; +import { + printInstructionValue, + printPlace, + printTerminal, +} from "../HIR/PrintHIR"; +import { mapInstructionOperands } from "../HIR/visitors"; import { assertExhaustive } from "../Utils/utils"; /** @@ -407,7 +420,16 @@ class Driver { const scheduleId = this.cx.schedule(fallthroughId, "if"); scheduleIds.push(scheduleId); - this.visitBlock(this.cx.ir.blocks.get(terminal.test)!, blockValue); + const { place, value } = this.visitValueTerminal(terminal); + blockValue.push({ + kind: "instruction", + instruction: { + id: terminal.id, + lvalue: { kind: InstructionKind.Const, place }, + value, + loc: terminal.loc, + }, + }); this.cx.unschedule(scheduleId); this.visitBlock(this.cx.ir.blocks.get(fallthroughId)!, blockValue); @@ -447,6 +469,119 @@ class Driver { } } + visitValueTerminal(terminal: Terminal): { + value: ReactiveValue; + place: Place; + fallthrough: BlockId; + } { + switch (terminal.kind) { + case "logical": { + let testBlock: BasicBlock; + let leftValue: ReactiveValue | null = null; + let leftPlace: Place | null = null; + const defaultTestBlock = this.cx.ir.blocks.get(terminal.test)!; + if (defaultTestBlock.terminal.kind === "branch") { + testBlock = defaultTestBlock; + } else { + const leftResult = this.visitValueTerminal(defaultTestBlock.terminal); + testBlock = this.cx.ir.blocks.get(leftResult.fallthrough)!; + leftValue = leftResult.value; + leftPlace = leftResult.place; + } + + invariant( + testBlock.terminal.kind === "branch", + "Unexpected terminal kind '%s' for logical test block", + testBlock.terminal.kind + ); + const leftInstructions: Array = + testBlock.instructions; + const leftBlock = this.cx.ir.blocks.get(testBlock.terminal.consequent)!; + leftInstructions.push(...leftBlock.instructions); + // TODO: If right block ends in a value terminal, recursively process with visitValueTerminal + // similar to handling for the compound lhs case. + const rightBlock = this.cx.ir.blocks.get(testBlock.terminal.alternate)!; + const rightInstructions: Array = + rightBlock.instructions; + const place = leftInstructions.at(-1)!.lvalue!.place; + invariant( + place.identifier === + rightInstructions.at(-1)!.lvalue!.place.identifier, + "Expected both branches of a logical expression to store to the same temporary" + ); + if (leftPlace !== null) { + leftInstructions.forEach((instr) => + mapInstructionOperands(instr as Instruction, (place) => { + return place.identifier === leftPlace!.identifier + ? (leftValue! as Place) + : place; + }) + ); + rightInstructions.forEach((instr) => + mapInstructionOperands(instr as Instruction, (place) => { + return place.identifier === leftPlace!.identifier + ? (leftValue! as Place) + : place; + }) + ); + } + + let left: ReactiveValue; + if (leftInstructions.length === 1) { + left = leftInstructions[0]!.value; + } else { + const sequence: ReactiveSequenceValue = { + kind: "SequenceExpression", + instructions: leftInstructions.slice(0, -1), + value: leftInstructions.at(-1)!.value, + loc: terminal.loc, + }; + left = sequence; + } + let right: ReactiveValue; + if (rightInstructions.length === 1) { + right = rightInstructions[0]!.value; + } else { + const sequence: ReactiveSequenceValue = { + kind: "SequenceExpression", + instructions: rightInstructions.slice(0, -1), + value: rightInstructions.at(-1)!.value, + loc: terminal.loc, + }; + right = sequence; + } + const value: ReactiveLogicalValue = { + kind: "LogicalExpression", + operator: terminal.operator, + left, + right, + loc: terminal.loc, + }; + console.log( + printTerminal(terminal) + + " testBlock=" + + testBlock.id + + " " + + printPlace(place) + + "=" + + printInstructionValue(value) + ); + return { + place: { ...place }, + value, + fallthrough: terminal.fallthrough, + }; + } + default: { + invariant( + false, + "Unexpected value block terminal kind '%s'", + terminal.kind + ); + } + } + } + visitInitBlock(parent: ReactiveBlock, block: BasicBlock): ReactiveValueBlock { const initBlock: ReactiveValueBlock = { kind: "value-block", diff --git a/compiler/forget/src/ReactiveScopes/CodegenReactiveFunction.ts b/compiler/forget/src/ReactiveScopes/CodegenReactiveFunction.ts index 8eb59e19be..1010ed5064 100644 --- a/compiler/forget/src/ReactiveScopes/CodegenReactiveFunction.ts +++ b/compiler/forget/src/ReactiveScopes/CodegenReactiveFunction.ts @@ -93,7 +93,7 @@ function codegenBlock(cx: Context, block: ReactiveBlock): t.BlockStatement { const statement = codegenInstructionNullable( cx, item.instruction, - codegenInstructionValue(cx.temp, item.instruction.value) + codegenInstructionValue(cx, item.instruction.value) ); if (statement !== null) { statements.push(statement); @@ -272,7 +272,7 @@ function codegenTerminal( } case "if": { return t.ifStatement( - codegenPlace(cx.temp, terminal.test), + codegenPlace(cx, terminal.test), codegenBlock(cx, terminal.consequent), terminal.alternate !== null ? codegenBlock(cx, terminal.alternate) @@ -281,22 +281,22 @@ function codegenTerminal( } case "return": { return t.returnStatement( - terminal.value !== null ? codegenPlace(cx.temp, terminal.value) : null + terminal.value !== null ? codegenPlace(cx, terminal.value) : null ); } case "switch": { return t.switchStatement( - codegenPlace(cx.temp, terminal.test), + codegenPlace(cx, terminal.test), terminal.cases.map((case_) => { const test = - case_.test !== null ? codegenPlace(cx.temp, case_.test) : null; + case_.test !== null ? codegenPlace(cx, case_.test) : null; const block = codegenBlock(cx, case_.block!); return t.switchCase(test, [block]); }) ); } case "throw": { - return t.throwStatement(codegenPlace(cx.temp, terminal.value)); + return t.throwStatement(codegenPlace(cx, terminal.value)); } case "while": { const test = codegenValueBlock(cx, terminal.test); @@ -319,7 +319,7 @@ function codegenInstructionNullable( let statement; if (instr.lvalue !== null && cx.declared(instr.lvalue.place.identifier)) { statement = codegenInstruction( - cx.temp, + cx, { ...instr, lvalue: { @@ -330,7 +330,7 @@ function codegenInstructionNullable( value ); } else { - statement = codegenInstruction(cx.temp, instr, value); + statement = codegenInstruction(cx, instr, value); } if (statement.type === "EmptyStatement") { return null; @@ -348,7 +348,7 @@ function codegenForInit( body.length === 0, "Expected for init block to produce only temporaries" ); - return codegenInstructionValue(cx.temp, init.last.value); + return codegenInstructionValue(cx, init.last.value); } else { invariant( body.length === 1, @@ -376,7 +376,7 @@ function codegenValueBlock( } }); if (block.last !== null) { - const value = codegenInstructionValue(cx.temp, block.last.value); + const value = codegenInstructionValue(cx, block.last.value); expressions.push(value); } invariant( @@ -431,6 +431,7 @@ const createVariableDeclaration = withLoc(t.variableDeclaration); const createWhileStatement = withLoc(t.whileStatement); const createTaggedTemplateExpression = withLoc(t.taggedTemplateExpression); const createLogicalExpression = withLoc(t.logicalExpression); +const createSequenceExpression = withLoc(t.sequenceExpression); type Temporaries = Map; @@ -439,7 +440,7 @@ function codegenLabel(id: BlockId): string { } function codegenInstruction( - temp: Temporaries, + cx: Context, instr: ReactiveInstruction, value: t.Expression ): t.Statement { @@ -451,7 +452,7 @@ function codegenInstruction( } if (instr.lvalue.place.identifier.name === null) { // temporary - temp.set(instr.lvalue.place.identifier.id, value); + cx.temp.set(instr.lvalue.place.identifier.id, value); return t.emptyStatement(); } else { switch (instr.lvalue.kind) { @@ -482,21 +483,21 @@ function codegenInstruction( } function codegenInstructionValue( - temp: Temporaries, + cx: Context, instrValue: ReactiveValue ): t.Expression { let value: t.Expression; switch (instrValue.kind) { case "ArrayExpression": { const elements = instrValue.elements.map((element) => - codegenPlace(temp, element) + codegenPlace(cx, element) ); value = t.arrayExpression(elements); break; } case "BinaryExpression": { - const left = codegenPlace(temp, instrValue.left); - const right = codegenPlace(temp, instrValue.right); + const left = codegenPlace(cx, instrValue.left); + const right = codegenPlace(cx, instrValue.right); value = createBinaryExpression( instrValue.loc, instrValue.operator, @@ -508,41 +509,41 @@ function codegenInstructionValue( case "UnaryExpression": { value = t.unaryExpression( instrValue.operator as "throw", // todo - codegenPlace(temp, instrValue.value) + codegenPlace(cx, instrValue.value) ); break; } case "Primitive": { - value = codegenValue(temp, instrValue.value); + value = codegenValue(cx, instrValue.value); break; } case "CallExpression": { - const callee = codegenPlace(temp, instrValue.callee); - const args = instrValue.args.map((arg) => codegenPlace(temp, arg)); + const callee = codegenPlace(cx, instrValue.callee); + const args = instrValue.args.map((arg) => codegenPlace(cx, arg)); value = createCallExpression(instrValue.loc, callee, args); break; } case "PropertyCall": { - const receiver = codegenPlace(temp, instrValue.receiver); + const receiver = codegenPlace(cx, instrValue.receiver); const callee = t.memberExpression( receiver, t.identifier(instrValue.property) ); - const args = instrValue.args.map((arg) => codegenPlace(temp, arg)); + const args = instrValue.args.map((arg) => codegenPlace(cx, arg)); value = createCallExpression(instrValue.loc, callee, args); break; } case "ComputedCall": { - const receiver = codegenPlace(temp, instrValue.receiver); - const property = codegenPlace(temp, instrValue.property); + const receiver = codegenPlace(cx, instrValue.receiver); + const property = codegenPlace(cx, instrValue.property); const callee = t.memberExpression(receiver, property, true); - const args = instrValue.args.map((arg) => codegenPlace(temp, arg)); + const args = instrValue.args.map((arg) => codegenPlace(cx, arg)); value = createCallExpression(instrValue.loc, callee, args); break; } case "NewExpression": { - const callee = codegenPlace(temp, instrValue.callee); - const args = instrValue.args.map((arg) => codegenPlace(temp, arg)); + const callee = codegenPlace(cx, instrValue.callee); + const args = instrValue.args.map((arg) => codegenPlace(cx, arg)); value = t.newExpression(callee, args); break; } @@ -551,10 +552,7 @@ function codegenInstructionValue( if (instrValue.properties !== null) { for (const [property, value] of instrValue.properties) { properties.push( - t.objectProperty( - t.stringLiteral(property), - codegenPlace(temp, value) - ) + t.objectProperty(t.stringLiteral(property), codegenPlace(cx, value)) ); } } @@ -571,11 +569,11 @@ function codegenInstructionValue( attributes.push( t.jsxAttribute( t.jsxIdentifier(prop), - t.jsxExpressionContainer(codegenPlace(temp, value)) + t.jsxExpressionContainer(codegenPlace(cx, value)) ) ); } - let tagValue = codegenPlace(temp, instrValue.tag); + let tagValue = codegenPlace(cx, instrValue.tag); let tag: string; if (tagValue.type === "Identifier") { tag = tagValue.name; @@ -588,7 +586,7 @@ function codegenInstructionValue( } const children = instrValue.children !== null - ? instrValue.children.map((child) => codegenJsxElement(temp, child)) + ? instrValue.children.map((child) => codegenJsxElement(cx, child)) : []; value = t.jsxElement( t.jsxOpeningElement( @@ -608,7 +606,7 @@ function codegenInstructionValue( value = t.jsxFragment( t.jsxOpeningFragment(), t.jsxClosingFragment(), - instrValue.children.map((child) => codegenJsxElement(temp, child)) + instrValue.children.map((child) => codegenJsxElement(cx, child)) ); break; } @@ -624,24 +622,24 @@ function codegenInstructionValue( value = t.assignmentExpression( "=", t.memberExpression( - codegenPlace(temp, instrValue.object), + codegenPlace(cx, instrValue.object), t.identifier(instrValue.property) ), - codegenPlace(temp, instrValue.value) + codegenPlace(cx, instrValue.value) ); break; } case "PropertyLoad": { if (instrValue.optional) { value = t.optionalMemberExpression( - codegenPlace(temp, instrValue.object), + codegenPlace(cx, instrValue.object), t.identifier(instrValue.property), undefined, true ); } else { value = t.memberExpression( - codegenPlace(temp, instrValue.object), + codegenPlace(cx, instrValue.object), t.identifier(instrValue.property) ); } @@ -651,24 +649,24 @@ function codegenInstructionValue( value = t.assignmentExpression( "=", t.memberExpression( - codegenPlace(temp, instrValue.object), - codegenPlace(temp, instrValue.property), + codegenPlace(cx, instrValue.object), + codegenPlace(cx, instrValue.property), true ), - codegenPlace(temp, instrValue.value) + codegenPlace(cx, instrValue.value) ); break; } case "ComputedLoad": { value = t.memberExpression( - codegenPlace(temp, instrValue.object), - codegenPlace(temp, instrValue.property), + codegenPlace(cx, instrValue.object), + codegenPlace(cx, instrValue.property), true ); break; } case "Identifier": { - value = codegenPlace(temp, instrValue); + value = codegenPlace(cx, instrValue); break; } case "FunctionExpression": { @@ -681,7 +679,7 @@ function codegenInstructionValue( case "TaggedTemplateExpression": { value = createTaggedTemplateExpression( instrValue.loc, - codegenPlace(temp, instrValue.tag), + codegenPlace(cx, instrValue.tag), t.templateLiteral([t.templateElement(instrValue.value)], []) ); break; @@ -690,11 +688,39 @@ function codegenInstructionValue( value = createLogicalExpression( instrValue.loc, instrValue.operator, - codegenInstructionValue(temp, instrValue.left), - codegenInstructionValue(temp, instrValue.right) + codegenInstructionValue(cx, instrValue.left), + codegenInstructionValue(cx, instrValue.right) ); break; } + case "SequenceExpression": { + const body = codegenBlock( + cx, + instrValue.instructions.map((instruction) => ({ + kind: "instruction", + instruction, + })) + ).body; + const expressions = body.map((stmt) => { + if (stmt.type === "ExpressionStatement") { + return stmt.expression; + } else { + todoInvariant( + false, + `Handle conversion of ${stmt.type} to expression` + ); + } + }); + if (expressions.length === 0) { + value = codegenInstructionValue(cx, instrValue.value); + } else { + value = createSequenceExpression(instrValue.loc, [ + ...expressions, + codegenInstructionValue(cx, instrValue.value), + ]); + } + break; + } default: { assertExhaustive( instrValue, @@ -706,7 +732,7 @@ function codegenInstructionValue( } function codegenJsxElement( - temp: Temporaries, + cx: Context, place: Place ): | t.JSXText @@ -714,7 +740,7 @@ function codegenJsxElement( | t.JSXSpreadChild | t.JSXElement | t.JSXFragment { - const value = codegenPlace(temp, place); + const value = codegenPlace(cx, place); switch (value.type) { case "StringLiteral": { return t.jsxText(value.value); @@ -730,7 +756,7 @@ function codegenLVal(lval: LValue): t.LVal { } function codegenValue( - temp: Temporaries, + cx: Context, value: boolean | number | string | null | undefined ): t.Expression { if (typeof value === "number") { @@ -748,9 +774,9 @@ function codegenValue( } } -function codegenPlace(temp: Temporaries, place: Place): t.Expression { +function codegenPlace(cx: Context, place: Place): t.Expression { todoInvariant(place.kind === "Identifier", "support scope values"); - let tmp = temp.get(place.identifier.id); + let tmp = cx.temp.get(place.identifier.id); if (tmp != null) { return tmp; } diff --git a/compiler/forget/src/ReactiveScopes/visitors.ts b/compiler/forget/src/ReactiveScopes/visitors.ts index 0f275d3417..de8f60fc11 100644 --- a/compiler/forget/src/ReactiveScopes/visitors.ts +++ b/compiler/forget/src/ReactiveScopes/visitors.ts @@ -91,6 +91,13 @@ export function* eachReactiveValueOperand( yield* eachReactiveValueOperand(instrValue.right); break; } + case "SequenceExpression": { + for (const instr of instrValue.instructions) { + yield* eachReactiveValueOperand(instr.value); + } + yield* eachReactiveValueOperand(instrValue.value); + break; + } default: { yield* eachInstructionValueOperand(instrValue); } diff --git a/compiler/forget/src/SSA/LeaveSSA.ts b/compiler/forget/src/SSA/LeaveSSA.ts index b479180fb9..5d360c50b2 100644 --- a/compiler/forget/src/SSA/LeaveSSA.ts +++ b/compiler/forget/src/SSA/LeaveSSA.ts @@ -123,7 +123,6 @@ export function leaveSSA(fn: HIRFunction) { (terminal.kind === "if" || terminal.kind === "switch" || terminal.kind === "while" || - terminal.kind === "logical" || terminal.kind === "for") && terminal.fallthrough !== null ) { @@ -149,6 +148,11 @@ export function leaveSSA(fn: HIRFunction) { pushPhis(rewritePhis, update); update.phis.clear(); } + if (terminal.kind === "logical") { + const fallthrough = fn.body.blocks.get(terminal.fallthrough)!; + pushPhis(rewritePhis, fallthrough); + fallthrough.phis.clear(); + } for (const { phi, block: phiBlock } of reassignmentPhis) { // In some cases one of the phi operands can be defined *before* the let binding diff --git a/compiler/forget/src/__tests__/fixtures/hir/logical-expression.expect.md b/compiler/forget/src/__tests__/fixtures/hir/logical-expression.expect.md index ceda427893..f2c3628cfc 100644 --- a/compiler/forget/src/__tests__/fixtures/hir/logical-expression.expect.md +++ b/compiler/forget/src/__tests__/fixtures/hir/logical-expression.expect.md @@ -2,8 +2,9 @@ ## Input ```javascript +// @only function component(props) { - let a = props.a && props.b; + let a = (props.a && props.b && props.c) || props.d; return a; // let b = props.c || props.d; // let c = props.e ?? props.f; @@ -15,25 +16,19 @@ function component(props) { ## Code ```javascript +// @only function component(props) { const $ = React.useMemoCache(); - const c_0 = $[0] !== props.a; - const c_1 = $[1] !== props.b; - let t2; - if (c_0 || c_1) { - t2 = undefined; - if (props.a) { - t2 = props.a; - } else { - t2 = props.b; - } - $[0] = props.a; - $[1] = props.b; - $[2] = t2; + const c_0 = $[0] !== props; + let t1; + if (c_0) { + t1 = (props.a && props.b && props.c) || props.d; + $[0] = props; + $[1] = t1; } else { - t2 = $[2]; + t1 = $[1]; } - const a = t2; + const a = t1; return a; } diff --git a/compiler/forget/src/__tests__/fixtures/hir/logical-expression.js b/compiler/forget/src/__tests__/fixtures/hir/logical-expression.js index 92847fcef9..122434a1b0 100644 --- a/compiler/forget/src/__tests__/fixtures/hir/logical-expression.js +++ b/compiler/forget/src/__tests__/fixtures/hir/logical-expression.js @@ -1,5 +1,6 @@ +// @only function component(props) { - let a = props.a && props.b; + let a = (props.a && props.b && props.c) || props.d; return a; // let b = props.c || props.d; // let c = props.e ?? props.f;