iter remove-mut-var-assign.1: atomic removal of mut/var/assign
mut/var/assign removed from AILang entirely and atomically. Deleted: Term::Mut/Term::Assign/struct MutVar; the three Form-A keywords + parse_mut/parse_assign + grammar EBNF; the 4 mut CheckError variants; the mut_scope_stack synth threading (param dropped from synth + every internal/external/test caller); the two lower_term arms; and every exhaustive no-_ Term::Mut/Term::Assign match arm across 17 source files — cut in lockstep with DESIGN.md, fixtures, the drift trio, carve-out and roadmap so the schema is honest at every commit. No catch-all wildcard introduced (verified). loop/recur + let/if are the surviving forms. The shared codegen alloca machinery survives (loop reuses it): mut_var_allocas renamed binder_allocas (representation-only, loop codegen byte-identical) and the shared Term::Lam escape guard simplified to !loop_stack.is_empty() with the loop half (LoopBinderCapturedByLambda) byte-equivalent. Feature-acceptance applied inverted: the removed feature fails clause 2 (redundant) and clause 3 (IS the iterated-mutable-state bug class). Behaviour preservation is executable: mut_counter/mut_sum_floats still print 55 after the faithful let/if rewrite. The removal is made executable by the new mut_removed_pin.rs (4 must-fail pins). Independent verification: cargo test --workspace 605/0, zero residual mut symbols in any crate source, loop/recur non-regression all green (55 / 500000500000 / infinite-compiles / the lambda_capturing_loop_binder pin), roundtrip_cli PASS. One DONE_WITH_CONCERNS: a 4th recurrence of the recon-undercount class (in-source mod tests + a drift-pin fn + 5 orphaned mut .ail.json carve-outs + a non-enumerated E0599); all resolved within implementer remit, no behaviour change. Milestone-close audit then fieldtest remain. spec docs/specs/2026-05-18-remove-mut-var-assign.md (grounding PASS) plan docs/plans/remove-mut-var-assign.1.md
This commit is contained in:
@@ -190,17 +190,6 @@ fn walk(t: &Term, out: &mut NonEscapeSet) {
|
||||
walk(source, out);
|
||||
walk(body, out);
|
||||
}
|
||||
// Iter mut.1: mut-vars are alloca-resident scalars (Int /
|
||||
// Float / Bool / Unit) and never participate in heap escape.
|
||||
// Their `init` and `body` and assign `value` terms may still
|
||||
// contain Let-allocation candidates, so walk through.
|
||||
Term::Mut { vars, body } => {
|
||||
for v in vars {
|
||||
walk(&v.init, out);
|
||||
}
|
||||
walk(body, out);
|
||||
}
|
||||
Term::Assign { value, .. } => walk(value, out),
|
||||
Term::Loop { binders, body } => {
|
||||
for b in binders {
|
||||
walk(&b.init, out);
|
||||
@@ -389,23 +378,6 @@ fn escapes(t: &Term, tainted: &BTreeSet<String>, in_tail: bool) -> bool {
|
||||
}
|
||||
escapes(body, tainted, in_tail)
|
||||
}
|
||||
// Iter mut.1: mut-vars are alloca-resident scalars and do
|
||||
// not themselves contribute to heap escape. Var inits are
|
||||
// evaluated for side-effect (their values are stored into
|
||||
// allocas, not used as the block's value), so they are
|
||||
// walked in non-tail mode. The body IS the block's value,
|
||||
// so it inherits `in_tail`. An assign's value is stored
|
||||
// into an alloca, not used as a tail value — walk
|
||||
// non-tail.
|
||||
Term::Mut { vars, body } => {
|
||||
for v in vars {
|
||||
if escapes(&v.init, tainted, false) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
escapes(body, tainted, in_tail)
|
||||
}
|
||||
Term::Assign { value, .. } => escapes(value, tainted, false),
|
||||
Term::Loop { binders, body } => {
|
||||
for b in binders {
|
||||
if escapes(&b.init, tainted, false) {
|
||||
@@ -526,31 +498,6 @@ fn collect_free_vars(t: &Term, bound: &mut BTreeSet<String>, out: &mut BTreeSet<
|
||||
collect_free_vars(source, bound, out);
|
||||
collect_free_vars(body, bound, out);
|
||||
}
|
||||
// Iter mut.1: a mut-var name binds inside the block. Var
|
||||
// inits before the binding see the outer scope; later inits
|
||||
// and the body see the var as bound. Mirrors `Term::Let`'s
|
||||
// add-then-walk-then-remove pattern, extended over N vars.
|
||||
Term::Mut { vars, body } => {
|
||||
let mut newly: Vec<String> = Vec::new();
|
||||
for v in vars {
|
||||
collect_free_vars(&v.init, bound, out);
|
||||
if bound.insert(v.name.clone()) {
|
||||
newly.push(v.name.clone());
|
||||
}
|
||||
}
|
||||
collect_free_vars(body, bound, out);
|
||||
for n in newly {
|
||||
bound.remove(&n);
|
||||
}
|
||||
}
|
||||
// Iter mut.1: the assigned `name` is a free var unless
|
||||
// bound by an enclosing `Term::Mut`.
|
||||
Term::Assign { name, value } => {
|
||||
if !bound.contains(name) {
|
||||
out.insert(name.clone());
|
||||
}
|
||||
collect_free_vars(value, bound, out);
|
||||
}
|
||||
Term::Loop { binders, body } => {
|
||||
let mut newly: Vec<String> = Vec::new();
|
||||
for b in binders {
|
||||
|
||||
@@ -85,12 +85,11 @@ impl<'a> Emitter<'a> {
|
||||
// hard internal error is right.
|
||||
// Tuple: (name, outer_ssa, llvm_type, ail_type, optional_fn_sig).
|
||||
//
|
||||
// Iter mut.4-tidy: post-Task-2 the typechecker rejects
|
||||
// lambda-captures-of-mut-var via
|
||||
// `CheckError::MutVarCapturedByLambda`. Reaching the `None`
|
||||
// arm below means typecheck was skipped or a bug let the AST
|
||||
// through — both are bugs in upstream layers, so panic rather
|
||||
// than return an Internal error.
|
||||
// The typechecker rejects lambda-captures-of-loop-binder via
|
||||
// `CheckError::LoopBinderCapturedByLambda`. Reaching the
|
||||
// `None` arm below means typecheck was skipped or a bug let
|
||||
// the AST through — both are bugs in upstream layers, so
|
||||
// panic rather than return an Internal error.
|
||||
let mut cap_meta: Vec<(String, String, String, Type, Option<FnSig>)> = Vec::new();
|
||||
for c in &captures {
|
||||
let (_, outer_ssa, lty, ail_ty) = self
|
||||
@@ -100,7 +99,7 @@ impl<'a> Emitter<'a> {
|
||||
.find(|(n, _, _, _)| n == c)
|
||||
.unwrap_or_else(|| {
|
||||
unreachable!(
|
||||
"lambda capture `{c}` not in locals — typecheck should have rejected via MutVarCapturedByLambda",
|
||||
"lambda capture `{c}` not in locals — typecheck should have rejected via LoopBinderCapturedByLambda",
|
||||
)
|
||||
})
|
||||
.clone();
|
||||
@@ -135,19 +134,19 @@ impl<'a> Emitter<'a> {
|
||||
let saved_moved = std::mem::take(&mut self.moved_slots);
|
||||
// Iter mut.3: a lambda thunk is its own fn frame for the
|
||||
// mut-var bookkeeping introduced in iter mut.3 — the outer
|
||||
// fn's mut-vars are not in scope inside the thunk (a lambda
|
||||
// body cannot reference enclosing mut-vars; the spec
|
||||
// disallows capture of mut-vars by lambdas via the
|
||||
// `mut_var_allocas` map being thunk-local). Save and reset
|
||||
// the three fields so the thunk emits its own hoisted-
|
||||
// alloca block at the thunk's entry, not the outer fn's.
|
||||
let saved_mut_allocas = std::mem::take(&mut self.mut_var_allocas);
|
||||
// fn's loop binders are not in scope inside the thunk (a
|
||||
// lambda body cannot reference enclosing loop binders; the
|
||||
// typechecker rejects such captures, and the
|
||||
// `binder_allocas` map is thunk-local). Save and reset the
|
||||
// three fields so the thunk emits its own hoisted-alloca
|
||||
// block at the thunk's entry, not the outer fn's.
|
||||
let saved_binder_allocas = std::mem::take(&mut self.binder_allocas);
|
||||
// loop-recur iter 3: a lambda thunk is its own fn frame —
|
||||
// a `recur` cannot target a loop enclosing the lambda
|
||||
// (iter-2 typecheck already rejects it; codegen resets so
|
||||
// the thunk's own loops work and the outer frames cannot
|
||||
// leak in). Save + reset (mem::take empties the Vec),
|
||||
// restored below alongside the mut.3 triple.
|
||||
// restored below alongside the binder-alloca map.
|
||||
let saved_loop_frames = std::mem::take(&mut self.loop_frames);
|
||||
let saved_pending_allocas = std::mem::take(&mut self.pending_entry_allocas);
|
||||
let saved_entry_marker = self.entry_block_end_marker.take();
|
||||
@@ -233,10 +232,10 @@ impl<'a> Emitter<'a> {
|
||||
self.body.push_str("}\n\n");
|
||||
}
|
||||
|
||||
// Iter mut.3: splice any hoisted mut-var allocas into the
|
||||
// thunk's entry block at the marker captured during the
|
||||
// thunk's `start_block("entry")` above. Empty `pending` is
|
||||
// a no-op (the thunk had no `Term::Mut` in its body).
|
||||
// Splice any hoisted loop-binder allocas into the thunk's
|
||||
// entry block at the marker captured during the thunk's
|
||||
// `start_block("entry")` above. Empty `pending` is a no-op
|
||||
// (the thunk had no `Term::Loop` in its body).
|
||||
if !self.pending_entry_allocas.is_empty() {
|
||||
let marker = self.entry_block_end_marker.ok_or_else(|| {
|
||||
CodegenError::Internal(
|
||||
@@ -261,8 +260,8 @@ impl<'a> Emitter<'a> {
|
||||
self.non_escape = saved_non_escape;
|
||||
self.moved_slots = saved_moved;
|
||||
self.current_param_modes = saved_param_modes;
|
||||
// Iter mut.3: restore outer fn's mut-var bookkeeping.
|
||||
self.mut_var_allocas = saved_mut_allocas;
|
||||
// Restore outer fn's loop-binder bookkeeping.
|
||||
self.binder_allocas = saved_binder_allocas;
|
||||
self.loop_frames = saved_loop_frames;
|
||||
self.pending_entry_allocas = saved_pending_allocas;
|
||||
self.entry_block_end_marker = saved_entry_marker;
|
||||
@@ -475,44 +474,6 @@ impl<'a> Emitter<'a> {
|
||||
Self::collect_captures(source, bound, captures, captures_set, builtins, top_level);
|
||||
Self::collect_captures(body, bound, captures, captures_set, builtins, top_level);
|
||||
}
|
||||
// Iter mut.1: a mut-var name binds inside the block. Var
|
||||
// inits before the binding see the outer scope; later
|
||||
// inits and the body see the var as bound. Mut-vars
|
||||
// cannot be captured by an inner lambda — the typecheck
|
||||
// pass rejects this via `CheckError::MutVarCapturedByLambda`
|
||||
// (added in mut.4-tidy). For the capture-collection scan
|
||||
// the var bindings are tracked identically to let-bound
|
||||
// names.
|
||||
Term::Mut { vars, body } => {
|
||||
let mut newly_bound: Vec<String> = Vec::new();
|
||||
for v in vars {
|
||||
Self::collect_captures(&v.init, bound, captures, captures_set, builtins, top_level);
|
||||
if bound.insert(v.name.clone()) {
|
||||
newly_bound.push(v.name.clone());
|
||||
}
|
||||
}
|
||||
Self::collect_captures(body, bound, captures, captures_set, builtins, top_level);
|
||||
for n in newly_bound {
|
||||
bound.remove(&n);
|
||||
}
|
||||
}
|
||||
Term::Assign { name, value, .. } => {
|
||||
// The assigned `name` is a use of a mut-var binding.
|
||||
// If it is bound in scope (it should be, by spec), it
|
||||
// is not a capture. Otherwise — well-formed inputs do
|
||||
// not produce this case (the typecheck pass at mut.2
|
||||
// rejects it via `MutAssignOutOfScope`); we
|
||||
// conservatively treat it as a possible capture so an
|
||||
// ill-formed input still flows through codegen.
|
||||
if !bound.contains(name)
|
||||
&& !builtins.contains(name.as_str())
|
||||
&& !top_level.contains(name)
|
||||
&& captures_set.insert(name.clone())
|
||||
{
|
||||
captures.push(name.clone());
|
||||
}
|
||||
Self::collect_captures(value, bound, captures, captures_set, builtins, top_level);
|
||||
}
|
||||
Term::Loop { binders, body } => {
|
||||
let mut newly_bound: Vec<String> = Vec::new();
|
||||
for b in binders {
|
||||
|
||||
@@ -715,36 +715,31 @@ struct Emitter<'a> {
|
||||
/// handed off ownership" signal that makes the dec safe.
|
||||
current_param_modes: BTreeMap<String, ParamMode>,
|
||||
/// Per-fn map of name → (alloca SSA name, AIL element type) for
|
||||
/// the two alloca-resident binding classes. Populated on entry
|
||||
/// to a `Term::Mut` block (iter mut.3 — mut-vars) AND on entry
|
||||
/// to a `Term::Loop` (iter loop-recur.3 — loop binders ride the
|
||||
/// same alloca representation; the `Term::Loop` arm inserts each
|
||||
/// binder here and `recur` stores into the slot). Entries are
|
||||
/// alloca-resident loop binders. Populated on entry to a
|
||||
/// `Term::Loop` (iter loop-recur.3): the `Term::Loop` arm inserts
|
||||
/// each binder here and `recur` stores into the slot. Entries are
|
||||
/// removed (or restored to their prior binding) on block exit.
|
||||
/// Consulted by the `Term::Var` arm of `lower_term` before
|
||||
/// `self.locals` — the single load path serving both classes.
|
||||
/// Note: only the mut-var half mirrors a typecheck-side
|
||||
/// `mut_scope_stack`; loop binders are tracked typecheck-side in
|
||||
/// the ordinary `locals` plus `loop_stack` (positional), not in
|
||||
/// `mut_scope_stack` — the shared codegen map is a
|
||||
/// `self.locals` — the single load path for loop binders. Loop
|
||||
/// binders are tracked typecheck-side in the ordinary `locals`
|
||||
/// plus `loop_stack` (positional); this codegen map is a
|
||||
/// representation detail, not a scope-precedence claim.
|
||||
mut_var_allocas: BTreeMap<String, (String, Type)>,
|
||||
binder_allocas: BTreeMap<String, (String, Type)>,
|
||||
/// loop-recur iter 3: stack of enclosing `Term::Loop` codegen
|
||||
/// frames. Each frame is `(header_block_label, binder_slots)`
|
||||
/// where `binder_slots` is `(binder_name, alloca_ssa, llvm_ty)`
|
||||
/// in declaration order. Pushed on `Term::Loop` body entry,
|
||||
/// popped on exit; `Term::Recur` reads `.last()` for its target
|
||||
/// header + the binder allocas to store into. Saved/reset/
|
||||
/// restored at the lambda-lowering boundary exactly as the
|
||||
/// mut.3 `mut_var_allocas` triple (a `recur` cannot cross a
|
||||
/// lambda boundary).
|
||||
/// restored at the lambda-lowering boundary alongside
|
||||
/// `binder_allocas` (a `recur` cannot cross a lambda boundary).
|
||||
loop_frames: Vec<(String, Vec<(String, String, String)>)>,
|
||||
/// Iter mut.3: side buffer for `alloca` instructions emitted
|
||||
/// during body lowering but hoisted to the fn's entry block.
|
||||
/// Flushed once into `self.body` at `entry_block_end_marker`
|
||||
/// after `lower_term` completes. Entry-block placement is
|
||||
/// what makes the allocas mem2reg-eligible regardless of how
|
||||
/// deeply nested the originating `Term::Mut` block is.
|
||||
/// Side buffer for `alloca` instructions emitted during body
|
||||
/// lowering but hoisted to the fn's entry block. Flushed once
|
||||
/// into `self.body` at `entry_block_end_marker` after
|
||||
/// `lower_term` completes. Entry-block placement is what makes
|
||||
/// the loop-binder allocas mem2reg-eligible regardless of how
|
||||
/// deeply nested the originating `Term::Loop` is.
|
||||
pending_entry_allocas: String,
|
||||
/// Iter mut.3: byte position in `self.body` immediately after
|
||||
/// the `entry:\n` label, captured during `start_block("entry")`
|
||||
@@ -868,7 +863,7 @@ impl<'a> Emitter<'a> {
|
||||
closure_drops: BTreeMap::new(),
|
||||
moved_slots: BTreeMap::new(),
|
||||
current_param_modes: BTreeMap::new(),
|
||||
mut_var_allocas: BTreeMap::new(),
|
||||
binder_allocas: BTreeMap::new(),
|
||||
loop_frames: Vec::new(),
|
||||
pending_entry_allocas: String::new(),
|
||||
entry_block_end_marker: None,
|
||||
@@ -1070,11 +1065,11 @@ impl<'a> Emitter<'a> {
|
||||
// consulted by `lower_match`'s Iter A gate to skip arm-close
|
||||
// pattern-binder dec when the scrutinee is a non-Own param.
|
||||
self.current_param_modes.clear();
|
||||
// Iter mut.3: per-fn-body mut-var bookkeeping. The map is
|
||||
// populated/restored by the `Term::Mut` arm of `lower_term`;
|
||||
// Per-fn-body loop-binder bookkeeping. The map is
|
||||
// populated/restored by the `Term::Loop` arm of `lower_term`;
|
||||
// the side buffer collects alloca instructions for hoisting;
|
||||
// the marker is captured by `start_block("entry")` below.
|
||||
self.mut_var_allocas.clear();
|
||||
self.binder_allocas.clear();
|
||||
self.pending_entry_allocas.clear();
|
||||
self.entry_block_end_marker = None;
|
||||
for (i, pname) in f.params.iter().enumerate() {
|
||||
@@ -1394,15 +1389,14 @@ impl<'a> Emitter<'a> {
|
||||
self.block_terminated = true;
|
||||
return Ok(("0".into(), "i8".into()));
|
||||
}
|
||||
// Iter mut.3: mut-vars take precedence over let-bound
|
||||
// / param resolutions, symmetric to the typecheck-side
|
||||
// mut-scope-stack walk in `synth`'s `Term::Var` arm.
|
||||
// Shadowing across nested mut blocks is handled by the
|
||||
// `Term::Mut` arm's push-and-restore on the BTreeMap
|
||||
// entry, so a single `get` suffices here (the entry
|
||||
// visible at this point is the innermost binding).
|
||||
// Loop binders take precedence over let-bound / param
|
||||
// resolutions. Shadowing across nested loops is
|
||||
// handled by the `Term::Loop` arm's push-and-restore
|
||||
// on the BTreeMap entry, so a single `get` suffices
|
||||
// here (the entry visible at this point is the
|
||||
// innermost binding).
|
||||
if let Some((alloca_name, ail_ty)) =
|
||||
self.mut_var_allocas.get(name).cloned()
|
||||
self.binder_allocas.get(name).cloned()
|
||||
{
|
||||
let llvm_ty = llvm_type(&ail_ty)?;
|
||||
let load_ssa = self.fresh_ssa();
|
||||
@@ -1763,107 +1757,11 @@ impl<'a> Emitter<'a> {
|
||||
};
|
||||
self.lower_reuse_as_rc(source, body_type_name, body_ctor, body_args)
|
||||
}
|
||||
// Iter mut.3: lowering of a `Term::Mut` block. Each var
|
||||
// gets an `alloca` emitted into the entry-block side
|
||||
// buffer (hoisted; mem2reg-eligible regardless of nesting
|
||||
// depth) plus a `store` of its init at the current body
|
||||
// position. The map binding (name → alloca, AIL type) is
|
||||
// installed AFTER the init is lowered, mirroring the
|
||||
// typecheck-side ordering in `synth`'s `Term::Mut` arm:
|
||||
// a var's own init does NOT see its own binding (uses
|
||||
// the outer scope plus any earlier vars of the same
|
||||
// block, which ARE bound because we install them as we
|
||||
// go through the loop). On block exit we restore the
|
||||
// prior binding (if any) — innermost-wins shadowing
|
||||
// across nested mut blocks.
|
||||
Term::Mut { vars, body } => {
|
||||
let mut saved: Vec<(String, Option<(String, Type)>)> = Vec::new();
|
||||
for v in vars {
|
||||
let alloca_name = format!("%mut_{}_{}", v.name, self.fresh_id());
|
||||
let llvm_ty = llvm_type(&v.ty)?;
|
||||
// Hoisted alloca — goes into the side buffer,
|
||||
// spliced into the entry block at the end of
|
||||
// emit_fn.
|
||||
self.pending_entry_allocas.push_str(&format!(
|
||||
" {alloca_name} = alloca {llvm_ty}\n"
|
||||
));
|
||||
// Init runs in the outer scope plus any vars
|
||||
// already pushed into `saved` (i.e. earlier vars
|
||||
// of this same `Term::Mut`), mirroring the
|
||||
// typecheck-side ordering at lib.rs:3578.
|
||||
let (init_ssa, init_ty) = self.lower_term(&v.init)?;
|
||||
if init_ty != llvm_ty {
|
||||
return Err(CodegenError::Internal(format!(
|
||||
"Term::Mut var `{}`: init LLVM type {init_ty} != declared {llvm_ty}",
|
||||
v.name
|
||||
)));
|
||||
}
|
||||
self.body.push_str(&format!(
|
||||
" store {llvm_ty} {init_ssa}, ptr {alloca_name}\n"
|
||||
));
|
||||
let prior = self
|
||||
.mut_var_allocas
|
||||
.insert(v.name.clone(), (alloca_name, v.ty.clone()));
|
||||
saved.push((v.name.clone(), prior));
|
||||
}
|
||||
let body_result = self.lower_term(body);
|
||||
// Restore prior bindings — done unconditionally so an
|
||||
// error during body lowering doesn't leak inner-frame
|
||||
// entries into the outer scope (defensive; `lower_term`
|
||||
// returns `Result`, and we propagate after restoring).
|
||||
for (name, prior) in saved.into_iter().rev() {
|
||||
match prior {
|
||||
Some(p) => {
|
||||
self.mut_var_allocas.insert(name, p);
|
||||
}
|
||||
None => {
|
||||
self.mut_var_allocas.remove(&name);
|
||||
}
|
||||
}
|
||||
}
|
||||
body_result
|
||||
}
|
||||
// Iter mut.3: lowering of a `Term::Assign`. Resolves the
|
||||
// target name through `mut_var_allocas` (the typecheck
|
||||
// pass has already pinned the lexical-scope invariant,
|
||||
// so a miss here is an internal error — the assign would
|
||||
// have been rejected with `MutAssignOutOfScope`). The
|
||||
// assign's static type is Unit; we return the canonical
|
||||
// Unit SSA form `("0", "i8")` matching `Literal::Unit`
|
||||
// in this same `match` (line ~1289).
|
||||
Term::Assign { name, value } => {
|
||||
let (alloca_name, ail_ty) = self
|
||||
.mut_var_allocas
|
||||
.get(name)
|
||||
.cloned()
|
||||
.ok_or_else(|| {
|
||||
CodegenError::Internal(format!(
|
||||
"Term::Assign `{name}` reached codegen without a mut-var alloca \
|
||||
— typecheck should have rejected this earlier"
|
||||
))
|
||||
})?;
|
||||
let llvm_ty = llvm_type(&ail_ty)?;
|
||||
let (value_ssa, value_ty) = self.lower_term(value)?;
|
||||
if value_ty != llvm_ty {
|
||||
return Err(CodegenError::Internal(format!(
|
||||
"Term::Assign `{name}`: value LLVM type {value_ty} != declared {llvm_ty}"
|
||||
)));
|
||||
}
|
||||
self.body.push_str(&format!(
|
||||
" store {llvm_ty} {value_ssa}, ptr {alloca_name}\n"
|
||||
));
|
||||
Ok(("0".into(), "i8".into()))
|
||||
}
|
||||
// loop-recur iter 1: real codegen (loop-header block,
|
||||
// per-binder phi, back-edge br) lands in iter 3. This
|
||||
// iter stubs the dispatch so the workspace compiles; no
|
||||
// loop/recur program is codegen'd in iter 1. Mirrors
|
||||
// mut.1's lower_term stub.
|
||||
Term::Loop { binders, body } => {
|
||||
// loop-recur iter 3: loop binders are loop-carried
|
||||
// values lowered as entry-block allocas (the mut.3
|
||||
// values lowered as entry-block allocas (the
|
||||
// `pending_entry_allocas` mechanism) registered in
|
||||
// `mut_var_allocas` so the existing `Term::Var` load
|
||||
// `binder_allocas` so the existing `Term::Var` load
|
||||
// path resolves them with zero new Var code
|
||||
// (representation-sharing; Boss calls 1+2). The loop
|
||||
// header is a fresh block reached by an
|
||||
@@ -1896,7 +1794,7 @@ impl<'a> Emitter<'a> {
|
||||
" store {llvm_ty} {init_ssa}, ptr {alloca_name}\n"
|
||||
));
|
||||
let prior = self
|
||||
.mut_var_allocas
|
||||
.binder_allocas
|
||||
.insert(b.name.clone(), (alloca_name.clone(), b.ty.clone()));
|
||||
saved.push((b.name.clone(), prior));
|
||||
frame_slots.push((b.name.clone(), alloca_name, llvm_ty));
|
||||
@@ -1906,15 +1804,15 @@ impl<'a> Emitter<'a> {
|
||||
self.start_block(&header);
|
||||
let body_result = self.lower_term(body);
|
||||
self.loop_frames.pop();
|
||||
// Restore prior bindings unconditionally (mirrors the
|
||||
// mut.3 `Term::Mut` arm — defensive on the `?` path).
|
||||
// Restore prior bindings unconditionally (defensive
|
||||
// on the `?` path).
|
||||
for (name, prior) in saved.into_iter().rev() {
|
||||
match prior {
|
||||
Some(p) => {
|
||||
self.mut_var_allocas.insert(name, p);
|
||||
self.binder_allocas.insert(name, p);
|
||||
}
|
||||
None => {
|
||||
self.mut_var_allocas.remove(&name);
|
||||
self.binder_allocas.remove(&name);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1924,11 +1822,10 @@ impl<'a> Emitter<'a> {
|
||||
// loop-recur iter 3: re-enter the innermost
|
||||
// enclosing loop. Typecheck (iter 2) pinned arity ==
|
||||
// binder count and per-arg type == binder type, so a
|
||||
// miss/mismatch here is an internal error (the
|
||||
// `Term::Assign`-arm precedent). ALL args are lowered
|
||||
// to SSA values BEFORE any store, so a recur arg that
|
||||
// reads a binder sees its OLD value (recur is a
|
||||
// simultaneous positional rebind, e.g.
|
||||
// miss/mismatch here is an internal error. ALL args
|
||||
// are lowered to SSA values BEFORE any store, so a
|
||||
// recur arg that reads a binder sees its OLD value
|
||||
// (recur is a simultaneous positional rebind, e.g.
|
||||
// `(recur (+ acc i) (+ i 1))`). The back-edge `br` is
|
||||
// a block terminator: set `block_terminated` at
|
||||
// recur's OWN emit site (the parallel setter — Boss
|
||||
@@ -1937,8 +1834,7 @@ impl<'a> Emitter<'a> {
|
||||
// exactly like a tail-app block. recur never falls
|
||||
// through; its value is never used — return the
|
||||
// canonical Unit/dead SSA (`("0","i8")`, the
|
||||
// `Term::Assign` / both-if-branches-terminated
|
||||
// precedent).
|
||||
// both-if-branches-terminated precedent).
|
||||
let (header, slots) = self
|
||||
.loop_frames
|
||||
.last()
|
||||
@@ -3011,7 +2907,7 @@ impl<'a> Emitter<'a> {
|
||||
return Ok(ty.clone());
|
||||
}
|
||||
}
|
||||
if let Some((_, ail_ty)) = self.mut_var_allocas.get(name) {
|
||||
if let Some((_, ail_ty)) = self.binder_allocas.get(name) {
|
||||
return Ok(ail_ty.clone());
|
||||
}
|
||||
if let Some((_, _, _, ail)) =
|
||||
@@ -3228,19 +3124,10 @@ impl<'a> Emitter<'a> {
|
||||
// type. The source is dropped at codegen.
|
||||
self.synth_with_extras(body, extras)
|
||||
}
|
||||
// Iter mut.1: `Term::Mut`'s static type is the body's
|
||||
// static type (spec §"JSON-AST schema"). `Term::Assign`'s
|
||||
// is Unit (spec §"Form A surface"). These won't be hit
|
||||
// along the shipping codegen path because the dispatcher
|
||||
// at `lower_term` stubs both — but `synth_with_extras`
|
||||
// is exhaustive on Term so the arms must exist.
|
||||
Term::Mut { body, .. } => self.synth_with_extras(body, extras),
|
||||
Term::Assign { .. } => Ok(Type::unit()),
|
||||
// loop-recur iter 1: a Term::Loop's static type is the
|
||||
// body's type (mirror Term::Mut). Term::Recur does not
|
||||
// fall through; a Unit stub is safe — this arm is never
|
||||
// hit on the shipping path because lower_term stubs
|
||||
// Loop/Recur, and iter 1 codegens no loop/recur program.
|
||||
// A `Term::Loop`'s static type is the body's type.
|
||||
// `Term::Recur` does not fall through; a Unit stub is
|
||||
// safe (recur transfers control and never produces a
|
||||
// value at its own position).
|
||||
Term::Loop { body, .. } => self.synth_with_extras(body, extras),
|
||||
Term::Recur { .. } => Ok(Type::unit()),
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user