Skip to content

fix(hy-check): close place-call, paren-callee, and A3/const bypasses (fixes #500) - #501

Open
o2splatform-cmyk wants to merge 13 commits into
limitlessprojects:mainfrom
o2splatform-cmyk:main
Open

o2splatform-cmyk wants to merge 13 commits into
limitlessprojects:mainfrom
o2splatform-cmyk:main

Conversation

@o2splatform-cmyk

Copy link
Copy Markdown
Contributor

Fixes #500.

Problem & Root Cause

On main (91ab10305b0a603516c073b9378243c76a04f002), four closely related AST traversal and call-resolution gaps in hy-check allow accepted Tier-0 programs to trigger concurrent map races (HYB-E3003 / HYB-E3102), mutate const composites (HYB-E3006), or pass invalid builtin arguments (HYB-E2005) with 0 diagnostics:

  1. Calls and closures inside Stmt::Assign target places (PlaceKind::Index, PlaceKind::Field, PlaceKind::AddrOf) are dropped by walk::own_calls, walk::own_fnlits, and builtins::check_place_builtin_args:
    • walk::own_calls (walk.rs:527-537) only recursed into a single outer PlaceKind::Index { index, .. } and never recursed into base across nested PlaceKind::Index, PlaceKind::Field, or PlaceKind::AddrOf. Consequently, calls inside assignment targets such as grid[copy(dst, src)][0] = 1, box[0].items[(fn() int { m["k"] = 9; return 0 })()] = 1, or grid[delete(42, "k")][0] = 1 bypassed copy may-share freeze tracking (HYB-E3003), const write checks (HYB-E3006), A3 map-slot loan invalidation (HYB-E3102), and builtin argument validation (HYB-E2005).
    • walk::own_fnlits (walk.rs:601-605) did not visit Stmt::Assign { targets, .. } at all, and builtins::check_place_builtin_args (builtins.rs:171-182) only inspected index without recursing into base.
  2. Parenthesized callees (copy)(dst, src), (delete)(m, "k"), and (corrupt)(m) bypass resolve_call and interprocedural mutation helpers:
    • loans.rs matched bare Expr::Ident / Expr::Field on cr.callee without stripping Expr::Paren. Wrapping a builtin or mutating function in parentheses bypassed Resolved::Copy, Resolved::Delete, call_mutates_param, collect_mutation_roots, and call_mutates_const_param.
    • When stripping Expr::Paren on cr.callee, selector bases inside Expr::Field { base, .. } must also unwrap parentheses so (fmt).println(m) continues to match A3_PURE_PKGS without false positives.
  3. A3 map-slot loan invalidation (HYB-E3102) and interprocedural const checks (HYB-E3006) miss closure callees, method callees, inline FnLit callees, call-returned closures, and Stmt::Switch/Stmt::Select closure bodies:
    • In process_call (loans.rs), Resolved::Fn only checked self.fns.get(&name) (skipping local closure bindings f(m) that mutate their map parameter), Resolved::Method did not check method bodies that mutate the receiver or map arguments during a live &m[k] slot loan, and Resolved::None skipped inline (fn(d map[string]int) { d["b"] = 2 })(m), slice/map-indexed closure callees fns[0](m), and call-returned closure callees make_mutator()(m).
    • Receiver-type disambiguation when multiple structs define a method with the same name must resolve lexical struct types across block scopes (LexicalStructMap), pointer receivers (*Right), call/method return types, and shadowing non-struct inner bindings so a read-only Right.touch never collides with a mutating Wrong.touch.
    • stmt_mutates_param, collect_mutation_roots, and stmt_mutates_const_param hand-rolled partial FnLit extractors that missed Stmt::Switch and Stmt::Select; unifying them on walk::own_fnlits closes all statement positions while walk::own_calls includes ForKind::Iterate for interprocedural callers (with Checker::check_stmt handling Stmt::For headers directly so top-level ForKind::Iterate calls are not double-processed).
  4. Lexically shadowed builtins (copy, delete, append, clear, len, cap, new, make, some, ok, err) in loans.rs:
    • When a user function or local closure shadows a builtin name (e.g. copy := fn(d map[string]int) { d["b"] = 2 } or fn len(d map[string]int) { d["k"] = 9 }), resolve_call and the builtin exemptions in loans.rs previously classified the call as the pure/builtin operation rather than a user-defined or closure call. Guarding builtin recognition with !self.is_builtin_shadowed_by_local(st, name) and !self.fns.contains_key(name) routes shadowed calls through normal A3/const/freeze analysis.

Verification

  • Regression suite: Added 14 integration tests in compiler/crates/hy-check/tests/place_call_and_a3_callee_soundness.rs covering every soundness bypass (HYB-E3003, HYB-E3006, HYB-E3102, HYB-E2005) alongside positive controls (Verdict::Accept for read-only closures, read-only methods with same-named mutating methods on another struct, and parenthesized (fmt).println(m)).
  • Full compiler & corpus battery on 91ab10305b0a603516c073b9378243c76a04f002:
    • cargo test -p hy-check: 560/560 passed across all 42 suites (0 failures).
    • python3 tools/corpus_harness.py: 60/60 verdict agreement (tripwire: not fired).
    • python3 tools/check_conflict_markers.py & python3 tools/check_test_registration.py: OK.

@o2splatform-cmyk

o2splatform-cmyk commented Oct 18, 2026 •

Copy link
Copy Markdown
Contributor Author

Resolved the merge conflict in compiler/crates/hy-check/src/loans.rs with #522 and re-ran cargo test -p hy-check and tools/corpus_harness.py (61/61). Also note: #500 was auto-closed by GitHub when #510 merged only because #510's description footer included the phrase "resolve #500".

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

Labels

None yet

Projects

None yet

1 participant