From 9010c87a75e1b6198567c06b7243fd4cdcdf571c Mon Sep 17 00:00:00 2001 From: CooperSheroy Date: Sat, 1 Aug 2026 09:12:05 +0530 Subject: [PATCH] fix: lower void conditionals as statements --- .../compiler/src/frontend/lowering/lower-calls.ts | 10 ++-------- .../compiler/src/frontend/lowering/lower-stmts.ts | 12 ++++++++++++ packages/compiler/src/frontend/lowering/lowerer.ts | 11 +++++++++++ tests/corpus/2352-void-coercions.ts | 7 +++++++ 4 files changed, 32 insertions(+), 8 deletions(-) diff --git a/packages/compiler/src/frontend/lowering/lower-calls.ts b/packages/compiler/src/frontend/lowering/lower-calls.ts index 766fb2976..0c20157e7 100644 --- a/packages/compiler/src/frontend/lowering/lower-calls.ts +++ b/packages/compiler/src/frontend/lowering/lower-calls.ts @@ -5432,14 +5432,8 @@ const inliningPredicates = new Set(); // A `void e` body rides the statement lowering (the value is // discarded here, so the operand evaluates for effect alone — // `(name) => void doThing(name)`, the fire-and-forget arrow). - let stripped: ts.Expression = bodyExpr; - while (ts.isParenthesizedExpression(stripped)) stripped = stripped.expression; - if (ts.isVoidExpression(stripped)) { - body = [L.lowerExprStatement(stripped)]; - } else { - const value = L.lowerExpr(bodyExpr); - body = value.kind === "unitLit" ? [] : [{ kind: "exprStmt", expr: value, loc: locOf(node.body!) }]; - } + const stmt = L.lowerExprStatement(bodyExpr); + body = stmt.kind === "block" && stmt.body.length === 0 ? [] : [stmt]; } else { let value = L.lowerExpr(bodyExpr); // An async concise body whose value is itself a promise diff --git a/packages/compiler/src/frontend/lowering/lower-stmts.ts b/packages/compiler/src/frontend/lowering/lower-stmts.ts index 9d08f5ea9..3766330db 100644 --- a/packages/compiler/src/frontend/lowering/lower-stmts.ts +++ b/packages/compiler/src/frontend/lowering/lower-stmts.ts @@ -4142,6 +4142,18 @@ function isEsModuleStamp(expr: ts.Expression): boolean { // Value-position `void` keeps the syntax fence (a standalone undefined // VALUE needs a union slot to live in). if (ts.isVoidExpression(expr)) return lowerExprStatement(L, expr.expression); + // Statement-position conditionals evaluate the condition, then exactly + // one arm for effect and drop that arm's value. Lower them as `if` + // statements so void-valued arms do not form invalid value ternaries. + if (ts.isConditionalExpression(expr)) { + return { + kind: "if", + cond: L.lowerCondition(expr.condition), + then: [lowerExprStatement(L, expr.whenTrue)], + else_: [lowerExprStatement(L, expr.whenFalse)], + loc: locOf(expr), + }; + } if (ts.isBinaryExpression(expr)) { const opKind = expr.operatorToken.kind; // Statement-position comma (`({} = a, [] = a);`, `i++, j++` in a diff --git a/packages/compiler/src/frontend/lowering/lowerer.ts b/packages/compiler/src/frontend/lowering/lowerer.ts index fab1e4a93..112405546 100644 --- a/packages/compiler/src/frontend/lowering/lowerer.ts +++ b/packages/compiler/src/frontend/lowering/lowerer.ts @@ -5805,6 +5805,17 @@ export class Lowerer { lowerReturnStmt(node: ts.Expression, loc: SrcLoc): IrStmt { const expected = this.ctx.returnType; if (expected.kind === "void") { + let expr = node; + while (ts.isParenthesizedExpression(expr)) expr = expr.expression; + if (ts.isConditionalExpression(expr)) { + return { + kind: "if", + cond: this.lowerCondition(expr.condition), + then: [this.lowerReturnStmt(expr.whenTrue, loc)], + else_: [this.lowerReturnStmt(expr.whenFalse, loc)], + loc, + }; + } let e = this.lowerExpr(node); if (this.ctx.isAsync && e.type.kind === "promise") { e = { kind: "awaitExpr", value: e, type: e.type.inner, loc: e.loc }; diff --git a/tests/corpus/2352-void-coercions.ts b/tests/corpus/2352-void-coercions.ts index 2f333667f..47d296a10 100644 --- a/tests/corpus/2352-void-coercions.ts +++ b/tests/corpus/2352-void-coercions.ts @@ -46,6 +46,13 @@ const box = { box.poke(); console.log("method unit return ok"); +// Concise void-returning arrows over conditional void calls lower as a +// branch, not a value ternary. +const branchVoid = (flag: boolean) => (flag ? box.poke() : fv()); +branchVoid(true); +branchVoid(false); +console.log("branch-void", effects); + // Async concise body over an existing promise: resolves through. async function inner(): Promise { return 42;