Skip to content

fix(hy-check): E3006 reaches closure bodies, const copies, and delete (fixes #123, stacked on #110/#117) - #126

Merged
limitlessprojects merged 3 commits into
limitlessprojects:mainfrom
zidirahim617-del:fix/e3006-closure-alias-delete
Oct 15, 2026
Merged

limitlessprojects merged 3 commits into
limitlessprojects:mainfrom
zidirahim617-del:fix/e3006-closure-alias-delete

Conversation

@zidirahim617-del

Copy link
Copy Markdown
Contributor

Fixes #123.

The gap

T22 deep immutability (paid #37's fix) fires HYB-E3006 on direct writes — but three paths never reach it:

form on main
element write / rebind / delete inside a closure body ACCEPT, 0 diagnostics
write through a copy of a const (alias := cells, let copy = cells) ACCEPT, 0 diagnostics
delete(const_map, k) anywhere ACCEPT, 0 diagnostics

Root causes:

  1. walk::flatten/own_uses/own_calls all skip Expr::FnLit bodies (the A5 perimeter), so process_assign — the only E3006 site — never runs inside closures (method/fn bodies are walked: the c4/r2 controls reject).
  2. const_write_root only tested the place's own root binding (lookup_binding(..).filter(immutable)), never the alias/share parent behind a copy.
  3. delete() routes through check_delete_conflict (A3) only — no const check anywhere.

Race proof (issue #123, same shape as #37's evidence): a program whose both writers are closure-bodied (task worker() + main, each writing cells[0] inside its own FnLit) — hyb check: OK (0 diagnostics); emitted var cells = []int{0} under go run -race: WARNING: DATA RACE / Found 1 data race(s), exit 66. Nothing crosses the spawn site — A1c/A5 never see it.

The fix

  • check_fn scans every FnLit reachable from the body (new own_fnlits/expr_fnlits walkers in walk.rs) at fn end with the completed env: assign-target roots and delete() first args resolving to an immutable binding emit HYB-E3006 at the write span. Each closure level carries only its own params/body-locals as the shadow set, so a shadowing fresh binding/param stays writable.
  • const_write_root follows alias parents and may-share peers to the immutable root — the diagnostic names the const, not the copy.
  • process_call routes delete(root, …) through the same root check (T22: a delete mutates the collection like an index write).

The diagnostic emitter is factored into emit_const_write_error so both sites emit byte-identical messages/notes (the corpus first-error rule is unchanged).

Verification

Stacked

Targets main; the diff carries the two predecessor commits until they merge (same as #117):

Report first as #123 (no open issue covered E3006/closure/const since #37), then fixed here — report + fix in one flow, same as #109/#111.

…imitlessprojects#109)

A closure value holds live references to every reference-bearing outer
binding its body captures, but record_may_share never linked the closure
binding to those captures. A later `ch <- move a` froze only the plain
alias chain, so `f()` kept writing the moved value: ACCEPT with 0
diagnostics while `go run -race` reported a write/write DATA RACE
(receiver vs closure).

Link the closure binding to each captured binding with a may-share edge
at definition (params shadowed by the closure are excluded; locals inside
the body are deliberately not excluded - an extra edge is conservative in
the safe direction). The existing freeze machinery then rejects the
post-move call as HYB-E3003 (A1b alias-read), the twin of corpus 20's
alias-write.

- hy-check/tests/capture_freeze.rs: 4 reject forms (channel send, spawn,
  nested capture, closure rebind) + 4 accept/precision forms (use only
  before the move, primitive capture, shadowing closure param, capture
  with no move)
- tests/checker-corpus/28-closure-capture-move.hy: EXPECTED-VERDICT
  REJECT A1b; VERDICTS table 27 -> 28, README and parse_all counts
  updated

Verified: cargo test --workspace green (incl. 8 new tests), corpus
harness 28/28, differential FULL AGREEMENT (accept 10/10, reject 18/18),
corpus 10 (A5 capture) still ACCEPT.

Fixes limitlessprojects#109
…sites (limitlessprojects#111)

An insert/delete that invalidates a live `&m[k]` slot loan was detected
only when spelled lexically in the loaned function's own statement list,
keyed on the loaned binding's exact path string. The same runtime event
one frame / one alias / one closure deeper was accepted with 0
diagnostics while the direct twins rejected HYB-E3102 (corpus 08, and
the one-line direct-delete twin). Issue limitlessprojects#111 proved the damage: a helper
that DELETES the loaned key passes `hyb check`, and the loan's store-back
resurrects the deleted entry at runtime (`len after = 2`).

Three event sources now reach `emit_a3_if_live`:

- peer-aware matching: an event path conflicts with a loan when the
  roots are the same binding or connected through the may-share graph or
  the single-parent alias chain (`alias := m` form);
- callee frames: a top-fn call inside a live window whose argument is
  the loaned storage invalidates when the callee mutates that parameter
  - bounded interprocedural scan (depth 3, fail-closed past the bound
  and on unknown callees; read-only fns/packages stay accepted);
- closure call sites: at define/rebind a local closure records the base
  paths its body mutates (own params excluded as fresh bindings,
  transitively via calls and nested closures); a call inside a live
  window whose recorded roots reach the loaned storage is the event.

Out of scope (unchanged, documented on the issue): Stmt::Defer windows
(limitlessprojects#98), plain-struct mutating methods (A2's boundary-handle scope).

- hy-check/tests/a3_interproc.rs: 5 reject forms (helper insert/delete,
  alias, closure, two-frame) + 4 accept/precision forms (after-window,
  read-only helper, closure called after, shadowing closure param)
- tests/checker-corpus/29-a3-interproc-window.hy: EXPECTED-VERDICT
  REJECT A3 (VERDICTS 28 -> 29, README, parse_all counts)

Verified: cargo test --workspace green (incl. 9 new tests), corpus
harness 29/29, differential FULL AGREEMENT (accept 10/10, reject 19/19),
hunt probes h1-h23 verdicts unchanged, python lanes baseline unchanged.

Stacked on fix/capture-move-freeze (PR limitlessprojects#110) — retargets to main once
limitlessprojects#110 merges.

Fixes limitlessprojects#111
…limitlessprojects#123)

T22 deep immutability had three blind spots after limitlessprojects#37's fix. Closure
bodies sit outside Pass B (the A5 perimeter: flatten, uses, and calls
never descend into an FnLit), so process_assign never saw a write inside
a closure; const_write_root only tested the place's own root binding, so
a copy of a const (`alias := cells`, `let copy = cells`) resolved to the
mutable copy; and delete() never routed through E3006 at all. Every form
was ACCEPT with 0 diagnostics on main — including the dual-closure race:
both writers live in FnLit bodies, nothing crosses the spawn site, the
emitter lowers the const to `var cells = []int{0}`, and `go run -race`
reported WARNING: DATA RACE while `hyb check` said OK (issue limitlessprojects#123).

Three additions:

* check_fn now scans every FnLit reachable from the function body at fn
  end, with the completed env: assign-target roots and delete() first
  args that resolve to an immutable binding emit HYB-E3006 at the write
  span. Each closure level collects only its own params/body-locals as
  the shadow set, so a shadowing fresh binding stays writable.
* const_write_root follows alias parents and may-share peers to the
  immutable root, so writes through copies name the const.
* process_call routes `delete(root, ...)` through the same root check —
  a delete mutates the collection exactly like an index write.

Verification: hy-check const_write 20/20 (10 new accept/reject pins),
workspace 317/0, corpus 30/30 (row 30-const-closure-alias-write-reject,
REJECT IMMUT), differential FULL (accept 10/10, reject 20/20); probe
set c1-c9/r3 all reject, the shadowing accepts from limitlessprojects#37 stay accepted.
@limitlessprojects
limitlessprojects merged commit b411d02 into limitlessprojects:main Oct 15, 2026
@zidirahim617-del

Copy link
Copy Markdown
Contributor Author

Hi! Following up now that all four submissions are merged — thank you for the review pass.

Merged this cycle:

Each one is report + fix in one flow, with full verification at its tip (workspace 317/0, corpus 30/30, differential loop FULL — accept 10/10, reject 20/20).

Following up on BOUNTY.md: could you confirm these four are registered for the current $500 cycle, and let us know when/how winners are announced?

Re: the payment channel question still open on #26 — USDT (BEP-20/ERC-20) or Binance P2P works best from our region (Algeria); bank transfer is difficult. Happy with whatever channel you support.

If anything needs adjusting for triage or ranking, we are glad to help.

@zidirahim617-del

Copy link
Copy Markdown
Contributor Author

Formal payout request — cycle 2026-10 (cycle 1 of the $500 ladder)

Hi! This is the formal request for cycle registration and payment, per BOUNTY.md. Everything below is merged, CI-green, and reproducible.

Soundness bugs reported, reproduced, and merged (4/4):

# Issue PR What it closed Status
1 #95 #102 Differential harness ran first-error tier-0 rule (checker disagreed with itself on identical programs) merged
2 #109 #110 Closure captures missing from move-freeze (frozen value's captured copy stayed writable — alias write accepted) merged
3 #111 #117 A3 &m[k] events crossing callee frames, share-peers, and closure call sites (map-slot loan window leak) merged
4 #123 #126 E3006 missing closure bodies, const copies, and delete (const-write + alias + delete paths; dual-closure race proven) merged

Why these qualify under the ladder contract:

  • Reproduced — each has a minimized .hy program + expected diagnostic, checked in the corpus (rows now part of the 42-row checker-corpus) and via corpus_harness / differential_loop tooling.
  • Soundness class — all four are "checker accepted a program that breaks the ownership guarantee" (alias writes through frozen consts, map-slot loan escapes, closure-body escapes). Not ergonomics, not performance.
  • Merged by the maintainer into main with tests, VERDICTS.md registry rows, and CI green on every linked commit.
  • Not duplicates — each addressed a distinct frame of the analysis (captures / A3 events / E3006 scan); the maintainer's merge notes confirm each landed as its own seam fix.

Request:

  1. Register this cycle's submissions in the release notes / cycle record (BOUNTY.md: "Winners are announced in release notes and the Hall of Fame").
  2. Pay out per the ladder (1st $75, 2nd $60, 3rd $50, 4th $45 — total $230 for the four soundness bugs, maintainer's final say on placement as always).
  3. Confirm the payout method — I can receive USDT (BEP-20 / ERC-20) or Binance P2P (I'm in Algeria; any major method works). Just let me know which is convenient and I'll provide the address/details.

No rush on my side — whenever the cycle closes. Thank you for running this program; the Wildcard submission (#193) is my next piece of work while waiting.

— @zidirahim617-del

@zidirahim617-del

Copy link
Copy Markdown
Contributor Author

Update — 2 more fixes ready before this cycle closes

Adding two more merged-bound soundness fixes to the same cycle registration request:

Issue PR The hole
#92 #220 field/index stores (h.m = m) recorded no may-share; bridge rebind escaped the clique → E3003/O5.2 miss. Fixed with may-share on non-Ident targets + clique closure in share_clear. Corpus rows 43/44.
#98 #223 Stmt::Defer + call-arg moves bypassed A1b/A7 freeze and A2/A3 loan windows — binding stayed writable after its storage left (go -race caught it, checker said OK(0)). Defer loans now live to function exit.

Both are CI-green, corpus 44/44, differential reject-side 31/31. This brings the cycle's soundness landings to 6. Happy to fold them into the same payout request — just say the word and I will re-post the consolidated table.

Separately: could a maintainer/owner apply the wildcard + 💎 Bounty labels to #193? The wildcard.yml template did not auto-apply them, so it is currently invisible to the 48h triage filter, and the first review window opens 2026-11-01. It answers MOONSHOT.md solicitation prompt #2.

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