Skip to content

fix(#98): defer/call-arg moves freeze the source; defer-loans live to function exit - #223

Closed
zidirahim617-del wants to merge 1 commit into
limitlessprojects:mainfrom
zidirahim617-del:fix/98-defer-freeze
Closed

zidirahim617-del wants to merge 1 commit into
limitlessprojects:mainfrom
zidirahim617-del:fix/98-defer-freeze

Conversation

@zidirahim617-del

Copy link
Copy Markdown
Contributor

What

Fixes #98: defer calls and call-argument moves previously bypassed the A1b/A7 freeze model and the A2/A3 loan windows. A binding could be moved (its storage leaving the task) and still stay writable, or a loan opened by a deferred call could be invalidated by an insert after the defer — the checker reported OK(0) while go -race caught the use-after-move.

Why

Three independent holes in hy-check:

  1. Stmt::Defer / Stmt::Expr never marked their move operands. defer cleanup(move m) and a bare m2 := id(move m) (via the assign arm's value scan) transferred ownership in the emitter but left the binding writable in the checker. mark_move_in_value was only reached from define/assign/return.

  2. collect_move_operands did not recurse into Expr::Call args. A move nested in a call argument (id(move m), eat(move m), w.finish(move w)) was invisible to the freeze machinery even when the statement arm did run.

  3. A defer call's loan window ended at the lexical defer statement, not at function exit. A deferred call runs at exit, so an A3 map-slot insert (or an A2 handle-borrow mutation) after the defer looked outside the loan window. Same for its uses: a binding moved after the defer was written was used-after-move at exit but never re-checked.

How

  • lib.rs — Stmt::Defer / Stmt::Expr arms call ck.mark_move_in_value; process_call for a defer statement receives defer_bound = Some(flat.fn_end()) so loans stay live to exit; after check_closure_const_writes, every Stmt::Defer's own_uses are collected and check_uses re-run against the completed move state.
  • model.rs — collect_move_operands recurses into Expr::Call { args }.
  • walk.rs — FlatInfo.defer_uses collects defer-body binding names; flatten() clamps those names' last_use to the end of the whole body (a mid-walk fn_end() would return the defer's own index); new FlatInfo::fn_end() helper.

Verification

  • cargo test --workspace — all suites green (hy-syntax parse_all count updated 42→44 to match rows 43/44 from [soundness] Field/index stores (h.m = m), projections in composite literals (holder{m: h1.m}), and intermediate-copy rebinds sever may_share and bypass O5.2 freeze with 0 diagnostics #92).
  • corpus_harness — 44/44, tripwire not fired.
  • differential reject-side — 31/31.
  • differential accept-side — 11/13; the 2 failures (12-a7-borrow-by-default, 13-a7-own-transfer-at-return) are runtime/cgo rows needing a C compiler. This machine has no gcc/clang; the build fails identically on main (env-only, not a regression). Proven by building the kept workspace standalone without any patch.
  • Targeted repro — d98.hy, a_move_defer.hy, b_defer_read_then_move.hy, d_callarg_move_param.hy, e_callarg_move_local.hy, h_bare_move.hy all now reject (HYB-E3003); f_defer_loan.hy rejects HYB-E3102 (defer loan window live to fn end). Previously all reported OK(0).

Fixes #98

@limitlessprojects

Copy link
Copy Markdown
Owner

Triage (2026-10-15, AI maintainer, post-CI-repair): defer/call-arg move freeze (issue #98) — fresh rebase onto current main appreciated; pr-99 is the older take on the same gap and gets verified against yours (the stronger coverage lands, both authors credited per the duplicate rule). Queued for the toolchain-verified batch. No action needed from you.

…fer-loans live to function exit

Rebased onto current origin/main. parse_all corpus assert pinned to 42 (the
actual file count) so the workspace test is green.
@zidirahim617-del

Copy link
Copy Markdown
Contributor Author

Status after rebase onto current main

Rebased onto aedfa2d (current main). Verification on Windows:

Gate Result
cargo test --workspace green (all crates)
corpus_harness 42/42 verdict agreement
differential_loop reject-side 29/29
Repro defer cleanup(m) + task worker(move m, done) rejects with HYB-E3003 (was accepted with 0 diagnostics on main)

Overlap with #99 (not a conflict)

fix/defer-soundness (#99) touches closures.rs / freeze.rs / loans.rs for the same bug family. This PR (#223) takes a different, narrower route:

  • Stmt::Defer / Stmt::Expr mark move operands (lib.rs) so defer cleanup(move m) and bare move-expression statements freeze the source — the same marking define/assign/return already did.
  • collect_move_operands recurses into Expr::Call args (model.rs) so nested moves inside a deferred call are not exempted.
  • Deferred loans live to function exit (defer_bound = Some(flat.fn_end()) + a fn-end defer-use re-check), so a map-slot/handle loan opened by the deferred call stays valid through later moves (A3).
  • parse_all.rs corpus assert pinned to 42 (actual file count on main — the previous 44 made the workspace test fail on main itself).

Happy to rebase onto whatever lands first from #99 vs #223, or to split the pieces if that makes review easier.

Payout question (so I can plan the next cycle correctly)

Before I file more soundness PRs for this cycle, could you clarify how paid work is actually settled here? Specifically:

  1. Currency / rail — you mentioned in MOONSHOT.md / BOUNTY.md a $500 cycle split by the ladder. Is payment sent via GitHub Sponsors, USDC/USDT (which chain?), or another method? I can provide a wallet address if that is easier.
  2. Timing — is payout done per merged PR, or batched at the 2026-11-01 review window?
  3. Scope for this cycle — with [soundness] Stmt::Defer and call-arg moves bypass A1b/A7 freeze and A2/A3 loan windows (data race at function exit) #98 (this PR) ready and [soundness] Field/index stores (h.m = m), projections in composite literals (holder{m: h1.m}), and intermediate-copy rebinds sever may_share and bypass O5.2 freeze with 0 diagnostics #92 already covered by another contributor, is [soundness] Stmt::Defer and call-arg moves bypass A1b/A7 freeze and A2/A3 loan windows (data race at function exit) #98 the last piece I should pursue for the current ladder, or should I open a Wildcard Track submission (e.g. [wildcard] Kill the reconstruction: ownership as a visible capability surface (linear types + one runtime bit) replacing alias/loan/freeze #193 capability-surface sketch) to also claim M1/M2?

I would rather ask once, up front, than file against a process that is not yet defined. Happy to answer over DM if that is easier.

@zidirahim617-del

Copy link
Copy Markdown
Contributor Author

Bump — checking in on this PR and the payout thread above.

State after the rebase onto current main (aedfa2d):

Two quick questions:

  1. Should I rebase fix/98-defer-freeze now to clear the reported conflict, or wait for fix(hy-check): enforce freeze and loan windows across defer and call-arg moves (#98) #99 to land first?
  2. Could you confirm this fix (and the bounty request in the comment above) is queued for the 2026-11-01 review window per BOUNTY.md?

Happy to do whatever is easiest for review. Thanks!

@zidirahim617-del

Copy link
Copy Markdown
Contributor Author

Bounty accounting — consolidated status of all contributions from this account + payout confirmation request

@limitlessprojects — per BOUNTY.md ("Every decision gets a written reason in the issue"; "We aim to acknowledge within 48 hours"), here is a consolidated accounting of every contribution from this account, with concrete asks about cycle placement and payment.

Merged — all green on your CI (payment rule: "CI must pass on our side")

PR What it fixed Merged Track
#44 issue #37 — writes through a const root (rebind/element/field) reject HYB-E3006; accepted write was a cross-task data race with zero spawn-site evidence 2026-10-14 Soundness (Critical-class repro)
#46 issue #19 — GUARANTEES rows 1/3 now link corpus files that exist 2026-10-14 Improvement (docs)
#26 README shows the real managed("reason", "category") escape-hatch form 2026-10-14 Improvement (docs)
#102 issue #95 — differential reject-side requires first-error tier-0 rule match 2026-10-15 Improvement (tooling/harness)
#110 issue #109 — closure captures missing from freeze machinery: move-sent capture write/write race accepted with 0 diagnostics 2026-10-15 Soundness
#117 issue #111 — A3 map-slot loans crossing a callee frame / shallow alias / closure body accepted (direct twins reject), deleted loaned key resurrects at runtime 2026-10-15 Soundness
#126 issue #123 — E3006 misses closure bodies, const copies, and delete; dual-closure program races at runtime 2026-10-15 Soundness

(#44, #46, #26 are already in the Hall of Fame; the four Oct-15 soundness/harness landings are not yet listed there.)

Open / in flight

Asks

  1. Ladder placement: confirm each of the 7 merged PRs above is entered as a distinct slot in the current cycle's top-10 ladder (BOUNTY.md: "One contribution takes one ladder slot"), and that fix(#98): defer/call-arg moves freeze the source; defer-loans live to function exit #223 counts on merge.
  2. Wildcard ledger: confirm [wildcard] Kill the reconstruction: ownership as a visible capability surface (linear types + one runtime bit) replacing alias/loan/freeze #193's entry (W-001) or tell us exactly what is missing for the $250-on-acceptance / $250-on-publication split.
  3. Payment: no payment has ever been received for any merged contribution from this account. Please state (a) which cycle these landings fall into and its close date (the first review window was advertised for 2026-11-01), (b) the payout method, and (c) the expected transfer date after the cycle closes. BOUNTY.md says winners are announced in the release notes — a one-line answer per item is enough.
  4. Hall of Fame: add the four Oct-15 landings (fix(tools): differential reject-side requires first-error tier0 rule match (fixes #95) #102, fix(hy-check): freeze closure bindings with their captured bindings (Fixes #109) #110, fix(hy-check): A3 events cross frames, share-peers, and closure call sites (Fixes #111, stacked on #110) #117, fix(hy-check): E3006 reaches closure bodies, const copies, and delete (fixes #123, stacked on #110/#117) #126), or say if they are intentionally deferred.

Happy to provide any additional repro/reverification needed for reproduction on your side — reproduction is the door to a payout, and every fix here already ships with its regression tests.

@limitlessprojects

Copy link
Copy Markdown
Owner

Wave-2 triage: queued — dup pair with #99. Same issue #98, smaller diff; strongest of the pair lands, both credited.

@limitlessprojects

Copy link
Copy Markdown
Owner

Recon (session-15/2-e): same seam as #99 (issue #98, still open — main has no Defer move marking and no exit-extent loan windows). This PR is the minimal seed (defer_bound in check_fn, collect_move_operands Defer handling, FlatInfo defer marks); #99 is the superset with the deferred_uses window model + exit re-check + a larger test suite. Recommend keeping #99 as the extraction source and closing this one with credit when that lands.

@zidirahim617-del

Copy link
Copy Markdown
Contributor Author

Thanks for the triage clarification! Sounds good — happy to have PR #223 serve as the seed/reference while PR #99 lands as the superset, with both credited toward the cycle ladder as confirmed. Let us know if any further rebase or test adjustments are needed on our side.

@limitlessprojects

Copy link
Copy Markdown
Owner

Thank you, @zidirahim617-del — clean, minimal statement of the two #98 holes (unmarked move operands in Stmt::Defer/Stmt::Expr, and collect_move_operands not recursing into call arguments).

Closing as subsumed: both holes are closed on current main (verified today at ff56902, cargo 1069/0, corpus 78/78, differential FULL 23+55):

  • defer cleanup(move m); m["after"] = 1 → REJECT E3003
  • m2 := id(move m); task worker(move m2); m["main"] = 2 → REJECT E3003
  • defer audit(m) before task worker(move m) → REJECT E3003 (corpus 56)
  • s := &m["a"]; defer bump(s); m["api"] = ... → REJECT E3102 (A3, loan live to fn exit)

Landed instead via: pr-421 (2defedf, value-precise deferred-binding freeze, fixes #415/#438), pr-395 (1ed45d5, mark moves in non-assignment statement expressions, fixes #393), pr-288 (5317e70, defer-operand E2001), plus the spliced regression pins at 1980a40 (corpus 56–60, defer_live_ref.rs, defer_move_soundness.rs). Your head 36cc062 is not an ancestor of main — the successors re-implemented the same semantics on top of the newer collector.

One note: this PR's parse_all.rs hunk only corrects the count assertion's message text ("expected exactly 39" vs 42) without adding a fixture; that text has since been superseded on main by the 78-fixture registry. Issue #98's scenarios are fully covered by the checks above; the maintainer may close it with this verification. Thanks for the precise repro set — it made verification fast.

@zidirahim617-del

Copy link
Copy Markdown
Contributor Author

Thank you for the detailed verification and update! Glad to hear the repro set helped validate the fix. Looking forward to the November 1st cycle payout!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[soundness] Stmt::Defer and call-arg moves bypass A1b/A7 freeze and A2/A3 loan windows (data race at function exit)

2 participants