fix(transform): closes #342 — cond ? await a() : b() evaluates BOTH branches - #356
Merged
Conversation
…ranches Issue #342: a conditional expression with `await` on one side ran the awaited call regardless of the condition. Previously a `digest`-bound codepath that wrote `args.command === 'fetch' ? await runFetch() : runDigest()` also executed `runFetch` (printing fetch progress) before falling through to `runDigest`. Root cause is in the async→generator pre-pass, not the codegen. The hoister blindly walks expression children looking for `Expr::Await` and lifts each into a `let __await_N = await X` placed BEFORE the containing statement. For let r = cond ? await a() : b(); it lifts `await a()` to `let __await_N = await a();` so the resulting HIR is let __await_N = await a(); let r = cond ? __await_N : b(); — which executes `a()` unconditionally. The HIR confirms it: dumping with `--print-hir` shows `then_expr: LocalGet(N)` and the original call site moved to state 0 of the generator state machine. Fix: detect `Expr::Conditional` whose then/else branch contains an await and, before any general hoisting walks into it, lift the whole conditional to a statement-level if/else with a temp local: let __cond_await_N: any; if (cond) { __cond_await_N = await a(); } else { __cond_await_N = b(); } let r = __cond_await_N; The recursive `hoist_awaits_in_stmts` call inside each branch then hoists the await to the top of its own if-arm — the position the await→yield rewrite expects. Both `hoist_awaits_in_expr_full` and `hoist_awaits_avoiding_top_level` get the early dispatch so the lift fires whether the conditional is at let-init or in a deeper position. Verified with all four await-shape combinations: cond ? await a() : b() — only the matched branch runs (was: both) cond ? a() : await b() — only the matched branch runs (was: both) cond ? await a() : await b() — only the matched branch runs (was: both) cond ? a() : b() — unchanged (was already correct) Plus the actual skelpo-listener pattern that surfaced the bug: const code = args.command === 'fetch' ? await runFetch(args) : runDigest(args); now runs only `runDigest` on a `digest` invocation. `cargo build --release -p perry` clean.
proggeramlug
force-pushed
the
fix/342-ternary-await
branch
from
April 30, 2026 16:51
aaa8a3e to
8c16ef7
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What's broken
A conditional expression with
awaiton one side ran the awaited call regardless of the condition. The original repro from #342:Buggy output:
ran a/ran b/r=2(a ran even though the digest branch was taken).Root cause
Not codegen — the async→generator pre-pass. The hoister blindly walks expression children looking for
Expr::Awaitand lifts each into alet __await_N = await Xplaced BEFORE the containing statement. Sobecomes
—
a()runs unconditionally.--print-hirconfirms it:then_expr: LocalGet(N), and the originalCallgot moved to state 0 of the generator state machine.Fix
Detect
Expr::Conditionalwhose then- or else-branch contains anawaitand, before any general hoisting walks into it, lift the whole conditional to a statement-level if/else with a temp local:The recursive
hoist_awaits_in_stmtscall inside each branch then hoists the await to the top of its own if-arm — the position the await→yield rewrite expects. Bothhoist_awaits_in_expr_fullandhoist_awaits_avoiding_top_levelget the early dispatch, so the lift fires whether the conditional is at let-init or in a deeper position.Verification
All four await-shape combinations behave correctly:
cond ? await a() : b()cond ? a() : await b()cond ? await a() : await b()cond ? a() : b()(sync only)Plus the actual skelpo-listener pattern that surfaced the bug:
now runs only
runDigeston adigestinvocation.cargo build --release -p perryclean.Closes #342