From ff321be1e63b98ec059ee0640e553e73a0558ce2 Mon Sep 17 00:00:00 2001 From: exantiv <31925387+exantiv@users.noreply.github.com> Date: Sun, 18 Oct 2026 06:13:34 +0900 Subject: [PATCH] fix(hy-check): close place-call, paren-callee, and A3/const bypasses (fixes #500) --- compiler/crates/hy-check/src/builtins.rs | 10 +- compiler/crates/hy-check/src/closures.rs | 18 - compiler/crates/hy-check/src/loans.rs | 2063 +++++++++++++---- compiler/crates/hy-check/src/model.rs | 71 +- compiler/crates/hy-check/src/walk.rs | 44 +- .../place_call_and_a3_callee_soundness.rs | 882 +++++++ 6 files changed, 2572 insertions(+), 516 deletions(-) create mode 100644 compiler/crates/hy-check/tests/place_call_and_a3_callee_soundness.rs diff --git a/compiler/crates/hy-check/src/builtins.rs b/compiler/crates/hy-check/src/builtins.rs index b79bac10..5b2f4b23 100644 --- a/compiler/crates/hy-check/src/builtins.rs +++ b/compiler/crates/hy-check/src/builtins.rs @@ -170,8 +170,14 @@ impl<'a> Checker<'a> { fn check_place_builtin_args(&mut self, place: &Place) { match &place.kind { - PlaceKind::Index { index, .. } => self.check_expr_builtin_args(index), - PlaceKind::Field { .. } | PlaceKind::Ident(_) | PlaceKind::AddrOf { .. } => {} + PlaceKind::Ident(_) => {} + PlaceKind::Index { base, index } => { + self.check_place_builtin_args(base); + self.check_expr_builtin_args(index); + } + PlaceKind::Field { base, .. } | PlaceKind::AddrOf { base } => { + self.check_place_builtin_args(base); + } } } diff --git a/compiler/crates/hy-check/src/closures.rs b/compiler/crates/hy-check/src/closures.rs index c01e724c..47442057 100644 --- a/compiler/crates/hy-check/src/closures.rs +++ b/compiler/crates/hy-check/src/closures.rs @@ -254,11 +254,6 @@ fn collect_stmt_direct_fnlits<'a>( ) { let mut flits = Vec::new(); crate::walk::own_fnlits(stmt, &mut flits); - if let Stmt::Assign { targets, .. } = stmt { - for t in targets { - collect_place_direct_fnlits(t, &mut flits); - } - } for flit in flits { if let Expr::FnLit { params, results, body, .. } = crate::model::unwrap_parens(flit) { nested.push(( @@ -274,19 +269,6 @@ fn collect_stmt_direct_fnlits<'a>( } } -fn collect_place_direct_fnlits<'a>(place: &'a hy_syntax::Place, out: &mut Vec<&'a Expr>) { - match &place.kind { - hy_syntax::PlaceKind::Ident(_) => {} - hy_syntax::PlaceKind::Index { base, index } => { - collect_place_direct_fnlits(base, out); - crate::walk::expr_fnlits(index, out); - } - hy_syntax::PlaceKind::Field { base, .. } | hy_syntax::PlaceKind::AddrOf { base } => { - collect_place_direct_fnlits(base, out); - } - } -} - fn check_stmt_own_uses(ck: &mut Checker<'_>, st: &mut FnState, stmt: &Stmt, managed: bool) { let mut uses = Vec::new(); crate::walk::own_uses_with_results(stmt, &st.named_results, &mut uses); diff --git a/compiler/crates/hy-check/src/loans.rs b/compiler/crates/hy-check/src/loans.rs index dd07b58f..a4d13322 100644 --- a/compiler/crates/hy-check/src/loans.rs +++ b/compiler/crates/hy-check/src/loans.rs @@ -40,6 +40,26 @@ fn path_root(path: &str) -> &str { path.split('.').next().unwrap_or(path) } +/// Candidate access paths or reference roots for an expression passed as a +/// receiver or argument in A3 checks: uses `walk::expr_path` when `expr` is a +/// direct access chain, otherwise falls back to `collect_ref_roots` so +/// composite literals (`engine{services: m}`) and wrapper expressions are not +/// skipped. +fn a3_expr_paths(expr: &Expr) -> Vec { + let mut out = Vec::new(); + if let Some(p) = walk::expr_path(expr) { + out.push(p); + } + collect_ref_roots(expr, &mut out); + out +} + +/// Lexical binding map: `Some(struct_name)` when a lexical variable is +/// statically known to hold (or point to) `struct_name`, or `None` when a +/// lexical variable is a non-struct, unknown, or reassigned binding that still +/// shadows outer bindings and top-level function names. +type LexicalStructMap = std::collections::HashMap>; + /// #111 A3 peer connectivity: an event path invalidates a loan on another /// path when the two ROOTS are the same binding or connected through the /// may-share graph (`alias := m`, shallow copies, closures) or the @@ -116,9 +136,9 @@ fn root_ident_span(expr: &Expr) -> Option { impl<'a> Checker<'a> { fn resolve_call(&self, st: &FnState, cr: &CallRef) -> Resolved { - match cr.callee { + match unwrap_parens(cr.callee) { Expr::Field { base, name, .. } => { - if let Expr::Ident { name: b, .. } = &**base { + if let Expr::Ident { name: b, .. } = unwrap_parens(base) { if self.lookup_binding(st, b).is_none() && self.known_package(b) { return Resolved::Package { pkg: b.clone(), @@ -211,6 +231,7 @@ impl<'a> Checker<'a> { } => { self.check_call_const_writes(st, cr, &HashSet::new()); self.record_callee_param_stores(st, cr); + self.check_a3_method_conflict(st, cr, idx, &root, &method, &recv); let Some(tname) = receiver_type_name(&recv) else { return; }; @@ -267,21 +288,22 @@ impl<'a> Checker<'a> { // carries mutable GC storage, so the destination and source // share it afterwards — a later `move` of either must freeze // the other (O5.2). Primitive-element slices share nothing. - if name == "copy" { + let is_unshadowed = self.lookup_binding(st, &name).is_none(); + if is_unshadowed && name == "copy" { self.record_copy_may_share(st, cr); } - if name == "delete" { + if is_unshadowed && name == "delete" { // A3: map delete is a map mutation self.check_delete_conflict(st, cr, idx); } - if name == "delete" || name == "copy" { + if is_unshadowed && (name == "delete" || name == "copy") { // T22 (#123/#188): a delete or copy mutates its first // argument — a `const` root is frozen like an index write. self.check_delete_const(st, cr, &HashSet::new()); } self.check_call_const_writes(st, cr, &HashSet::new()); self.record_callee_param_stores(st, cr); - if self.fns.contains_key(&name) { + if is_unshadowed && self.fns.contains_key(&name) { // A3 #111: the callee may insert/delete through the // argument — that event runs inside THIS call site's // window when the call is within it. @@ -289,15 +311,19 @@ impl<'a> Checker<'a> { // A7 borrow-by-default: a handle passed to a Hybrid // function is lent for the call only self.lend_handle_args(st, cr, idx, None); - } else if st.closure_mutates.contains_key(&name) { - // A3 #111 form 4: a local closure whose body mutates a - // captured binding — event attributed to this call site. - self.check_a3_closure_call(st, cr, idx, &name); + } else { + if st.closure_mutates.contains_key(&name) { + // A3 #111 form 4: a local closure whose body mutates a + // captured binding — event attributed to this call site. + self.check_a3_closure_call(st, cr, idx, &name); + } + self.check_a3_closure_param_conflict(st, cr, idx, &name); } } Resolved::None => { self.check_call_const_writes(st, cr, &HashSet::new()); self.record_callee_param_stores(st, cr); + self.check_a3_inline_or_expr_call(st, cr, idx); } } } @@ -445,25 +471,23 @@ impl<'a> Checker<'a> { } let mut conflict_paths: Vec = Vec::new(); for (i, a) in cr.args.iter().enumerate() { - let mut aroots = Vec::new(); - collect_ref_roots(&a.value, &mut aroots); - let arg_path_opt = walk::expr_path(&a.value); - for arg_path in arg_path_opt.into_iter().chain(aroots) { - if !live.iter().any(|p| a3_paths_conflict(st, p, &arg_path)) { - continue; - } - let Some(decl) = self.fns.get(name) else { - continue; - }; - match decl.params.get(i) { - Some(p) => { - if self.fn_mutates_param(decl, &p.name, 0) { - conflict_paths.push(arg_path); - } - } - // argument beyond the signature — fail closed - None => conflict_paths.push(arg_path), - } + let Some(decl) = self.fns.get(name) else { + continue; + }; + let candidates: Vec = a3_expr_paths(&a.value) + .into_iter() + .filter(|arg_path| live.iter().any(|p| a3_paths_conflict(st, p, arg_path))) + .collect(); + if candidates.is_empty() { + continue; + } + let mutates = match decl.params.get(i) { + Some(p) => self.fn_mutates_param(decl, &p.name, 0), + // argument beyond the signature — fail closed + None => true, + }; + if mutates { + conflict_paths.extend(candidates); } } let verb = format!("mutation via `{}`", cr.callee.render()); @@ -496,259 +520,1021 @@ impl<'a> Checker<'a> { } } - /// Does `decl` mutate its parameter `param` (map insert/delete — the - /// only A3 events)? Bounded interprocedural scan, fail-closed past the - /// depth bound and on unknown callees that receive the parameter (#111). - fn fn_mutates_param(&self, decl: &FnDecl, param: &str, depth: usize) -> bool { - if depth > 3 { - return true; // fail closed past the bound + fn check_a3_closure_param_conflict( + &mut self, + st: &FnState, + cr: &CallRef, + idx: usize, + name: &str, + ) { + let known_params = st.closure_param_mutates.get(name); + let is_closure_binding = self + .lookup_binding(st, name) + .map(|b| matches!(b.shape, TypeShape::Closure)) + .unwrap_or(false); + if known_params.is_none() && !is_closure_binding { + return; } - let Some(body) = &decl.body else { - return false; - }; - self.block_mutates_param(body, param, depth) - } - - fn block_mutates_param(&self, block: &Block, param: &str, depth: usize) -> bool { - let flat = walk::flatten(block); - let mut alias_stack: Vec> = vec![[param.to_string()].into_iter().collect()]; - let mut locals_stack: Vec> = vec![HashSet::new()]; - for (stmt, idx) in &flat.stmts { - if let Some(evs) = flat.events.get(idx) { - for ev in evs { - match ev { - walk::ScopeEvent::Push => { - alias_stack.push(alias_stack.last().unwrap().clone()); - locals_stack.push(HashSet::new()); - } - walk::ScopeEvent::Pop => { - if alias_stack.len() > 1 { - let popped_aliases = alias_stack.pop().unwrap(); - let popped_locals = locals_stack.pop().unwrap(); - for a in popped_aliases { - if !popped_locals.contains(&a) { - alias_stack.last_mut().unwrap().insert(a); - } - } - } - } - walk::ScopeEvent::MatchBind { name, subject } => { - let mut sroots = Vec::new(); - collect_ref_roots(subject, &mut sroots); - if sroots.iter().any(|r| alias_stack.last().unwrap().contains(r)) { - alias_stack.last_mut().unwrap().insert(name.to_string()); - } - } - _ => {} - } + let live: Vec = st + .loans + .iter() + .filter_map(|l| match &l.target { + LoanTarget::MapSlot { path, .. } if l.created <= idx && idx <= l.last => { + Some(path.clone()) + } + _ => None, + }) + .collect(); + if live.is_empty() { + return; + } + let mut conflict_paths: Vec = Vec::new(); + for (i, a) in cr.args.iter().enumerate() { + let mutates = match known_params { + Some(set) => set.contains(&i), + None => true, + }; + if !mutates { + continue; + } + for arg_path in a3_expr_paths(&a.value) { + if live.iter().any(|p| a3_paths_conflict(st, p, &arg_path)) { + conflict_paths.push(arg_path); } } + } + let verb = format!("mutation via `{}`", cr.callee.render()); + for p in conflict_paths { + self.emit_a3_if_live(st, &p, cr.span, idx, &verb); + } + } - // Check if stmt mutates any active alias of param - match stmt { - Stmt::Assign { targets, values, .. } => { - for t in targets { - if matches!(t.kind, PlaceKind::Index { .. }) && alias_stack.last().unwrap().contains(t.root()) { - return true; - } + fn check_a3_method_conflict( + &mut self, + st: &mut FnState, + cr: &CallRef, + idx: usize, + root: &str, + method: &str, + recv: &TypeShape, + ) { + let recv_struct = match recv { + TypeShape::Struct(Some(s)) => Some(s.clone()), + TypeShape::Pointer(inner) => match &**inner { + TypeShape::Struct(Some(s)) => Some(s.clone()), + _ => None, + }, + _ => None, + }; + if let Some(decl) = recv_struct.as_ref().and_then(|s| { + self.method_decls + .get(&(s.clone(), method.to_string())) + .copied() + }) { + self.lend_handle_args(st, cr, idx, None); + let live: Vec = st + .loans + .iter() + .filter_map(|l| match &l.target { + LoanTarget::MapSlot { path, .. } if l.created <= idx && idx <= l.last => { + Some(path.clone()) } - for v in values { - if alias_stack.last().unwrap().iter().any(|p| self.expr_fnlit_mutates_param(v, p, depth)) { - return true; - } + _ => None, + }) + .collect(); + if live.is_empty() { + return; + } + let mut conflict_paths: Vec = Vec::new(); + if let Some(r) = &decl.receiver { + let recv_path = match unwrap_parens(cr.callee) { + Expr::Field { base, .. } => { + walk::expr_path(base).unwrap_or_else(|| root.to_string()) } + _ => root.to_string(), + }; + if live.iter().any(|p| a3_paths_conflict(st, p, &recv_path)) + && self.fn_mutates_param(decl, &r.name, 0) + { + conflict_paths.push(recv_path); } - Stmt::Local(l) => { - for v in &l.values { - if alias_stack.last().unwrap().iter().any(|p| self.expr_fnlit_mutates_param(v, p, depth)) { - return true; - } - } + } + for (i, a) in cr.args.iter().enumerate() { + let candidates: Vec = a3_expr_paths(&a.value) + .into_iter() + .filter(|arg_path| live.iter().any(|p| a3_paths_conflict(st, p, arg_path))) + .collect(); + if candidates.is_empty() { + continue; } - Stmt::Return { values, .. } => { - // a returned closure that mutates the param counts as a - // mutation of the param (the helper-returns-closure form, - // issue #490's make_mutator shape) - for v in values { - if alias_stack.last().unwrap().iter().any(|p| self.expr_fnlit_mutates_param(v, p, depth)) { - return true; - } - } + let mutates = match decl.params.get(i) { + Some(p) => self.fn_mutates_param(decl, &p.name, 0), + None => true, + }; + if mutates { + conflict_paths.extend(candidates); } - _ => {} } - - // Check calls in this statement - let mut calls = Vec::new(); - walk::own_calls(stmt, &mut calls); - for cr in &calls { - if alias_stack.last().unwrap().iter().any(|p| self.call_mutates_param(cr, p, depth)) { - return true; - } + let verb = format!("mutation via `{}`", cr.callee.render()); + for p in conflict_paths { + self.emit_a3_if_live(st, &p, cr.span, idx, &verb); } - - // Update aliases - match stmt { - Stmt::Local(l) => { - for (name, val) in l.names.iter().zip(l.values.iter()) { - locals_stack.last_mut().unwrap().insert(name.clone()); - let mut roots = Vec::new(); - collect_ref_roots(val, &mut roots); - if roots.iter().any(|r| alias_stack.last().unwrap().contains(r)) { - alias_stack.last_mut().unwrap().insert(name.clone()); - } else { - alias_stack.last_mut().unwrap().remove(name); - } + } else if receiver_type_name(recv).is_none() { + if st.closure_mutates.contains_key(root) { + self.check_a3_closure_call(st, cr, idx, root); + } + let live: Vec = st + .loans + .iter() + .filter_map(|l| match &l.target { + LoanTarget::MapSlot { path, .. } if l.created <= idx && idx <= l.last => { + Some(path.clone()) } + _ => None, + }) + .collect(); + if live.is_empty() { + return; + } + let known_params = st.closure_param_mutates.get(root); + let mut conflict_paths: Vec = Vec::new(); + for (i, a) in cr.args.iter().enumerate() { + let mutates = match known_params { + Some(set) => set.contains(&i), + None => true, + }; + if !mutates { + continue; } - Stmt::Assign { targets, values, .. } if targets.len() == values.len() => { - for (t, val) in targets.iter().zip(values.iter()) { - if let PlaceKind::Ident(name) = &t.kind { - let mut roots = Vec::new(); - collect_ref_roots(val, &mut roots); - if roots.iter().any(|r| alias_stack.last().unwrap().contains(r)) { - alias_stack.last_mut().unwrap().insert(name.clone()); - } else { - alias_stack.last_mut().unwrap().remove(name); - } - } + for arg_path in a3_expr_paths(&a.value) { + if live.iter().any(|p| a3_paths_conflict(st, p, &arg_path)) { + conflict_paths.push(arg_path); } } - _ => {} + } + let verb = format!("mutation via `{}`", cr.callee.render()); + for p in conflict_paths { + self.emit_a3_if_live(st, &p, cr.span, idx, &verb); } } - false } - /// A nested FnLit inside the callee body mutates `param` only if it - /// captures it (a body declaring its own `param` shadows it — those - /// references are the closure's fresh binding, not the caller's map). - fn expr_fnlit_mutates_param(&self, e: &Expr, param: &str, depth: usize) -> bool { - match e { - Expr::FnLit { - params, - results, - body, - .. - } => { - if params.iter().any(|p| p.name == param) - || results.iter().any(|r| r.name.as_deref() == Some(param)) - { - false - } else { - self.block_mutates_param(body, param, depth) + fn check_a3_inline_or_expr_call(&mut self, st: &FnState, cr: &CallRef, idx: usize) { + let live: Vec = st + .loans + .iter() + .filter_map(|l| match &l.target { + LoanTarget::MapSlot { path, .. } if l.created <= idx && idx <= l.last => { + Some(path.clone()) + } + _ => None, + }) + .collect(); + if live.is_empty() { + return; + } + let callee = unwrap_parens(cr.callee); + let verb = format!("mutation via `{}`", cr.callee.render()); + if let Expr::FnLit { + params, + results, + body, + .. + } = callee + { + let mut bound: HashSet = params.iter().map(|p| p.name.clone()).collect(); + bound.extend(results.iter().filter_map(|r| r.name.clone())); + let p_structs = self.params_struct_map(params, results); + let roots = self.collect_mutation_roots(st, body, &bound, &p_structs); + for p in &live { + if roots.iter().any(|r| a3_paths_conflict(st, p, r)) { + self.emit_a3_if_live(st, p, cr.span, idx, &verb); + return; } } - Expr::Paren { inner, .. } | Expr::Move { operand: inner, .. } - | Expr::Try { operand: inner, .. } | Expr::Unary { operand: inner, .. } - | Expr::AddrOf { operand: inner, .. } => self.expr_fnlit_mutates_param(inner, param, depth), - Expr::Call { callee, args, .. } => { - self.expr_fnlit_mutates_param(callee, param, depth) - || args - .iter() - .any(|a| self.expr_fnlit_mutates_param(&a.value, param, depth)) + let mut conflict_paths: Vec = Vec::new(); + for (i, a) in cr.args.iter().enumerate() { + let candidates: Vec = a3_expr_paths(&a.value) + .into_iter() + .filter(|arg_path| live.iter().any(|p| a3_paths_conflict(st, p, arg_path))) + .collect(); + if candidates.is_empty() { + continue; + } + let mutates = match params.get(i) { + Some(p) => self.block_mutates_param(body, &p.name, 0, &p_structs), + None => true, + }; + if mutates { + conflict_paths.extend(candidates); + } } - Expr::Index { base, index, .. } | Expr::Binary { lhs: base, rhs: index, .. } => { - self.expr_fnlit_mutates_param(base, param, depth) - || self.expr_fnlit_mutates_param(index, param, depth) + for p in conflict_paths { + self.emit_a3_if_live(st, &p, cr.span, idx, &verb); } - Expr::StructLit { fields, .. } => fields - .iter() - .any(|f| self.expr_fnlit_mutates_param(&f.value, param, depth)), - Expr::SliceLit { items, .. } => items - .iter() - .any(|it| self.expr_fnlit_mutates_param(it, param, depth)), - Expr::MapLit { entries, .. } => entries.iter().any(|(k, v)| { - self.expr_fnlit_mutates_param(k, param, depth) - || self.expr_fnlit_mutates_param(v, param, depth) - }), - _ => false, + return; } - } - - /// Does this call site store through / delete `param`? - fn call_mutates_param(&self, cr: &CallRef, param: &str, depth: usize) -> bool { - let arg_root_is_param = |a: &hy_syntax::CallArg| { - let mut roots = Vec::new(); - collect_ref_roots(&a.value, &mut roots); - roots.iter().any(|r| r == param) - }; - match cr.callee { - Expr::Ident { name, .. } => { - if name == "delete" { - return cr.args.first().map(&arg_root_is_param).unwrap_or(false); + if let Expr::Field { + base, + name: method_name, + .. + } = callee + { + let empty_structs = LexicalStructMap::new(); + let matching = self.matching_methods(Some(st), base, method_name, &empty_structs, None); + let mut conflict_paths: Vec = Vec::new(); + let recv_mutates = matching.is_empty() + || matching.iter().any(|d| { + d.receiver + .as_ref() + .is_some_and(|r| self.fn_mutates_param(d, &r.name, 0)) + }); + if recv_mutates { + for bp in a3_expr_paths(base) { + if live.iter().any(|p| a3_paths_conflict(st, p, &bp)) { + conflict_paths.push(bp); + } } - if A3_PURE_FNS.contains(&name.as_str()) { - return false; + } + for (i, a) in cr.args.iter().enumerate() { + let mutates = matching.is_empty() + || matching.iter().any(|d| match d.params.get(i) { + Some(p) => self.fn_mutates_param(d, &p.name, 0), + None => true, + }); + if !mutates { + continue; } - if let Some(decl) = self.fns.get(name) { - for (i, a) in cr.args.iter().enumerate() { - if !arg_root_is_param(a) { - continue; - } - match decl.params.get(i) { - Some(p) => { - if self.fn_mutates_param(decl, &p.name, depth + 1) { - return true; - } - } - None => return true, // fail closed - } + for arg_path in a3_expr_paths(&a.value) { + if live.iter().any(|p| a3_paths_conflict(st, p, &arg_path)) { + conflict_paths.push(arg_path); } - return false; } - // unknown Ident callee (closure / builtin we don't model): - // passing the parameter fails closed - cr.args.iter().any(&arg_root_is_param) } - Expr::Field { base, .. } => { - if let Expr::Ident { name: pkg, .. } = &**base { - if A3_PURE_PKGS.contains(&pkg.as_str()) { - return false; + if matching.is_empty() { + if let Some(root) = callee.root_ident() { + if st.closure_mutates.contains_key(root) { + self.check_a3_closure_call(st, cr, idx, root); } } - cr.args.iter().any(&arg_root_is_param) } - _ => cr.args.iter().any(&arg_root_is_param), + for p in conflict_paths { + self.emit_a3_if_live(st, &p, cr.span, idx, &verb); + } + return; + } + if let Some(root) = callee.root_ident() { + if st.closure_mutates.contains_key(root) { + self.check_a3_closure_call(st, cr, idx, root); + } + } + // Unresolved non-FnLit callee expression (`fs[0](m)`, `getf()(m)`) — + // fail closed on any argument reaching a live map-slot loan. + let mut conflict_paths: Vec = Vec::new(); + for a in cr.args { + for arg_path in a3_expr_paths(&a.value) { + if live.iter().any(|p| a3_paths_conflict(st, p, &arg_path)) { + conflict_paths.push(arg_path); + } + } + } + for p in conflict_paths { + self.emit_a3_if_live(st, &p, cr.span, idx, &verb); } } - /// Record (at define/rebind time) which base paths a local closure's - /// body may mutate — consumed at the closure's call sites (#111 form 4). - /// The closure's own params are excluded: they are fresh bindings. - fn record_closure_mutations(&self, st: &mut FnState, name: &str, value: &Expr) { - match unwrap_parens(value) { + /// Seeds the lexical `ident -> Option` map from a function's + /// receiver, parameters, and named result bindings. Every declared binding + /// is recorded (`None` when non-struct) so lexical shadowing of top-level + /// functions is preserved even for non-struct parameters. + fn decl_struct_map(&self, decl: &FnDecl) -> LexicalStructMap { + let mut map = LexicalStructMap::new(); + if let Some(r) = &decl.receiver { + map.insert(r.name.clone(), self.type_expr_struct_name(&r.ty)); + } + for p in &decl.params { + map.insert(p.name.clone(), self.type_expr_struct_name(&p.ty)); + } + for res in &decl.results { + if let Some(name) = &res.name { + map.insert(name.clone(), self.type_expr_struct_name(&res.ty)); + } + } + map + } + + fn params_struct_map( + &self, + params: &[hy_syntax::Param], + results: &[hy_syntax::FnResult], + ) -> LexicalStructMap { + let mut map = LexicalStructMap::new(); + for p in params { + map.insert(p.name.clone(), self.type_expr_struct_name(&p.ty)); + } + for res in results { + if let Some(name) = &res.name { + map.insert(name.clone(), self.type_expr_struct_name(&res.ty)); + } + } + map + } + + fn update_local_structs_for_local( + &self, + st: Option<&FnState>, + l: &LocalDecl, + local_structs: &mut LexicalStructMap, + shadowed: Option<&HashSet>, + ) { + let ascribed = l.ty.as_ref().and_then(|ty| self.type_expr_struct_name(ty)); + let inferred_vals: Vec> = l + .names + .iter() + .enumerate() + .map(|(idx, _)| { + ascribed.clone().or_else(|| { + l.values + .get(idx) + .and_then(|v| self.infer_base_struct_name(st, v, local_structs, shadowed)) + }) + }) + .collect(); + for (name, inferred) in l.names.iter().zip(inferred_vals) { + local_structs.insert(name.clone(), inferred); + } + } + + fn update_local_structs_for_assign( + &self, + st: Option<&FnState>, + targets: &[Place], + values: &[Expr], + local_structs: &mut LexicalStructMap, + shadowed: Option<&HashSet>, + ) { + if targets.len() == values.len() { + let rhs_structs: Vec> = values + .iter() + .map(|val| self.infer_base_struct_name(st, val, local_structs, shadowed)) + .collect(); + for (t, s) in targets.iter().zip(rhs_structs) { + if let PlaceKind::Ident(name) = &t.kind { + local_structs.insert(name.clone(), s); + } + } + } else { + for t in targets { + if let PlaceKind::Ident(name) = &t.kind { + local_structs.insert(name.clone(), None); + } + } + } + } + + fn invalidate_for_header_structs( + kind: &hy_syntax::ForKind, + local_structs: &mut LexicalStructMap, + mut local_shadows: Option<&mut HashSet>, + ) { + match kind { + hy_syntax::ForKind::Iterate { vars, .. } => { + for v in vars { + local_structs.insert(v.clone(), None); + if let Some(sh) = local_shadows.as_deref_mut() { + sh.insert(v.clone()); + } + } + } + hy_syntax::ForKind::Range { var, .. } => { + local_structs.insert(var.clone(), None); + if let Some(sh) = local_shadows { + sh.insert(var.clone()); + } + } + _ => {} + } + } + + fn invalidate_reassigned_structs_in_block(block: &Block, local_structs: &mut LexicalStructMap) { + let flat = walk::flatten(block); + for (stmt, _) in &flat.stmts { + match stmt { + Stmt::Assign { targets, .. } => { + for t in targets { + if let PlaceKind::Ident(name) = &t.kind { + if let Some(entry) = local_structs.get_mut(name.as_str()) { + *entry = None; + } + } + } + } + Stmt::For { kind, .. } => match kind { + hy_syntax::ForKind::Iterate { vars, .. } => { + for v in vars { + if let Some(entry) = local_structs.get_mut(v.as_str()) { + *entry = None; + } + } + } + hy_syntax::ForKind::Range { var, .. } => { + if let Some(entry) = local_structs.get_mut(var.as_str()) { + *entry = None; + } + } + _ => {} + }, + _ => {} + } + } + } + + /// Invalidates any outer binding reassigned inside a branch or loop so + /// subsequent statements fall back to conservative all-method matching if + /// control flow could have altered its concrete struct type. + fn invalidate_reassigned_structs_in_stmt(stmt: &Stmt, local_structs: &mut LexicalStructMap) { + match stmt { + Stmt::Assign { targets, .. } => { + for t in targets { + if let PlaceKind::Ident(name) = &t.kind { + if let Some(entry) = local_structs.get_mut(name.as_str()) { + *entry = None; + } + } + } + } + Stmt::If { + init, + then, + otherwise, + .. + } => { + if let Some(i) = init { + Self::invalidate_reassigned_structs_in_stmt(i, local_structs); + } + Self::invalidate_reassigned_structs_in_block(then, local_structs); + if let Some(o) = otherwise { + Self::invalidate_reassigned_structs_in_stmt(o, local_structs); + } + } + Stmt::For { kind, body, .. } => { + match kind { + hy_syntax::ForKind::Iterate { vars, .. } => { + for v in vars { + if let Some(entry) = local_structs.get_mut(v.as_str()) { + *entry = None; + } + } + } + hy_syntax::ForKind::Range { var, .. } => { + if let Some(entry) = local_structs.get_mut(var.as_str()) { + *entry = None; + } + } + _ => {} + } + Self::invalidate_reassigned_structs_in_block(body, local_structs); + } + Stmt::Managed { body, .. } | Stmt::Block { block: body, .. } => { + Self::invalidate_reassigned_structs_in_block(body, local_structs); + } + Stmt::Match { arms, .. } => { + for arm in arms { + Self::invalidate_reassigned_structs_in_block(&arm.body, local_structs); + } + } + Stmt::Select { arms, .. } => { + for arm in arms { + Self::invalidate_reassigned_structs_in_block(&arm.body, local_structs); + } + } + _ => {} + } + } + + /// Does `decl` mutate its parameter `param` (map insert/delete — the + /// only A3 events)? Bounded interprocedural scan, fail-closed past the + /// depth bound and on unknown callees that receive the parameter (#111). + fn fn_mutates_param(&self, decl: &FnDecl, param: &str, depth: usize) -> bool { + if depth > 3 { + return true; // fail closed past the bound + } + let Some(body) = &decl.body else { + return false; + }; + let local_structs = self.decl_struct_map(decl); + self.block_mutates_param(body, param, depth, &local_structs) + } + + fn block_mutates_param( + &self, + block: &Block, + param: &str, + depth: usize, + outer_structs: &LexicalStructMap, + ) -> bool { + let flat = walk::flatten(block); + let mut alias_stack: Vec> = vec![[param.to_string()].into_iter().collect()]; + let mut structs_stack: Vec = vec![outer_structs.clone()]; + let mut locals_stack: Vec> = vec![HashSet::new()]; + for (stmt, idx) in &flat.stmts { + if let Some(evs) = flat.events.get(idx) { + for ev in evs { + match ev { + walk::ScopeEvent::Push => { + alias_stack.push(alias_stack.last().unwrap().clone()); + structs_stack.push(structs_stack.last().unwrap().clone()); + locals_stack.push(HashSet::new()); + } + walk::ScopeEvent::Pop => { + if alias_stack.len() > 1 { + let popped_aliases = alias_stack.pop().unwrap(); + let popped_structs = structs_stack.pop().unwrap(); + let popped_locals = locals_stack.pop().unwrap(); + let outer_aliases = alias_stack.last_mut().unwrap(); + let outer_s = structs_stack.last_mut().unwrap(); + let keys: Vec = outer_s + .keys() + .chain(popped_structs.keys()) + .cloned() + .collect(); + for k in keys { + if !popped_locals.contains(&k) + && popped_structs.get(&k) != outer_s.get(&k) + { + outer_s.insert(k, None); + } + } + for a in popped_aliases { + if !popped_locals.contains(&a) { + outer_aliases.insert(a); + } + } + } + } + walk::ScopeEvent::MatchBind { name, subject } => { + locals_stack.last_mut().unwrap().insert((*name).to_string()); + structs_stack + .last_mut() + .unwrap() + .insert((*name).to_string(), None); + let mut sroots = Vec::new(); + collect_ref_roots(subject, &mut sroots); + if sroots.iter().any(|r| alias_stack.last().unwrap().contains(r)) { + alias_stack.last_mut().unwrap().insert((*name).to_string()); + } else { + alias_stack.last_mut().unwrap().remove(*name); + } + } + walk::ScopeEvent::SelectRecv { names, .. } => { + for n in *names { + locals_stack.last_mut().unwrap().insert(n.clone()); + structs_stack.last_mut().unwrap().insert(n.clone(), None); + alias_stack.last_mut().unwrap().remove(n.as_str()); + } + } + } + } + } + + if let Stmt::Assign { targets, .. } = stmt { + for t in targets { + if matches!(t.kind, PlaceKind::Index { .. }) + && alias_stack.last().unwrap().contains(t.root()) + { + return true; + } + } + } + + let mut fnlits = Vec::new(); + walk::own_fnlits(stmt, &mut fnlits); + for flit in fnlits { + if alias_stack.last().unwrap().iter().any(|p| { + self.expr_fnlit_mutates_param(flit, p, depth, structs_stack.last().unwrap()) + }) { + return true; + } + } + + // Check calls in this statement + let mut calls = Vec::new(); + walk::own_calls(stmt, &mut calls); + for cr in &calls { + if alias_stack.last().unwrap().iter().any(|p| { + self.call_mutates_param(cr, p, depth, structs_stack.last().unwrap()) + }) { + return true; + } + } + + // Update aliases and lexical struct map + match stmt { + Stmt::Local(l) => { + self.update_local_structs_for_local( + None, + l, + structs_stack.last_mut().unwrap(), + None, + ); + for (name, val) in l.names.iter().zip(l.values.iter()) { + locals_stack.last_mut().unwrap().insert(name.clone()); + let mut roots = Vec::new(); + collect_ref_roots(val, &mut roots); + if roots.iter().any(|r| alias_stack.last().unwrap().contains(r)) { + alias_stack.last_mut().unwrap().insert(name.clone()); + } else { + alias_stack.last_mut().unwrap().remove(name); + } + } + if l.values.is_empty() { + for name in &l.names { + locals_stack.last_mut().unwrap().insert(name.clone()); + alias_stack.last_mut().unwrap().remove(name); + } + } + } + Stmt::Assign { + targets, values, .. + } if targets.len() == values.len() => { + self.update_local_structs_for_assign( + None, + targets, + values, + structs_stack.last_mut().unwrap(), + None, + ); + for (t, val) in targets.iter().zip(values.iter()) { + if let PlaceKind::Ident(name) = &t.kind { + let mut roots = Vec::new(); + collect_ref_roots(val, &mut roots); + if roots.iter().any(|r| alias_stack.last().unwrap().contains(r)) { + alias_stack.last_mut().unwrap().insert(name.clone()); + } else { + alias_stack.last_mut().unwrap().remove(name); + } + } + } + } + Stmt::For { kind, .. } => { + Self::invalidate_reassigned_structs_in_stmt( + stmt, + structs_stack.last_mut().unwrap(), + ); + Self::invalidate_for_header_structs( + kind, + structs_stack.last_mut().unwrap(), + None, + ); + match kind { + hy_syntax::ForKind::Iterate { vars, .. } => { + for v in vars { + locals_stack.last_mut().unwrap().insert(v.clone()); + alias_stack.last_mut().unwrap().remove(v.as_str()); + } + } + hy_syntax::ForKind::Range { var, .. } => { + locals_stack.last_mut().unwrap().insert(var.clone()); + alias_stack.last_mut().unwrap().remove(var.as_str()); + } + _ => {} + } + } + _ => {} + } + } + false + } + + /// A nested FnLit inside the callee body mutates `param` only if it + /// captures it (a body declaring its own `param` shadows it — those + /// references are the closure's fresh binding, not the caller's map). + fn expr_fnlit_mutates_param( + &self, + e: &Expr, + param: &str, + depth: usize, + outer_structs: &LexicalStructMap, + ) -> bool { + match unwrap_parens(e) { Expr::FnLit { params, results, body, .. } => { - let mut bound: HashSet = params.iter().map(|p| p.name.clone()).collect(); - bound.extend(results.iter().filter_map(|r| r.name.clone())); - let roots = self.collect_mutation_roots(st, body, &bound); - if !roots.is_empty() { - st.closure_mutates - .entry(name.to_string()) - .or_default() - .extend(roots); + if params.iter().any(|p| p.name == param) + || results.iter().any(|r| r.name.as_deref() == Some(param)) + { + false + } else { + let mut inner_structs = outer_structs.clone(); + for p in params { + inner_structs.insert(p.name.clone(), self.type_expr_struct_name(&p.ty)); + } + for r in results { + if let Some(name) = &r.name { + inner_structs.insert(name.clone(), self.type_expr_struct_name(&r.ty)); + } + } + self.block_mutates_param(body, param, depth, &inner_structs) } } - Expr::Ident { name: src, .. } => { - if let Some(existing) = st.closure_mutates.get(src).cloned() { - if !existing.is_empty() { - st.closure_mutates - .entry(name.to_string()) - .or_default() - .extend(existing); + Expr::Paren { inner, .. } + | Expr::Move { operand: inner, .. } + | Expr::Try { operand: inner, .. } + | Expr::Unary { operand: inner, .. } + | Expr::AddrOf { operand: inner, .. } => { + self.expr_fnlit_mutates_param(inner, param, depth, outer_structs) + } + Expr::Call { callee, args, .. } => { + self.expr_fnlit_mutates_param(callee, param, depth, outer_structs) + || args.iter().any(|a| { + self.expr_fnlit_mutates_param(&a.value, param, depth, outer_structs) + }) + } + Expr::Index { base, index, .. } + | Expr::Binary { + lhs: base, + rhs: index, + .. + } => { + self.expr_fnlit_mutates_param(base, param, depth, outer_structs) + || self.expr_fnlit_mutates_param(index, param, depth, outer_structs) + } + Expr::StructLit { fields, .. } => fields + .iter() + .any(|f| self.expr_fnlit_mutates_param(&f.value, param, depth, outer_structs)), + Expr::SliceLit { items, .. } => items + .iter() + .any(|it| self.expr_fnlit_mutates_param(it, param, depth, outer_structs)), + Expr::MapLit { entries, .. } => entries.iter().any(|(k, v)| { + self.expr_fnlit_mutates_param(k, param, depth, outer_structs) + || self.expr_fnlit_mutates_param(v, param, depth, outer_structs) + }), + _ => false, + } + } + + fn type_expr_struct_name(&self, ty: &hy_syntax::TypeExpr) -> Option { + let resolved = self.resolve_type_alias(ty); + match &resolved { + hy_syntax::TypeExpr::Named { name, .. } if self.structs.contains_key(name.as_str()) => { + Some(name.clone()) + } + hy_syntax::TypeExpr::Pointer { to, .. } => self.type_expr_struct_name(to), + _ => None, + } + } + + /// Resolves the concrete struct name of `base` using the active lexical + /// struct map, unshadowed top-level call return types, or `FnState` when + /// available (respecting `shadowed` inner bindings). + fn infer_base_struct_name( + &self, + st: Option<&FnState>, + base: &Expr, + local_structs: &LexicalStructMap, + shadowed: Option<&HashSet>, + ) -> Option { + let inner = unwrap_parens(base); + if let Some(operand) = as_addr_of(inner) { + return self.infer_base_struct_name(st, operand, local_structs, shadowed); + } + match inner { + Expr::StructLit { + ty: Some(hy_syntax::TypeExpr::Named { name, .. }), + .. + } => return Some(name.clone()), + Expr::Call { callee, .. } => { + if let Expr::Ident { name, .. } = unwrap_parens(callee) { + let is_shadowed = local_structs.contains_key(name.as_str()) + || shadowed.is_some_and(|sh| sh.contains(name.as_str())) + || st.is_some_and(|s| self.lookup_binding(s, name).is_some()); + if !is_shadowed { + if let Some(decl) = self.fns.get(name.as_str()) { + if let Some(res) = decl.results.first() { + if let Some(s) = self.type_expr_struct_name(&res.ty) { + return Some(s); + } + } + } } } } - Expr::Call { callee, args, .. } => { - if !matches!(self.shape_of_expr(st, value), TypeShape::Closure) { - return; + Expr::Ident { name, .. } => { + if let Some(entry) = local_structs.get(name.as_str()) { + return entry.clone(); + } + if shadowed.is_some_and(|sh| sh.contains(name.as_str())) { + return None; } - let mut mutated_roots = HashSet::new(); + } + _ => {} + } + if let Some(state) = st { + if let Some(root) = inner.root_ident() { + if local_structs.contains_key(root) || shadowed.is_some_and(|sh| sh.contains(root)) + { + return None; + } + } + match self.shape_of_expr(state, inner) { + TypeShape::Struct(Some(s)) => return Some(s), + TypeShape::Pointer(elem) => { + if let TypeShape::Struct(Some(s)) = *elem { + return Some(s); + } + } + _ => {} + } + } + None + } + + /// Returns the exact `(struct_name, method_name)` declaration when the + /// receiver's struct type is statically known, or all methods sharing + /// `method_name` when the receiver type cannot be narrowed. + fn matching_methods( + &self, + st: Option<&FnState>, + base: &Expr, + method_name: &str, + local_structs: &LexicalStructMap, + shadowed: Option<&HashSet>, + ) -> Vec<&'a FnDecl> { + if let Some(s) = self.infer_base_struct_name(st, base, local_structs, shadowed) { + if let Some(d) = self.method_decls.get(&(s, method_name.to_string())) { + return vec![*d]; + } + return Vec::new(); + } + self.method_decls + .iter() + .filter(|((_, m), _)| m == method_name) + .map(|(_, d)| *d) + .collect() + } + + /// Does this call site store through / delete `param`? + fn call_mutates_param( + &self, + cr: &CallRef, + param: &str, + depth: usize, + local_structs: &LexicalStructMap, + ) -> bool { + let arg_root_is_param = |a: &hy_syntax::CallArg| { + let mut roots = Vec::new(); + collect_ref_roots(&a.value, &mut roots); + roots.iter().any(|r| r == param) + }; + match unwrap_parens(cr.callee) { + Expr::Ident { name, .. } => { + if !local_structs.contains_key(name.as_str()) { + if name == "delete" { + return cr.args.first().map(arg_root_is_param).unwrap_or(false); + } + if A3_PURE_FNS.contains(&name.as_str()) { + return false; + } + if let Some(decl) = self.fns.get(name) { + for (i, a) in cr.args.iter().enumerate() { + if !arg_root_is_param(a) { + continue; + } + match decl.params.get(i) { + Some(p) => { + if self.fn_mutates_param(decl, &p.name, depth + 1) { + return true; + } + } + None => return true, // fail closed + } + } + return false; + } + } + // unknown Ident callee (closure / builtin we don't model): + // passing the parameter fails closed + cr.args.iter().any(&arg_root_is_param) + } + Expr::Field { + base, + name: method_name, + .. + } => { + if let Expr::Ident { name: pkg, .. } = unwrap_parens(base) { + if !local_structs.contains_key(pkg.as_str()) + && A3_PURE_PKGS.contains(&pkg.as_str()) + { + return false; + } + } + let matching = self.matching_methods(None, base, method_name, local_structs, None); + let recv_is_param = { + let mut r = Vec::new(); + collect_ref_roots(base, &mut r); + r.iter().any(|x| x == param) + }; + if recv_is_param + && (matching.is_empty() + || matching.iter().any(|d| { + d.receiver + .as_ref() + .is_some_and(|r| self.fn_mutates_param(d, &r.name, depth + 1)) + })) + { + return true; + } + if !matching.is_empty() { + for (i, a) in cr.args.iter().enumerate() { + if !arg_root_is_param(a) { + continue; + } + if matching.iter().any(|d| match d.params.get(i) { + Some(p) => self.fn_mutates_param(d, &p.name, depth + 1), + None => true, + }) { + return true; + } + } + return false; + } + cr.args.iter().any(&arg_root_is_param) + } + Expr::FnLit { + params, + results, + body, + .. + } => { + let mut inner_structs = local_structs.clone(); + for p in params { + inner_structs.insert(p.name.clone(), self.type_expr_struct_name(&p.ty)); + } + for r in results { + if let Some(name) = &r.name { + inner_structs.insert(name.clone(), self.type_expr_struct_name(&r.ty)); + } + } + for (i, a) in cr.args.iter().enumerate() { + if !arg_root_is_param(a) { + continue; + } + match params.get(i) { + Some(p) => { + if self.block_mutates_param(body, &p.name, depth + 1, &inner_structs) { + return true; + } + } + None => return true, + } + } + false + } + _ => cr.args.iter().any(&arg_root_is_param), + } + } + + /// Record (at define/rebind time) which base paths a local closure's + /// body may mutate — consumed at the closure's call sites (#111 form 4). + /// The closure's own params are excluded from captured roots and tracked + /// separately by parameter index in `closure_param_mutates` only when + /// `name` is directly bound to an `Expr::FnLit` or an alias of one. + fn record_closure_mutations( + &self, + st: &mut FnState, + name: &str, + value: &Expr, + is_direct_ident_bind: bool, + ) { + let mut new_roots: HashSet = HashSet::new(); + if let Some(src) = value.root_ident() { + if let Some(roots) = st.closure_mutates.get(src).cloned() { + new_roots.extend(roots); + } + } + let mut fnlits = Vec::new(); + walk::expr_fnlits(value, &mut fnlits); + for flit in fnlits { + let Expr::FnLit { + params, + results, + body, + .. + } = unwrap_parens(flit) + else { + continue; + }; + let mut bound: HashSet = params.iter().map(|p| p.name.clone()).collect(); + bound.extend(results.iter().filter_map(|r| r.name.clone())); + let p_structs = self.params_struct_map(params, results); + let roots = self.collect_mutation_roots(st, body, &bound, &p_structs); + new_roots.extend(roots); + } + if let Expr::Call { callee, args, .. } = unwrap_parens(value) { + if matches!(self.shape_of_expr(st, value), TypeShape::Closure) { if let Expr::Ident { name: fn_name, .. } = unwrap_parens(callee) { if let Some(decl) = self.fns.get(fn_name) { for (i, a) in args.iter().enumerate() { @@ -759,19 +1545,51 @@ impl<'a> Checker<'a> { if mutates { let mut r = Vec::new(); collect_ref_roots(&a.value, &mut r); - mutated_roots.extend(r); + new_roots.extend(r); } } } } - if !mutated_roots.is_empty() { - st.closure_mutates - .entry(name.to_string()) - .or_default() - .extend(mutated_roots); + } + } + if is_direct_ident_bind { + let new_params: Option> = match unwrap_parens(value) { + Expr::FnLit { + params, + results, + body, + .. + } => { + let p_structs = self.params_struct_map(params, results); + let mut pset = HashSet::new(); + for (i, p) in params.iter().enumerate() { + if self.block_mutates_param(body, &p.name, 0, &p_structs) { + pset.insert(i); + } + } + Some(pset) } + Expr::Ident { name: src, .. } => st.closure_param_mutates.get(src).cloned(), + _ => None, + }; + if new_roots.is_empty() { + st.closure_mutates.remove(name); + } else { + st.closure_mutates.insert(name.to_string(), new_roots); } - _ => {} + if let Some(pset) = new_params { + st.closure_param_mutates.insert(name.to_string(), pset); + } else { + st.closure_param_mutates.remove(name); + } + } else { + if !new_roots.is_empty() { + st.closure_mutates + .entry(name.to_string()) + .or_default() + .extend(new_roots); + } + st.closure_param_mutates.remove(name); } } @@ -784,15 +1602,23 @@ impl<'a> Checker<'a> { st: &FnState, body: &Block, bound: &HashSet, + param_structs: &LexicalStructMap, ) -> HashSet { let mut out: HashSet = HashSet::new(); - let mut shadow_stack: Vec> = vec![bound.clone()]; - let mut alias_stack: Vec> = - vec![std::collections::HashMap::new()]; - let mut locals_stack: Vec> = vec![HashSet::new()]; + let mut local_aliases: std::collections::HashMap = + std::collections::HashMap::new(); + let mut local_shadows: HashSet = bound.clone(); + let mut local_structs: LexicalStructMap = param_structs.clone(); let mut match_aliases: std::collections::HashMap> = std::collections::HashMap::new(); - + type MutationScopeLayer = ( + std::collections::HashMap, + HashSet, + LexicalStructMap, + HashSet, + ); + let mut scope_stack: Vec = Vec::new(); + let mut defined_in_scope: HashSet = HashSet::new(); let resolve = |raw: &str, aliases: &std::collections::HashMap, shadows: &HashSet| @@ -812,77 +1638,190 @@ impl<'a> Checker<'a> { for ev in evs { match ev { walk::ScopeEvent::Push => { - shadow_stack.push(shadow_stack.last().unwrap().clone()); - alias_stack.push(alias_stack.last().unwrap().clone()); - locals_stack.push(HashSet::new()); + let prev_defined = std::mem::take(&mut defined_in_scope); + scope_stack.push(( + local_aliases.clone(), + local_shadows.clone(), + local_structs.clone(), + prev_defined, + )); } walk::ScopeEvent::Pop => { - if shadow_stack.len() > 1 { - shadow_stack.pop(); - let popped_aliases = alias_stack.pop().unwrap(); - let popped_locals = locals_stack.pop().unwrap(); - for (k, v) in popped_aliases { - if !popped_locals.contains(&k) { - alias_stack.last_mut().unwrap().insert(k, v); + if let Some(( + mut outer_aliases, + mut outer_shadows, + mut outer_structs, + prev_defined, + )) = scope_stack.pop() + { + let inner_defined = + std::mem::replace(&mut defined_in_scope, prev_defined); + let keys: Vec = outer_structs + .keys() + .chain(local_structs.keys()) + .cloned() + .collect(); + for k in keys { + if !inner_defined.contains(&k) + && local_structs.get(&k) != outer_structs.get(&k) + { + outer_structs.insert(k, None); + } + } + for (k, v) in local_aliases.iter() { + if !inner_defined.contains(k) { + outer_aliases.insert(k.clone(), v.clone()); + outer_shadows.remove(k); } } + for s in local_shadows.iter() { + if !inner_defined.contains(s) && bound.contains(s) { + outer_shadows.insert(s.clone()); + } + } + local_aliases = outer_aliases; + local_shadows = outer_shadows; + local_structs = outer_structs; } } walk::ScopeEvent::MatchBind { name, subject } => { + defined_in_scope.insert((*name).to_string()); + local_structs.insert((*name).to_string(), None); let mut sroots = Vec::new(); collect_ref_roots(subject, &mut sroots); sroots.retain(|r| !bound.contains(r)); - let current_shadows = shadow_stack.last().unwrap(); - let current_aliases = alias_stack.last().unwrap(); for sr in sroots { - if let Some(target) = resolve(&sr, current_aliases, current_shadows) { - match_aliases.entry(name.to_string()).or_default().push(target.clone()); - alias_stack.last_mut().unwrap().insert(name.to_string(), target); + if let Some(target) = resolve(&sr, &local_aliases, &local_shadows) { + match_aliases + .entry((*name).to_string()) + .or_default() + .push(target.clone()); + local_aliases.insert((*name).to_string(), target); + local_shadows.remove(*name); break; } } } - _ => {} + walk::ScopeEvent::SelectRecv { names, .. } => { + for n in *names { + defined_in_scope.insert(n.clone()); + local_structs.insert(n.clone(), None); + local_aliases.remove(n.as_str()); + local_shadows.insert(n.clone()); + } + } + } + } + } + // FnLit values reachable in this statement (captures mutated by + // nested closures — evaluated before LHS bindings shadow names) + let mut fnlit_values: Vec<&Expr> = Vec::new(); + walk::own_fnlits(stmt, &mut fnlit_values); + for v in fnlit_values { + if let Expr::FnLit { + params: fp, + results: fr, + body: fb, + .. + } = unwrap_parens(v) + { + let mut inner_bound: HashSet = + fp.iter().map(|p| p.name.clone()).collect(); + inner_bound.extend(fr.iter().filter_map(|r| r.name.clone())); + let mut inner_structs = local_structs.clone(); + for p in fp { + inner_structs.insert(p.name.clone(), self.type_expr_struct_name(&p.ty)); + } + for r in fr { + if let Some(name) = &r.name { + inner_structs.insert(name.clone(), self.type_expr_struct_name(&r.ty)); + } + } + for ir in self.collect_mutation_roots(st, fb, &inner_bound, &inner_structs) { + if let Some(r) = resolve(&ir, &local_aliases, &local_shadows) { + out.insert(r); + } } } } - - let current_shadows = shadow_stack.last_mut().unwrap(); - let current_aliases = alias_stack.last_mut().unwrap(); - let current_locals = locals_stack.last_mut().unwrap(); - match stmt { Stmt::Local(l) => { + for name in &l.names { + defined_in_scope.insert(name.clone()); + } + self.update_local_structs_for_local( + Some(st), + l, + &mut local_structs, + Some(&local_shadows), + ); for (name, val) in l.names.iter().zip(l.values.iter()) { - current_locals.insert(name.clone()); let mut roots = Vec::new(); collect_ref_roots(val, &mut roots); - let resolved = roots.iter().find_map(|p| resolve(p, current_aliases, current_shadows)); + let resolved = roots + .iter() + .find_map(|p| resolve(p, &local_aliases, &local_shadows)); if let Some(r) = resolved { - current_shadows.remove(name.as_str()); - current_aliases.insert(name.clone(), r); + local_shadows.remove(name.as_str()); + local_aliases.insert(name.clone(), r); } else { - current_aliases.remove(name.as_str()); - current_shadows.insert(name.clone()); + local_aliases.remove(name.as_str()); + local_shadows.insert(name.clone()); + } + } + if l.values.is_empty() { + for name in &l.names { + local_aliases.remove(name.as_str()); + local_shadows.insert(name.clone()); } } } Stmt::Assign { targets, values, .. } if targets.len() == values.len() => { + self.update_local_structs_for_assign( + Some(st), + targets, + values, + &mut local_structs, + Some(&local_shadows), + ); for (t, val) in targets.iter().zip(values.iter()) { if let PlaceKind::Ident(name) = &t.kind { let mut roots = Vec::new(); collect_ref_roots(val, &mut roots); - let resolved = roots.iter().find_map(|p| resolve(p, current_aliases, current_shadows)); + let resolved = roots + .iter() + .find_map(|p| resolve(p, &local_aliases, &local_shadows)); if let Some(r) = resolved { - current_shadows.remove(name.as_str()); - current_aliases.insert(name.clone(), r); + local_shadows.remove(name.as_str()); + local_aliases.insert(name.clone(), r); } else if bound.contains(name.as_str()) { - current_aliases.remove(name.as_str()); - current_shadows.insert(name.clone()); + local_aliases.remove(name.as_str()); + local_shadows.insert(name.clone()); + } + } + } + } + Stmt::For { kind, .. } => { + Self::invalidate_reassigned_structs_in_stmt(stmt, &mut local_structs); + Self::invalidate_for_header_structs( + kind, + &mut local_structs, + Some(&mut local_shadows), + ); + match kind { + hy_syntax::ForKind::Iterate { vars, .. } => { + for v in vars { + defined_in_scope.insert(v.clone()); + local_aliases.remove(v.as_str()); } } + hy_syntax::ForKind::Range { var, .. } => { + defined_in_scope.insert(var.clone()); + local_aliases.remove(var.as_str()); + } + _ => {} } } Stmt::Match { subject, arms, .. } => { @@ -909,33 +1848,7 @@ impl<'a> Checker<'a> { for t in targets { if matches!(t.kind, PlaceKind::Index { .. }) { let raw = t.root().to_string(); - if let Some(r) = resolve(&raw, current_aliases, current_shadows) { - out.insert(r); - } - } - } - } - // FnLit values bound/rebound in this block (captures mutated by - // nested closures — flatten does not descend into FnLit bodies) - let mut fnlit_values: Vec<&Expr> = Vec::new(); - match stmt { - Stmt::Local(l) => fnlit_values.extend(l.values.iter()), - Stmt::Assign { values, .. } => fnlit_values.extend(values.iter()), - _ => {} - } - for v in fnlit_values { - if let Expr::FnLit { - params: fp, - results: fr, - body: fb, - .. - } = unwrap_parens(v) - { - let mut inner_bound: HashSet = - fp.iter().map(|p| p.name.clone()).collect(); - inner_bound.extend(fr.iter().filter_map(|r| r.name.clone())); - for ir in self.collect_mutation_roots(st, fb, &inner_bound) { - if let Some(r) = resolve(&ir, current_aliases, current_shadows) { + if let Some(r) = resolve(&raw, &local_aliases, &local_shadows) { out.insert(r); } } @@ -945,87 +1858,159 @@ impl<'a> Checker<'a> { let mut calls = Vec::new(); walk::own_calls(stmt, &mut calls); for cr in &calls { - match cr.callee { + match unwrap_parens(cr.callee) { Expr::Ident { name, .. } => { - if name == "delete" { - if let Some(first) = cr.args.first() { - let mut roots = Vec::new(); - collect_ref_roots(&first.value, &mut roots); - for root in roots { - if let Some(r) = - resolve(&root, current_aliases, current_shadows) - { - out.insert(r); + let is_inner_binding = local_structs.contains_key(name.as_str()) + || local_shadows.contains(name.as_str()); + if !is_inner_binding && self.lookup_binding(st, name).is_none() { + if name == "delete" { + if let Some(first) = cr.args.first() { + for p in a3_expr_paths(&first.value) { + if let Some(r) = + resolve(path_root(&p), &local_aliases, &local_shadows) + { + out.insert(r); + } } } + continue; } - continue; - } - if A3_PURE_FNS.contains(&name.as_str()) { - continue; - } - if let Some(decl) = self.fns.get(name) { - for (i, a) in cr.args.iter().enumerate() { - let mut roots = Vec::new(); - collect_ref_roots(&a.value, &mut roots); - for root in roots { - let Some(r) = - resolve(&root, current_aliases, current_shadows) - else { - continue; - }; + if A3_PURE_FNS.contains(&name.as_str()) { + continue; + } + if let Some(decl) = self.fns.get(name) { + for (i, a) in cr.args.iter().enumerate() { let mutates = match decl.params.get(i) { Some(tp) => self.fn_mutates_param(decl, &tp.name, 0), None => true, // fail closed }; - if mutates { - out.insert(r); + if !mutates { + continue; + } + for p in a3_expr_paths(&a.value) { + if let Some(r) = + resolve(path_root(&p), &local_aliases, &local_shadows) + { + out.insert(r); + } } } + continue; } - } else if let Some(existing) = st.closure_mutates.get(name.as_str()) { - // another local closure — union its mutation roots - out.extend(existing.iter().cloned()); - } else { - // unknown callee — fail closed on ref roots - for a in cr.args { - let mut roots = Vec::new(); - collect_ref_roots(&a.value, &mut roots); - for root in roots { - if let Some(r) = - resolve(&root, current_aliases, current_shadows) - { - out.insert(r); + } + if !is_inner_binding { + if let Some(existing) = st.closure_mutates.get(name.as_str()) { + // another local closure — union its mutation roots + out.extend(existing.iter().cloned()); + } + if let Some(param_set) = st.closure_param_mutates.get(name.as_str()) { + for (i, a) in cr.args.iter().enumerate() { + if param_set.contains(&i) { + for p in a3_expr_paths(&a.value) { + if let Some(r) = resolve( + path_root(&p), + &local_aliases, + &local_shadows, + ) { + out.insert(r); + } + } } } + continue; + } + } + // inner closure binding or unanalyzed callee — fail closed on args + for a in cr.args { + for p in a3_expr_paths(&a.value) { + if let Some(r) = + resolve(path_root(&p), &local_aliases, &local_shadows) + { + out.insert(r); + } } } } - Expr::Field { base, .. } => { - if let Expr::Ident { name: pkg, .. } = &**base { - if A3_PURE_PKGS.contains(&pkg.as_str()) { + Expr::Field { + base, + name: method_name, + .. + } => { + if let Expr::Ident { name: pkg, .. } = unwrap_parens(base) { + let is_shadowed_pkg = local_structs.contains_key(pkg.as_str()) + || local_shadows.contains(pkg.as_str()) + || self.lookup_binding(st, pkg).is_some(); + if !is_shadowed_pkg && A3_PURE_PKGS.contains(&pkg.as_str()) { continue; } } - for a in cr.args { - let mut roots = Vec::new(); - collect_ref_roots(&a.value, &mut roots); - for root in roots { + let matching = self.matching_methods( + Some(st), + base, + method_name, + &local_structs, + Some(&local_shadows), + ); + let recv_mutates = matching.is_empty() + || matching.iter().any(|d| { + d.receiver + .as_ref() + .is_some_and(|r| self.fn_mutates_param(d, &r.name, 0)) + }); + if recv_mutates { + for p in a3_expr_paths(base) { if let Some(r) = - resolve(&root, current_aliases, current_shadows) + resolve(path_root(&p), &local_aliases, &local_shadows) { out.insert(r); } } } + for (i, a) in cr.args.iter().enumerate() { + let mutates = matching.is_empty() + || matching.iter().any(|d| match d.params.get(i) { + Some(tp) => self.fn_mutates_param(d, &tp.name, 0), + None => true, + }); + if mutates { + for p in a3_expr_paths(&a.value) { + if let Some(r) = + resolve(path_root(&p), &local_aliases, &local_shadows) + { + out.insert(r); + } + } + } + } + } + Expr::FnLit { + params, + results, + body, + .. + } => { + let p_structs = self.params_struct_map(params, results); + for (i, a) in cr.args.iter().enumerate() { + let mutates = match params.get(i) { + Some(tp) => self.block_mutates_param(body, &tp.name, 0, &p_structs), + None => true, + }; + if mutates { + for p in a3_expr_paths(&a.value) { + if let Some(r) = + resolve(path_root(&p), &local_aliases, &local_shadows) + { + out.insert(r); + } + } + } + } } _ => { for a in cr.args { - let mut roots = Vec::new(); - collect_ref_roots(&a.value, &mut roots); - for root in roots { + for p in a3_expr_paths(&a.value) { if let Some(r) = - resolve(&root, current_aliases, current_shadows) + resolve(path_root(&p), &local_aliases, &local_shadows) { out.insert(r); } @@ -1051,22 +2036,6 @@ impl<'a> Checker<'a> { } out.extend(added); } - for _ in 0..8 { - let mut added = Vec::new(); - for r in &out { - if let Some(targets) = match_aliases.get(r) { - for t in targets { - if !bound.contains(t) && !out.contains(t) { - added.push(t.clone()); - } - } - } - } - if added.is_empty() { - break; - } - out.extend(added); - } out } @@ -1441,7 +2410,7 @@ impl<'a> Checker<'a> { // #111 form 4: record the base paths a closure value's body // mutates, so a call inside a live slot-loan window counts // as an A3 insert/delete event. - self.record_closure_mutations(st, name, value); + self.record_closure_mutations(st, name, value, true); // #141 (pr-140): the self-shadowing define keeps the SHADOWED // binding's non-owned origin — the RHS read the outer // storage, so the inner binding inherits O5.1's restriction @@ -4018,12 +4987,14 @@ impl<'a> Checker<'a> { } // #111: a closure rebind replaces the old body's // mutation set (or clears it for non-closure values) - st.closure_mutates.remove(name); - self.record_closure_mutations(st, name, v); + self.record_closure_mutations(st, name, v, true); } else { let root = t.root(); - if !root.is_empty() && self.binding_may_share(st, root) { - self.record_may_share(st, root, v); + if !root.is_empty() { + if self.binding_may_share(st, root) { + self.record_may_share(st, root, v); + } + self.record_closure_mutations(st, root, v, false); } } } @@ -4649,6 +5620,9 @@ impl<'a> Checker<'a> { let Expr::Ident { name, .. } = crate::model::unwrap_parens(cr.callee) else { return; }; + if declared.contains(name.as_str()) || self.lookup_binding(st, name).is_some() { + return; + } if name.as_str() != "delete" && name.as_str() != "copy" { return; } @@ -4729,26 +5703,24 @@ impl<'a> Checker<'a> { fn check_call_const_writes(&mut self, st: &FnState, cr: &CallRef, declared: &HashSet) { let callee = crate::model::unwrap_parens(cr.callee); if let Expr::Ident { name, .. } = callee { - if name == "delete" || name == "copy" || A3_PURE_FNS.contains(&name.as_str()) { + if !declared.contains(name.as_str()) + && self.lookup_binding(st, name).is_none() + && (name == "delete" || name == "copy" || A3_PURE_FNS.contains(&name.as_str())) + { return; } } let mut hits: Vec<(String, Span)> = Vec::new(); + let empty_structs = LexicalStructMap::new(); if let Expr::Field { base, name: method_name, .. } = callee { - let recv_base = match self.shape_of_expr(st, base) { - TypeShape::Struct(Some(s)) => Some(s), - TypeShape::Pointer(inner) => match *inner { - TypeShape::Struct(Some(s)) => Some(s), - _ => None, - }, - _ => None, - }; - if let Some(rname) = recv_base { + if let Some(rname) = + self.infer_base_struct_name(Some(st), base, &empty_structs, Some(declared)) + { if let Some(decl) = self .method_decls .get(&(rname, method_name.clone())) @@ -4788,17 +5760,13 @@ impl<'a> Checker<'a> { Expr::Ident { name, .. } => { if declared.contains(name) { true + } else if let Some(b) = self.lookup_binding(st, name) { + matches!(b.shape, TypeShape::Closure) } else if let Some(decl) = self.fns.get(name) { match decl.params.get(i) { Some(p) => self.fn_mutates_const_param(decl, &p.name, 0), None => true, } - } else if self - .lookup_binding(st, name) - .map(|b| matches!(b.shape, TypeShape::Closure)) - .unwrap_or(false) - { - true } else { false } @@ -4819,14 +5787,12 @@ impl<'a> Checker<'a> { } else if matches!(self.shape_of_expr(st, callee), TypeShape::Closure) { true } else { - let recv_base = match self.shape_of_expr(st, base) { - TypeShape::Struct(Some(s)) => Some(s), - TypeShape::Pointer(inner) => match *inner { - TypeShape::Struct(Some(s)) => Some(s), - _ => None, - }, - _ => None, - }; + let recv_base = self.infer_base_struct_name( + Some(st), + base, + &empty_structs, + Some(declared), + ); if let Some(decl) = recv_base.and_then(|rb| { self.method_decls.get(&(rb, method_name.clone())).copied() }) { @@ -4835,19 +5801,25 @@ impl<'a> Checker<'a> { None => true, } } else { - false + receiver_type_name(&self.shape_of_expr(st, base)).is_none() } } } - Expr::FnLit { params, body, .. } => match params.get(i) { + Expr::FnLit { + params, + results, + body, + .. + } => match params.get(i) { Some(p) => { let mut aliases = HashSet::new(); aliases.insert(p.name.clone()); - self.block_mutates_const_param(body, &mut aliases, 0) + let mut p_structs = self.params_struct_map(params, results); + self.block_mutates_const_param(body, &mut aliases, &mut p_structs, 0) } None => true, }, - _ => false, + _ => true, }; if mutates { hits.push((const_name, a.value.span())); @@ -4867,17 +5839,22 @@ impl<'a> Checker<'a> { }; let mut aliases = HashSet::new(); aliases.insert(param.to_string()); - self.block_mutates_const_param(body, &mut aliases, depth) + let mut local_structs = self.decl_struct_map(decl); + self.block_mutates_const_param(body, &mut aliases, &mut local_structs, depth) } fn block_mutates_const_param( &self, block: &Block, aliases: &mut HashSet, + local_structs: &mut LexicalStructMap, depth: usize, ) -> bool { for s in &block.stmts { - if self.stmt_mutates_const_param(s, aliases, depth) { + if matches!(s, Stmt::For { .. }) { + Self::invalidate_reassigned_structs_in_stmt(s, local_structs); + } + if self.stmt_mutates_const_param(s, aliases, local_structs, depth) { return true; } } @@ -4907,24 +5884,31 @@ impl<'a> Checker<'a> { &self, stmt: &Stmt, aliases: &mut HashSet, + local_structs: &mut LexicalStructMap, depth: usize, ) -> bool { + let mut fnlits = Vec::new(); + walk::own_fnlits(stmt, &mut fnlits); + for flit in fnlits { + if self.expr_fnlit_mutates_const_param(flit, aliases, local_structs, depth) { + return true; + } + } match stmt { Stmt::Assign { targets, values, .. } => { for t in targets { - if matches!(t.kind, PlaceKind::Index { .. } | PlaceKind::Field { .. }) - && aliases.contains(t.root()) + if matches!( + t.kind, + PlaceKind::Index { .. } + | PlaceKind::Field { .. } + | PlaceKind::AddrOf { .. } + ) && aliases.contains(t.root()) { return true; } } - for v in values { - if self.expr_fnlit_mutates_const_param(v, aliases, depth) { - return true; - } - } if targets.len() == values.len() { for (t, v) in targets.iter().zip(values.iter()) { if let PlaceKind::Ident(name) = &t.kind { @@ -4959,11 +5943,6 @@ impl<'a> Checker<'a> { } } Stmt::Local(l) => { - for v in &l.values { - if self.expr_fnlit_mutates_const_param(v, aliases, depth) { - return true; - } - } if l.values.is_empty() { for name in &l.names { aliases.remove(name); @@ -5004,13 +5983,13 @@ impl<'a> Checker<'a> { args, span: *span, }; - if self.call_mutates_const_param(&cr, aliases, depth) { + if self.call_mutates_const_param(&cr, aliases, local_structs, depth) { return true; } } Stmt::Return { values, .. } => { for v in values { - if self.expr_fnlit_mutates_const_param(v, aliases, depth) { + if self.expr_fnlit_mutates_const_param(v, aliases, local_structs, depth) { return true; } } @@ -5020,11 +5999,19 @@ impl<'a> Checker<'a> { let mut calls = Vec::new(); walk::own_calls(stmt, &mut calls); for cr in &calls { - if self.call_mutates_const_param(cr, aliases, depth) { + if self.call_mutates_const_param(cr, aliases, local_structs, depth) { return true; } } match stmt { + Stmt::Local(l) => { + self.update_local_structs_for_local(None, l, local_structs, None); + } + Stmt::Assign { + targets, values, .. + } => { + self.update_local_structs_for_assign(None, targets, values, local_structs, None); + } Stmt::If { init, then, @@ -5032,50 +6019,125 @@ impl<'a> Checker<'a> { .. } => { let mut branch_aliases = aliases.clone(); + let mut branch_structs = local_structs.clone(); if let Some(i) = init { - if self.stmt_mutates_const_param(i, &mut branch_aliases, depth) { + if self.stmt_mutates_const_param( + i, + &mut branch_aliases, + &mut branch_structs, + depth, + ) { return true; } } let mut then_aliases = branch_aliases.clone(); - if self.block_mutates_const_param(then, &mut then_aliases, depth) { + let mut then_structs = branch_structs.clone(); + if self.block_mutates_const_param(then, &mut then_aliases, &mut then_structs, depth) + { return true; } if let Some(o) = otherwise { let mut else_aliases = branch_aliases; - if self.stmt_mutates_const_param(o, &mut else_aliases, depth) { + let mut else_structs = branch_structs; + if self.stmt_mutates_const_param(o, &mut else_aliases, &mut else_structs, depth) + { return true; } } + Self::invalidate_reassigned_structs_in_stmt(stmt, local_structs); + } + Stmt::For { kind, body, .. } => { + let mut body_aliases = aliases.clone(); + let mut body_structs = local_structs.clone(); + Self::invalidate_reassigned_structs_in_stmt(stmt, &mut body_structs); + Self::invalidate_for_header_structs(kind, &mut body_structs, None); + match kind { + hy_syntax::ForKind::Iterate { vars, expr } => { + let iter_is_const = Self::expr_reaches_const_aliases(expr, aliases); + for v in vars { + if iter_is_const { + body_aliases.insert(v.clone()); + } else { + body_aliases.remove(v.as_str()); + } + } + } + hy_syntax::ForKind::Range { var, .. } => { + body_aliases.remove(var.as_str()); + } + _ => {} + } + if self.block_mutates_const_param(body, &mut body_aliases, &mut body_structs, depth) + { + return true; + } + Self::invalidate_reassigned_structs_in_stmt(stmt, local_structs); } - Stmt::For { body, .. } - | Stmt::Managed { body, .. } - | Stmt::Block { block: body, .. } => { + Stmt::Managed { body, .. } | Stmt::Block { block: body, .. } => { let mut body_aliases = aliases.clone(); - if self.block_mutates_const_param(body, &mut body_aliases, depth) { + let mut body_structs = local_structs.clone(); + if self.block_mutates_const_param(body, &mut body_aliases, &mut body_structs, depth) + { return true; } + Self::invalidate_reassigned_structs_in_stmt(stmt, local_structs); } Stmt::Match { arms, .. } => { for arm in arms { let mut arm_aliases = aliases.clone(); - if self.block_mutates_const_param(&arm.body, &mut arm_aliases, depth) { + let mut arm_structs = local_structs.clone(); + if let hy_syntax::MatchPattern::Path { + binding: Some(bname), + .. + } = &arm.pattern + { + arm_structs.insert(bname.clone(), None); + } + if self.block_mutates_const_param( + &arm.body, + &mut arm_aliases, + &mut arm_structs, + depth, + ) { return true; } } + Self::invalidate_reassigned_structs_in_stmt(stmt, local_structs); } Stmt::Select { arms, .. } => { for arm in arms { - if let hy_syntax::SelectArmKind::Send { value, .. } = &arm.kind { - if Self::expr_reaches_const_aliases(value, aliases) { - return true; + let mut arm_aliases = aliases.clone(); + let mut arm_structs = local_structs.clone(); + match &arm.kind { + hy_syntax::SelectArmKind::Send { value, .. } => { + if Self::expr_reaches_const_aliases(value, aliases) { + return true; + } + } + hy_syntax::SelectArmKind::Recv { names, chan } => { + let chan_reaches_const = + Self::expr_reaches_const_aliases(chan, aliases); + for (i, n) in names.iter().enumerate() { + arm_structs.insert(n.clone(), None); + if i == 0 && chan_reaches_const { + arm_aliases.insert(n.clone()); + } else { + arm_aliases.remove(n.as_str()); + } + } } + hy_syntax::SelectArmKind::Default => {} } - let mut arm_aliases = aliases.clone(); - if self.block_mutates_const_param(&arm.body, &mut arm_aliases, depth) { + if self.block_mutates_const_param( + &arm.body, + &mut arm_aliases, + &mut arm_structs, + depth, + ) { return true; } } + Self::invalidate_reassigned_structs_in_stmt(stmt, local_structs); } _ => {} } @@ -5086,9 +6148,10 @@ impl<'a> Checker<'a> { &self, e: &Expr, aliases: &HashSet, + outer_structs: &LexicalStructMap, depth: usize, ) -> bool { - match e { + match unwrap_parens(e) { Expr::FnLit { params, results, @@ -5096,18 +6159,26 @@ impl<'a> Checker<'a> { .. } => { let mut inner_aliases = aliases.clone(); + let mut inner_structs = outer_structs.clone(); for p in params { inner_aliases.remove(&p.name); + inner_structs.insert(p.name.clone(), self.type_expr_struct_name(&p.ty)); } for r in results { if let Some(name) = &r.name { inner_aliases.remove(name); + inner_structs.insert(name.clone(), self.type_expr_struct_name(&r.ty)); } } if inner_aliases.is_empty() { false } else { - self.block_mutates_const_param(body, &mut inner_aliases, depth) + self.block_mutates_const_param( + body, + &mut inner_aliases, + &mut inner_structs, + depth, + ) } } Expr::Paren { inner, .. } @@ -5115,13 +6186,13 @@ impl<'a> Checker<'a> { | Expr::Try { operand: inner, .. } | Expr::Unary { operand: inner, .. } | Expr::AddrOf { operand: inner, .. } => { - self.expr_fnlit_mutates_const_param(inner, aliases, depth) + self.expr_fnlit_mutates_const_param(inner, aliases, outer_structs, depth) } Expr::Call { callee, args, .. } => { - self.expr_fnlit_mutates_const_param(callee, aliases, depth) - || args - .iter() - .any(|a| self.expr_fnlit_mutates_const_param(&a.value, aliases, depth)) + self.expr_fnlit_mutates_const_param(callee, aliases, outer_structs, depth) + || args.iter().any(|a| { + self.expr_fnlit_mutates_const_param(&a.value, aliases, outer_structs, depth) + }) } Expr::Index { base, index, .. } | Expr::Binary { @@ -5129,18 +6200,18 @@ impl<'a> Checker<'a> { rhs: index, .. } => { - self.expr_fnlit_mutates_const_param(base, aliases, depth) - || self.expr_fnlit_mutates_const_param(index, aliases, depth) + self.expr_fnlit_mutates_const_param(base, aliases, outer_structs, depth) + || self.expr_fnlit_mutates_const_param(index, aliases, outer_structs, depth) } - Expr::StructLit { fields, .. } => fields - .iter() - .any(|f| self.expr_fnlit_mutates_const_param(&f.value, aliases, depth)), + Expr::StructLit { fields, .. } => fields.iter().any(|f| { + self.expr_fnlit_mutates_const_param(&f.value, aliases, outer_structs, depth) + }), Expr::SliceLit { items, .. } => items .iter() - .any(|it| self.expr_fnlit_mutates_const_param(it, aliases, depth)), + .any(|it| self.expr_fnlit_mutates_const_param(it, aliases, outer_structs, depth)), Expr::MapLit { entries, .. } => entries.iter().any(|(k, v)| { - self.expr_fnlit_mutates_const_param(k, aliases, depth) - || self.expr_fnlit_mutates_const_param(v, aliases, depth) + self.expr_fnlit_mutates_const_param(k, aliases, outer_structs, depth) + || self.expr_fnlit_mutates_const_param(v, aliases, outer_structs, depth) }), _ => false, } @@ -5150,33 +6221,36 @@ impl<'a> Checker<'a> { &self, cr: &CallRef, aliases: &HashSet, + local_structs: &LexicalStructMap, depth: usize, ) -> bool { let arg_reaches_alias = |a: &hy_syntax::CallArg| Self::expr_reaches_const_aliases(&a.value, aliases); match crate::model::unwrap_parens(cr.callee) { Expr::Ident { name, .. } => { - if name == "delete" || name == "copy" { - return cr.args.first().map(&arg_reaches_alias).unwrap_or(false); - } - if A3_PURE_FNS.contains(&name.as_str()) { - return false; - } - if let Some(decl) = self.fns.get(name) { - for (i, a) in cr.args.iter().enumerate() { - if !arg_reaches_alias(a) { - continue; - } - match decl.params.get(i) { - Some(p) => { - if self.fn_mutates_const_param(decl, &p.name, depth + 1) { - return true; + if !local_structs.contains_key(name.as_str()) { + if name == "delete" || name == "copy" { + return cr.args.first().map(arg_reaches_alias).unwrap_or(false); + } + if A3_PURE_FNS.contains(&name.as_str()) { + return false; + } + if let Some(decl) = self.fns.get(name) { + for (i, a) in cr.args.iter().enumerate() { + if !arg_reaches_alias(a) { + continue; + } + match decl.params.get(i) { + Some(p) => { + if self.fn_mutates_const_param(decl, &p.name, depth + 1) { + return true; + } } + None => return true, } - None => return true, } + return false; } - return false; } cr.args.iter().any(&arg_reaches_alias) } @@ -5185,17 +6259,14 @@ impl<'a> Checker<'a> { name: method_name, .. } => { - if let Expr::Ident { name: pkg, .. } = &**base { - if A3_PURE_PKGS.contains(&pkg.as_str()) { + if let Expr::Ident { name: pkg, .. } = unwrap_parens(base) { + if !local_structs.contains_key(pkg.as_str()) + && A3_PURE_PKGS.contains(&pkg.as_str()) + { return false; } } - let matching: Vec<&FnDecl> = self - .method_decls - .iter() - .filter(|((_, m), _)| m == method_name) - .map(|(_, d)| *d) - .collect(); + let matching = self.matching_methods(None, base, method_name, local_structs, None); if Self::expr_reaches_const_aliases(base, aliases) && (matching.is_empty() || matching.iter().any(|d| { @@ -5222,6 +6293,44 @@ impl<'a> Checker<'a> { } cr.args.iter().any(&arg_reaches_alias) } + Expr::FnLit { + params, + results, + body, + .. + } => { + let mut inner_structs = local_structs.clone(); + for p in params { + inner_structs.insert(p.name.clone(), self.type_expr_struct_name(&p.ty)); + } + for r in results { + if let Some(name) = &r.name { + inner_structs.insert(name.clone(), self.type_expr_struct_name(&r.ty)); + } + } + for (i, a) in cr.args.iter().enumerate() { + if !arg_reaches_alias(a) { + continue; + } + match params.get(i) { + Some(p) => { + let mut inner_aliases = HashSet::new(); + inner_aliases.insert(p.name.clone()); + let mut call_structs = inner_structs.clone(); + if self.block_mutates_const_param( + body, + &mut inner_aliases, + &mut call_structs, + depth + 1, + ) { + return true; + } + } + None => return true, + } + } + false + } _ => cr.args.iter().any(&arg_reaches_alias), } } diff --git a/compiler/crates/hy-check/src/model.rs b/compiler/crates/hy-check/src/model.rs index 9484ffa5..68b48d55 100644 --- a/compiler/crates/hy-check/src/model.rs +++ b/compiler/crates/hy-check/src/model.rs @@ -243,6 +243,17 @@ pub(crate) struct FnState { /// rebind time; consumed at the CALL site so an insert inside the body /// counts as an A3 event when the call sits in a live slot-loan window. pub closure_mutates: HashMap>, + /// Local closure binding -> parameter indices whose storage the closure + /// body may mutate (A3 map insert/delete). Presence of a key indicates a + /// locally analyzed closure literal (an empty set means parameters are + /// read-only); absence on a closure-typed binding fails closed. + pub closure_param_mutates: HashMap>, + /// Saved closure-mutation summaries for outer scopes when entering block + /// scopes so inner shadowing defines (`f := wrap(f)`) do not leak or + /// reuse outer summaries, while conditional rebinds of outer bindings + /// merge conservatively at block exit. + pub closure_mutate_scopes: Vec>>, + pub closure_param_scopes: Vec>>, /// Declared result shapes of the function being checked (rule N1's /// return-site nil checks). Empty for closure bodies (fn literals /// carry no result types on the AST surface yet). @@ -263,6 +274,10 @@ impl FnState { self.alias_scopes.push(self.aliases.clone()); self.share_scopes.push(self.may_share.clone()); self.shadow_link_scopes.push(self.shadow_links.clone()); + self.closure_mutate_scopes + .push(self.closure_mutates.clone()); + self.closure_param_scopes + .push(self.closure_param_mutates.clone()); } /// Exit the innermost block scope (walk.rs's Pop events). @@ -370,10 +385,55 @@ impl FnState { } } } + let inner_closure_mutates = std::mem::take(&mut self.closure_mutates); + let inner_closure_params = std::mem::take(&mut self.closure_param_mutates); + if let Some(mut outer_cm) = self.closure_mutate_scopes.pop() { + for (k, roots) in inner_closure_mutates { + if popped_scope.contains_key(&k) { + continue; + } + let entry = outer_cm.entry(k).or_default(); + for r in roots { + let (root_ident, suffix) = match r.find('.') { + Some(i) => (&r[..i], &r[i..]), + None => (r.as_str(), ""), + }; + let resolved = crate::freeze::resolve_alias(self, root_ident); + if !popped_scope.contains_key(&resolved) { + entry.insert(format!("{resolved}{suffix}")); + } + for peer in self.share_group(root_ident) { + if !popped_scope.contains_key(&peer) { + entry.insert(format!("{peer}{suffix}")); + } + let peer_root = crate::freeze::resolve_alias(self, &peer); + if !popped_scope.contains_key(&peer_root) { + entry.insert(format!("{peer_root}{suffix}")); + } + } + if !popped_scope.contains_key(root_ident) { + entry.insert(r); + } + } + } + self.closure_mutates = outer_cm; + } self.scopes.pop(); if let Some(outer_shadows) = self.shadow_link_scopes.pop() { self.shadow_links = outer_shadows; } + if let Some(outer_cp) = self.closure_param_scopes.pop() { + let mut merged_cp = HashMap::new(); + for (k, mut outer_set) in outer_cp { + if popped_scope.contains_key(&k) { + merged_cp.insert(k, outer_set); + } else if let Some(inner_set) = inner_closure_params.get(&k) { + outer_set.extend(inner_set.iter().copied()); + merged_cp.insert(k, outer_set); + } + } + self.closure_param_mutates = merged_cp; + } if let Some(outer_aliases) = self.alias_scopes.pop() { self.aliases = outer_aliases; if let Some(outer_share) = self.share_scopes.pop() { @@ -805,17 +865,6 @@ pub(crate) fn collect_ref_roots(e: &Expr, out: &mut Vec) { operand, .. } => collect_ref_roots(operand, out), - Expr::FnLit { - params, - results, - body, - .. - } => { - let mut bound: std::collections::HashSet = - params.iter().map(|p| p.name.clone()).collect(); - bound.extend(results.iter().filter_map(|r| r.name.clone())); - crate::loans::collect_capture_idents_block(body, &mut bound, out); - } _ => {} } } diff --git a/compiler/crates/hy-check/src/walk.rs b/compiler/crates/hy-check/src/walk.rs index 4deccfa1..ba33e3f8 100644 --- a/compiler/crates/hy-check/src/walk.rs +++ b/compiler/crates/hy-check/src/walk.rs @@ -513,9 +513,12 @@ pub(crate) struct CallRef<'a> { pub span: Span, } -/// Collect the calls of THIS statement only. For-statement ITERATE -/// expressions are excluded here: their borrow calls get loop-extent loans -/// and are processed by the For handler (loans.rs / lib.rs dispatch). +/// Collect the calls of THIS statement only (including a `for ... in EXPR` +/// header). Callers that handle `ForKind::Iterate` headers specially before +/// calling `own_calls` (`lib.rs::check_fn`, +/// `closures.rs::check_stmt_boundaries`, and +/// `loans.rs::check_closure_const_writes`) match `Stmt::For` first so the +/// header call is not processed twice. pub(crate) fn own_calls<'a>(stmt: &'a Stmt, out: &mut Vec>) { match stmt { Stmt::Local(l) => { @@ -527,9 +530,7 @@ pub(crate) fn own_calls<'a>(stmt: &'a Stmt, out: &mut Vec>) { targets, values, .. } => { for t in targets { - if let PlaceKind::Index { index, .. } = &t.kind { - expr_calls(index, out); - } + place_calls(t, out); } for v in values { expr_calls(v, out); @@ -547,7 +548,7 @@ pub(crate) fn own_calls<'a>(stmt: &'a Stmt, out: &mut Vec>) { expr_calls(lo, out); expr_calls(hi, out); } - ForKind::Iterate { .. } => {} // handled with loop-extent loans + ForKind::Iterate { expr, .. } => expr_calls(expr, out), ForKind::Infinite => {} }, Stmt::Return { values, .. } => { @@ -588,6 +589,17 @@ pub(crate) fn own_calls<'a>(stmt: &'a Stmt, out: &mut Vec>) { } } +pub(crate) fn place_calls<'e>(place: &'e Place, out: &mut Vec>) { + match &place.kind { + PlaceKind::Ident(_) => {} + PlaceKind::Index { base, index } => { + place_calls(base, out); + expr_calls(index, out); + } + PlaceKind::Field { base, .. } | PlaceKind::AddrOf { base } => place_calls(base, out), + } +} + /// Collect the `FnLit` expressions of THIS statement (#123). Closure BODIES /// are not descended here — each closure is handled as a unit by the caller /// (the A5 perimeter keeps flatten/uses/calls outside them). @@ -598,7 +610,12 @@ pub(crate) fn own_fnlits<'a>(stmt: &'a Stmt, out: &mut Vec<&'a Expr>) { expr_fnlits(v, out); } } - Stmt::Assign { values, .. } => { + Stmt::Assign { + targets, values, .. + } => { + for t in targets { + place_fnlits(t, out); + } for v in values { expr_fnlits(v, out); } @@ -655,6 +672,17 @@ pub(crate) fn own_fnlits<'a>(stmt: &'a Stmt, out: &mut Vec<&'a Expr>) { } } +pub(crate) fn place_fnlits<'e>(place: &'e Place, out: &mut Vec<&'e Expr>) { + match &place.kind { + PlaceKind::Ident(_) => {} + PlaceKind::Index { base, index } => { + place_fnlits(base, out); + expr_fnlits(index, out); + } + PlaceKind::Field { base, .. } | PlaceKind::AddrOf { base } => place_fnlits(base, out), + } +} + /// Collect every `FnLit` node reachable through `expr`'s own expression /// structure — the node itself is pushed, its body is NOT descended (the /// caller recurses into bodies through `own_fnlits` + flatten). diff --git a/compiler/crates/hy-check/tests/place_call_and_a3_callee_soundness.rs b/compiler/crates/hy-check/tests/place_call_and_a3_callee_soundness.rs new file mode 100644 index 00000000..04579ebb --- /dev/null +++ b/compiler/crates/hy-check/tests/place_call_and_a3_callee_soundness.rs @@ -0,0 +1,882 @@ +//! Regression tests for nested assignment-target `Place` call/closure +//! traversal (`walk::own_calls`, `walk::own_fnlits`, +//! `builtins::check_place_builtin_args`), parenthesized callee resolution, +//! `ForKind::Iterate` interprocedural mutation scanning, and A3 map-slot +//! loan invalidation (`HYB-E3102`) across Hybrid methods, closure parameters, +//! and inline closure invocations. + +use hy_check::{check_file, Verdict}; +use hy_diag::Severity; +use hy_syntax::parse_file; + +fn check(src: &str) -> hy_check::CheckResult { + let ast = parse_file("inline.hy", src) + .unwrap_or_else(|diags| panic!("must parse (parser contract): {:?}", diags)); + check_file(&ast) +} + +fn codes(r: &hy_check::CheckResult) -> Vec { + r.diagnostics + .iter() + .filter(|d| d.severity == Severity::Error) + .map(|d| d.code.clone()) + .collect() +} + +fn assert_code(r: &hy_check::CheckResult, want: &str, what: &str) { + let cs = codes(r); + assert!( + cs.iter().any(|c| c == want), + "expected {} ({}), got {:?}", + want, + what, + cs + ); + assert_eq!(r.verdict, Verdict::Reject); +} + +#[test] +fn nested_place_copy_links_may_share_and_rejects_post_move_write_e3003() { + let r = check( + "struct box { n int }\n\ + fn worker(d []map[string]int, done chan bool) {\n\ + d[0][\"k\"] = 1\n\ + done <- true\n\ + }\n\ + fn main() {\n\ + var src []map[string]int = []map[string]int{map[string]int{\"k\": 0}}\n\ + var dst []map[string]int = []map[string]int{map[string]int{\"k\": 0}}\n\ + boxes := []box{box{n: 0}, box{n: 0}}\n\ + boxes[copy(dst, src)].n = 42\n\ + done := make(chan bool, 1)\n\ + task worker(move dst, done)\n\ + src[0][\"k\"] = 2\n\ + <-done\n\ + }\n", + ); + assert_code( + &r, + "HYB-E3003", + "copy(dst, src) inside nested assignment place", + ); +} + +#[test] +fn nested_place_calls_and_closures_enforce_const_deep_immutability_e3006() { + let r_call = check( + "struct box { n int }\n\ + const shared = map[string]int{\"a\": 1}\n\ + fn corrupt(m map[string]int) int {\n\ + m[\"a\"] = 99\n\ + return 0\n\ + }\n\ + fn main() {\n\ + boxes := []box{box{n: 0}}\n\ + boxes[corrupt(shared)].n = 42\n\ + }\n", + ); + assert_code( + &r_call, + "HYB-E3006", + "interprocedural const write in nested place", + ); + + let r_closure = check( + "const shared = map[string]int{\"a\": 1}\n\ + fn main() {\n\ + arr := []int{0}\n\ + arr[fn() int {\n\ + shared[\"a\"] = 99\n\ + return 0\n\ + }()] = 10\n\ + }\n", + ); + assert_code( + &r_closure, + "HYB-E3006", + "closure const write in assignment target place", + ); +} + +#[test] +fn nested_place_call_and_builtin_checks_enforce_a3_and_e2005() { + let r_a3 = check( + "struct svc { n int }\n\ + struct box { n int }\n\ + fn del_and_idx(m map[string]svc) int {\n\ + delete(m, \"k\")\n\ + return 0\n\ + }\n\ + fn main() {\n\ + m := map[string]svc{\"k\": svc{n: 1}}\n\ + boxes := []box{box{n: 0}}\n\ + slot := &m[\"k\"]\n\ + boxes[del_and_idx(m)].n = 99\n\ + slot.n = 2\n\ + }\n", + ); + assert_code( + &r_a3, + "HYB-E3102", + "A3 map mutation inside nested assignment place", + ); + + let r_builtin = check( + "struct box { n int }\n\ + fn main() {\n\ + boxes := []box{box{n: 0}}\n\ + boxes[len(1, 2)].n = 3\n\ + }\n", + ); + assert_code( + &r_builtin, + "HYB-E2005", + "builtin arity error inside nested assignment place", + ); +} + +#[test] +fn parenthesized_callees_preserve_copy_and_delete_soundness() { + let r_copy = check( + "fn worker(d []map[string]int, done chan bool) {\n\ + d[0][\"k\"] = 1\n\ + done <- true\n\ + }\n\ + fn main() {\n\ + var src []map[string]int = []map[string]int{map[string]int{\"k\": 0}}\n\ + var dst []map[string]int = []map[string]int{map[string]int{\"k\": 0}}\n\ + if true {\n\ + (copy)(dst, src)\n\ + }\n\ + done := make(chan bool, 1)\n\ + task worker(move dst, done)\n\ + src[0][\"k\"] = 2\n\ + <-done\n\ + }\n", + ); + assert_code(&r_copy, "HYB-E3003", "parenthesized (copy)(dst, src)"); + + let r_del = check( + "struct svc { n int }\n\ + fn main() {\n\ + m := map[string]svc{\"k\": svc{n: 1}}\n\ + slot := &m[\"k\"]\n\ + if true {\n\ + (delete)(m, \"k\")\n\ + }\n\ + slot.n = 2\n\ + }\n", + ); + assert_code( + &r_del, + "HYB-E3102", + "parenthesized (delete)(m, \"k\") in A3 window", + ); +} + +#[test] +fn a3_map_slot_loan_rejects_inline_closures_closure_params_and_methods_e3102() { + let r_iife = check( + "struct svc { n int }\n\ + fn main() {\n\ + m := map[string]svc{\"k\": svc{n: 1}}\n\ + slot := &m[\"k\"]\n\ + fn() {\n\ + delete(m, \"k\")\n\ + }()\n\ + slot.n = 2\n\ + }\n", + ); + assert_code( + &r_iife, + "HYB-E3102", + "inline closure mutating captured map in A3 window", + ); + + let r_closure_param = check( + "struct svc { n int }\n\ + fn main() {\n\ + m := map[string]svc{\"k\": svc{n: 1}}\n\ + slot := &m[\"k\"]\n\ + f := fn(x map[string]svc) {\n\ + delete(x, \"k\")\n\ + }\n\ + f(m)\n\ + slot.n = 2\n\ + }\n", + ); + assert_code( + &r_closure_param, + "HYB-E3102", + "closure mutating map parameter in A3 window", + ); + + let r_method_arg = check( + "struct svc { n int }\n\ + struct mutator { tag int }\n\ + fn (u mutator) drop_k(m map[string]svc) {\n\ + _ = u\n\ + delete(m, \"k\")\n\ + }\n\ + fn main() {\n\ + m := map[string]svc{\"k\": svc{n: 1}}\n\ + u := mutator{tag: 1}\n\ + slot := &m[\"k\"]\n\ + u.drop_k(m)\n\ + slot.n = 2\n\ + }\n", + ); + assert_code( + &r_method_arg, + "HYB-E3102", + "method mutating map argument in A3 window", + ); + + let r_method_recv = check( + "struct svc { n int }\n\ + struct engine { services map[string]svc }\n\ + fn (e engine) reset() {\n\ + delete(e.services, \"k\")\n\ + }\n\ + fn main() {\n\ + e := engine{services: map[string]svc{\"k\": svc{n: 1}}}\n\ + slot := &e.services[\"k\"]\n\ + e.reset()\n\ + slot.n = 2\n\ + }\n", + ); + assert_code( + &r_method_recv, + "HYB-E3102", + "method mutating receiver map in A3 window", + ); +} + +#[test] +fn for_iterate_header_and_expr_closures_propagate_interproc_mutations() { + let r_const_iter = check( + "const shared = map[string]int{\"a\": 1}\n\ + fn corrupt_and_ret(m map[string]int) []int {\n\ + m[\"a\"] = 99\n\ + return []int{1}\n\ + }\n\ + fn helper(m map[string]int) {\n\ + for v in corrupt_and_ret(m) {\n\ + _ = v\n\ + }\n\ + }\n\ + fn main() {\n\ + helper(shared)\n\ + }\n", + ); + assert_code( + &r_const_iter, + "HYB-E3006", + "ForKind::Iterate header call mutating const param", + ); + + let r_const_iife = check( + "const shared = map[string]int{\"a\": 1}\n\ + fn helper(m map[string]int) {\n\ + fn() {\n\ + m[\"a\"] = 99\n\ + }()\n\ + }\n\ + fn main() {\n\ + helper(shared)\n\ + }\n", + ); + assert_code( + &r_const_iife, + "HYB-E3006", + "Stmt::Expr closure mutating const param", + ); +} + +#[test] +fn positive_controls_read_only_closures_methods_and_pure_pkg_parens_accept() { + let r = check( + "import \"fmt\"\n\ + struct svc { n int }\n\ + struct engine { services map[string]svc }\n\ + fn (e engine) count() int {\n\ + return len(e.services)\n\ + }\n\ + fn (e engine) count_map(m map[string]svc) int {\n\ + _ = e\n\ + return len(m)\n\ + }\n\ + fn log_map(m map[string]svc) {\n\ + (fmt).println(len(m))\n\ + }\n\ + fn main() {\n\ + e := engine{services: map[string]svc{\"k\": svc{n: 1}}}\n\ + slot := &e.services[\"k\"]\n\ + ro := fn(x map[string]svc) int {\n\ + return len(x)\n\ + }\n\ + log_map(e.services)\n\ + n := ro(e.services) + e.count() + e.count_map(e.services)\n\ + slot.n = n\n\ + }\n", + ); + assert!( + codes(&r).is_empty(), + "read-only closures, methods, and parenthesized pure package calls must accept: {:?}", + r.diagnostics + ); + assert_eq!(r.verdict, Verdict::Accept); +} + +#[test] +fn wrapped_closures_and_computed_callees_fail_closed_in_a3_and_const_checks() { + let r_wrapped = check( + "struct svc { n int }\n\ + fn wrap(cb fn(map[string]svc) int) fn(map[string]svc) int {\n\ + _ = cb\n\ + return fn(x map[string]svc) int {\n\ + delete(x, \"k\")\n\ + return 0\n\ + }\n\ + }\n\ + fn main() {\n\ + m := map[string]svc{\"k\": svc{n: 1}}\n\ + f := wrap(fn(x map[string]svc) int {\n\ + return len(x)\n\ + })\n\ + slot := &m[\"k\"]\n\ + _ = f(m)\n\ + slot.n = 2\n\ + }\n", + ); + assert_code( + &r_wrapped, + "HYB-E3102", + "wrapped closure must not inherit inner fnlit read-only param set", + ); + + let r_slice_callee = check( + "struct svc { n int }\n\ + fn main() {\n\ + m := map[string]svc{\"k\": svc{n: 1}}\n\ + fs := []fn(map[string]svc){fn(x map[string]svc) {\n\ + delete(x, \"k\")\n\ + }}\n\ + slot := &m[\"k\"]\n\ + fs[0](m)\n\ + slot.n = 2\n\ + }\n", + ); + assert_code( + &r_slice_callee, + "HYB-E3102", + "slice-indexed closure call in A3 window", + ); + + let r_call_callee = check( + "struct svc { n int }\n\ + fn getf() fn(map[string]svc) {\n\ + return fn(x map[string]svc) {\n\ + delete(x, \"k\")\n\ + }\n\ + }\n\ + fn main() {\n\ + m := map[string]svc{\"k\": svc{n: 1}}\n\ + slot := &m[\"k\"]\n\ + getf()(m)\n\ + slot.n = 2\n\ + }\n", + ); + assert_code( + &r_call_callee, + "HYB-E3102", + "call-returned closure invocation in A3 window", + ); + + let r_const_computed = check( + "const shared = map[string]int{\"a\": 1}\n\ + fn get_mutator() fn(map[string]int) {\n\ + return fn(x map[string]int) {\n\ + x[\"a\"] = 99\n\ + }\n\ + }\n\ + fn main() {\n\ + get_mutator()(shared)\n\ + }\n", + ); + assert_code( + &r_const_computed, + "HYB-E3006", + "computed callee with const map argument", + ); +} + +#[test] +fn pointer_receiver_methods_and_closure_rebinds_update_a3_state() { + let r_ptr_recv = check( + "struct svc { n int }\n\ + struct engine { services map[string]svc }\n\ + fn (e *engine) reset() {\n\ + delete(e.services, \"k\")\n\ + }\n\ + fn main() {\n\ + e := engine{services: map[string]svc{\"k\": svc{n: 1}}}\n\ + p := &e\n\ + slot := &e.services[\"k\"]\n\ + p.reset()\n\ + slot.n = 2\n\ + }\n", + ); + assert_code( + &r_ptr_recv, + "HYB-E3102", + "pointer-receiver method mutating map in A3 window", + ); + + let r_rebind_to_wrapped = check( + "struct svc { n int }\n\ + fn wrap(cb fn(map[string]svc) int) fn(map[string]svc) int {\n\ + return cb\n\ + }\n\ + fn main() {\n\ + m := map[string]svc{\"k\": svc{n: 1}}\n\ + f := fn(x map[string]svc) int {\n\ + return len(x)\n\ + }\n\ + f = wrap(f)\n\ + slot := &m[\"k\"]\n\ + _ = f(m)\n\ + slot.n = 2\n\ + }\n", + ); + assert_code( + &r_rebind_to_wrapped, + "HYB-E3102", + "rebinding read-only closure to unknown call result clears known param set", + ); +} + +#[test] +fn method_name_disambiguation_and_for_iterate_single_diagnostic() { + let r_disambig = check( + "struct svc { n int }\n\ + struct writer { tag int }\n\ + struct reader { tag int }\n\ + fn (w writer) apply(m map[string]svc) int {\n\ + _ = w\n\ + delete(m, \"k\")\n\ + return 0\n\ + }\n\ + fn (r reader) apply(m map[string]svc) int {\n\ + _ = r\n\ + return len(m)\n\ + }\n\ + fn helper(r reader, m map[string]svc) int {\n\ + return r.apply(m)\n\ + }\n\ + fn main() {\n\ + m := map[string]svc{\"k\": svc{n: 1}}\n\ + r := reader{tag: 1}\n\ + slot := &m[\"k\"]\n\ + n := helper(r, m)\n\ + slot.n = n\n\ + }\n", + ); + assert!( + codes(&r_disambig).is_empty(), + "helper calling read-only reader.apply must not collide with writer.apply: {:?}", + r_disambig.diagnostics + ); + assert_eq!(r_disambig.verdict, Verdict::Accept); + + let r_iter_single = check( + "const shared = map[string]int{\"a\": 1}\n\ + fn corrupt_items(m map[string]int) []int {\n\ + m[\"a\"] = 99\n\ + return []int{1}\n\ + }\n\ + fn main() {\n\ + for v in corrupt_items(shared) {\n\ + _ = v\n\ + }\n\ + }\n", + ); + assert_eq!( + codes(&r_iter_single), + vec!["HYB-E3006".to_string()], + "for-iterate header call processed by loans::process_call must emit a single diagnostic" + ); + + let r_call_init_writer = check( + "struct svc { n int }\n\ + struct writer { tag int }\n\ + struct reader { tag int }\n\ + fn make_writer() writer {\n\ + return writer{tag: 1}\n\ + }\n\ + fn (w writer) apply(m map[string]svc) int {\n\ + _ = w\n\ + delete(m, \"k\")\n\ + return 0\n\ + }\n\ + fn (r reader) apply(m map[string]svc) int {\n\ + _ = r\n\ + return len(m)\n\ + }\n\ + fn helper(m map[string]svc) int {\n\ + r := make_writer()\n\ + return r.apply(m)\n\ + }\n\ + fn main() {\n\ + m := map[string]svc{\"k\": svc{n: 1}}\n\ + slot := &m[\"k\"]\n\ + n := helper(m)\n\ + slot.n = n\n\ + }\n", + ); + assert_code( + &r_call_init_writer, + "HYB-E3102", + "call-initialized writer local named r must not inherit reader receiver type from another fn", + ); + + let r_same_var_name_diff_fns = check( + "struct svc { n int }\n\ + struct writer { tag int }\n\ + struct reader { tag int }\n\ + fn (r writer) apply(m map[string]svc) int {\n\ + _ = r\n\ + delete(m, \"k\")\n\ + return 0\n\ + }\n\ + fn (r reader) apply(m map[string]svc) int {\n\ + _ = r\n\ + return len(m)\n\ + }\n\ + fn read_helper(r reader, m map[string]svc) int {\n\ + return r.apply(m)\n\ + }\n\ + fn main() {\n\ + m := map[string]svc{\"k\": svc{n: 1}}\n\ + rd := reader{tag: 1}\n\ + slot := &m[\"k\"]\n\ + n := read_helper(rd, m)\n\ + slot.n = n\n\ + }\n", + ); + assert!( + codes(&r_same_var_name_diff_fns).is_empty(), + "lexical receiver type must disambiguate even when both methods name their receiver r: {:?}", + r_same_var_name_diff_fns.diagnostics + ); + assert_eq!(r_same_var_name_diff_fns.verdict, Verdict::Accept); +} + +#[test] +fn closure_shadowing_and_branch_rebinds_preserve_lexical_soundness() { + let r_shadow_wrapped = check( + "struct svc { n int }\n\ + fn wrap(cb fn(map[string]svc) int) fn(map[string]svc) int {\n\ + _ = cb\n\ + return fn(x map[string]svc) int {\n\ + delete(x, \"k\")\n\ + return 0\n\ + }\n\ + }\n\ + fn main() {\n\ + m := map[string]svc{\"k\": svc{n: 1}}\n\ + f := fn(x map[string]svc) int {\n\ + return len(x)\n\ + }\n\ + if true {\n\ + f := wrap(f)\n\ + slot := &m[\"k\"]\n\ + _ = f(m)\n\ + slot.n = 2\n\ + }\n\ + }\n", + ); + assert_code( + &r_shadow_wrapped, + "HYB-E3102", + "inner shadowed f := wrap(f) must not reuse outer read-only closure summary", + ); + + let r_branch_rebind = check( + "struct svc { n int }\n\ + fn main() {\n\ + m := map[string]svc{\"k\": svc{n: 1}}\n\ + f := fn(x map[string]svc) int {\n\ + return len(x)\n\ + }\n\ + if true {\n\ + f = fn(x map[string]svc) int {\n\ + delete(x, \"k\")\n\ + return 0\n\ + }\n\ + }\n\ + slot := &m[\"k\"]\n\ + _ = f(m)\n\ + slot.n = 2\n\ + }\n", + ); + assert_code( + &r_branch_rebind, + "HYB-E3102", + "branch rebind of outer closure to mutating closure must propagate out of block", + ); + + let r_shadow_restores_outer = check( + "struct svc { n int }\n\ + fn main() {\n\ + m := map[string]svc{\"k\": svc{n: 1}}\n\ + f := fn(x map[string]svc) int {\n\ + return len(x)\n\ + }\n\ + if true {\n\ + f := fn(x map[string]svc) int {\n\ + delete(x, \"k\")\n\ + return 0\n\ + }\n\ + _ = f\n\ + }\n\ + slot := &m[\"k\"]\n\ + n := f(m)\n\ + slot.n = n\n\ + }\n", + ); + assert!( + codes(&r_shadow_restores_outer).is_empty(), + "outer read-only closure summary must be restored after inner shadowing block exits: {:?}", + r_shadow_restores_outer.diagnostics + ); + assert_eq!(r_shadow_restores_outer.verdict, Verdict::Accept); +} + +#[test] +fn for_loop_var_and_closure_inner_block_shadowing_do_not_misclassify_receiver_struct() { + let r_for_shadow_fn = check( + "struct svc { n int }\n\ + struct reader { tag int }\n\ + struct writer { tag int }\n\ + fn (r reader) apply(m map[string]svc) int {\n\ + return len(m) + r.tag\n\ + }\n\ + fn (w writer) apply(m map[string]svc) int {\n\ + delete(m, \"k\")\n\ + return w.tag\n\ + }\n\ + fn helper(r reader, ws []writer, m map[string]svc) int {\n\ + for _, r in ws {\n\ + _ = r.apply(m)\n\ + }\n\ + return 0\n\ + }\n\ + fn main() {\n\ + m := map[string]svc{\"k\": svc{n: 1}}\n\ + ws := []writer{writer{tag: 1}}\n\ + slot := &m[\"k\"]\n\ + _ = helper(reader{tag: 0}, ws, m)\n\ + slot.n = 2\n\ + }\n", + ); + assert_code( + &r_for_shadow_fn, + "HYB-E3102", + "for-loop variable shadowing outer reader parameter with writer must reject with E3102", + ); + + let r_closure_inner_block_shadow = check( + "struct svc { n int }\n\ + struct reader { tag int }\n\ + struct writer { tag int }\n\ + fn (r reader) apply(m map[string]svc) int {\n\ + return len(m) + r.tag\n\ + }\n\ + fn (w writer) apply(m map[string]svc) int {\n\ + delete(m, \"k\")\n\ + return w.tag\n\ + }\n\ + fn main() {\n\ + m := map[string]svc{\"k\": svc{n: 1}}\n\ + f := fn() {\n\ + w := writer{tag: 1}\n\ + if true {\n\ + w := reader{tag: 0}\n\ + _ = w\n\ + }\n\ + _ = w.apply(m)\n\ + }\n\ + slot := &m[\"k\"]\n\ + f()\n\ + slot.n = 2\n\ + }\n", + ); + assert_code( + &r_closure_inner_block_shadow, + "HYB-E3102", + "inner block shadow w := reader{} inside closure must not leak past block exit", + ); +} + +#[test] +fn temporary_receiver_holding_loaned_map_and_inner_alias_closure_rebind_reject() { + let r_temp_recv = check( + "struct svc { n int }\n\ + struct engine { services map[string]svc }\n\ + fn (e *engine) reset() {\n\ + delete(e.services, \"k\")\n\ + }\n\ + fn main() {\n\ + m := map[string]svc{\"k\": svc{n: 1}}\n\ + slot := &m[\"k\"]\n\ + engine{services: m}.reset()\n\ + slot.n = 2\n\ + }\n", + ); + assert_code( + &r_temp_recv, + "HYB-E3102", + "temporary composite receiver holding loaned map must reject mutating method call with E3102", + ); + + let r_inner_alias_closure = check( + "struct svc { n int }\n\ + fn main() {\n\ + m := map[string]svc{\"k\": svc{n: 1}}\n\ + slot := &m[\"k\"]\n\ + f := fn() {}\n\ + if true {\n\ + tmp := m\n\ + f = fn() {\n\ + delete(tmp, \"k\")\n\ + }\n\ + }\n\ + f()\n\ + slot.n = 2\n\ + }\n", + ); + assert_code( + &r_inner_alias_closure, + "HYB-E3102", + "closure rebound in inner scope to mutate block-local alias of outer map must reject after scope pop", + ); +} + +#[test] +fn non_struct_lexical_binding_shadowing_top_level_fn_rejects() { + let r_factory_shadow_a3 = check( + "struct svc { n int }\n\ + struct reader { tag int }\n\ + struct writer { tag int }\n\ + fn (r reader) apply(m map[string]svc) int {\n\ + return len(m) + r.tag\n\ + }\n\ + fn (w writer) apply(m map[string]svc) int {\n\ + delete(m, \"k\")\n\ + return w.tag\n\ + }\n\ + fn factory() reader {\n\ + return reader{tag: 0}\n\ + }\n\ + fn helper(factory fn() writer, m map[string]svc) int {\n\ + r := factory()\n\ + return r.apply(m)\n\ + }\n\ + fn main() {\n\ + m := map[string]svc{\"k\": svc{n: 1}}\n\ + slot := &m[\"k\"]\n\ + _ = helper(fn() writer { return writer{tag: 1} }, m)\n\ + slot.n = 2\n\ + }\n", + ); + assert_code( + &r_factory_shadow_a3, + "HYB-E3102", + "closure parameter factory shadowing top-level fn factory() reader must not resolve r := factory() to reader", + ); + + let r_direct_fn_param_shadow_a3 = check( + "struct svc { n int }\n\ + fn op(m map[string]svc) int {\n\ + return len(m)\n\ + }\n\ + fn helper(op fn(map[string]svc) int, m map[string]svc) int {\n\ + return op(m)\n\ + }\n\ + fn main() {\n\ + m := map[string]svc{\"k\": svc{n: 1}}\n\ + slot := &m[\"k\"]\n\ + _ = helper(fn(x map[string]svc) int { delete(x, \"k\")\n return 0 }, m)\n\ + slot.n = 2\n\ + }\n", + ); + assert_code( + &r_direct_fn_param_shadow_a3, + "HYB-E3102", + "parameter op shadowing top-level read-only fn op must fail closed on op(m)", + ); + + let r_factory_shadow_const = check( + "struct svc { n int }\n\ + struct reader { tag int }\n\ + struct writer { tag int }\n\ + fn (r reader) apply(m map[string]svc) int {\n\ + return len(m) + r.tag\n\ + }\n\ + fn (w writer) apply(m map[string]svc) int {\n\ + delete(m, \"k\")\n\ + return w.tag\n\ + }\n\ + fn factory() reader {\n\ + return reader{tag: 0}\n\ + }\n\ + fn helper(factory fn() writer, m map[string]svc) int {\n\ + r := factory()\n\ + return r.apply(m)\n\ + }\n\ + fn main() {\n\ + const m = map[string]svc{\"k\": svc{n: 1}}\n\ + _ = helper(fn() writer { return writer{tag: 1} }, m)\n\ + }\n", + ); + assert_code( + &r_factory_shadow_const, + "HYB-E3006", + "closure parameter factory shadowing top-level fn factory() reader must reject const map write with E3006", + ); + + let r_shadowed_builtin_a3 = check( + "struct svc { n int }\n\ + fn helper(println fn(map[string]svc) int, m map[string]svc) int {\n\ + return println(m)\n\ + }\n\ + fn main() {\n\ + m := map[string]svc{\"k\": svc{n: 1}}\n\ + slot := &m[\"k\"]\n\ + _ = helper(fn(x map[string]svc) int { delete(x, \"k\")\n return 0 }, m)\n\ + slot.n = 2\n\ + }\n", + ); + assert_code( + &r_shadowed_builtin_a3, + "HYB-E3102", + "parameter println shadowing builtin println must not be treated as pure in A3", + ); + + let r_shadowed_builtin_const = check( + "struct svc { n int }\n\ + fn helper(println fn(map[string]svc) int, m map[string]svc) int {\n\ + return println(m)\n\ + }\n\ + fn main() {\n\ + const m = map[string]svc{\"k\": svc{n: 1}}\n\ + _ = helper(fn(x map[string]svc) int { delete(x, \"k\")\n return 0 }, m)\n\ + }\n", + ); + assert_code( + &r_shadowed_builtin_const, + "HYB-E3006", + "parameter println shadowing builtin println must not bypass const-write check with E3006", + ); +}