diff --git a/compiler/crates/hy-check/src/addr_loan.rs b/compiler/crates/hy-check/src/addr_loan.rs index 13b80cd7..99af28b0 100644 --- a/compiler/crates/hy-check/src/addr_loan.rs +++ b/compiler/crates/hy-check/src/addr_loan.rs @@ -29,12 +29,12 @@ fn is_slot_loan_expr(e: &Expr) -> bool { let Some(inner) = as_addr_of(unwrap_parens(e)) else { return false; }; - matches!(unwrap_parens(inner), Expr::Index { .. }) + matches!(unwrap_parens(inner), Expr::Index { .. } | Expr::Field { .. }) } impl<'a> Checker<'a> { - /// After `process_local`: flag every name bound to `&m[k]` (including - /// parenthesized `(&m[k])` and `&(m[k])`). + /// After `process_local`: flag every name bound to `&m[k]` or `&s.field` + /// (including parenthesized `(&m[k])` and `&(m[k])`). pub(crate) fn mark_slot_loans_local(&self, st: &mut FnState, l: &LocalDecl) { if l.names.len() != l.values.len() { return; @@ -48,8 +48,9 @@ impl<'a> Checker<'a> { } } - /// After `process_assign`: `x = &m[k]` makes `x` a slot loan; any other - /// plain re-assignment of a whole binding clears the flag. + /// After `process_assign`: `x = &m[k]` / `x = &s.field` makes `x` a slot + /// loan; any other plain re-assignment (including multi-return `x, ok = f()`) + /// of a whole binding clears the flag. pub(crate) fn mark_slot_loans_assign( &self, st: &mut FnState, @@ -57,14 +58,24 @@ impl<'a> Checker<'a> { op: AssignOp, values: &[hy_syntax::Expr], ) { - if op != AssignOp::Assign || targets.len() != values.len() { + if op != AssignOp::Assign { return; } - for (t, v) in targets.iter().zip(values.iter()) { - if let PlaceKind::Ident(name) = &t.kind { - let is_loan = is_slot_loan_expr(v); - if let Some(b) = st.env_lookup_mut(name) { - b.slot_loan = is_loan; + if targets.len() == values.len() { + for (t, v) in targets.iter().zip(values.iter()) { + if let PlaceKind::Ident(name) = &t.kind { + let is_loan = is_slot_loan_expr(v); + if let Some(b) = st.env_lookup_mut(name) { + b.slot_loan = is_loan; + } + } + } + } else if values.len() == 1 { + for t in targets { + if let PlaceKind::Ident(name) = &t.kind { + if let Some(b) = st.env_lookup_mut(name) { + b.slot_loan = false; + } } } } @@ -334,7 +345,7 @@ impl<'a> Checker<'a> { self.emit_addr_of_loan_diag(name, *id_span); } } - Expr::Field { .. } => { + Expr::Field { .. } | Expr::Index { .. } => { if let Some(root) = operand.root_ident() { if self.lookup_binding(st, root).is_some_and(|b| b.slot_loan) { self.emit_addr_of_loan_diag(root, *span); diff --git a/compiler/crates/hy-check/src/buildable.rs b/compiler/crates/hy-check/src/buildable.rs index 8e220b36..3210d7d1 100644 --- a/compiler/crates/hy-check/src/buildable.rs +++ b/compiler/crates/hy-check/src/buildable.rs @@ -862,7 +862,20 @@ impl<'a> Collector<'a> { return self.multi_value_kinds(&l.values[0]); } if n == l.values.len() { - return l.values.iter().map(|v| self.kind_of_expr(v)).collect(); + return l + .values + .iter() + .map(|v| { + if let Some(op) = as_addr_of(unwrap_parens(v)) { + if let Expr::Index { base, .. } = unwrap_parens(op) { + if matches!(self.kind_of_expr(base), OpKind::Map(..)) { + return self.kind_of_expr(unwrap_parens(op)); + } + } + } + self.kind_of_expr(v) + }) + .collect(); } vec![OpKind::Unknown; n] } diff --git a/compiler/crates/hy-check/src/loans.rs b/compiler/crates/hy-check/src/loans.rs index cc845c11..af9dae07 100644 --- a/compiler/crates/hy-check/src/loans.rs +++ b/compiler/crates/hy-check/src/loans.rs @@ -82,6 +82,13 @@ fn a3_paths_conflict(st: &FnState, loan_path: &str, event_path: &str) -> bool { false } +fn a3_path_is_prefix_or_eq(loan_path: &str, target_path: &str) -> bool { + loan_path == target_path + || loan_path + .strip_prefix(target_path) + .is_some_and(|rest| rest.starts_with('.')) +} + // --------------------------------------------------------------------------- // Call resolution + A2/A7 processing // --------------------------------------------------------------------------- @@ -125,14 +132,20 @@ 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 self.lookup_binding(st, b).is_none() && self.known_package(b) { - return Resolved::Package { - pkg: b.clone(), - method: name.clone(), - }; + if let Expr::Ident { name: b, .. } = unwrap_parens(base) { + if self.lookup_binding(st, b).is_none() { + if self.known_package(b) { + return Resolved::Package { + pkg: b.clone(), + method: name.clone(), + }; + } + let assoc = format!("{}.{}", b, name); + if self.fns.contains_key(&assoc) { + return Resolved::Fn { name: assoc }; + } } } if let Some(root) = base.root_ident() { @@ -224,6 +237,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_or_inline_call(st, cr, idx); let Some(tname) = receiver_type_name(&recv) else { self.check_a3_method_call(st, cr, idx, &root, &recv, &method); return; @@ -325,6 +339,7 @@ impl<'a> Checker<'a> { self.check_call_const_writes(st, cr, &HashSet::new()); self.record_callee_param_stores(st, cr); self.check_a3_inline_or_unknown_call(st, cr, idx); + self.check_a3_method_or_inline_call(st, cr, idx); } } } @@ -557,6 +572,73 @@ impl<'a> Checker<'a> { .push(Diagnostic::error("HYB-E3102", self.path.clone(), span, msg).with_notes(notes)); } + fn resolve_ptr_alias_path(&self, st: &FnState, path: &str) -> String { + let root = path_root(path); + let is_ptr = self + .lookup_binding(st, root) + .is_some_and(|b| matches!(b.shape, TypeShape::Pointer(_))); + if is_ptr { + if let Some(target) = st.aliases.get(root) { + let rest = &path[root.len()..]; + return format!("{}{}", target, rest); + } + } + path.to_string() + } + + fn a3_rebind_conflicts_with_loan_path( + &self, + st: &FnState, + loan_path: &str, + target_path: &str, + ) -> bool { + if a3_path_is_prefix_or_eq(loan_path, target_path) { + return true; + } + let resolved_loan = self.resolve_ptr_alias_path(st, loan_path); + let resolved_target = self.resolve_ptr_alias_path(st, target_path); + a3_path_is_prefix_or_eq(&resolved_loan, &resolved_target) + } + + fn emit_a3_rebind_if_live(&mut self, st: &FnState, target_path: &str, span: Span, idx: usize) { + let Some(loan) = st.loans.iter().find(|l| { + matches!(&l.target, LoanTarget::MapSlot { binding, path: p, .. } + if binding != target_path + && self.a3_rebind_conflicts_with_loan_path(st, p, target_path)) + && l.created < idx + && idx <= l.last + }) else { + return; + }; + let (binding, loan_path, loan_desc, loan_span, loan_last) = match &loan.target { + LoanTarget::MapSlot { binding, path, .. } => ( + binding.clone(), + path.clone(), + loan.desc.clone(), + loan.span, + loan.last, + ), + _ => unreachable!("filtered above"), + }; + let msg = format!( + "map rebind of `{}` invalidates the live slot loan `{}`", + target_path, binding + ); + let notes = vec![ + format!( + "{} ({}:{}) is a unique loan on a slot of `{}`, live from creation to its last use (O8, non-lexical window ending at statement {})", + loan_desc, + self.path, + loan_span.start, + loan_path, + loan_last + ), + "rebinding the loaned map or its enclosing container while the slot loan is live redirects the deferred store-back away from the loaned map — finish the slot mutation before rebinding".to_string(), + ]; + self.diags + .push(Diagnostic::error("HYB-E3102", self.path.clone(), span, msg).with_notes(notes)); + } + // ----------------------------------------------------------------------- // #111: A3 events that cross frames, aliases, and closure bodies // ----------------------------------------------------------------------- @@ -614,6 +696,151 @@ impl<'a> Checker<'a> { } } + /// A3 check for user-defined method calls (`h.insert_other()`, `ctx.put(m)`) + /// and inline closure calls (`(fn() { m["b"] = ... })()`) inside a live + /// slot-loan window. + fn check_a3_method_or_inline_call(&mut self, st: &FnState, cr: &CallRef, idx: usize) { + let live: Vec<(String, String)> = st + .loans + .iter() + .filter_map(|l| match &l.target { + LoanTarget::MapSlot { binding, path, .. } if l.created <= idx && idx <= l.last => { + Some((binding.clone(), path.clone())) + } + _ => None, + }) + .collect(); + if live.is_empty() { + return; + } + let mut conflict_paths: Vec = Vec::new(); + let conflicts_with_live = |candidate: &str| -> Option { + live.iter().find_map(|(binding, p)| { + let is_loan_binding = candidate == binding + || candidate + .strip_prefix(binding.as_str()) + .is_some_and(|rest| rest.starts_with('.')); + if !is_loan_binding && a3_paths_conflict(st, p, candidate) { + Some(p.clone()) + } else { + None + } + }) + }; + match unwrap_parens(cr.callee) { + Expr::Field { + base, + name: method_name, + .. + } => { + if let Expr::Ident { name: pkg, .. } = unwrap_parens(base) { + if self.lookup_binding(st, pkg).is_none() + && (self.known_package(pkg) || A3_PURE_PKGS.contains(&pkg.as_str())) + { + return; + } + } + let recv_shape = self.shape_of_expr(st, base); + let mut candidates: Vec<&FnDecl> = Vec::new(); + if let Some(tname) = receiver_type_name(&recv_shape) { + if let Some(decl) = self.method_decls.get(&(tname, method_name.clone())) { + candidates.push(decl); + } + } + if candidates.is_empty() { + for ((_, m), decl) in &self.method_decls { + if m == method_name { + candidates.push(decl); + } + } + } + if !candidates.is_empty() { + for decl in candidates { + if let Some(recv_param) = &decl.receiver { + if self.fn_mutates_param(decl, &recv_param.name, 0) { + let mut broots = Vec::new(); + collect_ref_roots(base, &mut broots); + let base_path_opt = walk::expr_path(base); + for bpath in base_path_opt.into_iter().chain(broots) { + if let Some(p) = conflicts_with_live(&bpath) { + conflict_paths.push(p); + } + } + } + } + for (i, a) in cr.args.iter().enumerate() { + let mutates = match decl.params.get(i) { + Some(p) => self.fn_mutates_param(decl, &p.name, 0), + None => true, + }; + if !mutates { + continue; + } + 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 let Some(p) = conflicts_with_live(&arg_path) { + conflict_paths.push(p); + } + } + } + } + } else if receiver_type_name(&recv_shape).is_none() { + // Unknown field-callable (e.g. closure stored in a struct field): + // fail closed when a loaned map is passed as an argument. + for a in cr.args { + 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 let Some(p) = conflicts_with_live(&arg_path) { + conflict_paths.push(p); + } + } + } + } + } + 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); + for r in &roots { + if let Some(p) = conflicts_with_live(r) { + conflict_paths.push(p); + } + } + for (i, a) in cr.args.iter().enumerate() { + let mutates = match params.get(i) { + Some(p) => self.block_mutates_param(body, &p.name, 0), + None => true, + }; + if !mutates { + continue; + } + 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 let Some(p) = conflicts_with_live(&arg_path) { + conflict_paths.push(p); + } + } + } + } + _ => {} + } + if let Some(p) = conflict_paths.into_iter().next() { + let verb = format!("mutation via `{}`", cr.callee.render()); + self.emit_a3_if_live(st, &p, cr.span, idx, &verb); + } + } + /// #111 form 4: a local closure invoked inside a live slot-loan window /// whose recorded mutation roots reach the loaned storage. fn check_a3_closure_call(&mut self, st: &FnState, cr: &CallRef, idx: usize, name: &str) { @@ -661,7 +888,9 @@ impl<'a> Checker<'a> { .filter_map(|l| { let eff_last = self.effective_loan_last(st, l); match &l.target { - LoanTarget::MapSlot { path, .. } if l.created <= idx && idx <= eff_last => { + LoanTarget::MapSlot { binding, path, .. } + if binding != root && l.created <= idx && idx <= eff_last => + { Some(path.clone()) } _ => None, @@ -851,7 +1080,9 @@ impl<'a> Checker<'a> { match stmt { Stmt::Assign { targets, values, .. } => { for t in targets { - if matches!(t.kind, PlaceKind::Index { .. }) && alias_stack.last().unwrap().contains(t.root()) { + if matches!(t.kind, PlaceKind::Index { .. } | PlaceKind::Field { .. }) + && alias_stack.last().unwrap().contains(t.root()) + { return true; } } @@ -977,7 +1208,7 @@ impl<'a> Checker<'a> { collect_ref_roots(&a.value, &mut roots); roots.iter().any(|r| r == param) }; - match cr.callee { + match unwrap_parens(cr.callee) { Expr::Ident { name, .. } => { if name == "delete" || name == "clear" { return cr.args.first().map(&arg_root_is_param).unwrap_or(false); @@ -1005,13 +1236,90 @@ impl<'a> Checker<'a> { // passing the parameter fails closed cr.args.iter().any(&arg_root_is_param) } - Expr::Field { base, .. } => { - if let Expr::Ident { name: pkg, .. } = &**base { + Expr::Field { + base, + name: method_name, + .. + } => { + if let Expr::Ident { name: pkg, .. } = unwrap_parens(base) { if A3_PURE_PKGS.contains(&pkg.as_str()) { return false; } + if let Some(decl) = self.fns.get(&format!("{}.{}", pkg, method_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, + } + } + return false; + } } - cr.args.iter().any(&arg_root_is_param) + let mut base_roots = Vec::new(); + collect_ref_roots(base, &mut base_roots); + let base_is_param = base_roots.iter().any(|r| r == param); + let matching_decls: Vec<&FnDecl> = self + .method_decls + .iter() + .filter_map(|((_, m), decl)| (m == method_name).then_some(*decl)) + .collect(); + if matching_decls.is_empty() { + return cr.args.iter().any(&arg_root_is_param); + } + if base_is_param + && matching_decls.iter().any(|decl| { + decl.receiver + .as_ref() + .is_some_and(|r| self.fn_mutates_param(decl, &r.name, depth + 1)) + }) + { + return true; + } + for (i, a) in cr.args.iter().enumerate() { + if !arg_root_is_param(a) { + continue; + } + if matching_decls.iter().any(|decl| match decl.params.get(i) { + Some(p) => self.fn_mutates_param(decl, &p.name, depth + 1), + None => true, + }) { + return true; + } + } + false + } + Expr::FnLit { + params, + results, + body, + .. + } => { + if !params.iter().any(|p| p.name == param) + && !results.iter().any(|r| r.name.as_deref() == Some(param)) + && self.block_mutates_param(body, param, depth + 1) + { + return true; + } + for (i, a) in cr.args.iter().enumerate() { + if !arg_root_is_param(a) { + continue; + } + if params + .get(i) + .map(|p| self.block_mutates_param(body, &p.name, depth + 1)) + .unwrap_or(true) + { + return true; + } + } + false } _ => cr.args.iter().any(&arg_root_is_param), } @@ -1219,19 +1527,35 @@ impl<'a> Checker<'a> { } } 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); - let resolved = roots.iter().find_map(|p| resolve(p, &pre_aliases, &pre_shadows)); - if let Some(r) = resolved { - current_shadows.remove(name.as_str()); - current_aliases.insert(name.clone(), r); - } else if bound.contains(name.as_str()) { - current_aliases.remove(name.as_str()); - current_shadows.insert(name.clone()); + targets, op, values, .. + } => { + if *op == AssignOp::Assign { + for t in targets { + if let PlaceKind::Ident(name) = &t.kind { + if !bound.contains(name.as_str()) + && !current_locals.contains(name.as_str()) + && !current_shadows.contains(name.as_str()) + { + out.insert(name.clone()); + } + } + } + } + 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); + let resolved = roots + .iter() + .find_map(|p| resolve(p, &pre_aliases, &pre_shadows)); + if let Some(r) = resolved { + current_shadows.remove(name.as_str()); + current_aliases.insert(name.clone(), r); + } else if bound.contains(name.as_str()) { + current_aliases.remove(name.as_str()); + current_shadows.insert(name.clone()); + } } } } @@ -1255,10 +1579,10 @@ impl<'a> Checker<'a> { } _ => {} } - // index-assign targets + // index-assign and field-assign targets if let Stmt::Assign { targets, .. } = stmt { for t in targets { - if matches!(t.kind, PlaceKind::Index { .. }) { + if matches!(t.kind, PlaceKind::Index { .. } | PlaceKind::Field { .. }) { let raw = t.root().to_string(); if let Some(r) = resolve(&raw, current_aliases, current_shadows) { out.insert(r); @@ -1296,7 +1620,7 @@ 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" || name == "clear" { if let Some(first) = cr.args.first() { @@ -1352,20 +1676,119 @@ impl<'a> Checker<'a> { } } } - Expr::Field { base, .. } => { - if let Expr::Ident { name: pkg, .. } = &**base { + Expr::Field { + base, + name: method_name, + .. + } => { + if let Expr::Ident { name: pkg, .. } = unwrap_parens(base) { if A3_PURE_PKGS.contains(&pkg.as_str()) { continue; } + if let Some(decl) = self.fns.get(&format!("{}.{}", pkg, method_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; + }; + let mutates = match decl.params.get(i) { + Some(tp) => self.fn_mutates_param(decl, &tp.name, 0), + None => true, + }; + if mutates { + out.insert(r); + } + } + } + continue; + } } - 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); + let matching_decls: Vec<&FnDecl> = self + .method_decls + .iter() + .filter_map(|((_, m), decl)| (m == method_name).then_some(*decl)) + .collect(); + if !matching_decls.is_empty() { + if matching_decls.iter().any(|decl| { + decl.receiver + .as_ref() + .is_some_and(|r| self.fn_mutates_param(decl, &r.name, 0)) + }) { + let mut broots = Vec::new(); + collect_ref_roots(base, &mut broots); + for root in broots { + if let Some(r) = + resolve(&root, current_aliases, current_shadows) + { + out.insert(r); + } + } + } + for (i, a) in cr.args.iter().enumerate() { + let mutates = matching_decls.iter().any(|decl| { + match decl.params.get(i) { + Some(tp) => self.fn_mutates_param(decl, &tp.name, 0), + None => true, + } + }); + if mutates { + 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); + } + } + } + } + } else { + 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); + } + } + } + } + } + Expr::FnLit { + params: fp, + results: fr, + body: fb, + .. + } => { + 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) { + out.insert(r); + } + } + for (i, a) in cr.args.iter().enumerate() { + let mutates = fp + .get(i) + .map(|p| self.block_mutates_param(fb, &p.name, 0)) + .unwrap_or(true); + if mutates { + 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); + } } } } @@ -1416,6 +1839,20 @@ impl<'a> Checker<'a> { idx: usize, managed: bool, ) { + for name in &l.names { + if name != "_" { + self.emit_a3_rebind_if_live(st, name, l.span, idx); + if st.scopes.is_empty() { + for loan in &mut st.loans { + if let LoanTarget::MapSlot { binding, .. } = &loan.target { + if binding == name && loan.created < idx { + loan.last = loan.last.min(idx); + } + } + } + } + } + } // A3 loan creation: `svc := &e.services[existing]` (including parallel defines) if l.names.len() == l.values.len() { for (name, val) in l.names.iter().zip(l.values.iter()) { @@ -5247,6 +5684,21 @@ impl<'a> Checker<'a> { _span: Span, idx: usize, ) { + if op == AssignOp::Assign && st.scopes.is_empty() { + for t in targets { + if let PlaceKind::Ident(name) = &t.kind { + if name != "_" { + for loan in &mut st.loans { + if let LoanTarget::MapSlot { binding, .. } = &loan.target { + if binding == name && loan.created < idx { + loan.last = loan.last.min(idx); + } + } + } + } + } + } + } // A3 loan creation on assignment: `x = &m[k]` if op == AssignOp::Assign && targets.len() == values.len() { for (t, v) in targets.iter().zip(values.iter()) { @@ -5320,7 +5772,10 @@ impl<'a> Checker<'a> { self.mark_move_in_value(st, v, format!("the move-assign `{}`", v.render())); } - // A3: an index-assign on a loaned base path is a potential INSERT + // A3: an index-assign on a loaned base path is a potential INSERT; + // a whole-binding or field rebind of the loaned map / its enclosing + // struct container while the slot loan is live redirects the deferred + // store-back away from the loaned map. for (i, t) in targets.iter().enumerate() { if is_const_write[i] { continue; @@ -5329,6 +5784,12 @@ impl<'a> Checker<'a> { if let Some(path) = walk::place_path(t) { self.emit_a3_if_live(st, &path, t.span, idx, "insert"); } + } else if op == AssignOp::Assign + && matches!(t.kind, PlaceKind::Ident(_) | PlaceKind::Field { .. }) + { + if let Some(path) = walk::place_path(t) { + self.emit_a3_rebind_if_live(st, &path, t.span, idx); + } } if let Some((path, key)) = walk::place_field_store_slot(t) { self.emit_o8_alias_if_live( diff --git a/compiler/crates/hy-check/tests/a3_interproc.rs b/compiler/crates/hy-check/tests/a3_interproc.rs index a822dc68..b001c655 100644 --- a/compiler/crates/hy-check/tests/a3_interproc.rs +++ b/compiler/crates/hy-check/tests/a3_interproc.rs @@ -284,3 +284,213 @@ fn clear_in_slot_loan_window_rejects_e3102() { assert_e3102(&r_helper, "helper clear in window"); } +#[test] +fn map_and_container_rebind_in_slot_loan_window_rejects_e3102() { + // Direct map rebind `m = m2` while `p := &m["a"]` is live + let r_map_rebind = check( + "struct cell { n int }\n\ + fn main() {\n\ + m := map[string]cell{\"a\": cell{n: 1}}\n\ + m2 := map[string]cell{\"a\": cell{n: 2}}\n\ + p := &m[\"a\"]\n\ + p.n = 42\n\ + m = m2\n\ + _ = p.n\n\ + }\n", + ); + assert_e3102(&r_map_rebind, "direct map rebind in window"); + + // Enclosing struct field rebind `h.m = m2` and whole-struct rebind `h = h2` + let r_field_rebind = check( + "struct cell { n int }\n\ + struct holder { m map[string]cell }\n\ + fn main() {\n\ + h := holder{m: map[string]cell{\"a\": cell{n: 1}}}\n\ + p := &h.m[\"a\"]\n\ + h.m = map[string]cell{\"a\": cell{n: 2}}\n\ + p.n = 42\n\ + }\n", + ); + assert_e3102(&r_field_rebind, "struct map-field rebind in window"); + + let r_struct_rebind = check( + "struct cell { n int }\n\ + struct holder { m map[string]cell }\n\ + fn main() {\n\ + h := holder{m: map[string]cell{\"a\": cell{n: 1}}}\n\ + h2 := holder{m: map[string]cell{\"a\": cell{n: 2}}}\n\ + p := &h.m[\"a\"]\n\ + h = h2\n\ + p.n = 42\n\ + }\n", + ); + assert_e3102(&r_struct_rebind, "enclosing struct rebind in window"); + + // Closure rebinding `m = m2` called inside the live window + let r_closure_rebind = check( + "struct cell { n int }\n\ + fn main() {\n\ + m := map[string]cell{\"a\": cell{n: 1}}\n\ + m2 := map[string]cell{\"a\": cell{n: 2}}\n\ + p := &m[\"a\"]\n\ + swap := fn() {\n\ + m = m2\n\ + }\n\ + swap()\n\ + p.n = 42\n\ + }\n", + ); + assert_e3102(&r_closure_rebind, "closure map rebind in window"); + + // Rebinding after the loan's last use stays accepted + let r_after = check( + "struct cell { n int }\n\ + fn main() {\n\ + m := map[string]cell{\"a\": cell{n: 1}}\n\ + m2 := map[string]cell{\"a\": cell{n: 2}}\n\ + p := &m[\"a\"]\n\ + p.n = 42\n\ + m = m2\n\ + }\n", + ); + assert!(codes(&r_after).is_empty(), "{:?}", r_after.diagnostics); + assert_eq!(r_after.verdict, Verdict::Accept); +} + +#[test] +fn user_method_assoc_fn_inline_closure_and_paren_callee_in_window_reject_e3102() { + // User method mutating receiver's map + let r_method_recv = check( + "struct cell { n int }\n\ + struct holder { m map[string]cell }\n\ + fn (h *holder) insert_fresh() {\n\ + h.m[\"fresh\"] = cell{n: 1}\n\ + }\n\ + fn main() {\n\ + h := holder{m: map[string]cell{\"a\": cell{n: 0}}}\n\ + p := &h.m[\"a\"]\n\ + h.insert_fresh()\n\ + p.n = 9\n\ + }\n", + ); + assert_e3102(&r_method_recv, "user method mutating receiver map in window"); + + // User method mutating map argument + let r_method_arg = check( + "struct cell { n int }\n\ + struct mutator { tag int }\n\ + fn (x *mutator) put(m map[string]cell) {\n\ + m[\"fresh\"] = cell{n: x.tag}\n\ + }\n\ + fn main() {\n\ + m := map[string]cell{\"a\": cell{n: 0}}\n\ + x := mutator{tag: 7}\n\ + p := &m[\"a\"]\n\ + x.put(m)\n\ + p.n = 9\n\ + }\n", + ); + assert_e3102(&r_method_arg, "user method mutating map arg in window"); + + // Associated function mutating map argument + let r_assoc = check( + "struct cell { n int }\n\ + struct holder { m map[string]cell }\n\ + fn holder.put(h *holder) {\n\ + h.m[\"fresh\"] = cell{n: 1}\n\ + }\n\ + fn main() {\n\ + h := holder{m: map[string]cell{\"a\": cell{n: 0}}}\n\ + p := &h.m[\"a\"]\n\ + holder.put(&h)\n\ + p.n = 9\n\ + }\n", + ); + assert_e3102(&r_assoc, "associated fn mutating map in window"); + + // Inline closure call `(fn() { m["fresh"] = ... })()` + let r_inline_closure = check( + "struct cell { n int }\n\ + fn main() {\n\ + m := map[string]cell{\"a\": cell{n: 0}}\n\ + p := &m[\"a\"]\n\ + if true {\n\ + (fn() {\n\ + m[\"fresh\"] = cell{n: 1}\n\ + })()\n\ + }\n\ + p.n = 9\n\ + }\n", + ); + assert_e3102(&r_inline_closure, "inline closure mutating map in window"); + + // Parenthesized callee `(bump)(m)` + let r_paren_callee = check( + "struct cell { n int }\n\ + fn bump(m map[string]cell) {\n\ + m[\"fresh\"] = cell{n: 1}\n\ + }\n\ + fn main() {\n\ + m := map[string]cell{\"a\": cell{n: 0}}\n\ + p := &m[\"a\"]\n\ + if true {\n\ + (bump)(m)\n\ + }\n\ + p.n = 9\n\ + }\n", + ); + assert_e3102(&r_paren_callee, "parenthesized callee mutating map in window"); + + // Read-only user method inside window stays accepted + let r_readonly_method = check( + "struct cell { n int }\n\ + struct holder { m map[string]cell }\n\ + fn (h *holder) size() int {\n\ + return len(h.m)\n\ + }\n\ + fn main() {\n\ + h := holder{m: map[string]cell{\"a\": cell{n: 0}}}\n\ + p := &h.m[\"a\"]\n\ + k := h.size()\n\ + p.n = k\n\ + }\n", + ); + assert!( + codes(&r_readonly_method).is_empty(), + "{:?}", + r_readonly_method.diagnostics + ); + assert_eq!(r_readonly_method.verdict, Verdict::Accept); +} + +#[test] +fn plain_rebind_ends_slot_loan_window_and_clears_t24_flag() { + // Once `p` is rebound to a plain value (`p = cell{n: 2}` or `p, ok = m["b"]`), + // the slot loan on `m` is closed (so mutating `m` afterwards is legal) and + // `&p` is the address of a plain local (not HYB-E3105). + let r = check( + "struct cell { n int }\n\ + fn touch(ptr *cell) {\n\ + ptr.n = 99\n\ + }\n\ + fn main() {\n\ + m := map[string]cell{\"a\": cell{n: 1}, \"b\": cell{n: 2}}\n\ + p := &m[\"a\"]\n\ + p.n = 10\n\ + p = cell{n: 20}\n\ + m[\"c\"] = cell{n: 30}\n\ + touch(&p)\n\ + q := &m[\"a\"]\n\ + q.n = 11\n\ + ok := false\n\ + q, ok = m[\"b\"]\n\ + _ = ok\n\ + m[\"d\"] = cell{n: 40}\n\ + touch(&q)\n\ + }\n", + ); + assert!(codes(&r).is_empty(), "{:?}", r.diagnostics); + assert_eq!(r.verdict, Verdict::Accept); +} + + diff --git a/compiler/crates/hy-emit/src/lib.rs b/compiler/crates/hy-emit/src/lib.rs index a982ebfc..fdd8a18d 100644 --- a/compiler/crates/hy-emit/src/lib.rs +++ b/compiler/crates/hy-emit/src/lib.rs @@ -517,6 +517,9 @@ pub struct Emitter { pointer_vars: BTreeSet, /// Local/parameter/receiver/result names bound in the current function frame. local_vars: BTreeSet, + /// Names shadowed by a local declaration in an inner scope since the last + /// slot-loan binding for that name. + shadowed_since_loan: BTreeSet, /// Functions whose first result is a pointer type. ptr_returning_fns: BTreeSet, /// N5 (zeroinit.rs): non-generic user struct → its declared fields, @@ -527,6 +530,7 @@ pub struct Emitter { pub(crate) struct ScopeSnapshot { local_vars: BTreeSet, + shadowed_since_loan: BTreeSet, bindings: BTreeMap, var_ty: BTreeMap, var_struct: BTreeMap, @@ -565,6 +569,7 @@ impl Emitter { var_ty: BTreeMap::new(), pointer_vars: BTreeSet::new(), local_vars: BTreeSet::new(), + shadowed_since_loan: BTreeSet::new(), ptr_returning_fns: BTreeSet::new(), explicit_imports: BTreeMap::new(), n5_structs: BTreeMap::new(), @@ -579,6 +584,7 @@ impl Emitter { pub(crate) fn save_scope(&self) -> ScopeSnapshot { ScopeSnapshot { local_vars: self.local_vars.clone(), + shadowed_since_loan: self.shadowed_since_loan.clone(), bindings: self.bindings.clone(), var_ty: self.var_ty.clone(), var_struct: self.var_struct.clone(), @@ -588,6 +594,7 @@ impl Emitter { pub(crate) fn restore_scope(&mut self, saved: ScopeSnapshot) { self.local_vars = saved.local_vars; + self.shadowed_since_loan = saved.shadowed_since_loan; self.bindings = saved.bindings; self.var_ty = saved.var_ty; self.var_struct = saved.var_struct; @@ -597,6 +604,7 @@ impl Emitter { pub(crate) fn shadow_local_name(&mut self, name: &str) { if name != "_" { self.local_vars.insert(name.to_string()); + self.shadowed_since_loan.insert(name.to_string()); self.bindings.remove(name); self.var_ty.remove(name); self.var_struct.remove(name); @@ -1182,6 +1190,7 @@ impl Emitter { self.var_ty.clear(); self.pointer_vars.clear(); self.local_vars.clear(); + self.shadowed_since_loan.clear(); self.temp_n = 0; self.try_temps.clear(); self.loop_labels.clear(); diff --git a/compiler/crates/hy-emit/src/stmt.rs b/compiler/crates/hy-emit/src/stmt.rs index f72ef93d..548ad4e5 100644 --- a/compiler/crates/hy-emit/src/stmt.rs +++ b/compiler/crates/hy-emit/src/stmt.rs @@ -399,10 +399,11 @@ impl Emitter { val_texts.push(self.emit_expr(v, 0)?); } } + self.flush_current_frame_loans(&l.names); self.shadow_local_names(&l.names); for (loan_name, _) in &new_loans { self.bindings.insert(loan_name.clone(), BindingKind::Plain); - self.flush_loan_for_name(loan_name); + self.shadowed_since_loan.remove(loan_name); } self.push(&format!( "{} := {}", @@ -420,11 +421,7 @@ impl Emitter { .iter() .map(|v| self.emit_expr(v, 0)) .collect::, _>>()?; - for name in &l.names { - if name != "_" { - self.flush_loan_for_name(name); - } - } + self.flush_current_frame_loans(&l.names); let names = l.names.join(", "); self.push(&format!("{} := {}", names, values.join(", "))); // A struct-literal define carries the receiver's struct name @@ -915,6 +912,7 @@ impl Emitter { self.hoist_tries_expr(call)?; let temp = self.next_temp("hyErr"); let call_text = self.emit_expr(call, 0)?; + self.flush_current_frame_loans(&l.names); let mut names = l.names.clone(); names.push(temp.clone()); self.push(&format!("{} := {}", names.join(", "), call_text)); @@ -940,6 +938,7 @@ impl Emitter { self.hoist_tries_expr(call)?; let temp = self.next_temp("hyErr"); let call_text = self.emit_expr(call, 0)?; + self.flush_current_frame_loans(&l.names); let mut names = l.names.clone(); names.push(temp.clone()); self.push(&format!("{} := {}", names.join(", "), call_text)); @@ -1227,9 +1226,12 @@ impl Emitter { _ => unreachable!(), }) .collect(); + let mut target_frames = std::collections::BTreeMap::new(); for name in &names { if name != "_" { - self.flush_loan_for_name(name); + if let Some(frame_idx) = self.flush_loan_for_name(name) { + target_frames.insert(name.clone(), frame_idx); + } } } let mut rhs_texts = Vec::with_capacity(values.len()); @@ -1257,8 +1259,17 @@ impl Emitter { } } self.push(&format!("{} = {}", names.join(", "), rhs_texts.join(", "))); - if let Some(frame) = self.loan_frames.last_mut() { - frame.extend(new_loans); + for (name, place) in new_loans { + self.shadowed_since_loan.remove(&name); + if let Some(&idx) = target_frames.get(&name) { + if let Some(frame) = self.loan_frames.get_mut(idx) { + frame.push((name, place)); + continue; + } + } + if let Some(frame) = self.loan_frames.last_mut() { + frame.push((name, place)); + } } return Ok(()); } @@ -1322,6 +1333,15 @@ impl Emitter { .iter() .map(|v| self.emit_expr(v, 0)) .collect::, _>>()?; + if op == AssignOp::Assign { + for t in targets { + if let PlaceKind::Ident(name) = &t.kind { + if name != "_" { + self.flush_loan_for_name(name); + } + } + } + } self.push(&format!("{} {} {}", ts.join(", "), op_text, vs.join(", "))); Ok(()) } @@ -2476,23 +2496,70 @@ impl Emitter { Ok((place.clone(), place)) } - /// Flushes and removes any active slot-loan entries for `target_name` in - /// the current loan frame (used before rebinding `target_name = &...`). - fn flush_loan_for_name(&mut self, target_name: &str) { + /// Flushes and removes any active slot-loan entries for `names` in the + /// current loan frame only (used before emitting a local `:=` declaration). + fn flush_current_frame_loans(&mut self, names: &[String]) { let mut to_flush = Vec::new(); if let Some(frame) = self.loan_frames.last_mut() { - let mut i = 0; - while i < frame.len() { - if frame[i].0 == target_name { - to_flush.push(frame.remove(i)); - } else { - i += 1; + for target_name in names { + if target_name == "_" { + continue; + } + let mut i = 0; + while i < frame.len() { + if frame[i].0 == *target_name { + to_flush.push(frame.remove(i)); + } else { + i += 1; + } + } + } + } + for (_, store) in to_flush { + self.push(&store); + } + } + + /// Flushes and removes any active slot-loan entries for `target_name` in + /// the current loan frame or (when `target_name` has not been shadowed by a + /// local declaration in an inner block) in enclosing loan frames within the + /// same loop/function scope. Returns the frame index where the loan lived. + fn flush_loan_for_name(&mut self, target_name: &str) -> Option { + let mut to_flush = Vec::new(); + let mut found_idx = None; + if self.shadowed_since_loan.contains(target_name) { + if let Some(last_idx) = self.loan_frames.len().checked_sub(1) { + let frame = &mut self.loan_frames[last_idx]; + let mut i = 0; + while i < frame.len() { + if frame[i].0 == target_name { + to_flush.push(frame.remove(i)); + found_idx = Some(last_idx); + } else { + i += 1; + } + } + } + } else { + for (idx, frame) in self.loan_frames.iter_mut().enumerate().rev() { + if frame.first().is_some_and(|(n, _)| n.is_empty()) { + break; + } + let mut i = 0; + while i < frame.len() { + if frame[i].0 == target_name { + to_flush.push(frame.remove(i)); + found_idx = Some(idx); + } else { + i += 1; + } } } } for (_, store) in to_flush { self.push(&store); } + found_idx } /// Emits a sequence of statements inside the current loan frame, flushing @@ -2528,23 +2595,29 @@ impl Emitter { let rest = &stmts[idx + 1..]; // #546: a compound statement (`if`, `match`, `select`, `block`, // `managed`, `for`) that is the loan's last user AND spawns, sends, - // deletes, clears, or writes using the loan's base map must not get - // a single store-back after it: on a path that moved or mutated the - // map, that write races the new task or clobbers the branch's write. - // Hand the loan down into every branch instead (or pre-flush when - // the statement's own header shadows the loan name or sends the - // base in a select case clause). + // deletes, clears, or writes using the loan's base map (or rebinds + // the loan variable inside a branch) must not get a single + // store-back after it: on a path that moved or mutated the map or + // rebound the variable, that write races the new task or clobbers + // the branch's write. Hand the loan down into every branch instead + // (or pre-flush when the statement's own header shadows the loan + // name or sends the base in a select case clause). let mut pre_flush = Vec::new(); let mut handoff = Vec::new(); if Self::stmt_supports_loan_handoff(st) { + let is_if = matches!(st, Stmt::If { .. }); if let Some(frame) = self.loan_frames.last_mut() { let mut i = 0; while i < frame.len() { let (name, place) = &frame[i]; let root = Self::place_root(place); + let no_rest_use = !Self::remaining_stmts_use_loan(rest, name); + let rebinds = Self::stmt_rebinds_ident(st, name); if !name.is_empty() - && !Self::remaining_stmts_use_loan(rest, name) - && Self::stmt_has_boundary_using(st, name, root) + && ((no_rest_use + && (Self::stmt_has_boundary_using(st, name, root) + || rebinds)) + || (is_if && rebinds)) { if Self::stmt_must_pre_flush_loan(st, name, root) { pre_flush.push(frame.remove(i)); @@ -2655,6 +2728,68 @@ impl Emitter { } } + /// Does `st` contain an unshadowed `=` assignment whose target is `PlaceKind::Ident(target)`? + fn stmt_rebinds_ident(st: &Stmt, target: &str) -> bool { + if target.is_empty() { + return false; + } + match st { + Stmt::Assign { targets, op, .. } if *op == AssignOp::Assign => targets + .iter() + .any(|t| matches!(&t.kind, PlaceKind::Ident(n) if n == target)), + Stmt::If { + init, + then, + otherwise, + .. + } => { + if init.as_ref().is_some_and(|i| Self::local_declares(i, target)) { + return false; + } + Self::block_rebinds_ident(then, target) + || otherwise + .as_deref() + .is_some_and(|o| Self::stmt_rebinds_ident(o, target)) + } + Stmt::For { kind, body, .. } => { + !Self::for_binder_shadows(kind, target) && Self::block_rebinds_ident(body, target) + } + Stmt::Match { arms, .. } => arms.iter().any(|a| { + let shadowed = matches!( + &a.pattern, + MatchPattern::Path { + binding: Some(b), + .. + } if b == target + ); + !shadowed && Self::block_rebinds_ident(&a.body, target) + }), + Stmt::Select { arms, .. } => arms.iter().any(|a| { + !Self::select_arm_binds(&a.kind, target) + && Self::block_rebinds_ident(&a.body, target) + }), + Stmt::Block { block: body, .. } | Stmt::Managed { body, .. } => { + Self::block_rebinds_ident(body, target) + } + _ => false, + } + } + + fn block_rebinds_ident(b: &Block, target: &str) -> bool { + for st in &b.stmts { + if let Stmt::Local(l) = st { + if l.names.iter().any(|n| n == target) { + return false; + } + continue; + } + if Self::stmt_rebinds_ident(st, target) { + return true; + } + } + false + } + /// #546: does `st` (recursively) contain a task spawn, channel send, /// select send, `delete`/`clear`, direct write/move touching `root`, or /// a branch block that uses `name` and subsequently reads `root`? @@ -2806,14 +2941,13 @@ impl Emitter { op, values, .. - } if *op == AssignOp::Assign - && targets.len() == values.len() - && targets - .iter() - .all(|t| matches!(&t.kind, PlaceKind::Ident(_))) - && values.iter().any(|v| Self::as_slot_loan_operand(v).is_some()) => - { - if values.iter().any(|v| Self::expr_mentions_ident(v, name)) { + } if *op == AssignOp::Assign => { + if values.iter().any(|v| Self::expr_mentions_ident(v, name)) + || targets.iter().any(|t| { + !matches!(&t.kind, PlaceKind::Ident(_)) + && Self::place_mentions_ident(t, name) + }) + { return true; } if targets diff --git a/compiler/crates/hy-emit/src/tests.rs b/compiler/crates/hy-emit/src/tests.rs index 002271c4..59c0db50 100644 --- a/compiler/crates/hy-emit/src/tests.rs +++ b/compiler/crates/hy-emit/src/tests.rs @@ -2908,3 +2908,138 @@ fn m4_initialism_mapping_for_foreign_go_names_issue_658() { assert!(go.contains("strings.ToValidUTF8(\"a\", \"b\")"), "expected strings.ToValidUTF8, got:\n{}", go); } +#[test] +fn slot_loan_flushes_before_plain_value_rebind_and_does_not_overwrite_map_after_move() { + let go = emit( + "struct item {\n x int\n}\n\nfn worker(m map[string]item, done chan bool) {\n m[\"w\"] = item{x: 1}\n done <- true\n}\n\nfn main() {\n m := map[string]item{\"a\": item{x: 1}}\n done := make(chan bool, 1)\n n := &m[\"a\"]\n n.x = 42\n n = item{x: 999}\n task worker(move m, done)\n fmt.println(n.x)\n <-done\n}\n", + ); + let mut_at = go.find("n.x = 42").expect("loan mutation"); + let flush_at = go.find("m[\"a\"] = n").expect("store-back emitted"); + let rebind_at = go.find("n = item{x: 999}").expect("plain rebind emitted"); + let spawn_at = go.find("go worker(m, done)").expect("task spawn emitted"); + assert!( + mut_at < flush_at && flush_at < rebind_at && rebind_at < spawn_at, + "store-back must flush before plain-value rebind and before spawn:\n{}", + go + ); + assert_eq!( + go.matches("m[\"a\"] = n").count(), + 1, + "store-back must be emitted exactly once:\n{}", + go + ); +} + +#[test] +fn slot_loan_flushes_when_plain_rebind_rhs_reads_loan() { + let go = emit( + "struct item {\n x int\n}\n\nfn main() {\n m := map[string]item{\"a\": item{x: 1}}\n n := &m[\"a\"]\n n.x = 42\n n = item{x: n.x + 10}\n fmt.println(n.x)\n}\n", + ); + let mut_at = go.find("n.x = 42").expect("loan mutation"); + let flush_at = go.find("m[\"a\"] = n").expect("store-back emitted"); + let rebind_at = go + .find("n = item{x: n.x + 10}") + .expect("plain rebind reading n emitted"); + assert!( + mut_at < flush_at && flush_at < rebind_at, + "store-back must flush before plain rebind even when RHS reads the loan:\n{}", + go + ); + assert_eq!( + go.matches("m[\"a\"] = n").count(), + 1, + "store-back must be emitted exactly once:\n{}", + go + ); +} + +#[test] +fn slot_loan_flushes_before_comma_ok_rebind() { + let go = emit( + "struct item {\n x int\n}\n\nfn main() {\n m := map[string]item{\"a\": item{x: 1}, \"b\": item{x: 2}}\n n := &m[\"a\"]\n n.x = 42\n ok := false\n n, ok = m[\"b\"]\n fmt.println(n.x, ok)\n}\n", + ); + let mut_at = go.find("n.x = 42").expect("loan mutation"); + let flush_at = go.find("m[\"a\"] = n").expect("store-back emitted"); + let rebind_at = go.find("n, ok = m[\"b\"]").expect("comma-ok rebind emitted"); + assert!( + mut_at < flush_at && flush_at < rebind_at, + "store-back must flush before comma-ok rebind:\n{}", + go + ); + assert_eq!( + go.matches("m[\"a\"] = n").count(), + 1, + "store-back must be emitted exactly once:\n{}", + go + ); +} + +#[test] +fn slot_loan_rebind_in_nested_block_flushes_previous_slot_before_rebind() { + let go = emit( + "struct item {\n x int\n}\n\nfn main() {\n m := map[string]item{\"a\": item{x: 1}, \"b\": item{x: 2}}\n n := &m[\"a\"]\n n.x = 10\n {\n n = &m[\"b\"]\n n.x = 20\n }\n}\n", + ); + let mut_a = go.find("n.x = 10").expect("mutation of a"); + let flush_a = go.find("m[\"a\"] = n").expect("store-back of a"); + let rebind_b = go.find("n = m[\"b\"]").expect("rebind to b"); + let mut_b = go.find("n.x = 20").expect("mutation of b"); + let flush_b = go.find("m[\"b\"] = n").expect("store-back of b"); + assert!( + mut_a < flush_a && flush_a < rebind_b && rebind_b < mut_b && mut_b < flush_b, + "nested-block slot-loan rebind must flush `a` before rebinding to `b` and only flush `b` afterwards:\n{}", + go + ); + assert_eq!(go.matches("m[\"a\"] = n").count(), 1, "{}", go); + assert_eq!(go.matches("m[\"b\"] = n").count(), 1, "{}", go); +} + +#[test] +fn slot_loan_rebind_in_if_branch_hands_off_cleanly_to_both_branches() { + let go = emit( + "struct item {\n x int\n}\n\nfn main() {\n m := map[string]item{\"a\": item{x: 1}, \"b\": item{x: 2}}\n n := &m[\"a\"]\n n.x = 10\n if true {\n n.x = 15\n n = &m[\"b\"]\n n.x = 20\n }\n}\n", + ); + let first_flush_a = go.find("m[\"a\"] = n").expect("store-back of a in then"); + let rebind_b = go.find("n = m[\"b\"]").expect("rebind to b in then"); + let flush_b = go.find("m[\"b\"] = n").expect("store-back of b in then"); + let else_branch = go.find("} else {").expect("synthesized else branch"); + let last_flush_a = go.rfind("m[\"a\"] = n").expect("store-back of a in else"); + assert!( + first_flush_a < rebind_b + && rebind_b < flush_b + && flush_b < else_branch + && else_branch < last_flush_a, + "if-branch mutate+rebind must flush `a` before rebind and `b` in then, and synthesize else store-back for `a`:\n{}", + go + ); + assert_eq!(go.matches("m[\"a\"] = n").count(), 2, "{}", go); + assert_eq!(go.matches("m[\"b\"] = n").count(), 1, "{}", go); + + let go_immediate = emit( + "struct item {\n x int\n}\n\nfn main() {\n m := map[string]item{\"a\": item{x: 1}, \"b\": item{x: 2}}\n n := &m[\"a\"]\n n.x = 10\n if true {\n n = &m[\"b\"]\n n.x = 20\n }\n}\n", + ); + let flush_a_imm = go_immediate.find("m[\"a\"] = n").expect("early flush of a"); + let if_head = go_immediate.find("if true {").expect("if head"); + let flush_b_imm = go_immediate.find("m[\"b\"] = n").expect("flush of b"); + assert!( + flush_a_imm < if_head && if_head < flush_b_imm, + "immediate rebind in if-branch flushes `a` before `if` and `b` inside branch:\n{}", + go_immediate + ); + assert_eq!(go_immediate.matches("m[\"a\"] = n").count(), 1, "{}", go_immediate); + assert_eq!(go_immediate.matches("m[\"b\"] = n").count(), 1, "{}", go_immediate); +} + +#[test] +fn branch_inherit_not_consumed_by_closure_in_if_condition() { + let go = emit( + "struct item {\n x int\n}\n\nfn worker(m map[string]item, done chan bool) {\n done <- true\n}\n\nfn main() {\n m := map[string]item{\"a\": item{x: 1}}\n done := make(chan bool, 1)\n n := &m[\"a\"]\n n.x = 10\n if (fn() bool { return true })() {\n n.x = 20\n task worker(move m, done)\n <-done\n }\n}\n", + ); + let mut_in_then = go.find("n.x = 20").expect("mutation in then"); + let flush_in_then = go.find("m[\"a\"] = n").expect("store-back in then"); + let spawn_in_then = go.find("go worker(m, done)").expect("spawn in then"); + assert!( + mut_in_then < flush_in_then && flush_in_then < spawn_in_then, + "closure in if-cond must not steal branch_inherit from the then-block:\n{}", + go + ); +}