Skip to content

fix(hy-check): reject non-lowered O8 map-slot borrows in var, assign, and struct-lit positions (HYB-E3104) - #1424

Closed
hamzy-hams wants to merge 1 commit into
limitlessprojects:mainfrom
hamzy-hams:fix/non-lowered-slot-loan-positions
Closed

hamzy-hams wants to merge 1 commit into
limitlessprojects:mainfrom
hamzy-hams:fix/non-lowered-slot-loan-positions

Conversation

@hamzy-hams

Copy link
Copy Markdown

Fixes #1423

Problem

On main (7d63d5f), Checker::check_stmt_addr_of_loan_projections and Checker::walk_addr_of_loan_projections in compiler/crates/hy-check/src/addr_loan.rs exempt &m[k] (allow_slot_loan_root = true) in four positions where hy-emit (compiler/crates/hy-emit/src/stmt.rs) never lowers &m[k] as an O8 map-slot loan:

  1. var declarations (LocalKind::Var, var p = &m["a"] / var p *acct = &m["a"]): emit_local_in_seq (stmt.rs:295-323, 371-386) only lowers LocalKind::Define | LocalKind::Let bindings with l.ty.is_none() and l.names.len() == l.values.len(), returning early for LocalKind::Var.
  2. Non-identifier assignment targets (ptrs[0] = &m["a"], *pp = &m["a"]): emit_assign_in_seq (stmt.rs:1207-1213) only lowers AssignOp::Assign statements when targets.len() == values.len() and every target is PlaceKind::Ident(_).
  3. Mixed-target parallel assignments (q, arr[0] = &m["a"], 1): because targets.iter().all(|t| matches!(&t.kind, PlaceKind::Ident(_))) is false when any parallel target is an index or field place, hy-emit emits &m["a"] verbatim even for the identifier target q.
  4. Expr::StructLit fields on structs not rejected by A6 (struct box { p any }; b := box{p: &m["a"]}): walk_addr_of_loan_projections (addr_loan.rs:390) unconditionally skipped self.map_slot_borrow_span(st, &f.value).is_some() assuming A6 (HYB-E3103) had already fired on the struct declaration, which does not happen for any-typed or generic type-parameter fields (and continue; also skipped recursing into nested operands under Stmt::Return and Expr::StructLit).

All four shapes pass hyb check with 0 diagnostics (Verdict::Accept) and fail hyb build (go build: invalid operation: cannot take address of m["a"]), violating GUARANTEES.md Row 13 (accepted-implies-buildable) and T24 (HYB-E3104).

Fix

In compiler/crates/hy-check/src/addr_loan.rs:

  • Restrict allow_root in Stmt::Local(l) (check_stmt_addr_of_loan_projections) and mark_slot_loans_local to LocalKind::Define | LocalKind::Let with l.ty.is_none() && l.names.len() == l.values.len() (preserving LocalKind::Const for HYB-E6003).
  • Restrict allow_root in Stmt::Assign (check_stmt_addr_of_loan_projections) and mark_slot_loans_assign to *op == AssignOp::Assign && targets.len() == values.len() && targets.iter().all(|t| matches!(&t.kind, PlaceKind::Ident(_))), while exempting direct PlaceKind::Field root borrows that are already owned by A6 (HYB-E3103).
  • In Expr::StructLit, only exempt a root &m[k] field value when HYB-E3103 has already been recorded, and recurse through walk_addr_of_loan_projections(..., allow_root) in both Stmt::Return and Expr::StructLit so nested loan projections are still checked.

Verification

  • Added compiler/crates/hy-check/tests/non_lowered_slot_loan_positions.rs covering var p = &m["a"], var p *acct = &m["a"], ptrs[0] = &m["a"], q, arr[0] = &m["a"], 1, box{p: &m["a"]}, and positive controls (q := &m["a"], q = &m["a"], and A6 b.p = &m["a"]).
  • cargo test --manifest-path compiler/Cargo.toml --workspace --quiet
  • python3 tools/check_corpus_census.py
  • python3 tools/check_conflict_markers.py
  • python3 tools/check_test_registration.py
  • python3 tools/corpus_harness.py --bin target/debug/hyb
  • python3 tools/managed_gate.py target/debug/hyb gates/managed-baseline.json tests/checker-corpus/02-shared-map-managed.hy tests/checker-corpus/18-pool-shared-struct-managed.hy
  • python3 tools/differential_loop.py --bin target/debug/hyb

@hamzy-hams
hamzy-hams force-pushed the fix/non-lowered-slot-loan-positions branch from 22332ec to 721093b Compare October 24, 2026 08:02
@limitlessprojects

Copy link
Copy Markdown
Owner

Verified and subsumed by #1441, which we just landed on main (merge onto post-#1432 tip, battery 1775/0 · corpus tripwire clear · differential FULL 40+86) — credit-closing rather than landing both.

Your five scenarios, probed against the landed tree:

  • var p = &m["k"] (var init, no type) → REJECT HYB-E3104 ✅
  • var p *a = &m["k"] (typed var) → REJECT HYB-E3104 ✅
  • ps[0] = &m["k"] (slice-elem store) → REJECT HYB-E3104 ✅
  • q, xs[0] = &m["k"], 1 (mixed targets) → REJECT HYB-E3104 ✅
  • box{p: &m["k"]} (composite field) → REJECT HYB-E3104 ✅
  • loan re-loan q = &m["k"] → still ACCEPT ✅

Both PRs restrict loan marking/gating to the positions hy-emit actually lowers (define/let without type ascription, all-ident assign targets). #1441 carries the same gates plus the call-argument position coverage (52-line test file, merged verbatim), so nothing from your diff is missing. One implementation note: your struct-field early-continue keyed on self.diags.iter().any(|d| d.code == "HYB-E3103") is order-dependent on the diagnostic list — #1441's unconditional variant avoids that fragility, which also made it the safer base for the post-#1432 merge.

Credit: you and @Dimank92 (#1441) independently spotted the same non-lowered-position gap. The landed fix keeps your five repro shapes pinned green.

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

Labels

None yet

Projects

None yet

2 participants