Skip to content

fix(emit): flush slot loans between operand eval and return, and on ? exits (#1398) - #1428

Closed
Ljunqiang wants to merge 1 commit into
limitlessprojects:mainfrom
Ljunqiang:fix/issue-1398-return-loan-flush
Closed

Ljunqiang wants to merge 1 commit into
limitlessprojects:mainfrom
Ljunqiang:fix/issue-1398-return-loan-flush

Conversation

@Ljunqiang

Copy link
Copy Markdown

Fixes #1398.

Bug

hy-emit flushed O8/OQ slot loans at the top of emit_return — before the operands were evaluated — and the ? error-propagation exits never flushed at all:

  • return c.next() wrote m["k"] = c back pre-call: the post-call map kept the stale value (bump: 11 10 instead of bump: 11 11), and the evaluated result came from the loan's own copy, not the map.
  • c.n = 42; x := fail_step()? dropped the 42 entirely on the propagate-error return (printed 10 instead of 42).

Fix (compiler/crates/hy-emit/src/stmt.rs)

  • emit_return: with live loans, materialise every non-trivial operand into a temp (hyRetN := <expr>; Ident/FnLit/Lit pass through untouched — nil would not even build as hyRet := nil), then flush, then return the temps. Evaluation and write-back now straddle the flush exactly as the runtime contract requires.
  • propagate_error: flush before its three early-return branches (covers every ? exit, incl. the emitter's httptest helper).
  • The E11 handler (handler_writer + http.Error) flushes before its early return too.
  • New has_live_loans helper mirrors flush_loans_for_return's sentinel-frame skip exactly.

With no live loans the emission path is untouched (byte-identical), so no existing golden output moved — loan_terminator/closure_loan tests still pass unchanged.

Regression pins

  • hy-emit/tests/loan_terminator.rs: +2 tests (eval < store-back < return; store-back < propagate-error return).
  • tests/checker-corpus/slot-loan-return-operand-flush.hy (ACCEPT, Tier-0, --deny-managed clean) with EXPECTED-OUTPUT: bump: 11 11 try: 42 locking both the return path and the ? path at runtime. Census 126 → 127 (ACCEPT 39 · AM 2 · REJECT 86); VERDICTS.md + README registration appended.

Verification

  • cargo test -p hy-emit / -p hy-syntax: green (incl. the 2 new tests, parse_all now asserts 127).
  • tools/corpus_harness.py: 127/127, tripwire not fired.
  • tools/differential_loop.py: accept-side 41/41 (build+run, stdout matched), reject-side 86/86.
  • Negative validation: verdict flipped to REJECT A1b → harness 126/127; EXPECTED-OUTPUT flipped to bump: 11 10 try: 42 → differential 40/41 with exact stdout-mismatch; both restored to green.
  • Repro from the issue on this branch: bump: 11 11 / try: 42.

Note: sibling PR #1190 also moves the census numbers (126 → 130 with 4 REJECT pins). Whichever lands second rebases and renumbers.

… exits (limitlessprojects#1398)

emit_return flushed O8/OQ slot loans at the top of the function, before
the operands were evaluated: `return c.next()` stored the PRE-call value
back into the map (bump printed `11 10` instead of `11 11`), and the
propagate-error (`?`) exits never flushed at all, dropping every mutation
performed before the failing call (printed `10` instead of `42`).

Materialise non-trivial operands (everything except Ident/FnLit/Lit) into
temps, flush the live loans, then return the temps; propagate_error and
the E11 handler flush before their early returns. Loans-inactive emission
is byte-identical, so no existing output moved.

Pinned by 2 hy-emit tests (eval < store-back < return; store-back <
propagate-error return) and the corpus fixture
slot-loan-return-operand-flush (EXPECTED-OUTPUT locks `bump: 11 11 try: 42`);
corpus census 126 -> 127. Verified: harness 127/127, differential accept
41/41 + reject 86/86, cargo test hy-emit / hy-syntax green.
@github-actions github-actions Bot added the bot:land-candidate Shepherd: fresh, gate-clean, verifiable (set from classify()) label Oct 24, 2026
Ljunqiang pushed a commit to Ljunqiang/Go-Hybrid that referenced this pull request Oct 24, 2026
…nd comparisons (fixes limitlessprojects#922)

Root cause: check_binary_operands compared numeric FAMILIES, not concrete
kinds — for `< <= > >=` and `+` the mismatch term was
`lk.is_numeric() != rk.is_numeric()` (int and float64 are both numeric, so
never mismatched), and `- * /` only checked kind membership. `x + f`,
`x - f`, `x * f`, `x / f`, `x < f`, `f <= x` checked clean while go build
refused each with `mismatched types int and float64` (GUARANTEES Row 13).

Fix: a new Collector::numeric_adapt_conflict routes cross-kind operand
pairs through Go's only implicit numeric conversion — untyped-constant
adaptation — and emits HYB-E1002 when it cannot bridge them:
  - `1 + 2.5` (both untyped) and `f + 1` / `1 + f` (int const -> float64)
    keep building, including untyped `const` identifiers (`f + k`);
  - `x + f`, `f + x`, `x - f`, `x < f`, `f <= x`, and the untyped float
    const against a typed int (`x + 2.5`) are rejected;
  - `Define.untyped_const` (from LocalDecl.ty) distinguishes `const k = 1`
    (untyped) from `const k int = 1` (typed like a variable) for fn-local
    consts — the annotation was recorded but never consulted.

Validation: new hy-check/tests/binop_operand_types_issue_922.rs 24/24
(15 reject faces + 9 accept controls); corpus pin
binop-mismatched-numeric-reject.hy (REJECT BUILDABLE + HYB-E1002), census
126 -> 127 on all 7 surfaces; harness 127/127, tripwire not fired;
negative flip: baseline upstream accepts the pin (126/127, pin FAILs) and
restores to 127/127; differential 40/40 accept + 87/87 reject;
cargo test --workspace green except the pre-existing local-CRLF
spec_t3_strings failure (reproduces on clean HEAD).

Harvest credit: stale companion rickymulya PR limitlessprojects#923 (bot:stale-base +
bot:conflict) proposed the same family of rules with literal-only
helpers; this take supersedes it with const-ident support and the
untyped_const annotation check.

Coordination: limitlessprojects#1428, limitlessprojects#1430 and limitlessprojects#1447 also bump census 126 -> 127 — the
PR that lands last rebases the count to 128+. The numeric_adapt_conflict
doc carries a dedupe NOTE against limitlessprojects#1447's is_untyped_constant (the
kinds_conflict / PR limitlessprojects#620 duplicate-helper precedent) for whoever lands
second.
@limitlessprojects

Copy link
Copy Markdown
Owner

Queue-reset sweep: this repo now runs a rolling conveyor, and open PRs
outside the active review window are being closed as not planned to keep the
queue honest — a stale queue wastes every contributor's time, including yours.

This is not a verdict on the work. If this change still matters:

  1. rebase against current main (main has landed a lot since this PR opened),
  2. confirm the diff still applies and the affected verdicts still hold,
  3. reopen — the triage board picks reopened PRs up as fresh arrivals.

Wildcard holds (verdict window 2026-11-01) and Pareto land candidates are
unaffected. Thanks for the effort — high-volume repos only stay reviewable
with a queue that actually drains.

Ljunqiang pushed a commit to Ljunqiang/Go-Hybrid that referenced this pull request Oct 24, 2026
…nd comparisons (fixes limitlessprojects#922)

Root cause: check_binary_operands compared numeric FAMILIES, not concrete
kinds — for `< <= > >=` and `+` the mismatch term was
`lk.is_numeric() != rk.is_numeric()` (int and float64 are both numeric, so
never mismatched), and `- * /` only checked kind membership. `x + f`,
`x - f`, `x * f`, `x / f`, `x < f`, `f <= x` checked clean while go build
refused each with `mismatched types int and float64` (GUARANTEES Row 13).

Fix: a new Collector::numeric_adapt_conflict routes cross-kind operand
pairs through Go's only implicit numeric conversion — untyped-constant
adaptation — and emits HYB-E1002 when it cannot bridge them:
  - `1 + 2.5` (both untyped) and `f + 1` / `1 + f` (int const -> float64)
    keep building, including untyped `const` identifiers (`f + k`);
  - `x + f`, `f + x`, `x - f`, `x < f`, `f <= x`, and the untyped float
    const against a typed int (`x + 2.5`) are rejected;
  - `Define.untyped_const` (from LocalDecl.ty) distinguishes `const k = 1`
    (untyped) from `const k int = 1` (typed like a variable) for fn-local
    consts — the annotation was recorded but never consulted.

Validation: new hy-check/tests/binop_operand_types_issue_922.rs 24/24
(15 reject faces + 9 accept controls); corpus pin
binop-mismatched-numeric-reject.hy (REJECT BUILDABLE + HYB-E1002), census
126 -> 127 on all 7 surfaces; harness 127/127, tripwire not fired;
negative flip: baseline upstream accepts the pin (126/127, pin FAILs) and
restores to 127/127; differential 40/40 accept + 87/87 reject;
cargo test --workspace green except the pre-existing local-CRLF
spec_t3_strings failure (reproduces on clean HEAD).

Harvest credit: stale companion rickymulya PR limitlessprojects#923 (bot:stale-base +
bot:conflict) proposed the same family of rules with literal-only
helpers; this take supersedes it with const-ident support and the
untyped_const annotation check.

Coordination: limitlessprojects#1428, limitlessprojects#1430 and limitlessprojects#1447 also bump census 126 -> 127 — the
PR that lands last rebases the count to 128+. The numeric_adapt_conflict
doc carries a dedupe NOTE against limitlessprojects#1447's is_untyped_constant (the
kinds_conflict / PR limitlessprojects#620 duplicate-helper precedent) for whoever lands
second.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bot:land-candidate Shepherd: fresh, gate-clean, verifiable (set from classify())

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[soundness] hy-emit: O8 slot loans flush before return-operand evaluation and drop store-backs on ? early returns

2 participants