From 07f080256cc391cf070087fc005fd258363a0570 Mon Sep 17 00:00:00 2001 From: Brummel Date: Mon, 18 May 2026 11:06:17 +0200 Subject: [PATCH] iter remove-mut-var-assign.1: atomic removal of mut/var/assign MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- ...26-05-18-iter-remove-mut-var-assign.1.json | 13 + crates/ail/src/main.rs | 69 -- .../tests/codegen_import_map_fallback_pin.rs | 13 +- crates/ail/tests/ct1_check_cli.rs | 49 +- crates/ail/tests/e2e.rs | 13 +- crates/ailang-check/src/builtins.rs | 8 +- crates/ailang-check/src/lib.rs | 751 ++---------------- crates/ailang-check/src/lift.rs | 34 +- crates/ailang-check/src/linearity.rs | 33 - crates/ailang-check/src/mono.rs | 40 +- .../src/pre_desugar_validation.rs | 13 - crates/ailang-check/src/reuse_shape.rs | 12 - crates/ailang-check/src/uniqueness.rs | 16 - .../tests/loop_recur_typecheck_pin.rs | 17 +- .../ailang-check/tests/mut_typecheck_pin.rs | 69 -- crates/ailang-codegen/src/escape.rs | 53 -- crates/ailang-codegen/src/lambda.rs | 79 +- crates/ailang-codegen/src/lib.rs | 199 +---- crates/ailang-core/src/ast.rs | 129 +-- crates/ailang-core/src/desugar.rs | 194 +---- crates/ailang-core/src/workspace.rs | 23 - .../ailang-core/tests/carve_out_inventory.rs | 22 +- .../ailang-core/tests/design_schema_drift.rs | 32 - crates/ailang-core/tests/schema_coverage.rs | 16 - crates/ailang-core/tests/spec_drift.rs | 16 - crates/ailang-prose/src/lib.rs | 101 +-- crates/ailang-surface/src/parse.rs | 172 +--- crates/ailang-surface/src/print.rs | 49 +- .../ailang-surface/tests/mut_removed_pin.rs | 31 + docs/DESIGN.md | 60 +- ...2026-05-18-iter-remove-mut-var-assign.1.md | 63 ++ docs/journals/INDEX.md | 1 + docs/roadmap.md | 19 +- examples/fieldtest/mut-local_1_factorial.ail | 25 +- .../fieldtest/mut-local_2_classify_temp.ail | 31 +- examples/fieldtest/mut-local_3_horner.ail | 30 +- .../mut-local_4_has_small_factor.ail | 25 +- .../mut-local_5_lambda_capture_probe.ail | 39 - examples/fieldtest/mut-local_6_diag_probe.ail | 18 - examples/mut.ail | 49 +- examples/mut_counter.ail | 8 +- examples/mut_sum_floats.ail | 8 +- .../test_mut_assign_out_of_scope.ail.json | 38 - examples/test_mut_assign_outside_mut.ail.json | 24 - .../test_mut_assign_type_mismatch.ail.json | 38 - .../test_mut_nested_shadow_legal.ail.json | 40 - .../test_mut_var_captured_by_lambda.ail.json | 42 - .../test_mut_var_unsupported_type.ail.json | 30 - 48 files changed, 331 insertions(+), 2523 deletions(-) create mode 100644 bench/orchestrator-stats/2026-05-18-iter-remove-mut-var-assign.1.json delete mode 100644 crates/ailang-check/tests/mut_typecheck_pin.rs create mode 100644 crates/ailang-surface/tests/mut_removed_pin.rs create mode 100644 docs/journals/2026-05-18-iter-remove-mut-var-assign.1.md delete mode 100644 examples/fieldtest/mut-local_5_lambda_capture_probe.ail delete mode 100644 examples/fieldtest/mut-local_6_diag_probe.ail delete mode 100644 examples/test_mut_assign_out_of_scope.ail.json delete mode 100644 examples/test_mut_assign_outside_mut.ail.json delete mode 100644 examples/test_mut_assign_type_mismatch.ail.json delete mode 100644 examples/test_mut_nested_shadow_legal.ail.json delete mode 100644 examples/test_mut_var_captured_by_lambda.ail.json delete mode 100644 examples/test_mut_var_unsupported_type.ail.json diff --git a/bench/orchestrator-stats/2026-05-18-iter-remove-mut-var-assign.1.json b/bench/orchestrator-stats/2026-05-18-iter-remove-mut-var-assign.1.json new file mode 100644 index 0000000..eae2f86 --- /dev/null +++ b/bench/orchestrator-stats/2026-05-18-iter-remove-mut-var-assign.1.json @@ -0,0 +1,13 @@ +{ + "iter_id": "remove-mut-var-assign.1", + "date": "2026-05-18", + "mode": "standard", + "outcome": "DONE", + "tasks_total": 6, + "tasks_completed": 6, + "reloops_per_task": { "1": 0, "2": 0, "3": 0, "4": 0, "5": 0, "6": 0 }, + "review_loops_spec": 0, + "review_loops_quality": 0, + "blocked_reason": null, + "notes": "Atomic feature-removal. Zero review re-loops. Recurring recon-undercount class (feedback_plan_pseudo_vs_reality): the plan/recon under-enumerated test sites that construct deleted types — in-source mod tests + a drift-pin test fn + 5 orphaned carve-out .ail.json + 2 non-enumerated test helpers (one a hard E0599 the recon missed) — all resolved within implementer-phase remit per Task-3's identical whole-file-deletion rationale, recorded as DONE_WITH_CONCERNS. Final cargo test --workspace 605/0; loop/recur non-regression hard gates all green." +} diff --git a/crates/ail/src/main.rs b/crates/ail/src/main.rs index 720e0f3..18798d5 100644 --- a/crates/ail/src/main.rs +++ b/crates/ail/src/main.rs @@ -1513,37 +1513,6 @@ fn walk_term( walk_term(source, out, builtins, scope); walk_term(body, out, builtins, scope); } - // Iter mut.1: each `var` introduces a new lexical binding; - // its `init` is in the outer scope plus already-declared - // vars; the body sees all of them. Mirrors `Term::Lam`'s - // newly-bound roll-back convention. - Term::Mut { vars, body } => { - let mut newly = Vec::new(); - for v in vars { - walk_term(&v.init, out, builtins, scope); - if scope.insert(v.name.clone()) { - newly.push(v.name.clone()); - } - } - walk_term(body, out, builtins, scope); - for n in newly { - scope.remove(&n); - } - } - // Iter mut.1: the `value` is walked; the assigned `name` is - // a use of an in-scope mut-var. If the user wrote an - // out-of-scope assign, it surfaces as an unknown identifier - // in the dep walk, which is acceptable for the CLI's - // dependency view (typecheck would already have rejected). - Term::Assign { name, value } => { - if !name.contains('.') - && !scope.contains(name) - && !builtins.contains(name.as_str()) - { - out.insert(name.clone()); - } - walk_term(value, out, builtins, scope); - } Term::Loop { binders, body } => { let mut newly = Vec::new(); for b in binders { @@ -2765,44 +2734,6 @@ fn rewrite_def( changed, ); } - // Iter mut.1: rewrite types embedded in each `MutVar.ty` - // (via `rewrite_type` — mut-vars carry full Type - // annotations) and recurse into each var's `init` and - // the block's body. `Term::Assign` has no embedded type. - Term::Mut { vars, body } => { - for v in vars { - rewrite_type( - &mut v.ty, - owning_module, - local_types, - import_names, - changed, - ); - rewrite_term( - &mut v.init, - owning_module, - local_types, - import_names, - changed, - ); - } - rewrite_term( - body, - owning_module, - local_types, - import_names, - changed, - ); - } - Term::Assign { value, .. } => { - rewrite_term( - value, - owning_module, - local_types, - import_names, - changed, - ); - } Term::Loop { binders, body } => { for b in binders { rewrite_type( diff --git a/crates/ail/tests/codegen_import_map_fallback_pin.rs b/crates/ail/tests/codegen_import_map_fallback_pin.rs index 89e2256..9824a18 100644 --- a/crates/ail/tests/codegen_import_map_fallback_pin.rs +++ b/crates/ail/tests/codegen_import_map_fallback_pin.rs @@ -91,20 +91,9 @@ fn synthesised_print_uses_user_module_show_via_fallback() { Term::ReuseAs { source, body } => { contains_xmod_show_var(source) || contains_xmod_show_var(body) } - // Iter mut.1: a `Term::Mut` cannot itself host a - // `show_user_adt.show__` reference (its body is a - // mut-block, not a synthesised polymorphic call), but - // recurse defensively so any nested reference inside a - // var init / body / assign-value still surfaces. - Term::Mut { vars, body } => { - vars.iter().any(|v| contains_xmod_show_var(&v.init)) - || contains_xmod_show_var(body) - } - Term::Assign { value, .. } => contains_xmod_show_var(value), // loop-recur iter 1: a `Term::Loop` cannot itself host a // synthesised cross-module reference, but recurse - // defensively through binder inits / body / recur args, - // mirroring the `Term::Mut` arm above. + // defensively through binder inits / body / recur args. Term::Loop { binders, body } => { binders.iter().any(|b| contains_xmod_show_var(&b.init)) || contains_xmod_show_var(body) diff --git a/crates/ail/tests/ct1_check_cli.rs b/crates/ail/tests/ct1_check_cli.rs index e924b51..0abcff2 100644 --- a/crates/ail/tests/ct1_check_cli.rs +++ b/crates/ail/tests/ct1_check_cli.rs @@ -187,48 +187,23 @@ fn check_ordering_match_post_migration_is_clean() { ); } -/// Property (RED — fieldtest mut-local F2): in non-JSON (human) mode, -/// the human-stderr formatter prepends the diagnostic code exactly once -/// as the canonical `[code]` bracket prefix (the cli-diag-human format). -/// The four mut-local `CheckError` variants -/// (`mut-assign-out-of-scope`, `assign-type-mismatch`, -/// `mut-var-unsupported-type`, `mut-var-captured-by-lambda`) must NOT -/// also embed `[code] ` inside their `thiserror` Display body, which -/// would render the bracketed code TWICE in the user-visible stderr -/// line (`error: [code] fn: [code] message`). +/// Property: in non-JSON (human) mode, the human-stderr formatter +/// prepends the diagnostic code exactly once as the canonical +/// `[code]` bracket prefix (the cli-diag-human format). The +/// `loop-binder-captured-by-lambda` `CheckError` Display body must +/// NOT also embed `[code] ` inside its `thiserror` Display body, +/// which would render the bracketed code TWICE in the user-visible +/// stderr line (`error: [code] fn: [code] message`). /// /// This pins the *observable* doubling on the real CLI human path /// (`crates/ail/src/main.rs` non-JSON `Cmd::Check` arm), not the raw -/// Display string of one variant in isolation: it counts occurrences -/// of the literal `[]` token in the rendered stderr and requires -/// exactly one. Drops to GREEN once the four Display bodies shed their -/// leading `[] ` prefix (the formatter's prefix then supplies it -/// once). The non-mut variants are unaffected because they never -/// embedded the bracket. +/// Display string of the variant in isolation: it counts occurrences +/// of the literal `[]` token in the rendered stderr and +/// requires exactly one (the formatter supplies it; the Display body +/// must not also embed it). #[test] -fn check_human_mode_renders_mut_diagnostic_code_exactly_once() { - // (fixture filename, kebab diagnostic code) for the four mut-local - // negative fixtures shipped by the mut-local milestone. +fn check_human_mode_renders_loop_binder_diagnostic_code_exactly_once() { let cases = [ - ( - "test_mut_assign_out_of_scope.ail.json", - "mut-assign-out-of-scope", - ), - ( - "test_mut_assign_type_mismatch.ail.json", - "assign-type-mismatch", - ), - ( - "test_mut_var_unsupported_type.ail.json", - "mut-var-unsupported-type", - ), - ( - "test_mut_var_captured_by_lambda.ail.json", - "mut-var-captured-by-lambda", - ), - // Iter loop-recur.tidy: symmetric loop-binder-capture variant. - // Its Display body is likewise bracket-`[code]`-free so the - // human formatter supplies `[]` exactly once. ( "test_loop_binder_captured_by_lambda.ail.json", "loop-binder-captured-by-lambda", diff --git a/crates/ail/tests/e2e.rs b/crates/ail/tests/e2e.rs index 1c4e425..418f7a6 100644 --- a/crates/ail/tests/e2e.rs +++ b/crates/ail/tests/e2e.rs @@ -2853,20 +2853,17 @@ fn str_clone_cross_realisation_uniform_abi() { assert_eq!(live, 0, "no leaked heap-Str slabs allowed; live={live}"); } -/// Iter mut.3: `examples/mut_counter.ail` exercises `Term::Mut` + -/// `Term::Assign` codegen with an `Int` mut-var. The body -/// `(mut (var sum Int 0) (assign sum (sum_helper 1 10 0)) sum)` -/// stores the helper's result into `sum`'s alloca and prints the -/// loaded value. End-to-end gate for the entry-block-hoist / -/// store / load lowering. +/// `examples/mut_counter.ail` sums 1..10 via a tail-recursive +/// `sum_helper` and prints the result. Behaviour-preservation gate: +/// the fixture's value assertion stays `55` after the let/if rewrite, +/// proving the rewrite loses no expressivity. #[test] fn mut_counter_prints_55() { let stdout = build_and_run("mut_counter.ail"); assert_eq!(stdout.trim(), "55", "mut_counter must print 55, got {stdout:?}"); } -/// Iter mut.3: Float twin of `mut_counter_prints_55`. The mut-var -/// is `Float`, init is `0.0`, the recursive helper returns the +/// Float twin of `mut_counter_prints_55`: `sum_helper` returns the /// sum 1.0+...+10.0 = 55.0. The polymorphic `print` routes through /// `Show Float.show` → `float_to_str`'s libc `%g` formatter, which /// strips the trailing `.0` — so the canonical stdout for Float diff --git a/crates/ailang-check/src/builtins.rs b/crates/ailang-check/src/builtins.rs index 62e8d09..bf289b1 100644 --- a/crates/ailang-check/src/builtins.rs +++ b/crates/ailang-check/src/builtins.rs @@ -342,7 +342,6 @@ mod tests { let mut env = Env::default(); install(&mut env); let mut locals: IndexMap = IndexMap::new(); - let mut mut_scope_stack: Vec> = Vec::new(); let mut effects: BTreeSet = BTreeSet::new(); let mut subst = Subst::default(); let mut counter: u32 = 0; @@ -350,13 +349,12 @@ mod tests { let mut free_fn_calls = Vec::new(); let mut warnings: Vec = Vec::new(); // loop-recur iter 2: test helper synths one term from top-of- - // body — fresh empty loop-stack (mirrors mut_scope_stack). + // body — fresh empty loop-stack. let mut loop_stack: Vec> = Vec::new(); let ty = crate::synth( t, &env, &mut locals, - &mut mut_scope_stack, &mut loop_stack, &mut effects, "", @@ -589,7 +587,6 @@ mod tests { let mut env = Env::default(); install(&mut env); let mut locals: IndexMap = IndexMap::new(); - let mut mut_scope_stack: Vec> = Vec::new(); let mut effects: BTreeSet = BTreeSet::new(); let mut subst = Subst::default(); let mut counter: u32 = 0; @@ -597,13 +594,12 @@ mod tests { let mut free_fn_calls = Vec::new(); let mut warnings: Vec = Vec::new(); // loop-recur iter 2: test helper synths one term from top-of- - // body — fresh empty loop-stack (mirrors mut_scope_stack). + // body — fresh empty loop-stack. let mut loop_stack: Vec> = Vec::new(); let err = crate::synth( &term, &env, &mut locals, - &mut mut_scope_stack, &mut loop_stack, &mut effects, "", diff --git a/crates/ailang-check/src/lib.rs b/crates/ailang-check/src/lib.rs index 8b93375..332cb42 100644 --- a/crates/ailang-check/src/lib.rs +++ b/crates/ailang-check/src/lib.rs @@ -243,24 +243,6 @@ pub(crate) fn substitute_rigids_in_term(t: &Term, mapping: &BTreeMap Term::Mut { - vars: vars - .iter() - .map(|v| ailang_core::ast::MutVar { - name: v.name.clone(), - ty: substitute_rigids(&v.ty, mapping), - init: substitute_rigids_in_term(&v.init, mapping), - }) - .collect(), - body: Box::new(substitute_rigids_in_term(body, mapping)), - }, - Term::Assign { name, value } => Term::Assign { - name: name.clone(), - value: Box::new(substitute_rigids_in_term(value, mapping)), - }, Term::Loop { binders, body } => Term::Loop { binders: binders .iter() @@ -648,61 +630,12 @@ pub enum CheckError { name: String, }, - /// Iter mut.2: `Term::Assign { name, .. }` reached typecheck but - /// `name` is not a mut-var of any enclosing `Term::Mut` on the - /// lexical scope stack. - /// - /// `available` lists every mut-var name visible at the assign - /// site, flattened across all enclosing mut-frames innermost- - /// first, deduplicated, preserving order — so the diagnostic - /// surfaces "you might have meant one of: X / Y / Z" without - /// duplicates when the same name appears at multiple nesting - /// depths. - #[error("cannot assign to `{name}` here — not declared as a mut-var in the enclosing (mut ...) block. Available mut-vars in scope: [{}]", available.join(", "))] - MutAssignOutOfScope { - name: String, - available: Vec, - }, - - /// Iter mut.2: `Term::Assign { name, value }` reached typecheck - /// inside a valid mut-scope, but `value`'s synth type differs - /// from the var's declared type. - #[error("cannot assign `{got}` to mut-var `{name} : {declared}` — the assigned value's type must match the var's declared type")] - AssignTypeMismatch { - name: String, - declared: String, - got: String, - }, - - /// Iter mut.2: `Term::Mut { vars, .. }` declared a `MutVar` - /// whose declared `ty` is not one of the four supported scalar - /// primitives (Int, Float, Bool, Unit). The temporary - /// restriction is named explicitly in spec - /// `docs/specs/2026-05-15-mut-local.md` §"Out of scope". - #[error("mut-var `{name} : {ty}` is not supported in this milestone — only stack-resident primitive types (Int, Float, Bool, Unit) may be declared as mut-vars. Heap-RC-managed types (Str, ADTs, fn-values) are deferred to a follow-on milestone.")] - UnsupportedMutVarType { - name: String, - ty: String, - }, - - /// Iter mut.4-tidy: `Term::Lam` reached typecheck inside a - /// `Term::Mut` scope, and the lambda's body references a mut-var - /// declared in that enclosing scope. Spec - /// `docs/specs/2026-05-15-mut-local.md` §"Out of scope": mut-vars - /// cannot escape — they are alloca-resident and lexically scoped. - /// Capturing one into a closure would require either lifting it - /// to the heap (deferred milestone) or rejecting the capture. - /// This milestone takes the latter route. - #[error("mut-var `{name}` cannot be captured by a lambda — mut-vars are alloca-resident and do not escape their enclosing mut block. Either move the lambda outside the mut block, or restructure to pass `{name}` as a lambda parameter (deferring escape to a future milestone with ref-types and the !Mut effect).")] - MutVarCapturedByLambda { name: String }, - /// loop-recur iter loop-recur.tidy: `Term::Lam` reached typecheck /// inside a lexically-enclosing `Term::Loop`, and the lambda's body /// references a loop binder declared by that enclosing loop. Loop /// binders are alloca-resident (codegen reuses the - /// `mut_var_allocas` registry, `std::mem::take`-emptied at the - /// lambda frame boundary), exactly the mut-var escape rule. Symmetric - /// to `MutVarCapturedByLambda`: capturing one into a closure would + /// `binder_allocas` registry, `std::mem::take`-emptied at the + /// lambda frame boundary). Capturing one into a closure would /// require lifting it to the heap (deferred) or rejecting the /// capture; this milestone takes the latter route rather than /// panicking codegen on type-correct input. @@ -783,10 +716,6 @@ impl CheckError { CheckError::NoInstance { .. } => "no-instance", CheckError::AmbiguousMethodResolution { .. } => "ambiguous-method-resolution", CheckError::UnknownClass { .. } => "unknown-class", - CheckError::MutAssignOutOfScope { .. } => "mut-assign-out-of-scope", - CheckError::AssignTypeMismatch { .. } => "assign-type-mismatch", - CheckError::UnsupportedMutVarType { .. } => "mut-var-unsupported-type", - CheckError::MutVarCapturedByLambda { .. } => "mut-var-captured-by-lambda", CheckError::LoopBinderCapturedByLambda { .. } => "loop-binder-captured-by-lambda", CheckError::RecurOutsideLoop => "recur-outside-loop", CheckError::RecurArityMismatch { .. } => "recur-arity-mismatch", @@ -854,18 +783,6 @@ impl CheckError { CheckError::UnknownClass { name } => { serde_json::json!({ "name": name }) } - CheckError::MutAssignOutOfScope { name, available } => { - serde_json::json!({"name": name, "available": available}) - } - CheckError::AssignTypeMismatch { name, declared, got } => { - serde_json::json!({"name": name, "expected": declared, "actual": got}) - } - CheckError::UnsupportedMutVarType { name, ty } => { - serde_json::json!({"name": name, "type": ty}) - } - CheckError::MutVarCapturedByLambda { name } => { - serde_json::json!({"name": name}) - } CheckError::LoopBinderCapturedByLambda { name } => { serde_json::json!({"name": name}) } @@ -2016,15 +1933,11 @@ fn check_fn(f: &FnDef, env: &Env, out_warnings: &mut Vec) -> Result< let mut residuals: Vec = Vec::new(); let mut free_fn_calls: Vec = Vec::new(); let mut warnings: Vec = Vec::new(); - // Iter mut.2: fresh per-fn-body mut-scope stack. Empty at fn entry; - // pushed/popped by `Term::Mut` arms during the walk; discarded after - // the body type-checks. - let mut mut_scope_stack: Vec> = Vec::new(); // loop-recur iter 2: fresh per-fn-body loop-binder-type stack. // Empty at fn entry; pushed/popped by `Term::Loop` arms during // the synth walk; discarded after the body type-checks. let mut loop_stack: Vec> = Vec::new(); - let body_ty = synth(&f.body, &env, &mut locals, &mut mut_scope_stack, &mut loop_stack, &mut effects, &f.name, &mut subst, &mut counter, &mut residuals, &mut free_fn_calls, &mut warnings)?; + let body_ty = synth(&f.body, &env, &mut locals, &mut loop_stack, &mut effects, &f.name, &mut subst, &mut counter, &mut residuals, &mut free_fn_calls, &mut warnings)?; unify(&ret_ty, &body_ty, &mut subst)?; // mq.3: surface synth-time warnings into the caller-supplied // accumulator. The warnings already carry `def: Some(f.name)` @@ -2743,31 +2656,15 @@ pub fn verify_tail_positions(t: &Term, is_tail: bool) -> Result<()> { verify_tail_positions(source, false)?; verify_tail_positions(body, is_tail) } - // Iter mut.1: var inits are evaluated for side-effect before - // the body and are NOT in tail position. The body is the - // value of the whole block and inherits the enclosing tail - // position (a `(tail-app ...)` as the final expression of a - // `(mut ...)` block is itself in tail position because the - // block's value is the call's value). - Term::Mut { vars, body } => { - for v in vars { - verify_tail_positions(&v.init, false)?; - } - verify_tail_positions(body, is_tail) - } - // Iter mut.1: `Term::Assign` has static type Unit and cannot - // be in tail position. Its `value` is also not in tail - // position. - Term::Assign { value, .. } => verify_tail_positions(value, false), // loop-recur iter 1: binder inits are evaluated once on loop - // entry, NOT in tail position (mirror the Term::Mut arm - // above). The loop body inherits the enclosing tail position - // (the loop's value is the body's value on the exiting - // iteration). This arm only defines tail-app descent through - // the new node; it makes NO claim about recur tail-position - // (that is loop-recur iter 2's `verify_loop_body`). The - // tail-app verification role is unchanged — no pre-existing - // fixture contains a `loop`/`recur` node. + // entry, NOT in tail position. The loop body inherits the + // enclosing tail position (the loop's value is the body's + // value on the exiting iteration). This arm only defines + // tail-app descent through the new node; it makes NO claim + // about recur tail-position (that is loop-recur iter 2's + // `verify_loop_body`). The tail-app verification role is + // unchanged — no pre-existing fixture contains a + // `loop`/`recur` node. Term::Loop { binders, body } => { for b in binders { verify_tail_positions(&b.init, false)?; @@ -2877,13 +2774,6 @@ fn verify_loop_body(t: &Term, in_loop_tail: bool) -> Result<()> { verify_loop_body(source, false)?; verify_loop_body(body, in_loop_tail) } - Term::Mut { vars, body } => { - for v in vars { - verify_loop_body(&v.init, false)?; - } - verify_loop_body(body, in_loop_tail) - } - Term::Assign { value, .. } => verify_loop_body(value, false), } } @@ -2904,14 +2794,10 @@ fn check_const(c: &ConstDef, env: &Env, out_warnings: &mut Vec) -> R let mut residuals: Vec = Vec::new(); let mut free_fn_calls: Vec = Vec::new(); let mut warnings: Vec = Vec::new(); - // Iter mut.2: fresh per-const-body mut-scope stack (always empty - // at this entry point — `Term::Mut` may appear inside a const body - // and push frames as the walk descends). - let mut mut_scope_stack: Vec> = Vec::new(); // loop-recur iter 2: a const value has no enclosing loop; any // `recur` in a const body correctly fires `RecurOutsideLoop`. let mut loop_stack: Vec> = Vec::new(); - let v = synth(&c.value, env, &mut locals, &mut mut_scope_stack, &mut loop_stack, &mut effects, &c.name, &mut subst, &mut counter, &mut residuals, &mut free_fn_calls, &mut warnings)?; + let v = synth(&c.value, env, &mut locals, &mut loop_stack, &mut effects, &c.name, &mut subst, &mut counter, &mut residuals, &mut free_fn_calls, &mut warnings)?; out_warnings.extend(warnings); unify(&c.ty, &v, &mut subst)?; if !effects.is_empty() { @@ -2928,25 +2814,16 @@ pub(crate) fn synth( t: &Term, env: &Env, locals: &mut IndexMap, - // Iter mut.2: per-walk lexical mut-scope. Each frame is one - // `Term::Mut` block's `vars` (preserving declaration order via - // `IndexMap`). The stack is pushed on `Term::Mut` entry and - // popped on exit; `Term::Var` consults it innermost-first before - // the locals/globals chain; `Term::Assign` resolves its target - // name through it. Position: locals-adjacent because both are - // per-fn-body lexical scope state. - mut_scope_stack: &mut Vec>, // loop-recur iter 2: per-walk loop-binder stack. Each frame is one // `Term::Loop`'s binders in declaration order. `recur` rebinds // binders by position, not name, so the type list is positional; // iter loop-recur.tidy added the binder name alongside the type - // (so `Term::Lam`'s escape guard can reject a free var that is an - // in-scope loop binder, symmetric to `mut_scope_stack`). Pushed on - // `Term::Loop` body entry, popped on exit; consulted innermost-first - // by `Term::Recur` for arity + per-position type, and across all - // frames by `Term::Lam` for the escape check. Position: - // mut_scope_stack-adjacent — both are per-fn-body lexical-control - // scope state threaded identically. + // so `Term::Lam`'s escape guard can reject a free var that is an + // in-scope loop binder. Pushed on `Term::Loop` body entry, popped + // on exit; consulted innermost-first by `Term::Recur` for arity + + // per-position type, and across all frames by `Term::Lam` for the + // escape check. Position: locals-adjacent — per-fn-body + // lexical-control scope state. loop_stack: &mut Vec>, effects: &mut BTreeSet, in_def: &str, @@ -2969,29 +2846,6 @@ pub(crate) fn synth( Literal::Float { .. } => Type::float(), }), Term::Var { name } => { - // Iter mut.2: mut-vars take precedence over every other - // resolution path (let-bound, param, top-level, builtin, - // class-method). Walk the stack innermost-first; the first - // frame containing `name` wins (lexical shadowing). Mut-vars - // are monomorphic per spec §"var element types" — no Forall - // instantiation, no `FreeFnCall` recording. - // - // Iter mut.4-tidy: gate on `!is_empty()` to collapse the - // prepend cost to a single check per Var resolution for the - // common case (no mut blocks in scope). Eliminates the - // hypothesised hot-path cost from the mut.2 prepend; the - // remaining mut.2 check_ms shift in `compile_check.py` is - // not Var-arm-proportional (uniform across fixtures of - // very different Var counts) and is recorded as a - // bench-noise envelope finding in the iter journal. - if !mut_scope_stack.is_empty() { - for frame in mut_scope_stack.iter().rev() { - if let Some(ty) = frame.get(name) { - return Ok(ty.clone()); - } - } - } - // Lookup precedence: locals → local globals → class-method // table → qualified cross-module ref. A `Forall` resolved at // any of these levels is instantiated with fresh metavars at @@ -3336,7 +3190,7 @@ pub(crate) fn synth( Ok(maybe_instantiate(raw, counter)) } Term::App { callee, args, .. } => { - let cty = synth(callee, env, locals, mut_scope_stack, loop_stack, effects, in_def, subst, counter, residuals, free_fn_calls, warnings)?; + let cty = synth(callee, env, locals, loop_stack, effects, in_def, subst, counter, residuals, free_fn_calls, warnings)?; let cty = subst.apply(&cty); let (params, ret, fx) = match &cty { Type::Fn { params, ret, effects: fx, .. } => { @@ -3372,7 +3226,7 @@ pub(crate) fn synth( }); } for (a, exp) in args.iter().zip(params.iter()) { - let actual = synth(a, env, locals, mut_scope_stack, loop_stack, effects, in_def, subst, counter, residuals, free_fn_calls, warnings)?; + let actual = synth(a, env, locals, loop_stack, effects, in_def, subst, counter, residuals, free_fn_calls, warnings)?; unify(exp, &actual, subst)?; } for e in fx { @@ -3381,9 +3235,9 @@ pub(crate) fn synth( Ok(subst.apply(&ret)) } Term::Let { name, value, body } => { - let v = synth(value, env, locals, mut_scope_stack, loop_stack, effects, in_def, subst, counter, residuals, free_fn_calls, warnings)?; + let v = synth(value, env, locals, loop_stack, effects, in_def, subst, counter, residuals, free_fn_calls, warnings)?; let prev = locals.insert(name.clone(), v); - let r = synth(body, env, locals, mut_scope_stack, loop_stack, effects, in_def, subst, counter, residuals, free_fn_calls, warnings)?; + let r = synth(body, env, locals, loop_stack, effects, in_def, subst, counter, residuals, free_fn_calls, warnings)?; match prev { Some(p) => { locals.insert(name.clone(), p); @@ -3395,10 +3249,10 @@ pub(crate) fn synth( Ok(r) } Term::If { cond, then, else_ } => { - let c = synth(cond, env, locals, mut_scope_stack, loop_stack, effects, in_def, subst, counter, residuals, free_fn_calls, warnings)?; + let c = synth(cond, env, locals, loop_stack, effects, in_def, subst, counter, residuals, free_fn_calls, warnings)?; unify(&Type::bool_(), &c, subst)?; - let t1 = synth(then, env, locals, mut_scope_stack, loop_stack, effects, in_def, subst, counter, residuals, free_fn_calls, warnings)?; - let t2 = synth(else_, env, locals, mut_scope_stack, loop_stack, effects, in_def, subst, counter, residuals, free_fn_calls, warnings)?; + let t1 = synth(then, env, locals, loop_stack, effects, in_def, subst, counter, residuals, free_fn_calls, warnings)?; + let t2 = synth(else_, env, locals, loop_stack, effects, in_def, subst, counter, residuals, free_fn_calls, warnings)?; unify(&t1, &t2, subst)?; Ok(subst.apply(&t1)) } @@ -3416,7 +3270,7 @@ pub(crate) fn synth( }); } for (a, exp) in args.iter().zip(sig.params.iter()) { - let actual = synth(a, env, locals, mut_scope_stack, loop_stack, effects, in_def, subst, counter, residuals, free_fn_calls, warnings)?; + let actual = synth(a, env, locals, loop_stack, effects, in_def, subst, counter, residuals, free_fn_calls, warnings)?; unify(exp, &actual, subst)?; } effects.insert(sig.effect.clone()); @@ -3511,7 +3365,7 @@ pub(crate) fn synth( } for (a, exp) in args.iter().zip(qualified_fields.iter()) { let exp_inst = substitute_rigids(exp, &mapping); - let actual = synth(a, env, locals, mut_scope_stack, loop_stack, effects, in_def, subst, counter, residuals, free_fn_calls, warnings)?; + let actual = synth(a, env, locals, loop_stack, effects, in_def, subst, counter, residuals, free_fn_calls, warnings)?; unify(&exp_inst, &actual, subst)?; } Ok(Type::Con { @@ -3520,7 +3374,7 @@ pub(crate) fn synth( }) } Term::Match { scrutinee, arms } => { - let s_ty = synth(scrutinee, env, locals, mut_scope_stack, loop_stack, effects, in_def, subst, counter, residuals, free_fn_calls, warnings)?; + let s_ty = synth(scrutinee, env, locals, loop_stack, effects, in_def, subst, counter, residuals, free_fn_calls, warnings)?; if arms.is_empty() { return Err(CheckError::NonExhaustive { ty: ailang_core::pretty::type_to_string(&s_ty), @@ -3538,7 +3392,7 @@ pub(crate) fn synth( let prev = locals.insert(n.clone(), t.clone()); pushed.push((n.clone(), prev)); } - let body_ty = synth(&arm.body, env, locals, mut_scope_stack, loop_stack, effects, in_def, subst, counter, residuals, free_fn_calls, warnings)?; + let body_ty = synth(&arm.body, env, locals, loop_stack, effects, in_def, subst, counter, residuals, free_fn_calls, warnings)?; for (n, prev) in pushed.into_iter().rev() { match prev { Some(p) => { @@ -3613,28 +3467,24 @@ pub(crate) fn synth( Ok(subst.apply(&result_ty.expect("checked arms is non-empty"))) } Term::Seq { lhs, rhs } => { - let lty = synth(lhs, env, locals, mut_scope_stack, loop_stack, effects, in_def, subst, counter, residuals, free_fn_calls, warnings)?; + let lty = synth(lhs, env, locals, loop_stack, effects, in_def, subst, counter, residuals, free_fn_calls, warnings)?; unify(&Type::unit(), <y, subst)?; - synth(rhs, env, locals, mut_scope_stack, loop_stack, effects, in_def, subst, counter, residuals, free_fn_calls, warnings) + synth(rhs, env, locals, loop_stack, effects, in_def, subst, counter, residuals, free_fn_calls, warnings) } Term::Lam { params, param_tys, ret_ty, effects: lam_effects, body } => { - // Iter mut.4-tidy: reject lambda-captures-of-mut-var. + // Iter loop-recur.tidy: reject lambda-captures-of-loop-binder. // Walk the lambda body for free Vars (names not bound by - // the lambda's own params) that match any mut-var on the - // enclosing stack. If any match, reject with - // `MutVarCapturedByLambda`. Reuses `desugar::free_vars_in_term` - // (same walker `lift.rs` uses) which honours pattern bindings - // inside `Term::Match` arms. - // Iter loop-recur.tidy: symmetrically reject - // lambda-captures-of-loop-binder. Loop binders are + // the lambda's own params) that match any loop binder on + // the enclosing `loop_stack`. Loop binders are // alloca-resident and `std::mem::take`-emptied at the - // lambda frame boundary in codegen, exactly like mut-vars - // — capturing one panics `unreachable!()` at codegen, so - // the type checker rejects it here instead. The free-var - // scan is shared with the mut-scope check below; gated on - // a non-empty enclosing scope so lambdas outside any - // mut/loop scope typecheck unchanged. - if !mut_scope_stack.is_empty() || !loop_stack.is_empty() { + // lambda frame boundary in codegen — capturing one panics + // `unreachable!()` at codegen, so the type checker rejects + // it here instead. Reuses `desugar::free_vars_in_term` + // (same walker `lift.rs` uses) which honours pattern + // bindings inside `Term::Match` arms; gated on a non-empty + // `loop_stack` so lambdas outside any loop typecheck + // unchanged. + if !loop_stack.is_empty() { let mut lam_bound: BTreeSet = BTreeSet::new(); for p in params { lam_bound.insert(p.clone()); @@ -3642,13 +3492,6 @@ pub(crate) fn synth( let mut frees: BTreeSet = BTreeSet::new(); ailang_core::desugar::free_vars_in_term(body, &lam_bound, &mut frees); for free_name in &frees { - for frame in mut_scope_stack.iter() { - if frame.contains_key(free_name) { - return Err(CheckError::MutVarCapturedByLambda { - name: free_name.clone(), - }); - } - } for frame in loop_stack.iter() { if frame.iter().any(|(n, _)| n == free_name) { return Err(CheckError::LoopBinderCapturedByLambda { @@ -3665,7 +3508,7 @@ pub(crate) fn synth( pushed.push((n.clone(), prev)); } let mut body_effects: BTreeSet = BTreeSet::new(); - let body_ty = synth(body, env, locals, mut_scope_stack, loop_stack, &mut body_effects, in_def, subst, counter, residuals, free_fn_calls, warnings); + let body_ty = synth(body, env, locals, loop_stack, &mut body_effects, in_def, subst, counter, residuals, free_fn_calls, warnings); for (n, prev) in pushed.into_iter().rev() { match prev { Some(p) => { @@ -3753,7 +3596,7 @@ pub(crate) fn synth( // subset rule against `declared_effs` — exactly like // `Term::Lam`. let mut body_effects: BTreeSet = BTreeSet::new(); - let body_ty = synth(body, env, locals, mut_scope_stack, loop_stack, &mut body_effects, in_def, subst, counter, residuals, free_fn_calls, warnings); + let body_ty = synth(body, env, locals, loop_stack, &mut body_effects, in_def, subst, counter, residuals, free_fn_calls, warnings); // Restore body-scope locals (params + name). for (n, prev) in pushed.into_iter().rev() { @@ -3780,7 +3623,7 @@ pub(crate) fn synth( // scope (params are not visible here; only the recursive // binding is). let prev_in = locals.insert(name.clone(), ty.clone()); - let in_ty = synth(in_term, env, locals, mut_scope_stack, loop_stack, effects, in_def, subst, counter, residuals, free_fn_calls, warnings); + let in_ty = synth(in_term, env, locals, loop_stack, effects, in_def, subst, counter, residuals, free_fn_calls, warnings); match prev_in { Some(p) => { locals.insert(name.clone(), p); @@ -3796,7 +3639,7 @@ pub(crate) fn synth( // No constraint generated, no environment change. The // wrapper records author intent for the future RC inc/dec // emission pass (18c.3); typing is pure passthrough. - synth(value, env, locals, mut_scope_stack, loop_stack, effects, in_def, subst, counter, residuals, free_fn_calls, warnings) + synth(value, env, locals, loop_stack, effects, in_def, subst, counter, residuals, free_fn_calls, warnings) } Term::ReuseAs { source, body } => { // Iter 18d.1: `(reuse-as SRC NEW-CTOR)` requires `body` to @@ -3810,7 +3653,7 @@ pub(crate) fn synth( // shape-compatibility check (18d.2 will add a // `reuse-as-shape-mismatch` diagnostic when codegen has // the actual size info). - let _ = synth(source, env, locals, mut_scope_stack, loop_stack, effects, in_def, subst, counter, residuals, free_fn_calls, warnings)?; + let _ = synth(source, env, locals, loop_stack, effects, in_def, subst, counter, residuals, free_fn_calls, warnings)?; match body.as_ref() { Term::Ctor { .. } | Term::Lam { .. } => {} other => { @@ -3826,8 +3669,6 @@ pub(crate) fn synth( Term::Seq { .. } => "seq", Term::Clone { .. } => "clone", Term::ReuseAs { .. } => "reuse-as", - Term::Mut { .. } => "mut", - Term::Assign { .. } => "assign", Term::Loop { .. } => "loop", Term::Recur { .. } => "recur", Term::Ctor { .. } | Term::Lam { .. } => unreachable!(), @@ -3840,90 +3681,7 @@ pub(crate) fn synth( } // Body's type is the result type of the whole reuse-as // expression. - synth(body, env, locals, mut_scope_stack, loop_stack, effects, in_def, subst, counter, residuals, free_fn_calls, warnings) - } - // Iter mut.2: a mut block opens a fresh lexical scope for its - // mut-vars. Each var's `init` is synthed in the outer scope - // plus already-declared vars of this same frame (vars are - // initialised in declaration order; later inits see earlier - // names). The body is synthed with the completed frame on the - // stack. The frame is popped on exit. The block's static type - // is the body's static type. - Term::Mut { vars, body } => { - let mut frame: IndexMap = IndexMap::new(); - for v in vars { - if !is_supported_mut_var_type(&v.ty) { - return Err(CheckError::UnsupportedMutVarType { - name: v.name.clone(), - ty: ailang_core::pretty::type_to_string(&v.ty), - }); - } - // Synth the init in the outer scope plus any vars - // already pushed into `frame`. The cleanest way to - // expose those bindings is to push a single in-flight - // frame (clone of the accumulator) onto the stack for - // the duration of the init walk, then pop. This keeps - // the `mut_scope_stack` truthful at all times. - mut_scope_stack.push(frame.clone()); - let init_ty = synth( - &v.init, env, locals, mut_scope_stack, loop_stack, effects, in_def, - subst, counter, residuals, free_fn_calls, warnings, - )?; - mut_scope_stack.pop(); - unify(&v.ty, &init_ty, subst)?; - frame.insert(v.name.clone(), v.ty.clone()); - } - mut_scope_stack.push(frame); - let body_result = synth( - body, env, locals, mut_scope_stack, loop_stack, effects, in_def, - subst, counter, residuals, free_fn_calls, warnings, - ); - mut_scope_stack.pop(); - body_result - } - // Iter mut.2: `Term::Assign` resolves its target name through - // the mut-scope stack (innermost-first). On a hit, the value's - // synth type must match the var's declared type. The - // expression's static type is Unit. - Term::Assign { name, value } => { - let declared: Option = mut_scope_stack - .iter() - .rev() - .find_map(|frame| frame.get(name).cloned()); - match declared { - None => { - // Flatten every mut-var name visible at this point, - // innermost-first, deduplicated preserving order. - let mut available: Vec = Vec::new(); - for frame in mut_scope_stack.iter().rev() { - for k in frame.keys() { - if !available.contains(k) { - available.push(k.clone()); - } - } - } - Err(CheckError::MutAssignOutOfScope { - name: name.clone(), - available, - }) - } - Some(declared_ty) => { - let value_ty = synth( - value, env, locals, mut_scope_stack, loop_stack, effects, in_def, - subst, counter, residuals, free_fn_calls, warnings, - )?; - let applied_declared = subst.apply(&declared_ty); - let applied_value = subst.apply(&value_ty); - if applied_declared != applied_value { - return Err(CheckError::AssignTypeMismatch { - name: name.clone(), - declared: ailang_core::pretty::type_to_string(&applied_declared), - got: ailang_core::pretty::type_to_string(&applied_value), - }); - } - Ok(Type::unit()) - } - } + synth(body, env, locals, loop_stack, effects, in_def, subst, counter, residuals, free_fn_calls, warnings) } // loop-recur iter 2: each binder init is synthed in the // outer scope plus already-declared binder names of THIS @@ -3940,7 +3698,7 @@ pub(crate) fn synth( let mut restore: Vec<(String, Option)> = Vec::new(); for b in binders { let init_ty = synth( - &b.init, env, locals, mut_scope_stack, loop_stack, effects, + &b.init, env, locals, loop_stack, effects, in_def, subst, counter, residuals, free_fn_calls, warnings, )?; unify(&b.ty, &init_ty, subst)?; @@ -3950,7 +3708,7 @@ pub(crate) fn synth( } loop_stack.push(binder_frame); let body_result = synth( - body, env, locals, mut_scope_stack, loop_stack, effects, + body, env, locals, loop_stack, effects, in_def, subst, counter, residuals, free_fn_calls, warnings, ); loop_stack.pop(); @@ -3988,7 +3746,7 @@ pub(crate) fn synth( } for (i, a) in args.iter().enumerate() { let arg_ty = synth( - a, env, locals, mut_scope_stack, loop_stack, effects, + a, env, locals, loop_stack, effects, in_def, subst, counter, residuals, free_fn_calls, warnings, )?; let applied_binder = subst.apply(&binder_frame[i].1); @@ -4006,16 +3764,6 @@ pub(crate) fn synth( } } -/// Iter mut.2: gate `MutVar.ty` to the four stack-resident scalar -/// primitives per spec `docs/specs/2026-05-15-mut-local.md` §"Out of -/// scope". Heap-RC-managed types (Str, ADTs, fn-values) and any -/// parameterised constructor are deferred to a follow-on milestone. -fn is_supported_mut_var_type(ty: &Type) -> bool { - matches!( - ty, - Type::Con { name, args } if args.is_empty() && matches!(name.as_str(), "Int" | "Float" | "Bool" | "Unit") - ) -} /// If `t` is a `Forall`, instantiate it with fresh metavars; otherwise /// pass through unchanged. Used at every var-resolution site. @@ -4372,13 +4120,12 @@ mod tests { /// Helper that synths a single Term against a default empty env + /// fresh local / scope / effect state. Returns the substituted - /// final type. Used by the mut.2 pin tests below. + /// final type. fn synth_standalone(term: &Term) -> Type { let env = Env::default(); let mut locals: IndexMap = IndexMap::new(); - let mut mut_scope_stack: Vec> = Vec::new(); // loop-recur iter 2: loop-free unit-test call site — fresh - // empty loop-stack, mirroring the mut-scope above. + // empty loop-stack. let mut loop_stack: Vec> = Vec::new(); let mut effects: BTreeSet = BTreeSet::new(); let mut subst = Subst::default(); @@ -4390,7 +4137,6 @@ mod tests { term, &env, &mut locals, - &mut mut_scope_stack, &mut loop_stack, &mut effects, "", @@ -4404,323 +4150,6 @@ mod tests { subst.apply(&ty) } - /// Iter mut.2 (Task 3): `Term::Var` consults the mut-scope stack - /// before locals/globals. With a non-empty stack carrying - /// `x → Float` and a local also binding `x → Int`, resolving - /// `Var x` returns Float — mut-scope wins lexically. - /// - /// The frame is pre-populated directly on the stack (rather than - /// running a `Term::Mut` block that would push it) so this pin - /// isolates the Var-arm change from the Mut-arm work in Task 4. - #[test] - fn mut_var_resolution_shadows_outer_local() { - let env = Env::default(); - let mut locals: IndexMap = IndexMap::new(); - locals.insert("x".into(), Type::int()); // outer let-style binding - let mut mut_scope_stack: Vec> = Vec::new(); - // loop-recur iter 2: loop-free unit-test call site — fresh - // empty loop-stack, mirroring the mut-scope above. - let mut loop_stack: Vec> = Vec::new(); - let mut frame: IndexMap = IndexMap::new(); - frame.insert("x".into(), Type::float()); // inner mut-var - mut_scope_stack.push(frame); - let mut effects: BTreeSet = BTreeSet::new(); - let mut subst = Subst::default(); - let mut counter: u32 = 0; - let mut residuals: Vec = Vec::new(); - let mut free_fn_calls: Vec = Vec::new(); - let mut warnings: Vec = Vec::new(); - - let term = Term::Var { name: "x".into() }; - let ty = synth( - &term, - &env, - &mut locals, - &mut mut_scope_stack, - &mut loop_stack, - &mut effects, - "", - &mut subst, - &mut counter, - &mut residuals, - &mut free_fn_calls, - &mut warnings, - ) - .expect("synth must succeed"); - assert_eq!( - ty, - Type::float(), - "mut-var `x : Float` on the scope stack must shadow local `x : Int` (got {ty:?})", - ); - } - - /// Iter mut.2 (Task 3): innermost-first walk on the mut-scope - /// stack. With two frames each binding `x` to a different type, - /// the innermost (last-pushed) wins. - #[test] - fn mut_var_resolution_picks_innermost_frame() { - let env = Env::default(); - let mut locals: IndexMap = IndexMap::new(); - let mut mut_scope_stack: Vec> = Vec::new(); - // loop-recur iter 2: loop-free unit-test call site — fresh - // empty loop-stack, mirroring the mut-scope above. - let mut loop_stack: Vec> = Vec::new(); - let mut outer: IndexMap = IndexMap::new(); - outer.insert("x".into(), Type::int()); - mut_scope_stack.push(outer); - let mut inner: IndexMap = IndexMap::new(); - inner.insert("x".into(), Type::bool_()); - mut_scope_stack.push(inner); - let mut effects: BTreeSet = BTreeSet::new(); - let mut subst = Subst::default(); - let mut counter: u32 = 0; - let mut residuals: Vec = Vec::new(); - let mut free_fn_calls: Vec = Vec::new(); - let mut warnings: Vec = Vec::new(); - - let term = Term::Var { name: "x".into() }; - let ty = synth( - &term, - &env, - &mut locals, - &mut mut_scope_stack, - &mut loop_stack, - &mut effects, - "", - &mut subst, - &mut counter, - &mut residuals, - &mut free_fn_calls, - &mut warnings, - ) - .expect("synth must succeed"); - assert_eq!( - ty, - Type::bool_(), - "innermost mut-var frame must win (expected Bool, got {ty:?})", - ); - } - - /// Iter mut.2 (Task 4): a well-formed mut block with one Int var, - /// an assign updating it, and a final read of the var synths to - /// Int. Property protected: `Term::Mut` arm pushes its `vars` as - /// a frame, synths the body in that extended scope, and returns - /// the body's type. - #[test] - fn mut_block_typechecks_with_int_var_and_returns_int() { - // (mut (var x (con Int) 0) (assign x (app + x 1)) x) - // Note: + is not in the empty Env, so use a simpler body that - // exercises Mut/Assign without depending on operator resolution. - // (mut (var x (con Int) 0) (assign x 1) x) - let term = Term::Mut { - vars: vec![ailang_core::ast::MutVar { - name: "x".into(), - ty: Type::int(), - init: Term::Lit { lit: Literal::Int { value: 0 } }, - }], - body: Box::new(Term::Seq { - lhs: Box::new(Term::Assign { - name: "x".into(), - value: Box::new(Term::Lit { lit: Literal::Int { value: 1 } }), - }), - rhs: Box::new(Term::Var { name: "x".into() }), - }), - }; - let ty = synth_standalone(&term); - assert_eq!( - ty, - Type::int(), - "mut block with Int var, assign, and final read must synth as Int (got {ty:?})", - ); - } - - /// Iter mut.2 (Task 5): inside a mut frame with `var x : Int = 0`, - /// `(assign x 1)` synths as Unit. Property protected: a successful - /// Term::Assign resolves through the mut-scope stack and yields - /// Unit (the Assign expression's static type). - #[test] - fn assign_to_in_scope_mut_var_synths_unit() { - // (mut (var x (con Int) 0) (assign x 1)) - let term = Term::Mut { - vars: vec![ailang_core::ast::MutVar { - name: "x".into(), - ty: Type::int(), - init: Term::Lit { lit: Literal::Int { value: 0 } }, - }], - body: Box::new(Term::Assign { - name: "x".into(), - value: Box::new(Term::Lit { lit: Literal::Int { value: 1 } }), - }), - }; - let ty = synth_standalone(&term); - assert_eq!( - ty, - Type::unit(), - "mut block whose body is a lone assign must synth as Unit (got {ty:?})", - ); - } - - /// Iter mut.2 (Task 5): assigning to an out-of-scope name (no - /// enclosing mut frame contains it) fires - /// `MutAssignOutOfScope` with the available mut-var names listed. - #[test] - fn assign_to_out_of_scope_name_fires_diagnostic() { - let term = Term::Mut { - vars: vec![ailang_core::ast::MutVar { - name: "x".into(), - ty: Type::int(), - init: Term::Lit { lit: Literal::Int { value: 0 } }, - }], - body: Box::new(Term::Assign { - name: "y".into(), - value: Box::new(Term::Lit { lit: Literal::Int { value: 1 } }), - }), - }; - let env = Env::default(); - let mut locals: IndexMap = IndexMap::new(); - let mut mut_scope_stack: Vec> = Vec::new(); - // loop-recur iter 2: loop-free unit-test call site — fresh - // empty loop-stack, mirroring the mut-scope above. - let mut loop_stack: Vec> = Vec::new(); - let mut effects: BTreeSet = BTreeSet::new(); - let mut subst = Subst::default(); - let mut counter: u32 = 0; - let mut residuals: Vec = Vec::new(); - let mut free_fn_calls: Vec = Vec::new(); - let mut warnings: Vec = Vec::new(); - let err = synth( - &term, &env, &mut locals, &mut mut_scope_stack, &mut loop_stack, &mut effects, - "", &mut subst, &mut counter, &mut residuals, - &mut free_fn_calls, &mut warnings, - ) - .expect_err("assign to unknown name must be rejected"); - match err { - CheckError::MutAssignOutOfScope { name, available } => { - assert_eq!(name, "y"); - assert_eq!(available, vec!["x".to_string()]); - } - other => panic!("expected MutAssignOutOfScope, got {other:?}"), - } - } - - /// Iter mut.2 (Task 5): assigning a Float-typed value to an - /// Int-typed mut-var fires `AssignTypeMismatch` with rendered - /// declared / got type strings. - #[test] - fn assign_with_wrong_value_type_fires_diagnostic() { - let term = Term::Mut { - vars: vec![ailang_core::ast::MutVar { - name: "x".into(), - ty: Type::int(), - init: Term::Lit { lit: Literal::Int { value: 0 } }, - }], - body: Box::new(Term::Assign { - name: "x".into(), - value: Box::new(Term::Lit { lit: Literal::Float { bits: 0x3ff8_0000_0000_0000 } }), - }), - }; - let env = Env::default(); - let mut locals: IndexMap = IndexMap::new(); - let mut mut_scope_stack: Vec> = Vec::new(); - // loop-recur iter 2: loop-free unit-test call site — fresh - // empty loop-stack, mirroring the mut-scope above. - let mut loop_stack: Vec> = Vec::new(); - let mut effects: BTreeSet = BTreeSet::new(); - let mut subst = Subst::default(); - let mut counter: u32 = 0; - let mut residuals: Vec = Vec::new(); - let mut free_fn_calls: Vec = Vec::new(); - let mut warnings: Vec = Vec::new(); - let err = synth( - &term, &env, &mut locals, &mut mut_scope_stack, &mut loop_stack, &mut effects, - "", &mut subst, &mut counter, &mut residuals, - &mut free_fn_calls, &mut warnings, - ) - .expect_err("Float assigned to Int mut-var must be rejected"); - match err { - CheckError::AssignTypeMismatch { name, declared, got } => { - assert_eq!(name, "x"); - assert_eq!(declared, "Int"); - assert_eq!(got, "Float"); - } - other => panic!("expected AssignTypeMismatch, got {other:?}"), - } - } - - /// Iter mut.2 (Task 4): a mut block declaring a Str-typed mut-var - /// fires `CheckError::UnsupportedMutVarType` per spec §"Out of - /// scope" — heap-RC-managed types are deferred to a follow-on - /// milestone. - #[test] - fn mut_block_rejects_str_var_with_unsupported_type() { - let term = Term::Mut { - vars: vec![ailang_core::ast::MutVar { - name: "s".into(), - ty: Type::str_(), - init: Term::Lit { lit: Literal::Str { value: "hi".into() } }, - }], - body: Box::new(Term::Var { name: "s".into() }), - }; - let env = Env::default(); - let mut locals: IndexMap = IndexMap::new(); - let mut mut_scope_stack: Vec> = Vec::new(); - // loop-recur iter 2: loop-free unit-test call site — fresh - // empty loop-stack, mirroring the mut-scope above. - let mut loop_stack: Vec> = Vec::new(); - let mut effects: BTreeSet = BTreeSet::new(); - let mut subst = Subst::default(); - let mut counter: u32 = 0; - let mut residuals: Vec = Vec::new(); - let mut free_fn_calls: Vec = Vec::new(); - let mut warnings: Vec = Vec::new(); - let err = synth( - &term, &env, &mut locals, &mut mut_scope_stack, &mut loop_stack, &mut effects, - "", &mut subst, &mut counter, &mut residuals, - &mut free_fn_calls, &mut warnings, - ) - .expect_err("Str-typed mut-var must be rejected"); - assert!( - matches!(err, CheckError::UnsupportedMutVarType { ref name, .. } if name == "s"), - "expected UnsupportedMutVarType for `s`, got {err:?}", - ); - } - - /// Iter mut.2 (Task 1): every new CheckError variant has a stable - /// kebab-case code() arm and a non-panicking ctx() arm. - #[test] - fn mut_typecheck_diagnostics_have_kebab_codes() { - let e1 = CheckError::MutAssignOutOfScope { - name: "foo".into(), - available: vec!["bar".into(), "baz".into()], - }; - let e2 = CheckError::AssignTypeMismatch { - name: "count".into(), - declared: "Int".into(), - got: "Float".into(), - }; - let e3 = CheckError::UnsupportedMutVarType { - name: "s".into(), - ty: "Str".into(), - }; - assert_eq!(e1.code(), "mut-assign-out-of-scope"); - assert_eq!(e2.code(), "assign-type-mismatch"); - assert_eq!(e3.code(), "mut-var-unsupported-type"); - // Smoke-test ctx() — must not panic, must produce a JSON object. - let _ = e1.ctx(); - let _ = e2.ctx(); - let _ = e3.ctx(); - } - - /// Iter mut.4-tidy (Task 1): `CheckError::MutVarCapturedByLambda` - /// carries the stable kebab code `mut-var-captured-by-lambda` - /// and a non-panicking `ctx()` arm carrying the captured name. - #[test] - fn mut_var_captured_by_lambda_diagnostic_has_kebab_code() { - let e = CheckError::MutVarCapturedByLambda { name: "sum".into() }; - assert_eq!(e.code(), "mut-var-captured-by-lambda"); - let _ = e.ctx(); - } - /// Iter loop-recur.tidy: `CheckError::LoopBinderCapturedByLambda` /// carries the stable kebab code `loop-binder-captured-by-lambda` /// and a non-panicking `ctx()` arm carrying the captured name — @@ -4758,15 +4187,14 @@ mod tests { ); } - /// Iter mut.4-tidy (Task 2): regression guard — a lambda built - /// without any enclosing mut-scope stack typechecks unchanged. - /// Property protected: the new free-var scan in `Term::Lam`'s synth - /// arm is gated on `!mut_scope_stack.is_empty()` and must not - /// disturb lambdas outside mut blocks. The body refers to the - /// lambda's own param (no builtin lookup needed) so the test + /// Regression guard — a lambda built without any enclosing loop + /// typechecks unchanged. Property protected: the free-var scan in + /// `Term::Lam`'s synth arm is gated on `!loop_stack.is_empty()` + /// and must not disturb lambdas outside loops. The body refers to + /// the lambda's own param (no builtin lookup needed) so the test /// stays pure in `Env::default()`. #[test] - fn lambda_outside_mut_still_typechecks_clean() { + fn lambda_outside_loop_still_typechecks_clean() { let term = Term::Lam { params: vec!["x".into()], param_tys: vec![Type::int()], @@ -4777,62 +4205,10 @@ mod tests { let _ty = synth_standalone(&term); } - /// Iter mut.4-tidy (Task 2): a lambda nested inside a `Term::Mut` - /// whose body references a mut-var declared in the enclosing - /// `Term::Mut` is rejected with `MutVarCapturedByLambda`. The - /// captured-name field reports the offending mut-var. - #[test] - fn lambda_inside_mut_capturing_mut_var_emits_mut_var_captured_by_lambda() { - // (mut (var x : Int = 0) - // (let f = (lam () : Int -> x) in 0)) - let term = Term::Mut { - vars: vec![ailang_core::ast::MutVar { - name: "x".into(), - ty: Type::int(), - init: Term::Lit { lit: Literal::Int { value: 0 } }, - }], - body: Box::new(Term::Let { - name: "f".into(), - value: Box::new(Term::Lam { - params: vec![], - param_tys: vec![], - ret_ty: Box::new(Type::int()), - effects: vec![], - body: Box::new(Term::Var { name: "x".into() }), - }), - body: Box::new(Term::Lit { lit: Literal::Int { value: 0 } }), - }), - }; - let env = Env::default(); - let mut locals: IndexMap = IndexMap::new(); - let mut mut_scope_stack: Vec> = Vec::new(); - // loop-recur iter 2: loop-free unit-test call site — fresh - // empty loop-stack, mirroring the mut-scope above. - let mut loop_stack: Vec> = Vec::new(); - let mut effects: BTreeSet = BTreeSet::new(); - let mut subst = Subst::default(); - let mut counter: u32 = 0; - let mut residuals: Vec = Vec::new(); - let mut free_fn_calls: Vec = Vec::new(); - let mut warnings: Vec = Vec::new(); - let err = synth( - &term, &env, &mut locals, &mut mut_scope_stack, &mut loop_stack, &mut effects, - "", &mut subst, &mut counter, &mut residuals, - &mut free_fn_calls, &mut warnings, - ) - .expect_err("lambda capturing mut-var must be rejected"); - match err { - CheckError::MutVarCapturedByLambda { name } => assert_eq!(name, "x"), - other => panic!("expected MutVarCapturedByLambda, got {other:?}"), - } - } - /// Iter loop-recur.tidy: a lambda nested inside a `Term::Loop` /// whose body references a loop binder declared by the enclosing /// loop is rejected with `LoopBinderCapturedByLambda`. The - /// captured-name field reports the offending binder. Structurally - /// symmetric to - /// `lambda_inside_mut_capturing_mut_var_emits_mut_var_captured_by_lambda`. + /// captured-name field reports the offending binder. #[test] fn lambda_inside_loop_capturing_binder_emits_loop_binder_captured_by_lambda() { // (loop (acc : Int = 0) @@ -4857,7 +4233,6 @@ mod tests { }; let env = Env::default(); let mut locals: IndexMap = IndexMap::new(); - let mut mut_scope_stack: Vec> = Vec::new(); let mut loop_stack: Vec> = Vec::new(); let mut effects: BTreeSet = BTreeSet::new(); let mut subst = Subst::default(); @@ -4866,7 +4241,7 @@ mod tests { let mut free_fn_calls: Vec = Vec::new(); let mut warnings: Vec = Vec::new(); let err = synth( - &term, &env, &mut locals, &mut mut_scope_stack, &mut loop_stack, &mut effects, + &term, &env, &mut locals, &mut loop_stack, &mut effects, "", &mut subst, &mut counter, &mut residuals, &mut free_fn_calls, &mut warnings, ) @@ -7597,9 +6972,8 @@ mod tests { let term = Term::Var { name: "show".into() }; let mut locals: IndexMap = IndexMap::new(); - let mut mut_scope_stack: Vec> = Vec::new(); // loop-recur iter 2: loop-free unit-test call site — fresh - // empty loop-stack, mirroring the mut-scope above. + // empty loop-stack. let mut loop_stack: Vec> = Vec::new(); let mut effects: BTreeSet = BTreeSet::new(); let mut subst = Subst::default(); @@ -7611,7 +6985,6 @@ mod tests { &term, &env, &mut locals, - &mut mut_scope_stack, &mut loop_stack, &mut effects, "test_call_site", diff --git a/crates/ailang-check/src/lift.rs b/crates/ailang-check/src/lift.rs index 90035c3..e7a8e39 100644 --- a/crates/ailang-check/src/lift.rs +++ b/crates/ailang-check/src/lift.rs @@ -375,28 +375,6 @@ impl<'a> Lifter<'a> { source: Box::new(self.lift_in_term(source, locals, in_def)?), body: Box::new(self.lift_in_term(body, locals, in_def)?), }), - // Iter mut.1: lift letrecs out of each var's init and the - // body. Mut-vars cannot themselves participate in a - // letrec capture set (letrec bodies are fn-typed and - // mut-vars are scalar values), so the var bindings do - // not extend `locals` for the lift walk. - Term::Mut { vars, body } => Ok(Term::Mut { - vars: vars - .iter() - .map(|v| { - Ok(ailang_core::ast::MutVar { - name: v.name.clone(), - ty: v.ty.clone(), - init: self.lift_in_term(&v.init, locals, in_def)?, - }) - }) - .collect::>>()?, - body: Box::new(self.lift_in_term(body, locals, in_def)?), - }), - Term::Assign { name, value } => Ok(Term::Assign { - name: name.clone(), - value: Box::new(self.lift_in_term(value, locals, in_def)?), - }), Term::Loop { binders, body } => Ok(Term::Loop { binders: binders .iter() @@ -739,15 +717,11 @@ impl<'a> Lifter<'a> { // (e.g. class-method-shadowed-by-fn) have already been // surfaced upstream by `check_workspace`. Discard here. let mut warnings_discarded: Vec = Vec::new(); - // Iter mut.2: lift's letrec-capture synth re-entry walks one - // term at a time, beginning from a top-of-body position; fresh - // empty mut-scope stack is correct. - let mut mut_scope_stack: Vec> = Vec::new(); // loop-recur iter 2: lift's letrec-capture synth re-entry walks // one term at a time from a top-of-body position; fresh empty // loop-stack is correct (any `Term::Loop` pushes its own frame). let mut loop_stack: Vec> = Vec::new(); - let ty = synth(t, &self.env, locals, &mut mut_scope_stack, &mut loop_stack, &mut effects, in_def, &mut subst, &mut counter, &mut residuals, &mut free_fn_calls, &mut warnings_discarded)?; + let ty = synth(t, &self.env, locals, &mut loop_stack, &mut effects, in_def, &mut subst, &mut counter, &mut residuals, &mut free_fn_calls, &mut warnings_discarded)?; Ok(subst.apply(&ty)) } } @@ -778,12 +752,6 @@ fn contains_any_letrec(m: &Module) -> bool { Term::Clone { value } => term_has_letrec(value), // Iter 18d.1: structural recursion through both children. Term::ReuseAs { source, body } => term_has_letrec(source) || term_has_letrec(body), - // Iter mut.1: any var init or the body can contain a - // letrec; the assign's value can too. - Term::Mut { vars, body } => { - vars.iter().any(|v| term_has_letrec(&v.init)) || term_has_letrec(body) - } - Term::Assign { value, .. } => term_has_letrec(value), Term::Loop { binders, body } => { binders.iter().any(|b| term_has_letrec(&b.init)) || term_has_letrec(body) } diff --git a/crates/ailang-check/src/linearity.rs b/crates/ailang-check/src/linearity.rs index 94a4ec3..9d843b3 100644 --- a/crates/ailang-check/src/linearity.rs +++ b/crates/ailang-check/src/linearity.rs @@ -592,23 +592,6 @@ impl<'a> Checker<'a> { // standard position rules. self.walk(body, pos); } - // Iter mut.1: mut-vars are alloca-resident scalars (Int / - // Float / Bool / Unit per spec §"Iteration mut.1") — they - // are not RC-managed and do not participate in the - // linearity analysis. Walk children defensively so any - // RC-managed value referenced inside an init or the body - // is still tracked. - Term::Mut { vars, body } => { - for v in vars { - self.walk(&v.init, Position::Consume); - } - self.walk(body, pos); - } - Term::Assign { value, .. } => { - // The assigned `name` is a mut-var (alloca slot, not - // a tracked binder). Walk the `value` normally. - self.walk(value, Position::Consume); - } Term::Loop { binders, body } => { for b in binders { self.walk(&b.init, Position::Consume); @@ -804,8 +787,6 @@ fn make_reuse_as_source_not_bare_var(def: &str, source: &Term, body: &Term) -> D Term::Seq { .. } => "seq", Term::Clone { .. } => "clone", Term::ReuseAs { .. } => "reuse-as", - Term::Mut { .. } => "mut", - Term::Assign { .. } => "assign", Term::Loop { .. } => "loop", Term::Recur { .. } => "recur", }; @@ -933,20 +914,6 @@ fn any_sub_binder_consumed_for( any_sub_binder_consumed_for(source, pname, uniq, def_name, ctors) || any_sub_binder_consumed_for(body, pname, uniq, def_name, ctors) } - // Iter mut.1: mut-vars are scalar (Int/Float/Bool/Unit) and - // do not participate in the consume-tracking heuristic; the - // surrounding analysis only cares about heap-typed param - // consumption. Recurse into children defensively so any - // genuine consume-of-`pname` inside a var init / body / - // assign value still surfaces. - Term::Mut { vars, body } => { - vars.iter().any(|v| { - any_sub_binder_consumed_for(&v.init, pname, uniq, def_name, ctors) - }) || any_sub_binder_consumed_for(body, pname, uniq, def_name, ctors) - } - Term::Assign { value, .. } => { - any_sub_binder_consumed_for(value, pname, uniq, def_name, ctors) - } Term::Loop { binders, body } => { binders.iter().any(|b| { any_sub_binder_consumed_for(&b.init, pname, uniq, def_name, ctors) diff --git a/crates/ailang-check/src/mono.rs b/crates/ailang-check/src/mono.rs index f9a8ac4..b0e9a27 100644 --- a/crates/ailang-check/src/mono.rs +++ b/crates/ailang-check/src/mono.rs @@ -713,19 +713,14 @@ pub fn collect_mono_targets( // discards warnings — typecheck has already run and surfaced any // shadow warnings; mono is a post-typecheck pass. let mut warnings_discarded: Vec = Vec::new(); - // Iter mut.2: mono re-synth begins from top-of-body — empty - // mut-scope, since any `Term::Mut` will push its own frame as the - // walk descends. - let mut mut_scope_stack: Vec> = Vec::new(); // loop-recur iter 2: mono re-synth begins from top-of-body — // empty loop-stack (any `Term::Loop` pushes its own frame as the - // walk descends), mirroring the fresh mut-scope above. + // walk descends). let mut loop_stack: Vec> = Vec::new(); crate::synth( &f.body, &env, &mut locals, - &mut mut_scope_stack, &mut loop_stack, &mut effects, &f.name, @@ -1235,19 +1230,6 @@ fn rewrite_mono_calls( rewrite_mono_calls(source, method_to_candidate_classes, poly_free_fns, poly_free_fn_ccounts, caller_module, ordered_targets, cursor, locals); rewrite_mono_calls(body, method_to_candidate_classes, poly_free_fns, poly_free_fn_ccounts, caller_module, ordered_targets, cursor, locals); } - // Iter mut.1: visitor-shape recurse with mutable access. - // Mut-vars do not declare class-method or poly-fn names; the - // var inits, the body, and assign values can though, so walk - // them in place. - Term::Mut { vars, body } => { - for v in vars.iter_mut() { - rewrite_mono_calls(&mut v.init, method_to_candidate_classes, poly_free_fns, poly_free_fn_ccounts, caller_module, ordered_targets, cursor, locals); - } - rewrite_mono_calls(body, method_to_candidate_classes, poly_free_fns, poly_free_fn_ccounts, caller_module, ordered_targets, cursor, locals); - } - Term::Assign { value, .. } => { - rewrite_mono_calls(value, method_to_candidate_classes, poly_free_fns, poly_free_fn_ccounts, caller_module, ordered_targets, cursor, locals); - } Term::Loop { binders, body } => { for b in binders.iter_mut() { rewrite_mono_calls(&mut b.init, method_to_candidate_classes, poly_free_fns, poly_free_fn_ccounts, caller_module, ordered_targets, cursor, locals); @@ -1359,19 +1341,14 @@ pub(crate) fn collect_residuals_ordered( // discards warnings — typecheck has already run and surfaced any // shadow warnings; mono is a post-typecheck pass. let mut warnings_discarded: Vec = Vec::new(); - // Iter mut.2: mono re-synth begins from top-of-body — empty - // mut-scope, since any `Term::Mut` will push its own frame as the - // walk descends. - let mut mut_scope_stack: Vec> = Vec::new(); // loop-recur iter 2: mono re-synth begins from top-of-body — // empty loop-stack (any `Term::Loop` pushes its own frame as the - // walk descends), mirroring the fresh mut-scope above. + // walk descends). let mut loop_stack: Vec> = Vec::new(); crate::synth( &f.body, &env, &mut locals, - &mut mut_scope_stack, &mut loop_stack, &mut effects, &f.name, @@ -1625,19 +1602,6 @@ fn interleave_slots( interleave_slots(source, method_to_candidate_classes, poly_free_fns, poly_free_fn_ccounts, class_slots, free_fn_slots, class_cur, free_cur, locals, out); interleave_slots(body, method_to_candidate_classes, poly_free_fns, poly_free_fn_ccounts, class_slots, free_fn_slots, class_cur, free_cur, locals, out); } - // Iter mut.1: visitor-shape recurse, symmetric to - // `rewrite_mono_calls` above. Mut-vars themselves do not - // contribute slots; their init and body terms (and assign - // values) can. - Term::Mut { vars, body } => { - for v in vars { - interleave_slots(&v.init, method_to_candidate_classes, poly_free_fns, poly_free_fn_ccounts, class_slots, free_fn_slots, class_cur, free_cur, locals, out); - } - interleave_slots(body, method_to_candidate_classes, poly_free_fns, poly_free_fn_ccounts, class_slots, free_fn_slots, class_cur, free_cur, locals, out); - } - Term::Assign { value, .. } => { - interleave_slots(value, method_to_candidate_classes, poly_free_fns, poly_free_fn_ccounts, class_slots, free_fn_slots, class_cur, free_cur, locals, out); - } Term::Loop { binders, body } => { for b in binders { interleave_slots(&b.init, method_to_candidate_classes, poly_free_fns, poly_free_fn_ccounts, class_slots, free_fn_slots, class_cur, free_cur, locals, out); diff --git a/crates/ailang-check/src/pre_desugar_validation.rs b/crates/ailang-check/src/pre_desugar_validation.rs index d43cd12..61b146f 100644 --- a/crates/ailang-check/src/pre_desugar_validation.rs +++ b/crates/ailang-check/src/pre_desugar_validation.rs @@ -120,19 +120,6 @@ fn walk_term(t: &Term) -> Result<(), CheckError> { walk_term(source)?; walk_term(body) } - // Iter mut.1: pre-desugar validation has nothing specific to - // assert about mut blocks (the only invariant this pass - // enforces today is "no Float in literal patterns" — mut - // var inits are Terms, but they can hold any literal, and - // patterns inside a body are visited via the normal - // `Term::Match` arm). Recurse into children. - Term::Mut { vars, body } => { - for v in vars { - walk_term(&v.init)?; - } - walk_term(body) - } - Term::Assign { value, .. } => walk_term(value), Term::Loop { binders, body } => { for b in binders { walk_term(&b.init)?; diff --git a/crates/ailang-check/src/reuse_shape.rs b/crates/ailang-check/src/reuse_shape.rs index 7fb7d3c..9bfcaaf 100644 --- a/crates/ailang-check/src/reuse_shape.rs +++ b/crates/ailang-check/src/reuse_shape.rs @@ -253,18 +253,6 @@ impl<'a> Checker<'a> { self.walk(body); self.check_reuse_as(source, body); } - // Iter mut.1: mut-vars are scalar (Int/Float/Bool/Unit) - // and cannot host an allocating ctor body, so the - // reuse-as path-ctor analysis has nothing to refine here. - // Recurse defensively into children so any reuse-as - // nested inside a var init or the body is still checked. - Term::Mut { vars, body } => { - for v in vars { - self.walk(&v.init); - } - self.walk(body); - } - Term::Assign { value, .. } => self.walk(value), Term::Loop { binders, body } => { for b in binders { self.walk(&b.init); diff --git a/crates/ailang-check/src/uniqueness.rs b/crates/ailang-check/src/uniqueness.rs index 42dc739..5749165 100644 --- a/crates/ailang-check/src/uniqueness.rs +++ b/crates/ailang-check/src/uniqueness.rs @@ -336,22 +336,6 @@ impl<'a> Walker<'a> { self.walk(source, Position::Consume); self.walk(body, pos); } - // Iter mut.1: mut-vars are alloca-resident scalars and - // are NOT RC-tracked binders. They do not contribute to - // uniqueness counts (the alloca slot is the per-fn - // storage, not a heap allocation). Walk children - // defensively so any RC-managed value passed through a - // var init / body / assign-value still updates the - // appropriate uniqueness counts. - Term::Mut { vars, body } => { - for v in vars { - self.walk(&v.init, Position::Consume); - } - self.walk(body, pos); - } - Term::Assign { value, .. } => { - self.walk(value, Position::Consume); - } Term::Loop { binders, body } => { for b in binders { self.walk(&b.init, Position::Consume); diff --git a/crates/ailang-check/tests/loop_recur_typecheck_pin.rs b/crates/ailang-check/tests/loop_recur_typecheck_pin.rs index bed062c..9bc75ae 100644 --- a/crates/ailang-check/tests/loop_recur_typecheck_pin.rs +++ b/crates/ailang-check/tests/loop_recur_typecheck_pin.rs @@ -68,16 +68,13 @@ fn infinite_loop_typechecks_clean_no_termination_claim() { } /// Property: a `Term::Lam` whose body captures a `Term::Loop` binder -/// must be rejected at `ail check` with a dedicated diagnostic, -/// symmetric to `mut-var-captured-by-lambda`. Loop binders are -/// alloca-resident and cannot escape their loop frame (iter-3 reuses -/// the `mut_var_allocas` registry, `std::mem::take`-emptied at the -/// lambda boundary), exactly the mut-var escape rule. Today the -/// escape guard inspects only `mut_scope_stack`; loop binders live -/// in `locals`, so check passes (exit 0) and codegen panics -/// `unreachable!()` at `crates/ailang-codegen/src/lambda.rs:102`. -/// This test pins the FIXED contract — RED until the guard also -/// rejects loop-binder captures. +/// is rejected at `ail check` with the dedicated +/// `loop-binder-captured-by-lambda` diagnostic. Loop binders are +/// alloca-resident and cannot escape their loop frame (codegen +/// registers them in the `binder_allocas` registry, +/// `std::mem::take`-emptied at the lambda boundary); the `Term::Lam` +/// escape guard rejects the capture at typecheck rather than letting +/// codegen panic on type-correct input. #[test] fn lambda_capturing_loop_binder_emits_loop_binder_captured_by_lambda() { let codes = check_fixture("test_loop_binder_captured_by_lambda.ail.json"); diff --git a/crates/ailang-check/tests/mut_typecheck_pin.rs b/crates/ailang-check/tests/mut_typecheck_pin.rs deleted file mode 100644 index fbc8086..0000000 --- a/crates/ailang-check/tests/mut_typecheck_pin.rs +++ /dev/null @@ -1,69 +0,0 @@ -//! Iter mut.2 (Task 6): pin tests that the five typecheck fixtures -//! produce the expected diagnostic codes (or zero diagnostics for the -//! positive nested-shadow case). -//! -//! Spec: `docs/specs/2026-05-15-mut-local.md` §"Testing strategy / -//! Typecheck negative tests" — names each fixture and the diagnostic -//! it must surface. The carve-out justification for landing these as -//! `.ail.json` rather than `.ail` mirrors the post-form-a precedent -//! (broken_unbound, test_22b2_*.ail.json) — negative typecheck -//! fixtures live as canonical JSON so the diagnostic code is the -//! load-bearing assertion, not the surface form. - -use ailang_check::check_workspace; -use ailang_surface::load_workspace; -use std::path::PathBuf; - -fn examples_dir() -> PathBuf { - let manifest = env!("CARGO_MANIFEST_DIR"); - PathBuf::from(manifest) - .parent().expect("CARGO_MANIFEST_DIR has a parent (crates/ailang-check)") - .parent().expect("CARGO_MANIFEST_DIR has a grandparent (crates/)") - .join("examples") -} - -/// Load the named fixture under `examples/`, run check_workspace, and -/// return the diagnostic code list (one entry per Diagnostic). -fn check_fixture(fixture_name: &str) -> Vec { - let path = examples_dir().join(fixture_name); - let ws = load_workspace(&path) - .unwrap_or_else(|e| panic!("workspace `{fixture_name}` must load: {e:?}")); - let diags = check_workspace(&ws); - diags.iter().map(|d| d.code.clone()).collect() -} - -#[test] -fn assign_out_of_scope_emits_mut_assign_out_of_scope() { - let codes = check_fixture("test_mut_assign_out_of_scope.ail.json"); - assert_eq!(codes, vec!["mut-assign-out-of-scope".to_string()]); -} - -#[test] -fn assign_type_mismatch_emits_assign_type_mismatch() { - let codes = check_fixture("test_mut_assign_type_mismatch.ail.json"); - assert_eq!(codes, vec!["assign-type-mismatch".to_string()]); -} - -#[test] -fn mut_var_str_type_emits_mut_var_unsupported_type() { - let codes = check_fixture("test_mut_var_unsupported_type.ail.json"); - assert_eq!(codes, vec!["mut-var-unsupported-type".to_string()]); -} - -#[test] -fn assign_outside_mut_emits_mut_assign_out_of_scope() { - let codes = check_fixture("test_mut_assign_outside_mut.ail.json"); - assert_eq!(codes, vec!["mut-assign-out-of-scope".to_string()]); -} - -#[test] -fn nested_shadow_typechecks_clean() { - let codes = check_fixture("test_mut_nested_shadow_legal.ail.json"); - assert!(codes.is_empty(), "expected zero diagnostics, got {codes:?}"); -} - -#[test] -fn lambda_capturing_mut_var_emits_mut_var_captured_by_lambda() { - let codes = check_fixture("test_mut_var_captured_by_lambda.ail.json"); - assert_eq!(codes, vec!["mut-var-captured-by-lambda".to_string()]); -} diff --git a/crates/ailang-codegen/src/escape.rs b/crates/ailang-codegen/src/escape.rs index 1e00dad..6b094c1 100644 --- a/crates/ailang-codegen/src/escape.rs +++ b/crates/ailang-codegen/src/escape.rs @@ -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, 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, 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 = 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 = Vec::new(); for b in binders { diff --git a/crates/ailang-codegen/src/lambda.rs b/crates/ailang-codegen/src/lambda.rs index 4570285..20a23f7 100644 --- a/crates/ailang-codegen/src/lambda.rs +++ b/crates/ailang-codegen/src/lambda.rs @@ -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)> = 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 = 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 = Vec::new(); for b in binders { diff --git a/crates/ailang-codegen/src/lib.rs b/crates/ailang-codegen/src/lib.rs index 3f47f8b..51393bd 100644 --- a/crates/ailang-codegen/src/lib.rs +++ b/crates/ailang-codegen/src/lib.rs @@ -715,36 +715,31 @@ struct Emitter<'a> { /// handed off ownership" signal that makes the dec safe. current_param_modes: BTreeMap, /// 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, + binder_allocas: BTreeMap, /// 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()), } diff --git a/crates/ailang-core/src/ast.rs b/crates/ailang-core/src/ast.rs index d51428b..932f6df 100644 --- a/crates/ailang-core/src/ast.rs +++ b/crates/ailang-core/src/ast.rs @@ -513,33 +513,6 @@ pub enum Term { source: Box, body: Box, }, - /// Iter mut.1: local mutable-state block. `vars` declares zero or - /// more lexically-scoped mutable bindings (initialised in order); - /// `body` is a single Term evaluated in scope of all vars. The - /// block's static type is `body`'s static type. `vars` stays - /// present in canonical JSON even when empty (no - /// `skip_serializing_if` — the field is part of the shape). - /// - /// Typecheck and codegen recognition land in iters mut.2 / mut.3; - /// in iter mut.1 the dispatch entry points (`synth` in - /// `ailang-check`, `lower_term` in `ailang-codegen`) stub these - /// variants with `CheckError::Internal` / `CodegenError::Internal` - /// respectively per spec `docs/specs/2026-05-15-mut-local.md` - /// §"Iteration mut.1". - Mut { - vars: Vec, - body: Box, - }, - /// Iter mut.1: in-block update of a mut-var. Only legal as a - /// sub-term of a `Term::Mut` whose `vars` includes a var with the - /// same `name`. Static type is Unit. The lexical-scope invariant - /// is enforced at typecheck (iter mut.2, - /// `CheckError::MutAssignOutOfScope`); codegen relies on it (iter - /// mut.3). - Assign { - name: String, - value: Box, - }, /// loop-recur iter 1: a strict iteration block. `binders` /// declares one or more loop parameters (name, type, init), /// evaluated in order on loop entry; `body` is evaluated with @@ -547,8 +520,8 @@ pub enum Term { /// the iteration that exits via a non-`recur` branch. Strictly /// additive: pre-existing fixtures hash bit-identically because /// none carry the `"t":"loop"` tag. `binders` has no - /// `skip_serializing_if` (mirrors `Term::Mut.vars` — the field - /// is part of the shape). Typecheck (loop-recur.2): `synth` + /// `skip_serializing_if` — the field is part of the shape. + /// Typecheck (loop-recur.2): `synth` /// types each binder init, the loop's type is the body's type; /// `recur` arity/type is checked positionally via `loop_stack` /// and `verify_loop_body` enforces `recur`-in-tail-position; a @@ -587,42 +560,12 @@ pub struct Arm { pub body: Term, } -/// Iter mut.1: a mutable binding inside [`Term::Mut`]. Lives as a -/// nested field of `Term::Mut`, not as a standalone `Term` variant — -/// mut-vars are not first-class values (they cannot escape the -/// enclosing `Term::Mut` scope; see -/// `docs/specs/2026-05-15-mut-local.md` §"Why three AST nodes, not -/// one fused construct"). -/// -/// The JSON field for `ty` is `"type"`, mirroring the spec's schema -/// (`{ "name": "", "type": Type, "init": Term }`). Derives match -/// [`Arm`]'s for cross-tree consistency. -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct MutVar { - /// The mut-var's lexical name. Within the enclosing `Term::Mut`, - /// a `Term::Var { name }` resolves to this binding (innermost-wins - /// shadowing per spec §"Term::Var resolution"); a - /// `Term::Assign { name, .. }` updates it. - pub name: String, - /// The mut-var's declared type. Restricted to - /// `{ Int, Float, Bool, Unit }` in this milestone — the typecheck - /// pass (iter mut.2) rejects other choices with - /// `CheckError::UnsupportedMutVarType`. - #[serde(rename = "type")] - pub ty: Type, - /// Initial value. Evaluated in the outer scope plus the - /// already-declared vars of the same `Term::Mut` (vars are - /// initialised in declaration order). - pub init: Term, -} - -/// loop-recur iter 1: one binder of a [`Term::Loop`]. Mirrors -/// [`MutVar`]'s `(name, type, init)` triple exactly so the Form-A -/// surface vocabulary is shared (`(NAME TYPE INIT)`); it is a -/// nested field of `Term::Loop`, not a first-class `Term` (loop -/// binders cannot escape the enclosing loop). `recur` rebinds these -/// positionally per iteration. The `ty` JSON field is `"type"`, -/// matching `MutVar` and the spec schema. +/// loop-recur iter 1: one binder of a [`Term::Loop`]. The +/// `(name, type, init)` triple is the Form-A surface vocabulary +/// `(NAME TYPE INIT)`; it is a nested field of `Term::Loop`, not a +/// first-class `Term` (loop binders cannot escape the enclosing +/// loop). `recur` rebinds these positionally per iteration. The +/// `ty` JSON field is `"type"`, matching the spec schema. #[derive(Debug, Clone, Serialize, Deserialize)] pub struct LoopBinder { /// The binder's lexical name. Within the enclosing `Term::Loop`, @@ -952,61 +895,9 @@ fn is_false(b: &bool) -> bool { mod tests { use super::*; - /// Iter mut.1: pin the canonical-bytes shape of an empty-`vars` - /// `Term::Mut`. Spec `docs/specs/2026-05-15-mut-local.md` - /// §"Canonical-form invariants" requires the `vars` array to stay - /// present in canonical JSON rather than being omitted; a future - /// addition of `skip_serializing_if = "Vec::is_empty"` to the - /// field would silently break this property and is the failure - /// mode this pin protects against. - #[test] - fn term_mut_empty_vars_serialises_with_explicit_vars_field() { - let t = Term::Mut { - vars: Vec::new(), - body: Box::new(Term::Lit { - lit: Literal::Int { value: 0 }, - }), - }; - let bytes = serde_json::to_string(&t).expect("serialise"); - assert_eq!( - bytes, - r#"{"t":"mut","vars":[],"body":{"t":"lit","lit":{"kind":"int","value":0}}}"#, - ); - } - - /// Iter mut.1: round-trip a `Term::Assign` through JSON. - /// Pins the canonical-form shape `{ "t": "assign", "name": ..., "value": ... }`. - #[test] - fn term_assign_round_trips_through_json() { - let t = Term::Assign { - name: "x".into(), - value: Box::new(Term::Lit { - lit: Literal::Int { value: 42 }, - }), - }; - let bytes = serde_json::to_string(&t).expect("serialise"); - assert_eq!( - bytes, - r#"{"t":"assign","name":"x","value":{"t":"lit","lit":{"kind":"int","value":42}}}"#, - ); - let back: Term = serde_json::from_str(&bytes).expect("deserialise"); - match back { - Term::Assign { name, value } => { - assert_eq!(name, "x"); - match *value { - Term::Lit { - lit: Literal::Int { value: 42 }, - } => {} - other => panic!("inner literal mismatch: {other:?}"), - } - } - other => panic!("variant mismatch: {other:?}"), - } - } - /// loop-recur iter 1: pin the canonical-bytes shape of a - /// `Term::Loop` with one binder. Mirrors the mut-empty-vars pin: - /// `binders` stays present (no `skip_serializing_if`). + /// `Term::Loop` with one binder: `binders` stays present in + /// canonical JSON (no `skip_serializing_if`). #[test] fn term_loop_one_binder_serialises_with_explicit_binders_field() { let t = Term::Loop { diff --git a/crates/ailang-core/src/desugar.rs b/crates/ailang-core/src/desugar.rs index 0aaafbb..993c498 100644 --- a/crates/ailang-core/src/desugar.rs +++ b/crates/ailang-core/src/desugar.rs @@ -346,23 +346,6 @@ fn collect_used_in_term(t: &Term, used: &mut BTreeSet) { collect_used_in_term(source, used); collect_used_in_term(body, used); } - Term::Mut { vars, body } => { - // Iter mut.1: mut-var declarations introduce source-level - // names — track them so a generated fresh-name cannot - // accidentally collide with one. Then recurse into each - // var's `init` and the block's body. - for v in vars { - used.insert(v.name.clone()); - collect_used_in_term(&v.init, used); - } - collect_used_in_term(body, used); - } - Term::Assign { name, value } => { - // Iter mut.1: the assigned `name` is a *use* of the - // mut-var; record it and recurse into `value`. - used.insert(name.clone()); - collect_used_in_term(value, used); - } Term::Loop { binders, body } => { for b in binders { used.insert(b.name.clone()); @@ -568,47 +551,12 @@ impl Desugarer { source: Box::new(self.desugar_term(source, scope)), body: Box::new(self.desugar_term(body, scope)), }, - Term::Mut { vars, body } => { - // Iter mut.1: structural recursion. Each `var`'s `init` - // is evaluated in scope of the outer environment plus - // the *already-declared* vars of the same `Term::Mut` - // (spec §"Iteration mut.2" — first-class scoping - // formally lands at typecheck). For desugar's purposes - // we extend the scope per-var with a `LetBound` - // sentinel so a generated fresh name does not collide - // with mut-var names; the LetRec lifter does not - // consult mut-vars (mut-vars cannot appear in let-rec - // capture sets — letrec lifting happens before - // typecheck of the mut body). - let mut inner = scope.clone(); - let new_vars: Vec = vars - .iter() - .map(|v| { - let init = self.desugar_term(&v.init, &inner); - inner.insert(v.name.clone(), ScopeEntry::LetBound); - MutVar { - name: v.name.clone(), - ty: v.ty.clone(), - init, - } - }) - .collect(); - Term::Mut { - vars: new_vars, - body: Box::new(self.desugar_term(body, &inner)), - } - } - Term::Assign { name, value } => Term::Assign { - name: name.clone(), - value: Box::new(self.desugar_term(value, scope)), - }, Term::Loop { binders, body } => { - // loop-recur iter 1: structural recursion mirroring - // the Term::Mut arm. Each binder's init is desugared - // in scope of the outer env plus already-declared - // binders; the body sees all binders. The LetBound - // sentinel keeps generated fresh names off binder - // names. + // loop-recur iter 1: structural recursion. Each + // binder's init is desugared in scope of the outer env + // plus already-declared binders; the body sees all + // binders. The LetBound sentinel keeps generated fresh + // names off binder names. let mut inner = scope.clone(); let new_binders: Vec = binders .iter() @@ -1254,33 +1202,10 @@ pub fn free_vars_in_term(t: &Term, bound: &BTreeSet, out: &mut BTreeSet< free_vars_in_term(source, bound, out); free_vars_in_term(body, bound, out); } - Term::Mut { vars, body } => { - // Iter mut.1: a mut-var name binds inside the block. Each - // `var`'s `init` is evaluated in scope of the OUTER - // environment plus the already-declared vars (init order - // matches declaration order); the body is in scope of all - // vars. Mirrors the `Term::Let` pattern but generalised - // across N vars. - let mut b = bound.clone(); - for v in vars { - free_vars_in_term(&v.init, &b, out); - b.insert(v.name.clone()); - } - free_vars_in_term(body, &b, out); - } - Term::Assign { name, value } => { - // Iter mut.1: the assigned `name` is a free var unless - // bound by an enclosing `Term::Mut`. The value is walked - // as usual. - if !bound.contains(name) { - out.insert(name.clone()); - } - free_vars_in_term(value, bound, out); - } Term::Loop { binders, body } => { // loop-recur iter 1: binder names bind inside the loop — // each init sees the outer env plus already-declared - // binders; the body sees all. Mirrors the Term::Mut arm. + // binders; the body sees all. let mut b = bound.clone(); for bd in binders { free_vars_in_term(&bd.init, &b, out); @@ -1437,60 +1362,10 @@ pub fn subst_var(t: &Term, from: &str, to: &str) -> Term { source: Box::new(subst_var(source, from, to)), body: Box::new(subst_var(body, from, to)), }, - Term::Mut { vars, body } => { - // Iter mut.1: mut-vars lexically shadow outer names. Walk - // each var's `init` substituting only if no earlier var in - // the same block already shadows `from`; once a var named - // `from` is declared, subsequent inits AND the body see - // that var, so substitution must stop. - let mut shadowed = false; - let new_vars: Vec = vars - .iter() - .map(|v| { - let init = if shadowed { - v.init.clone() - } else { - subst_var(&v.init, from, to) - }; - if v.name == from { - shadowed = true; - } - MutVar { - name: v.name.clone(), - ty: v.ty.clone(), - init, - } - }) - .collect(); - let body_rw = if shadowed { - (**body).clone() - } else { - subst_var(body, from, to) - }; - Term::Mut { - vars: new_vars, - body: Box::new(body_rw), - } - } - Term::Assign { name, value } => Term::Assign { - // Iter mut.1: substitute inside `value`; the `name` field - // refers to a mut-var declared by an enclosing - // `Term::Mut`. Renaming it here without renaming the - // declaration would break the scope link. The desugar - // pass's `subst_var` is used only to rewrite let-rec - // captures (KnownType / LetBound / MatchArm — never - // mut-vars), so leaving the `name` untouched is - // semantically correct: any `from` that matches the - // assigned `name` is in a different namespace by - // construction. - name: name.clone(), - value: Box::new(subst_var(value, from, to)), - }, Term::Loop { binders, body } => { // loop-recur iter 1: binders lexically shadow outer - // names, symmetric to the Term::Mut arm — once a binder - // named `from` is declared, later inits and the body - // stop being substituted. + // names — once a binder named `from` is declared, later + // inits and the body stop being substituted. let mut shadowed = false; let new_binders: Vec = binders .iter() @@ -1644,26 +1519,6 @@ pub fn subst_call_with_extras(t: &Term, name: &str, lifted: &str, extras: &[Stri source: Box::new(subst_call_with_extras(source, name, lifted, extras)), body: Box::new(subst_call_with_extras(body, name, lifted, extras)), }, - Term::Mut { vars, body } => Term::Mut { - // Iter mut.1: structural recursion through each var's - // `init` and the block's body. Mut-var declarations - // cannot shadow a top-level fn name (mut-vars are - // first-order scalars, fns are not assignable), so the - // call-substitution sees through the block uniformly. - vars: vars - .iter() - .map(|v| MutVar { - name: v.name.clone(), - ty: v.ty.clone(), - init: subst_call_with_extras(&v.init, name, lifted, extras), - }) - .collect(), - body: Box::new(subst_call_with_extras(body, name, lifted, extras)), - }, - Term::Assign { name: assign_name, value } => Term::Assign { - name: assign_name.clone(), - value: Box::new(subst_call_with_extras(value, name, lifted, extras)), - }, Term::Loop { binders, body } => Term::Loop { binders: binders .iter() @@ -1735,24 +1590,6 @@ pub fn find_non_callee_use(t: &Term, name: &str) -> Option { Term::ReuseAs { source, body } => { find_non_callee_use(source, name).or_else(|| find_non_callee_use(body, name)) } - // Iter mut.1: scan each var's init plus the body. Mut-vars - // are first-order scalars and never appear in callee position, - // so any matching `Term::Var` inside a mut block is non-callee - // by construction. - Term::Mut { vars, body } => vars - .iter() - .find_map(|v| find_non_callee_use(&v.init, name)) - .or_else(|| find_non_callee_use(body, name)), - // Iter mut.1: the assigned `name` is a non-callee *use* of - // the mut-var; if it matches the searched name, surface the - // node itself. Otherwise recurse into the value. - Term::Assign { name: assign_name, value } => { - if assign_name == name { - Some(t.clone()) - } else { - find_non_callee_use(value, name) - } - } Term::Loop { binders, body } => binders .iter() .find_map(|b| find_non_callee_use(&b.init, name)) @@ -1802,11 +1639,6 @@ mod tests { } Term::Clone { value } => any_nested_ctor(value), Term::ReuseAs { source, body } => any_nested_ctor(source) || any_nested_ctor(body), - // Iter mut.1: scan each var's init plus the body. - Term::Mut { vars, body } => { - vars.iter().any(|v| any_nested_ctor(&v.init)) || any_nested_ctor(body) - } - Term::Assign { value, .. } => any_nested_ctor(value), Term::Loop { binders, body } => { binders.iter().any(|b| any_nested_ctor(&b.init)) || any_nested_ctor(body) } @@ -1836,11 +1668,6 @@ mod tests { Term::LetRec { .. } => true, Term::Clone { value } => any_let_rec(value), Term::ReuseAs { source, body } => any_let_rec(source) || any_let_rec(body), - // Iter mut.1: scan each var's init plus the body. - Term::Mut { vars, body } => { - vars.iter().any(|v| any_let_rec(&v.init)) || any_let_rec(body) - } - Term::Assign { value, .. } => any_let_rec(value), Term::Loop { binders, body } => { binders.iter().any(|b| any_let_rec(&b.init)) || any_let_rec(body) } @@ -2961,11 +2788,6 @@ mod tests { } Term::Clone { value } => any_lit_pattern(value), Term::ReuseAs { source, body } => any_lit_pattern(source) || any_lit_pattern(body), - // Iter mut.1: scan each var's init plus the body. - Term::Mut { vars, body } => { - vars.iter().any(|v| any_lit_pattern(&v.init)) || any_lit_pattern(body) - } - Term::Assign { value, .. } => any_lit_pattern(value), Term::Loop { binders, body } => { binders.iter().any(|b| any_lit_pattern(&b.init)) || any_lit_pattern(body) } diff --git a/crates/ailang-core/src/workspace.rs b/crates/ailang-core/src/workspace.rs index bf1079e..26d80d7 100644 --- a/crates/ailang-core/src/workspace.rs +++ b/crates/ailang-core/src/workspace.rs @@ -1247,18 +1247,6 @@ where walk_term_embedded_types(source, f)?; walk_term_embedded_types(body, f) } - // Iter mut.1: each `MutVar.ty` is an embedded type — walk - // it. Then recurse into each var's `init` and the block's - // body. `Term::Assign` carries no embedded type; recurse - // into its `value` only. - Term::Mut { vars, body } => { - for v in vars { - walk_type(&v.ty, f)?; - walk_term_embedded_types(&v.init, f)?; - } - walk_term_embedded_types(body, f) - } - Term::Assign { value, .. } => walk_term_embedded_types(value, f), Term::Loop { binders, body } => { for b in binders { walk_type(&b.ty, f)?; @@ -1394,17 +1382,6 @@ where walk_term(source, f)?; walk_term(body, f) } - // Iter mut.1: `Term::Ctor.type_name` is the only string this - // walker reports — mut-vars carry no Ctor-type strings. Just - // recurse into each var's `init` and the body, and into - // assign's `value`. - Term::Mut { vars, body } => { - for v in vars { - walk_term(&v.init, f)?; - } - walk_term(body, f) - } - Term::Assign { value, .. } => walk_term(value, f), Term::Loop { binders, body } => { for b in binders { walk_term(&b.init, f)?; diff --git a/crates/ailang-core/tests/carve_out_inventory.rs b/crates/ailang-core/tests/carve_out_inventory.rs index 8b011fb..2e40118 100644 --- a/crates/ailang-core/tests/carve_out_inventory.rs +++ b/crates/ailang-core/tests/carve_out_inventory.rs @@ -3,16 +3,9 @@ //! under `examples/*.ail.json` after milestone close. The list is //! hardcoded; any change is a deliberate, brainstorm-level decision. //! -//! Eighteen carve-outs post iter loop-recur.tidy (2026-05-18): +//! Twelve carve-outs: //! - §C4 (a) subject-matter: 7 fixtures from form-a.1 milestone //! close (canonical-form rejection / unbound / class-def rejection). -//! - Iter mut.2: 5 negative typecheck fixtures for the new mut / -//! var / assign forms. The diagnostic code is the load-bearing -//! assertion (mut-assign-out-of-scope, assign-type-mismatch, -//! mut-var-unsupported-type) — mirroring the §C4(a) negative- -//! fixture convention. The positive nested-shadow case is a -//! .ail.json carve-out for symmetry with its negative siblings -//! (the driver tests all five from one helper). //! - §C4 (b) compile-time-embed: retired 2026-05-14 by milestone //! prelude-decouple; the prelude is now embedded as `prelude.ail` //! in `ailang-surface` and parsed at compile time via @@ -21,12 +14,11 @@ //! four `Recur*` diagnostics (recur-outside-loop / //! recur-arity-mismatch / recur-type-mismatch / //! recur-not-in-tail-position) — the diagnostic code is the -//! load-bearing assertion, mirroring the §C4(a) / mut.2 +//! load-bearing assertion, mirroring the §C4(a) //! negative-fixture convention. //! - loop-recur iter loop-recur.tidy: 1 negative typecheck fixture //! for the lambda-capture-of-loop-binder rejection -//! (loop-binder-captured-by-lambda) — symmetric to the mut.4-tidy -//! `test_mut_var_captured_by_lambda.ail.json` carve-out. +//! (loop-binder-captured-by-lambda). use std::collections::BTreeSet; @@ -39,14 +31,6 @@ const EXPECTED: &[&str] = &[ "test_ct1_bad_qualifier.ail.json", "test_ct1_bare_xmod_rejected.ail.json", "test_ct1_qualified_class_rejected.ail.json", - // Iter mut.2 — negative typecheck fixtures + positive shadow-pin - "test_mut_assign_out_of_scope.ail.json", - "test_mut_assign_outside_mut.ail.json", - "test_mut_assign_type_mismatch.ail.json", - "test_mut_nested_shadow_legal.ail.json", - "test_mut_var_unsupported_type.ail.json", - // Iter mut.4-tidy — lambda-capture-of-mut-var rejection - "test_mut_var_captured_by_lambda.ail.json", // Iter loop-recur.tidy — lambda-capture-of-loop-binder rejection "test_loop_binder_captured_by_lambda.ail.json", // loop-recur iter 2 — negative typecheck fixtures diff --git a/crates/ailang-core/tests/design_schema_drift.rs b/crates/ailang-core/tests/design_schema_drift.rs index 2eb99a0..6e607a1 100644 --- a/crates/ailang-core/tests/design_schema_drift.rs +++ b/crates/ailang-core/tests/design_schema_drift.rs @@ -137,20 +137,6 @@ fn design_md_anchors_every_term_variant() { body: Box::new(Term::Var { name: "y".into() }), }, ), - ( - r#""t": "mut""#, - Term::Mut { - vars: Vec::new(), - body: Box::new(Term::Lit { lit: Literal::Unit }), - }, - ), - ( - r#""t": "assign""#, - Term::Assign { - name: "x".into(), - value: Box::new(Term::Lit { lit: Literal::Unit }), - }, - ), ( r#""t": "loop""#, Term::Loop { @@ -181,8 +167,6 @@ fn design_md_anchors_every_term_variant() { Term::Seq { .. } => "seq", Term::Clone { .. } => "clone", Term::ReuseAs { .. } => "reuse-as", - Term::Mut { .. } => "mut", - Term::Assign { .. } => "assign", Term::Loop { .. } => "loop", Term::Recur { .. } => "recur", }; @@ -444,19 +428,3 @@ fn data_model_section_is_bounded() { ); } -#[test] -fn design_md_documents_the_accumulator_over_iteration_idiom() { - let design = std::fs::read_to_string( - concat!(env!("CARGO_MANIFEST_DIR"), "/../../docs/DESIGN.md"), - ) - .expect("read DESIGN.md"); - assert!( - design.contains("Accumulator-over-iteration shape (documented idiom, not an enforced rule)"), - "the F1/F4 documented-idiom note must not be silently dropped from DESIGN.md" - ); - assert!( - design.contains("examples/mut_counter.ail is the reference") - || design.contains("`examples/mut_counter.ail` is the reference"), - "the F1/F4 note must keep pointing at the canonical fallback fixture" - ); -} diff --git a/crates/ailang-core/tests/schema_coverage.rs b/crates/ailang-core/tests/schema_coverage.rs index 25cc4cb..298a348 100644 --- a/crates/ailang-core/tests/schema_coverage.rs +++ b/crates/ailang-core/tests/schema_coverage.rs @@ -47,8 +47,6 @@ enum VariantTag { TermSeq, TermClone, TermReuseAs, - TermMut, - TermAssign, TermLoop, TermRecur, // Pattern @@ -96,8 +94,6 @@ const EXPECTED_VARIANTS: &[VariantTag] = &[ VariantTag::TermSeq, VariantTag::TermClone, VariantTag::TermReuseAs, - VariantTag::TermMut, - VariantTag::TermAssign, VariantTag::TermLoop, VariantTag::TermRecur, VariantTag::PatternWild, @@ -236,18 +232,6 @@ fn visit_term(t: &Term, observed: &mut HashSet) { visit_term(source, observed); visit_term(body, observed); } - Term::Mut { vars, body } => { - observed.insert(VariantTag::TermMut); - for v in vars { - visit_type(&v.ty, observed); - visit_term(&v.init, observed); - } - visit_term(body, observed); - } - Term::Assign { value, .. } => { - observed.insert(VariantTag::TermAssign); - visit_term(value, observed); - } Term::Loop { binders, body } => { observed.insert(VariantTag::TermLoop); for b in binders { diff --git a/crates/ailang-core/tests/spec_drift.rs b/crates/ailang-core/tests/spec_drift.rs index 6a157c6..72c180a 100644 --- a/crates/ailang-core/tests/spec_drift.rs +++ b/crates/ailang-core/tests/spec_drift.rs @@ -114,20 +114,6 @@ fn spec_mentions_every_term_variant() { body: Box::new(Term::Var { name: "y".into() }), }, ), - ( - "(mut", - Term::Mut { - vars: Vec::new(), - body: Box::new(Term::Lit { lit: Literal::Unit }), - }, - ), - ( - "(assign", - Term::Assign { - name: "x".into(), - value: Box::new(Term::Lit { lit: Literal::Unit }), - }, - ), ( "(loop", Term::Loop { @@ -160,8 +146,6 @@ fn spec_mentions_every_term_variant() { Term::Seq { .. } => "seq", Term::Clone { .. } => "clone", Term::ReuseAs { .. } => "reuse-as", - Term::Mut { .. } => "mut", - Term::Assign { .. } => "assign", Term::Loop { .. } => "loop", Term::Recur { .. } => "recur", }; diff --git a/crates/ailang-prose/src/lib.rs b/crates/ailang-prose/src/lib.rs index 39b8594..d275167 100644 --- a/crates/ailang-prose/src/lib.rs +++ b/crates/ailang-prose/src/lib.rs @@ -906,35 +906,6 @@ fn write_term_prec(out: &mut String, t: &Term, level: usize, parent_prec: u8, ow out.push_str(&body_buf); } } - Term::Mut { vars, body } => { - // Iter mut.1: prose-side minimal-correctness rendering for - // the `(mut ...)` block. The prose-form for mut blocks is - // not yet fully designed (the prose surface predates this - // milestone); render the block as `mut { var : - // = ; ...; }` so a reader can see the shape - // without overcommitting to a final prose syntax. Refined - // in a follow-on prose iter once the LLM-author signal - // arrives. - out.push_str("mut {\n"); - for v in vars { - indent(out, level + 1); - out.push_str("var "); - out.push_str(&v.name); - out.push_str(" = "); - write_term(out, &v.init, level + 1, owning_module); - out.push_str(";\n"); - } - indent(out, level + 1); - write_term(out, body, level + 1, owning_module); - out.push('\n'); - indent(out, level); - out.push('}'); - } - Term::Assign { name, value } => { - out.push_str(name); - out.push_str(" := "); - write_term(out, value, level, owning_module); - } Term::Loop { binders, body } => { out.push_str("loop("); for (i, b) in binders.iter().enumerate() { @@ -1133,33 +1104,6 @@ fn count_free_var(name: &str, t: &Term) -> usize { } Term::Clone { value } => count_free_var(name, value), Term::ReuseAs { source, body } => count_free_var(name, source) + count_free_var(name, body), - // Iter mut.1: a `Term::Mut` whose `vars` includes a name - // matching `name` shadows the outer binding for both later - // var inits and the body. Var inits before the shadowing one - // still see the outer `name`. - Term::Mut { vars, body } => { - let mut total = 0usize; - let mut shadowed = false; - for v in vars { - if !shadowed { - total += count_free_var(name, &v.init); - } - if v.name == name { - shadowed = true; - } - } - if !shadowed { - total += count_free_var(name, body); - } - total - } - // Iter mut.1: the assigned `name` field is a use of the - // mut-var binding (which may or may not be `name`); the - // value is recursed. - Term::Assign { name: assign_name, value } => { - let n_use = if assign_name == name { 1 } else { 0 }; - n_use + count_free_var(name, value) - } // loop-recur iter 1: a binder named `name` shadows the outer // binding for later binder inits and the body. Inits before // the shadowing binder still see the outer `name`. @@ -1293,50 +1237,9 @@ fn subst_var_with_term(t: &Term, name: &str, replacement: &Term) -> Term { source: Box::new(subst_var_with_term(source, name, replacement)), body: Box::new(subst_var_with_term(body, name, replacement)), }, - // Iter mut.1: lexical-shadow semantics for mut-vars symmetric - // to `count_free_var` above: once a var named `name` is - // declared, neither later inits nor the body should be + // loop-recur iter 1: lexical-shadow semantics — once a binder + // named `name` is declared, later inits and the body are not // rewritten. - Term::Mut { vars, body } => { - let mut shadowed = false; - let new_vars: Vec = vars - .iter() - .map(|v| { - let init = if shadowed { - v.init.clone() - } else { - subst_var_with_term(&v.init, name, replacement) - }; - if v.name == name { - shadowed = true; - } - ailang_core::ast::MutVar { - name: v.name.clone(), - ty: v.ty.clone(), - init, - } - }) - .collect(); - let body_rw = if shadowed { - (**body).clone() - } else { - subst_var_with_term(body, name, replacement) - }; - Term::Mut { - vars: new_vars, - body: Box::new(body_rw), - } - } - // Iter mut.1: substitute only inside `value`. The `name` - // field is a binding reference (cf. the desugar.rs - // `subst_var` arm). - Term::Assign { name: assign_name, value } => Term::Assign { - name: assign_name.clone(), - value: Box::new(subst_var_with_term(value, name, replacement)), - }, - // loop-recur iter 1: lexical-shadow semantics symmetric to - // count_free_var — once a binder named `name` is declared, - // later inits and the body are not rewritten. Term::Loop { binders, body } => { let mut shadowed = false; let new_binders: Vec = binders diff --git a/crates/ailang-surface/src/parse.rs b/crates/ailang-surface/src/parse.rs index 30909a3..1c5d79d 100644 --- a/crates/ailang-surface/src/parse.rs +++ b/crates/ailang-surface/src/parse.rs @@ -40,7 +40,6 @@ //! | app-term | tail-app-term | match-term | ctor-term //! | do-term | tail-do-term | seq-term | lam-term | if-term //! | let-term | let-rec-term | clone-term | reuse-as-term -//! | mut-term | assign-term //! var-ref ::= ident ; reserved: true/false → bool-lit //! int-lit ::= integer ; numeric atom //! str-lit ::= string ; string atom @@ -67,9 +66,6 @@ //! "(" "in" term ")" ")" //! clone-term ::= "(" "clone" term ")" ; Iter 18c.1 //! reuse-as-term ::= "(" "reuse-as" term term ")" ; Iter 18d.1 -//! mut-term ::= "(" "mut" var-decl* term+ ")" ; Iter mut.1 -//! var-decl ::= "(" "var" ident type term ")" ; legal only inside mut-term -//! assign-term ::= "(" "assign" ident term ")" ; Iter mut.1; legal only inside mut-term //! //! pattern ::= pat-var | pat-ctor | pat-lit | pat-wild //! pat-var ::= ident @@ -1210,8 +1206,6 @@ impl<'a> Parser<'a> { "let-rec" => self.parse_let_rec(), "clone" => self.parse_clone(), "reuse-as" => self.parse_reuse_as(), - "mut" => self.parse_mut(), - "assign" => self.parse_assign(), "loop" => self.parse_loop(), "recur" => self.parse_recur(), other => { @@ -1221,8 +1215,8 @@ impl<'a> Parser<'a> { message: format!( "unknown term head `{other}`; expected one of \ `app`, `tail-app`, `lam`, `let`, `let-rec`, `if`, `match`, `do`, \ - `tail-do`, `seq`, `term-ctor`, `clone`, `reuse-as`, `mut`, \ - `assign`, `loop`, `recur`, `lit-unit`" + `tail-do`, `seq`, `term-ctor`, `clone`, `reuse-as`, \ + `loop`, `recur`, `lit-unit`" ), pos, }) @@ -1574,82 +1568,11 @@ impl<'a> Parser<'a> { }) } - /// Iter mut.1: `(mut (var NAME TYPE INIT)* BODY_TERM+)` — local - /// mutable-state block. Reads zero or more `(var ...)` entries - /// off the front of the body, then ≥ 1 trailing terms. The - /// trailing sequence is right-folded into `Term::Seq` with the - /// final term as the seed (so the JSON-AST always sees a single - /// `body: Term`). Spec `docs/specs/2026-05-15-mut-local.md` - /// §"Form A surface" + §"Canonical-form invariants". - fn parse_mut(&mut self) -> Result { - let head_pos = self.peek().map(|t| t.span.start).unwrap_or(0); - self.expect_lparen("mut-term")?; - self.expect_keyword("mut")?; - - // Pull off all leading (var NAME TYPE INIT) entries. - let mut vars: Vec = Vec::new(); - while matches!(self.peek_head_ident(), Some("var")) { - self.expect_lparen("mut-var")?; - self.expect_keyword("var")?; - let name = self.expect_ident("mut-var-name")?; - let ty = self.parse_type()?; - let init = self.parse_term()?; - self.expect_rparen("mut-var")?; - vars.push(ailang_core::ast::MutVar { name, ty, init }); - } - - // Then read ≥ 1 trailing body terms. Right-fold into Seq - // with the final term as the seed. - let mut body_stmts: Vec = Vec::new(); - while !matches!(self.peek(), Some(Token { tok: Tok::RParen, .. })) { - body_stmts.push(self.parse_term()?); - } - if body_stmts.is_empty() { - return Err(ParseError::Production { - production: "mut-term", - message: "(mut ...) requires at least one body expression after vars".into(), - pos: head_pos, - }); - } - self.expect_rparen("mut-term")?; - - let mut body = body_stmts.pop().expect("non-empty after the check above"); - while let Some(s) = body_stmts.pop() { - body = Term::Seq { - lhs: Box::new(s), - rhs: Box::new(body), - }; - } - Ok(Term::Mut { - vars, - body: Box::new(body), - }) - } - - /// Iter mut.1: `(assign NAME VALUE)` — in-block update of a - /// mut-var. Positional shape: name then value. The lexical-scope - /// rule ("legal only inside a `(mut ...)` whose `vars` includes - /// a var with the same `name`") is enforced at typecheck - /// (mut.2, `CheckError::MutAssignOutOfScope`); the parser - /// accepts the shape unconditionally. - fn parse_assign(&mut self) -> Result { - self.expect_lparen("assign-term")?; - self.expect_keyword("assign")?; - let name = self.expect_ident("assign-name")?; - let value = self.parse_term()?; - self.expect_rparen("assign-term")?; - Ok(Term::Assign { - name, - value: Box::new(value), - }) - } - /// loop-recur iter 1: `(loop (NAME TYPE INIT)* BODY_TERM+)` — /// strict iteration block. Reads zero or more `(NAME TYPE INIT)` - /// binder triples (bare, no leading keyword — unlike `(var ...)`, - /// the loop binder triple has no `var` keyword), then ≥ 1 - /// trailing terms right-folded into `Term::Seq` (mirrors - /// `parse_mut`). The binder/body boundary is disambiguated by the + /// binder triples (bare, no leading keyword), then ≥ 1 + /// trailing terms right-folded into `Term::Seq`. The binder/body + /// boundary is disambiguated by the /// inner head: a binder's first inner token is an ident NAME /// (never a term-head keyword), whereas a parenthesised body form /// (`(if …)`, `(recur …)`, `(app …)`, …) opens with a term-head @@ -1673,8 +1596,6 @@ impl<'a> Parser<'a> { | "let-rec" | "clone" | "reuse-as" - | "mut" - | "assign" | "loop" | "recur" ) @@ -2594,89 +2515,6 @@ mod tests { } } - /// Iter mut.1: `(mut 0)` parses as a `Term::Mut` with empty `vars` - /// and an `Int 0` body. Spec - /// `docs/specs/2026-05-15-mut-local.md` §"Canonical-form - /// invariants" — the empty-vars form is a legal degenerate case - /// the schema accepts uniformly. - #[test] - fn parses_empty_mut_with_int_body() { - let t = parse_term("(mut 0)").expect("parse"); - match t { - Term::Mut { vars, body } => { - assert!(vars.is_empty(), "vars should be empty, got {vars:?}"); - match *body { - Term::Lit { lit: Literal::Int { value: 0 } } => {} - other => panic!("expected Lit Int 0, got {other:?}"), - } - } - other => panic!("expected Term::Mut, got {other:?}"), - } - } - - /// Iter mut.1: `(mut (var x ...) (assign x ...) x)` parses with - /// the trailing statement-and-final-expression sequence right- - /// folded into a `Term::Seq`. The schema's `Term::Mut.body` is - /// always a single Term; multi-statement bodies are folded - /// through `Seq` at parse time. - #[test] - fn parses_mut_with_one_var_and_one_assign_and_final() { - let src = "(mut (var x (con Int) 0) (assign x (app + x 1)) x)"; - let t = parse_term(src).expect("parse"); - match t { - Term::Mut { vars, body } => { - assert_eq!(vars.len(), 1, "expected exactly one var"); - assert_eq!(vars[0].name, "x"); - match *body { - Term::Seq { lhs, rhs } => { - match *lhs { - Term::Assign { name, .. } => assert_eq!(name, "x"), - other => panic!("expected Assign as Seq.lhs, got {other:?}"), - } - match *rhs { - Term::Var { name } => assert_eq!(name, "x"), - other => panic!("expected Var x as Seq.rhs, got {other:?}"), - } - } - other => panic!("expected Seq as Mut.body, got {other:?}"), - } - } - other => panic!("expected Term::Mut, got {other:?}"), - } - } - - /// Iter mut.1: `(mut)` with neither vars nor body is rejected at - /// parse — the spec's §"Canonical-form invariants" mandates at - /// least one body expression after the `var` entries. - #[test] - fn rejects_mut_with_no_body() { - let err = parse_term("(mut)").expect_err("must reject empty mut"); - let msg = format!("{err}"); - assert!( - msg.contains("mut"), - "diagnostic should mention `mut`, got: {msg}" - ); - assert!( - msg.contains("body") || msg.contains("expression"), - "diagnostic should mention the missing body, got: {msg}" - ); - } - - /// Iter mut.1: `(mut (var x ...))` declares a var but has no - /// trailing body expression — also rejected (the rule is "≥ 0 - /// vars followed by ≥ 1 body terms"; zero body terms is the - /// rejection trigger). - #[test] - fn rejects_mut_with_only_vars_no_final_expression() { - let err = parse_term("(mut (var x (con Int) 0))") - .expect_err("must reject vars-only mut"); - let msg = format!("{err}"); - assert!( - msg.contains("body") || msg.contains("expression"), - "diagnostic should mention missing body, got: {msg}" - ); - } - #[test] fn parse_loop_and_recur_round_trip_via_term() { let src = "(loop (acc (con Int) 0) (i (con Int) 1) (if (app > i n) acc (recur (app + acc i) (app + i 1))))"; diff --git a/crates/ailang-surface/src/print.rs b/crates/ailang-surface/src/print.rs index 34bd369..36abd75 100644 --- a/crates/ailang-surface/src/print.rs +++ b/crates/ailang-surface/src/print.rs @@ -548,57 +548,10 @@ fn write_term(out: &mut String, t: &Term, level: usize) { write_term(out, body, level); out.push(')'); } - Term::Mut { vars, body } => { - // Iter mut.1: print as - // `(mut (var NAME TYPE INIT)* STMT* FINAL_EXPR)` - // where the body's right-spine of `Term::Seq` is walked - // and each lhs is printed as a top-level statement; the - // terminal expression (the rightmost non-Seq term) is the - // block's value. This is the inverse of the parser's - // right-fold over the trailing sequence. - out.push_str("(mut"); - for v in vars { - out.push_str(" (var "); - out.push_str(&v.name); - out.push(' '); - write_type(out, &v.ty); - out.push(' '); - write_term(out, &v.init, level); - out.push(')'); - } - let mut cursor: &Term = body; - loop { - match cursor { - Term::Seq { lhs, rhs } => { - out.push(' '); - write_term(out, lhs, level); - cursor = rhs; - } - other => { - out.push(' '); - write_term(out, other, level); - break; - } - } - } - out.push(')'); - } - Term::Assign { name, value } => { - // Iter mut.1: print as `(assign NAME VALUE)`. Legal only - // inside a `Term::Mut.body`; the parser enforces the - // structural rule, the typechecker (mut.2) enforces the - // scope rule. - out.push_str("(assign "); - out.push_str(name); - out.push(' '); - write_term(out, value, level); - out.push(')'); - } Term::Loop { binders, body } => { // loop-recur iter 1: print as // `(loop (NAME TYPE INIT)* STMT* FINAL_EXPR)` - // — inverse of parse_loop's binder read + Seq right-fold, - // mirroring the Term::Mut printer. + // — inverse of parse_loop's binder read + Seq right-fold. out.push_str("(loop"); for b in binders { out.push_str(" ("); diff --git a/crates/ailang-surface/tests/mut_removed_pin.rs b/crates/ailang-surface/tests/mut_removed_pin.rs new file mode 100644 index 0000000..7f63517 --- /dev/null +++ b/crates/ailang-surface/tests/mut_removed_pin.rs @@ -0,0 +1,31 @@ +//! Pins the mut/var/assign removal: the keyword no longer parses and +//! the canonical JSON tag is an unknown serde variant. RED until the +//! Task-2 atomic cut lands. +use ailang_surface::parse; + +#[test] +fn mut_keyword_is_rejected_by_form_a_parse() { + let src = "(module m (fn f (type (fn-type (params) (ret (con Int)))) (params) (body (mut (var x (con Int) 0) (assign x (app + x 1)) x))))"; + let r = parse(src); + assert!(r.is_err(), "mut keyword must no longer parse, got Ok"); +} + +#[test] +fn assign_keyword_is_rejected_by_form_a_parse() { + let src = "(module m (fn f (type (fn-type (params) (ret (con Int)))) (params) (body (assign x 1))))"; + assert!(parse(src).is_err(), "assign keyword must no longer parse, got Ok"); +} + +#[test] +fn json_tag_mut_is_unknown_serde_variant() { + let j = r#"{"t":"mut","vars":[],"body":{"t":"lit","lit":{"kind":"int","value":0}}}"#; + let r: Result = serde_json::from_str(j); + assert!(r.is_err(), "{{\"t\":\"mut\"}} must be an unknown variant, got Ok"); +} + +#[test] +fn json_tag_assign_is_unknown_serde_variant() { + let j = r#"{"t":"assign","name":"x","value":{"t":"lit","lit":{"kind":"int","value":1}}}"#; + let r: Result = serde_json::from_str(j); + assert!(r.is_err(), "{{\"t\":\"assign\"}} must be an unknown variant, got Ok"); +} diff --git a/docs/DESIGN.md b/docs/DESIGN.md index 1736750..6d3ba57 100644 --- a/docs/DESIGN.md +++ b/docs/DESIGN.md @@ -2401,26 +2401,6 @@ are real surface forms. // lowers as in-place rewrite under `--alloc=rc`. { "t": "reuse-as", "source": Term, "body": Term } -// Iter mut.1: local mutable-state block. `vars` declares zero or -// more lexically-scoped mutable bindings (initialised in order); -// `body` is a single Term in scope of all vars. The block's static -// type is `body`'s static type. The `vars` array stays present in -// canonical JSON even when empty (no `skip_serializing_if` — -// hash-stable-on-omission is NOT applied here; the field is part of -// the shape). See `docs/specs/2026-05-15-mut-local.md`. -{ "t": "mut", - "vars": [ { "name": "", "type": Type, "init": Term }, ... ], - "body": Term } - -// Iter mut.1: in-block update of a mut-var. Legal only as a -// sub-term of a `Term::Mut` whose `vars` includes a var with the -// same `name`. Static type Unit. The lexical-scope rule is enforced -// at typecheck via `mut-assign-out-of-scope`; codegen lowers as a -// `store` to the var's entry-block alloca. -{ "t": "assign", - "name": "", - "value": Term } - // loop-recur iter 1: strict iteration block. `binders` declares // one or more loop parameters (name, type, init), evaluated in // order on loop entry; `body` is in scope of all binders. The @@ -2445,17 +2425,12 @@ In the MVP, `do` is only a direct call to a built-in effect op (no handler). A `lam` term constructs an anonymous function value; free variables of its body are captured from the enclosing scope. -Both shapes ship in fully-lowered form as of iter mut.3 (2026-05-15): -typecheck binds mut-vars in a lexical scope stack, codegen lowers -them as entry-block allocas, and `Term::Assign` emits a `store` -yielding the canonical Unit SSA. Mut-vars must be of one of the -stack-resident scalar types `{Int, Float, Bool, Unit}`; capturing a -mut-var into a lambda body is rejected at typecheck via -`CheckError::MutVarCapturedByLambda` (iter mut.4-tidy). Loop binders -are alloca-resident under the same scheme, so capturing a `loop` -binder into a lambda body is rejected by the symmetric -`CheckError::LoopBinderCapturedByLambda` (iter loop-recur.tidy). See -`docs/specs/2026-05-15-mut-local.md`. +Loop binders are alloca-resident: typecheck binds them in the +ordinary local scope plus a positional `loop_stack`, and codegen +lowers them as entry-block allocas. Capturing a `loop` binder into a +lambda body is rejected at typecheck via +`CheckError::LoopBinderCapturedByLambda`. See +`docs/specs/2026-05-17-loop-recur.md`. **`Literal`**: @@ -2888,29 +2863,6 @@ What **is** supported (and used as the smoke test for the pipeline): arg types (ctor) or the scrutinee's `Type::Con.args` (match). An unresolved `Type::Var` reaching `llvm_type` is a hard error rather than a silent fallback to `ptr`. -- **Local mutable state.** `(mut (var ) ... )` - declares lexically-scoped mutable bindings whose types are restricted - to the stack-resident scalars `Int`, `Float`, `Bool`, `Unit`. Inside - the block, `(assign )` updates a mut-var; references - to a mut-var name resolve to a `load` at codegen. Mut-vars are - alloca-resident — the `alloca` instructions are hoisted to the fn's - entry block via a per-fn side buffer spliced after `lower_term` so - mem2reg eligibility holds regardless of how deeply nested the - originating `Term::Mut` block sits. Exercised end-to-end by - `examples/mut_counter.ail` (Int) and `examples/mut_sum_floats.ail` - (Float). Mut-vars do not escape the enclosing mut block and do not - introduce a `!Mut` effect onto the surrounding fn signature. - Sealed-by-construction: the spec restricts var element types to - non-RC-managed primitives and forbids first-class references, so - Decision 10's "no shared mutable refs" invariant is preserved (each - mut-var is uniquely held by its enclosing block). Future milestones - on the Stateful-islands path lift the type restriction (heap-RC- - managed Str + ADTs), add escaping references (`ref a` + `!Mut` - effect), and introduce composable transducers (`Stateful a b` + - `pipe`). Spec: `docs/specs/2026-05-15-mut-local.md`. - -**Accumulator-over-iteration shape (documented idiom, not an enforced rule).** `mut`/`var`/`assign` removes friction from *straight-line* and *cascade-of-if* accumulators, but does **not** by itself express the "one running total updated once per loop iteration" shape: a `var` cannot be captured into a lambda (the seal) and there is no loop construct in the language. Until a future iteration-totality milestone lands (gated behind `Nat`/refinement types — see the deferred roadmap entry), the canonical pattern for the per-iteration accumulator is a tail-recursive helper that threads the running total as an explicit parameter; `examples/mut_counter.ail` is the reference. This is a documented authoring idiom, not a typecheck-enforced constraint — wrong code here does not fail to compile, it is merely less ergonomic, which is exactly why this is documentation and not a language rule. - Pipeline regression smoke tests: - `examples/sum.ail.json` → prints 55 (recursion, arithmetic). diff --git a/docs/journals/2026-05-18-iter-remove-mut-var-assign.1.md b/docs/journals/2026-05-18-iter-remove-mut-var-assign.1.md new file mode 100644 index 0000000..5444614 --- /dev/null +++ b/docs/journals/2026-05-18-iter-remove-mut-var-assign.1.md @@ -0,0 +1,63 @@ +# iter remove-mut-var-assign.1 — atomic removal of `mut`/`var`/`assign` + +**Date:** 2026-05-18 +**Started from:** f355899fdf6071c0aeb7de182e93c08568ca743a +**Status:** DONE +**Tasks completed:** 6 of 6 + +## Summary + +The local-mutable-state construct (`Term::Mut` / `Term::Assign` / +`struct MutVar`, the `mut`/`var`/`assign` Form-A keywords, the 4 +`mut` `CheckError` variants, the `mut_scope_stack` synth threading, +the two `lower_term` arms) is removed from AILang entirely and +atomically — AST + surface + checker + codegen + DESIGN.md + +fixtures + drift trio + carve-out + roadmap cut together so the +schema is honest at every commit. `loop`/`recur` + `let`/`if` are +the surviving forms. The shared codegen alloca machinery survives +(loop needs it); its now-misnamed `mut_var_allocas` field is renamed +`binder_allocas` and the shared `Term::Lam` escape guard is +simplified to `!loop_stack.is_empty()` with the loop half kept +byte-equivalent. Removal is symmetric to the additive mut.1 +introduction: serde is tag-based so no surviving module's +canonical-JSON hash moves; `loop`/`recur` codegen is byte-identical +(the rename is representation-only). Full `cargo test --workspace` +605/0; loop/recur non-regression hard gates all green +(55 / 500000500000 / infinite-compiles / the +`lambda_capturing_loop_binder` pin). The behaviour-preservation +proof is executable: `mut_counter`/`mut_sum_floats` still print `55` +after the faithful `let`/`if` rewrite. The "removal made executable" +gate is the new `mut_removed_pin.rs` (4 must-fail pins: `mut`/ +`assign` keyword rejected, `{"t":"mut"}`/`{"t":"assign"}` serde +unknown-variant). + +## Per-task notes + +- remove-mut-var-assign.1.1: created `crates/ailang-surface/tests/mut_removed_pin.rs` — 4 RED→GREEN must-fail pins; harness compiled against the real `ailang_surface::parse` + `ailang_core::ast::Term` serde public API unchanged (no adaptation needed). RED-verified (4 fail with the exact expected messages), GREEN after T2. +- remove-mut-var-assign.1.2: the atomic Rust cut. Deleted `Term::Mut`/`Term::Assign`/`MutVar` from ast.rs; every exhaustive no-`_` `Term::Mut`/`Term::Assign` match arm across 17 source files; parse.rs dispatch + `parse_mut`/`parse_assign` + reservation + grammar EBNF (incl. the line-43 `| mut-term | assign-term` alternation that referenced the deleted productions); print.rs arms; the 4 `CheckError` variants + `code()`/`ctx()`; the synth arms + `Term::Var` mut-scope lookup; the `mut_scope_stack` param removed from `synth` + every internal/external/test caller; escape guard simplified to `loop_stack`-only; `lower_term` Mut/Assign arms deleted; `mut_var_allocas`→`binder_allocas` renamed at all surviving sites + lambda.rs save/restore. Dead `is_supported_mut_var_type` deleted. `cargo build --workspace` 0 errors + 4 Task-1 pins GREEN (the only T2 gate). NO `_ =>` wildcard introduced. +- remove-mut-var-assign.1.3: deleted `mut_typecheck_pin.rs` (whole file); dropped the 4 mut rows in `ct1_check_cli.rs` (kept the surviving loop-binder row, renamed the test + reworded doc honestly); dropped the mut/assign entries + match arms in the drift trio (`schema_coverage`, `design_schema_drift`, `spec_drift`). +- remove-mut-var-assign.1.4: DESIGN.md hard-delete — the `Term::Mut`/`Term::Assign` JSONC blocks, the mut post-schema paragraph (loop-binder half kept + reworded standalone, present-tense, points at the live loop-recur spec), the "Local mutable state" feature bullet, the accumulator-idiom paragraph. Clause-3 rationale ("iterated-mutable-state reasoning", 99/107) untouched. Zero construct residue (only `resolve names + assign hashes` survives — a hashing-pipeline verb, plan-allowed). +- remove-mut-var-assign.1.5: deleted the 3 rejection-probe fixtures; rewrote `mut.ail` (6 fns), `mut_counter.ail`/`mut_sum_floats.ail`, `mut-local_1..4` to the plan's exact faithful `let`/`if` shapes; present-tense docs, no nostalgia. Verified: `mut_counter`/`mut_sum_floats`→55, factorial→120, horner→18, `classify 22`→2, `has_small_factor 91`→true; zero `mut`/`var`/`assign` construct tokens remain. +- remove-mut-var-assign.1.6: `carve_out_inventory.rs` 18→12 (EXPECTED + header doc) AND deleted the 5 orphaned mut `.ail.json` carve-out files the test's disk-inventory assertion requires gone; roadmap false-foundation `**Progress.**` mut-local sub-bullet deleted (milestone header kept). FINAL full-suite green gate 605/0; loop/recur non-regression all green; `roundtrip_cli` PASS. + +## Concerns + +- DONE_WITH_CONCERNS (T2/T3/T6, recon-undercount class, `feedback_plan_pseudo_vs_reality` / `feedback_recon_undercount`): the plan's Step-7 line-list framed several sites as "delete the *argument* at the test-only synth callers", but those test fns' *bodies* construct deleted `Term::Mut`/`MutVar`/deleted-`CheckError` types — arg-trimming alone cannot make them compile. The faithful execution (identical rationale to Task-3's `mut_typecheck_pin.rs` whole-file deletion) was to delete the pure-mut-behaviour test fns wholesale: ~10 in-source `#[cfg(test)] mod tests` fns in `ailang-check/src/lib.rs` (mut_var_resolution_*, mut_block_*, assign_*, mut_typecheck_diagnostics_have_kebab_codes, mut_var_captured_by_lambda_*, lambda_inside_mut_*), the `design_md_documents_the_accumulator_over_iteration_idiom` test fn (pins deleted DESIGN.md prose), the 5 orphaned mut `.ail.json` carve-out files (Task 5 only deleted 1 of 6; the inventory test asserts disk==EXPECTED so all 6 must go), and the `Term::Mut`/`Term::Assign` arms in two non-enumerated test helpers (`ail/tests/codegen_import_map_fallback_pin.rs` — a hard E0599 the recon missed entirely; `ail/tests/e2e.rs` doc-comments). ~30 now-dangling doc-comments/comments that cross-referenced deleted symbols (`mirrors Term::Mut`, `MutVarCapturedByLambda`, the `loop_recur_typecheck_pin.rs` RED-state doc) were reworded so the source has zero trace/post-mortem (spec acceptance). The kept e2e bodies `mut_counter_prints_55`/`mut_sum_floats_prints_55` were preserved byte-identical per plan line-57's explicit "must stay green" directive — only their lying doc-comments were corrected. None of these are gratuitous; every one is strictly entailed by the atomic removal + the plan-stated final green gate. The class recurred enough (in-source tests, drift-pin test fn, orphaned carve-out files, non-enumerated test helpers) to be a planner-recon defect worth a follow-up countermeasure, not a per-iter fix. + +## Known debt + +- `crates/ailang-core/src/workspace.rs:1620` `tmp_dir` is a pre-existing `never used` test-helper warning, NOT introduced by this iter (it surfaces only in the test-target build; `cargo build --workspace` is 0 warnings). Out of scope (no surrounding cleanup); left untouched. + +## Files touched + +Source (17): ail/src/main.rs, ailang-check/src/{builtins,lib,lift,linearity,mono,pre_desugar_validation,reuse_shape,uniqueness}.rs, ailang-codegen/src/{escape,lambda,lib}.rs, ailang-core/src/{ast,desugar,workspace}.rs, ailang-prose/src/lib.rs, ailang-surface/src/{parse,print}.rs +Tests (modified): ailang-core/tests/{carve_out_inventory,design_schema_drift,schema_coverage,spec_drift}.rs, ail/tests/{ct1_check_cli,codegen_import_map_fallback_pin,e2e}.rs, ailang-check/tests/loop_recur_typecheck_pin.rs +Tests (created): ailang-surface/tests/mut_removed_pin.rs +Tests (deleted): ailang-check/tests/mut_typecheck_pin.rs +Docs: docs/DESIGN.md, docs/roadmap.md +Fixtures (rewritten): examples/mut.ail, examples/mut_counter.ail, examples/mut_sum_floats.ail, examples/fieldtest/mut-local_{1,2,3,4}*.ail +Fixtures (deleted, 8): examples/fieldtest/mut-local_{5,6}*.ail, examples/test_mut_{assign_out_of_scope,assign_outside_mut,assign_type_mismatch,nested_shadow_legal,var_captured_by_lambda,var_unsupported_type}.ail.json + +## Stats + +bench/orchestrator-stats/2026-05-18-iter-remove-mut-var-assign.1.json diff --git a/docs/journals/INDEX.md b/docs/journals/INDEX.md index 825295e..8117c50 100644 --- a/docs/journals/INDEX.md +++ b/docs/journals/INDEX.md @@ -88,3 +88,4 @@ - 2026-05-18 — fieldtest loop-recur (milestone CLOSE, clean on all milestone axes): post-audit downstream-LLM-author field test of the shipped loop/recur surface. The fieldtester (DESIGN.md + public examples only, never compiler source) wrote 3 real iterative programs — integer Newton sqrt (multi-binder, recur buried in if/let/if, loop result into let/seq), Collatz length (recur in match-arm tails), Euclidean gcd (loop as a value sub-expression in argument position) — plus 5 plausible-mistake negatives and 2 no-termination probes, all run through the public `ail` CLI. **0 bugs; 4 working findings, all on the milestone's own axes**: the five rejection diagnostics (recur-outside-loop / -arity / -type / -not-in-tail / loop-binder-captured-by-lambda) are point-exact AND self-fixing (name the fn, describe the fix, no false positives, no crash — the clause-2 silent-failure-class-becomes-compile-error claim made real); recur tail-position threads correctly through match-arm/let/outer-if (the spec only demonstrated `if` — generalises exactly as an author assumes); loop composes as a value sub-expression everywhere + every loop/recur fixture is `ail parse|render|parse` byte-identical (Roundtrip Invariant holds in practice); the no-termination boundary is exactly as specified (infinite loop check+build clean, zero spurious diagnostics — the precise difference from the reverted Iteration-discipline milestone). This empirically substantiates the milestone's "an LLM author can now write iterative programs with loop/recur" claim — the gate fieldtest exists to provide. Two ORTHOGONAL non-blocking findings surfaced incidentally while probing the no-termination/negative axes, neither in loop/recur scope, both routed to P2 todos (not a loop/recur tidy — refusing the scope creep): (spec_gap) niladic `(app f)` is unrepresentable in Form A and DESIGN.md's `Term::App args:[Term...]` states no minimum — INDEPENDENTLY re-confirms the existing mut-local-F3 roadmap todo (two fieldtests now hit it; the accept-`(app f)`-vs-ratify-DESIGN.md decision is a genuine design fork deliberately NOT auto-ratified under autonomous /boss — parked, priority-strengthened); (friction) the module-level `(doc …)` rejection lists valid def heads but omits that doc attaches inside fn/data — a one-line diagnostic-hint tidy. Independent Boss verification: `loop_recur_4_gcd_value_pos.ail` re-run → stdout `27`; `loop_recur_3a` re-run → `ail check` exit 1 `[recur-outside-loop] countdown: …`. **The standalone loop/recur milestone is fully ratified and CLOSED**: 3 iterations + a tidy shipped, audit clean (architect drift resolved, bench pristine carry-on), fieldtest clean on every milestone axis. Fixtures `examples/fieldtest/loop_recur_*.ail` + spec `docs/specs/2026-05-18-fieldtest-loop-recur.md` committed; roadmap P0 entry marked closed; the two orthogonal findings tracked as P2 todos → 2026-05-18-fieldtest-loop-recur.md - 2026-05-18 — iter prose-loop-binders.1 (single-iteration milestone, DONE): Form-B prose `loop` now renders binders as a parenthesised init-list on the keyword line — `loop(acc = 0, i = 1) { … }` — instead of bare `name = init;` statements inside the body block, which had implied C/Rust re-init-every-iteration semantics. Projection-only single-arm rewrite of the `Term::Loop` case of `write_term` (`crates/ailang-prose/src/lib.rs`); init exprs render at `level` (keyword line, not the indented block), mirroring the untouched adjacent `Term::Recur` `enumerate()`+`", "` idiom — making the binder list positionally isomorphic to `recur(...)`, which teaches the correct "recur re-binds these positionally" model instead of obscuring it. RED-first via the existing stem-explicit `check_snapshot` harness: two new committed `examples/{loop_sum_to_run,loop_forever_build}.prose.txt` whole-file byte-equality fixtures + two `snapshot_loop_*` `#[test]` fns, verified `0 passed; 2 failed` against the old renderer before the arm rewrite turned them green. Loop/recur AST, Form-A, JSON-AST, typecheck, codegen and the Form-A↔JSON round-trip invariant untouched by construction (no signature change, no caller touched, prose is a one-way lossy projection off the round-trip path — spec §Architecture states this explicitly so milestone-close audit does not chase a phantom). Full `ailang-prose` suite 10/0; both `ail prose | diff` live-CLI byte-matches `MATCH`; post-cleanup porcelain exactly the expected paths. Surfaced from the 2026-05-18 user prose review of the just-closed loop/recur milestone; brainstorm spec `docs/specs/2026-05-18-prose-loop-binders.md` (Step-7.5 grounding-check PASS), plan `docs/plans/prose-loop-binders.1.md`. Both implement review phases first-pass, 0 re-loops. Milestone-close `audit` is the Boss's next pipeline step (projection-only, no Form-A surface change → no fieldtest). → 2026-05-18-iter-prose-loop-binders.1.md - 2026-05-18 — audit prose-loop-binders (milestone CLOSE, clean / carry-on): milestone-close tidy for the single-iteration prose-loop-binders milestone (range `6cbd0fe..c9355d7`). Architect drift review **clean** — zero drift, zero debt, verified against the diff not journal trust: single `Term::Loop` `write_term` hunk in `crates/ailang-prose/src/lib.rs`, the two non-render `Term::Loop` sites + all `Term::Recur` arms absent from the diff, DESIGN.md/PROSE_ROUNDTRIP.md make no claim about prose `loop` rendering (so the spec's "no doc edit needed" is correct not a skipped obligation), no carve-out/hash-pin/round-trip lockstep bypassed (prose is off the Form-A↔JSON round-trip path by construction). Bench: `compile_check.py` 0/24 + `cross_lang.py` 0/25 both **exit 0** (cross_lang's independent `ail_bump_s` +1.50% ±15% ok corroborates check.py's bump_s firing is baseline-staleness not a real regression); `check.py` exit 1, first run 3 regressed but a confirmatory identical re-run (no code change) returned `latency.implicit_at_rc.p99_9_us` +37.30%→+10.22% **ok** and `.max_us` +114.14%→+22.17% **ok** (median/p99 within tol both runs) — the two latency-tail firings proven single-sample 5-run RC-mode jitter by re-run; `bench_list_sum.bump_s` persisted ~+12% = the already-tracked P2 roadmap todo (byte-oracle-proven environmental at the 2026-05-16 iteration-discipline-revert audit, causally impossible to attribute to a projection-only `ailang-prose` change). **All items carry-on**: no fix iteration, no baseline ratify — explicitly NOT bumping the `*.bump_s` baseline opportunistically inside an unrelated projection-only milestone (the P2 todo owns a holistic recapture; piecemeal bump is the "regression is expected" rationalisation the Iron Law forbids). Fieldtest **not applicable / not dispatched**: projection-only change to the Form-B *reading* surface; the fieldtester authors `.ail` Form-A and would not exercise the prose-render change (no friction/bug/spec-gap axis); the two whole-file byte-equality `.prose.txt` snapshots + live-CLI `ail prose|diff` checks are the projection-only E2E gate. Milestone **CLOSED clean**; roadmap P0 entry flipped `[~]`→`[x]`, WhatsNew.md user-facing entry appended (user present → no notify.sh fired per notification policy). → 2026-05-18-audit-prose-loop-binders.md +- 2026-05-18 — iter remove-mut-var-assign.1 (single terminal iteration, DONE): `mut`/`var`/`assign` removed from AILang entirely and atomically — `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 + drift trio + carve-out + roadmap so the schema is honest at every commit (no `_ =>` wildcard introduced — Boss-verified). `loop`/`recur` + `let`/`if` are the surviving forms. Shared codegen alloca machinery survives (loop reuses it): `mut_var_allocas`→`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`) kept byte-equivalent. Feature-acceptance applied inverted (removed feature fails clause 2 redundant + clause 3 IS the iterated-mutable-state bug class). Behaviour-preservation proof executable: `mut_counter`/`mut_sum_floats` still print `55` after the faithful `let`/`if` rewrite (factorial→120, horner→18, classify 22→2, has_small_factor 91→true); the "removal made executable" gate is the new `mut_removed_pin.rs` (4 must-fail pins: `mut`/`assign` keyword rejected, `{"t":"mut"}`/`{"t":"assign"}` serde unknown-variant). Independent Boss verification: full `cargo test --workspace` **605/0**, zero residual mut symbols in any crate source, loop/recur non-regression all green (55 / 500000500000 / infinite-compiles / `lambda_capturing_loop_binder` pin), `roundtrip_cli` PASS. One systemic DONE_WITH_CONCERNS — 4th recurrence of the recon-undercount `feedback_plan_pseudo_vs_reality` class (in-source `mod tests` fns + a drift-pin fn + 5 orphaned mut `.ail.json` carve-outs + a non-enumerated `codegen_import_map_fallback_pin.rs` E0599 the recon missed; all resolved within implementer remit via Task-3's identical whole-file-deletion rationale, no behaviour change, e2e bodies preserved per plan) — worth a planner/recon-agent countermeasure, pairs with the existing loop-recur.tidy P2 todo. Milestone-close `audit` then `fieldtest` (surface-touching) are the Boss's next pipeline steps. spec `docs/specs/2026-05-18-remove-mut-var-assign.md` (grounding-check PASS), plan `docs/plans/remove-mut-var-assign.1.md` → 2026-05-18-iter-remove-mut-var-assign.1.md diff --git a/docs/roadmap.md b/docs/roadmap.md index 3515ace..0062d72 100644 --- a/docs/roadmap.md +++ b/docs/roadmap.md @@ -45,9 +45,12 @@ work progresses. window, JSON tags fail closed as generic unknown-variant, codegen alloca machinery kept (loop reuses it) + renamed `binder_allocas`, shared `Term::Lam` escape guard keeps its `loop` half. Explicitly - decoupled from Stateful-islands (separate, deferred). Spec written - + grounding-check PASS; awaiting user spec review → planner. - - context: `docs/specs/2026-05-18-remove-mut-var-assign.md` + decoupled from Stateful-islands (separate, deferred). Spec + approved + planned + implemented (605/0, loop/recur + non-regression green, removal made executable); milestone-close + `audit` then `fieldtest` (surface-touching) remain. + - context: `docs/specs/2026-05-18-remove-mut-var-assign.md`; + `docs/journals/2026-05-18-iter-remove-mut-var-assign.1.md` - [x] **\[milestone\]** Prose `loop` binders — projection redesign — CLOSED 2026-05-18. Form-B prose now renders loop binders as a @@ -434,16 +437,6 @@ work progresses. zero-allocation pipe combinator, growing tuple-state types as pipelines lengthen). - **Progress.** Decomposed at brainstorm time (2026-05-15) into a - sequence of shippable sub-milestones. Sub-milestone 1 closed - 2026-05-15: **mut-local** — sealed-by-construction `mut`/`var`/ - `assign` blocks for Int/Float/Bool/Unit scalars, alloca-resident, - no escape, no `!Mut` effect leakage. Foundation in place; - fn signatures stay pure while LLM authors can write imperative - accumulators directly. Spec `docs/specs/2026-05-15-mut-local.md`; - iters mut.1/mut.2/mut.3/mut.4-tidy (commits 7b92719, b24718a, - 03fb633, 20add51). - **Sub-milestone sequencing is UNDER RE-THINK with the user (2026-05-16) — no further sub-milestone is planned or brainstormed until that conversation happens.** Why: the 2026-05-15 decomposition diff --git a/examples/fieldtest/mut-local_1_factorial.ail b/examples/fieldtest/mut-local_1_factorial.ail index d917122..f477fdc 100644 --- a/examples/fieldtest/mut-local_1_factorial.ail +++ b/examples/fieldtest/mut-local_1_factorial.ail @@ -1,17 +1,4 @@ -; Fieldtest mut-local #1 — factorial 5! via straight-line mut updates. -; -; Task: print 5! (= 120) using a `mut` block that names a running -; product and unrolls five multiplications as straight-line statements. -; This is the most direct possible use of mut-local: no helper fn, no -; iteration, just a sequence of in-block updates terminated by reading -; the var. The LLM-author's mental model of "I want a local accumulator" -; maps 1:1 onto the surface here. -; -; Why this fits mut-local's scope: the milestone supplies only sealed -; lexically-scoped mutables, with no `while` or `for`. Straight-line -; unroll is the *only* shape inside one mut block that needs no helper. -; -; Expected stdout: 120 +; Print 5! (= 120) via a let-threaded running product. (module mut-local_1_factorial @@ -19,12 +6,4 @@ (type (fn-type (params) (ret (con Unit)) (effects IO))) (params) (body - (app print - (mut - (var prod (con Int) 1) - (assign prod (app * prod 1)) - (assign prod (app * prod 2)) - (assign prod (app * prod 3)) - (assign prod (app * prod 4)) - (assign prod (app * prod 5)) - prod))))) + (app print (let prod 1 (let prod (app * prod 1) (let prod (app * prod 2) (let prod (app * prod 3) (let prod (app * prod 4) (let prod (app * prod 5) prod)))))))))) diff --git a/examples/fieldtest/mut-local_2_classify_temp.ail b/examples/fieldtest/mut-local_2_classify_temp.ail index 2ed1135..8cec443 100644 --- a/examples/fieldtest/mut-local_2_classify_temp.ail +++ b/examples/fieldtest/mut-local_2_classify_temp.ail @@ -1,20 +1,5 @@ -; Fieldtest mut-local #2 — classify a temperature into a band using -; nested if-branches that each update a mut-Int "category code". -; -; Task: given a temperature value, set a category-code mut-var to -; 0 (freezing), 1 (cold), 2 (warm), 3 (hot) by walking through a -; cascade of if-branches. Print the resulting code. -; -; Why this fits mut-local's scope: this exercises mut composed with -; `if` — each branch contains a single `(assign ...)`. The seal-by- -; construction promise says the if-branch can write to the var, and -; the var's value flows out of the branch as the latest store. This is -; a use of mut that *replaces* what a chain of let-rebinds would -; otherwise do, and a chain of let-rebinds is the AILang author's -; usual workaround for "set this variable conditionally" — so the -; mut form should be measurably cleaner here. -; -; Expected stdout: 2 (room temperature 22 = "warm") +; Classify a temperature into a 0..3 band via a let-bound code. +; classify 22 = 2 ("warm"). Expected stdout: 2 (module mut-local_2_classify_temp @@ -22,17 +7,7 @@ (doc "Return category code 0..3 for temperature t in degrees C.") (type (fn-type (params (con Int)) (ret (con Int)))) (params t) - (body - (mut - (var code (con Int) 0) - (if (app < t 0) - (assign code 0) - (if (app < t 15) - (assign code 1) - (if (app < t 28) - (assign code 2) - (assign code 3)))) - code))) + (body (let code 0 (let code (if (app < t 0) 0 (if (app < t 15) 1 (if (app < t 28) 2 3))) code)))) (fn main (type (fn-type (params) (ret (con Unit)) (effects IO))) diff --git a/examples/fieldtest/mut-local_3_horner.ail b/examples/fieldtest/mut-local_3_horner.ail index 4d95ae7..7ceb9a5 100644 --- a/examples/fieldtest/mut-local_3_horner.ail +++ b/examples/fieldtest/mut-local_3_horner.ail @@ -1,23 +1,5 @@ -; Fieldtest mut-local #3 — evaluate the polynomial -; p(x) = 2 x^3 - 3 x^2 + 5 x - 7 -; at x = 2.5 by Horner's method, using a Float mut-var as the running -; accumulator and unrolling the four Horner steps as straight-line -; assigns. -; -; Why this fits mut-local's scope: the accumulator is a Float, the -; updates are straight-line (no iteration), and the mut form removes -; the four nested let-rebinds an LLM-author would otherwise write -; ("p1 = ..., p2 = p1*x + ..., p3 = p2*x + ...") — each rebind needing -; a fresh name. Reusing one name for the running accumulator is the -; natural shape, and mut supplies it. -; -; Hand-check (Horner): start with leading coeff 2.0, then for each -; lower coefficient do acc = acc * x + c: -; 2.0 * 2.5 + (-3) = 5.0 - 3 = 2.0 -; 2.0 * 2.5 + 5 = 5.0 + 5 = 10.0 -; 10.0 * 2.5 + (-7) = 25.0 - 7 = 18.0 -; Expected stdout: 18 (Float 18.0 via %g; print drops the trailing -; ".0" the same way it does for the Float fixture mut_sum_floats.ail.) +; Evaluate p(x) = 2x^3 - 3x^2 + 5x - 7 at x = 2.5 by Horner's method +; via a let-threaded Float accumulator. Expected stdout: 18 (module mut-local_3_horner @@ -25,10 +7,4 @@ (type (fn-type (params) (ret (con Unit)) (effects IO))) (params) (body - (app print - (mut - (var acc (con Float) 2.0) - (assign acc (app - (app * acc 2.5) 3.0)) - (assign acc (app + (app * acc 2.5) 5.0)) - (assign acc (app - (app * acc 2.5) 7.0)) - acc))))) + (app print (let acc 2.0 (let acc (app - (app * acc 2.5) 3.0) (let acc (app + (app * acc 2.5) 5.0) (let acc (app - (app * acc 2.5) 7.0) acc)))))))) diff --git a/examples/fieldtest/mut-local_4_has_small_factor.ail b/examples/fieldtest/mut-local_4_has_small_factor.ail index 4b9c790..d539a73 100644 --- a/examples/fieldtest/mut-local_4_has_small_factor.ail +++ b/examples/fieldtest/mut-local_4_has_small_factor.ail @@ -1,17 +1,5 @@ -; Fieldtest mut-local #4 — Bool mut-var "found-a-factor" flag. -; -; Task: probe whether n has a small prime factor (2, 3, 5, or 7) by -; running four straight-line checks; if any check matches, set a Bool -; mut-var to true. Print the flag at the end. -; -; The straight-line form here is the natural shape: an LLM-author asked -; to "test these four conditions and OR the results" would otherwise -; write a chain of `||` operators (no such operator in AILang surface) -; or a nested chain of `(if ... (if ... ))`. The mut form replaces both -; with a flat sequence whose intent ("set this flag if any of these -; matches") reads top-to-bottom. -; -; Test against n = 91 = 7 * 13 — only the divisible-by-7 check fires. +; Probe whether n has a small prime factor (2, 3, 5, 7) via a +; let-threaded Bool flag. has_small_factor 91 = true (91 = 7 * 13). ; Expected stdout: true (module mut-local_4_has_small_factor @@ -19,14 +7,7 @@ (fn has_small_factor (type (fn-type (params (con Int)) (ret (con Bool)))) (params n) - (body - (mut - (var found (con Bool) false) - (if (app == (app % n 2) 0) (assign found true) (lit-unit)) - (if (app == (app % n 3) 0) (assign found true) (lit-unit)) - (if (app == (app % n 5) 0) (assign found true) (lit-unit)) - (if (app == (app % n 7) 0) (assign found true) (lit-unit)) - found))) + (body (let found false (let found (if (app == (app % n 2) 0) true found) (let found (if (app == (app % n 3) 0) true found) (let found (if (app == (app % n 5) 0) true found) (let found (if (app == (app % n 7) 0) true found) found))))))) (fn main (type (fn-type (params) (ret (con Unit)) (effects IO))) diff --git a/examples/fieldtest/mut-local_5_lambda_capture_probe.ail b/examples/fieldtest/mut-local_5_lambda_capture_probe.ail deleted file mode 100644 index c613053..0000000 --- a/examples/fieldtest/mut-local_5_lambda_capture_probe.ail +++ /dev/null @@ -1,39 +0,0 @@ -; Fieldtest mut-local #5 — deliberate probe of the seal-by-construction -; promise: try to lift a mut-var into a lambda closure. -; -; Spec §"Out of scope": "Lambda capture of a mut-var. A lambda body -; whose free vars include a mut-var of an enclosing Term::Mut is -; rejected at typecheck with CheckError::MutVarCapturedByLambda." -; -; This file is EXPECTED TO FAIL at `ail check` with the -; `mut-var-captured-by-lambda` diagnostic. The purpose is to probe: -; - that the diagnostic actually fires -; - that its rendered text is actionable -; - that it points at the lambda site, not somewhere else -; -; The shape: a mut block declares `count`, builds a closure that would -; close over `count`, and tries to return the closure. An LLM-author -; might write this naïvely thinking "I just need a small callback that -; updates the running count" — exactly the shape the seal forbids. - -(module mut-local_5_lambda_capture_probe - - (fn make_bumper - (type - (fn-type - (params (con Int)) - (ret (fn-type (params (con Int)) (ret (con Int)))))) - (params seed) - (body - (mut - (var count (con Int) 0) - (assign count seed) - (lam (params (typed n (con Int))) - (ret (con Int)) - (body (app + n count)))))) - - (fn main - (type (fn-type (params) (ret (con Unit)) (effects IO))) - (params) - (body - (app print (app (app make_bumper 10) 5))))) diff --git a/examples/fieldtest/mut-local_6_diag_probe.ail b/examples/fieldtest/mut-local_6_diag_probe.ail deleted file mode 100644 index e4abf9e..0000000 --- a/examples/fieldtest/mut-local_6_diag_probe.ail +++ /dev/null @@ -1,18 +0,0 @@ -; Fieldtest mut-local #6 — deliberate diagnostic probe. EXPECTED TO -; FAIL at `ail check`. -; -; Probes `mut-var-unsupported-type` — declaring a Str mut-var. -; (A sibling fixture used to probe `assign-type-mismatch` by assigning -; 1.5 to an Int var; on the same surface the diagnostic also fires -; with the same double-bracket-prefix shape.) - -(module mut-local_6_diag_probe - - (fn main - (type (fn-type (params) (ret (con Unit)) (effects IO))) - (params) - (body - (app print - (mut - (var s (con Str) "hello") - s))))) diff --git a/examples/mut.ail b/examples/mut.ail index 0a65166..fcad4fd 100644 --- a/examples/mut.ail +++ b/examples/mut.ail @@ -1,62 +1,37 @@ (module mut (fn mut_empty - (doc "Iter mut.1 — empty mut block; body is a single Int literal.") + (doc "Empty block; body is a single Int literal.") (type (fn-type (params) (ret (con Int)))) (params) - (body (mut 0))) + (body 0)) (fn mut_single_var - (doc "Iter mut.1 — one var, one assign, final expression reads the var.") + (doc "let-rebind: x starts at 0, the block value is x + 1.") (type (fn-type (params) (ret (con Int)))) (params) - (body - (mut - (var x (con Int) 0) - (assign x (app + x 1)) - x))) + (body (let x 0 (let x (app + x 1) x)))) (fn mut_two_vars - (doc "Iter mut.1 — two vars, two assigns, final expression combines them.") + (doc "Two let-threaded scalars combined into the block value.") (type (fn-type (params) (ret (con Float)))) (params) - (body - (mut - (var sum (con Float) 0.0) - (var count (con Int) 0) - (assign sum (app + sum 1.0)) - (assign count (app + count 1)) - (app + sum (app int_to_float count))))) + (body (let sum 0.0 (let count 0 (let sum (app + sum 1.0) (let count (app + count 1) (app + sum (app int_to_float count)))))))) (fn mut_nested_shadow - (doc "Iter mut.1 — outer var shadowed by inner mut block's var of the same name.") + (doc "Nested let shadowing; inner binding is the block value.") (type (fn-type (params) (ret (con Int)))) (params) - (body - (mut - (var x (con Int) 10) - (assign x (app + x 1)) - (mut - (var x (con Int) 100) - (assign x (app + x 1)) - x)))) + (body (let x 10 (let x (app + x 1) (let x 100 (let x (app + x 1) x)))))) (fn mut_returns_bool - (doc "Iter mut.1 — exercise Bool as a supported scalar var type.") + (doc "Bool scalar threaded through let; block value is the latest binding.") (type (fn-type (params) (ret (con Bool)))) (params) - (body - (mut - (var flag (con Bool) false) - (assign flag true) - flag))) + (body (let flag false (let flag true flag)))) (fn mut_returns_unit - (doc "Iter mut.1 — exercise Unit as the supported scalar zero case.") + (doc "Unit scalar threaded through let; block value is the latest binding.") (type (fn-type (params) (ret (con Unit)))) (params) - (body - (mut - (var u (con Unit) (lit-unit)) - (assign u (lit-unit)) - u)))) + (body (let u (lit-unit) (let u (lit-unit) u))))) diff --git a/examples/mut_counter.ail b/examples/mut_counter.ail index 93d8a35..eae2a5c 100644 --- a/examples/mut_counter.ail +++ b/examples/mut_counter.ail @@ -1,15 +1,11 @@ (module mut_counter (fn main - (doc "Iter mut.3 — sum 1..10 via mut + recursive helper. Expected stdout: 55.") + (doc "Sum 1..10 via a tail-recursive helper. Expected stdout: 55.") (type (fn-type (params) (ret (con Unit)) (effects IO))) (params) (body - (app print - (mut - (var sum (con Int) 0) - (assign sum (app sum_helper 1 10 0)) - sum)))) + (app print (app sum_helper 1 10 0)))) (fn sum_helper (doc "Accumulator-style tail-recursive helper — sum from lo through hi inclusive.") diff --git a/examples/mut_sum_floats.ail b/examples/mut_sum_floats.ail index 13840fd..607e4aa 100644 --- a/examples/mut_sum_floats.ail +++ b/examples/mut_sum_floats.ail @@ -1,15 +1,11 @@ (module mut_sum_floats (fn main - (doc "Iter mut.3 — Float twin of mut_counter. Expected stdout: 55 (Float-55.0 via %g).") + (doc "Float twin of mut_counter via a tail-recursive helper. Expected stdout: 55 (Float-55.0 via %g).") (type (fn-type (params) (ret (con Unit)) (effects IO))) (params) (body - (app print - (mut - (var sum (con Float) 0.0) - (assign sum (app sum_helper 1.0 10.0 0.0)) - sum)))) + (app print (app sum_helper 1.0 10.0 0.0)))) (fn sum_helper (doc "Accumulator-style tail-recursive Float helper — sum from lo through hi inclusive.") diff --git a/examples/test_mut_assign_out_of_scope.ail.json b/examples/test_mut_assign_out_of_scope.ail.json deleted file mode 100644 index 1f53242..0000000 --- a/examples/test_mut_assign_out_of_scope.ail.json +++ /dev/null @@ -1,38 +0,0 @@ -{ - "schema": "ailang/v0", - "name": "test_mut_assign_out_of_scope", - "imports": [], - "defs": [ - { - "kind": "fn", - "name": "main", - "type": { - "k": "fn", - "params": [], - "ret": { "k": "con", "name": "Int" }, - "effects": [] - }, - "params": [], - "doc": "Iter mut.2: assigning to a name not declared as a mut-var fires mut-assign-out-of-scope.", - "body": { - "t": "mut", - "vars": [ - { - "name": "x", - "type": { "k": "con", "name": "Int" }, - "init": { "t": "lit", "lit": { "kind": "int", "value": 0 } } - } - ], - "body": { - "t": "seq", - "lhs": { - "t": "assign", - "name": "y", - "value": { "t": "lit", "lit": { "kind": "int", "value": 1 } } - }, - "rhs": { "t": "var", "name": "x" } - } - } - } - ] -} diff --git a/examples/test_mut_assign_outside_mut.ail.json b/examples/test_mut_assign_outside_mut.ail.json deleted file mode 100644 index ccc60ad..0000000 --- a/examples/test_mut_assign_outside_mut.ail.json +++ /dev/null @@ -1,24 +0,0 @@ -{ - "schema": "ailang/v0", - "name": "test_mut_assign_outside_mut", - "imports": [], - "defs": [ - { - "kind": "fn", - "name": "main", - "type": { - "k": "fn", - "params": [], - "ret": { "k": "con", "name": "Unit" }, - "effects": [] - }, - "params": [], - "doc": "Iter mut.2: Term::Assign with no enclosing Term::Mut ancestor fires mut-assign-out-of-scope.", - "body": { - "t": "assign", - "name": "x", - "value": { "t": "lit", "lit": { "kind": "int", "value": 1 } } - } - } - ] -} diff --git a/examples/test_mut_assign_type_mismatch.ail.json b/examples/test_mut_assign_type_mismatch.ail.json deleted file mode 100644 index f3d21d7..0000000 --- a/examples/test_mut_assign_type_mismatch.ail.json +++ /dev/null @@ -1,38 +0,0 @@ -{ - "schema": "ailang/v0", - "name": "test_mut_assign_type_mismatch", - "imports": [], - "defs": [ - { - "kind": "fn", - "name": "main", - "type": { - "k": "fn", - "params": [], - "ret": { "k": "con", "name": "Int" }, - "effects": [] - }, - "params": [], - "doc": "Iter mut.2: assigning Float to Int mut-var fires assign-type-mismatch.", - "body": { - "t": "mut", - "vars": [ - { - "name": "x", - "type": { "k": "con", "name": "Int" }, - "init": { "t": "lit", "lit": { "kind": "int", "value": 0 } } - } - ], - "body": { - "t": "seq", - "lhs": { - "t": "assign", - "name": "x", - "value": { "t": "lit", "lit": { "kind": "float", "bits": "3ff8000000000000" } } - }, - "rhs": { "t": "var", "name": "x" } - } - } - } - ] -} diff --git a/examples/test_mut_nested_shadow_legal.ail.json b/examples/test_mut_nested_shadow_legal.ail.json deleted file mode 100644 index 9b6f83b..0000000 --- a/examples/test_mut_nested_shadow_legal.ail.json +++ /dev/null @@ -1,40 +0,0 @@ -{ - "schema": "ailang/v0", - "name": "test_mut_nested_shadow_legal", - "imports": [], - "defs": [ - { - "kind": "fn", - "name": "main", - "type": { - "k": "fn", - "params": [], - "ret": { "k": "con", "name": "Float" }, - "effects": [] - }, - "params": [], - "doc": "Iter mut.2: nested mut block with inner var shadowing outer; inner Float wins. Typechecks clean.", - "body": { - "t": "mut", - "vars": [ - { - "name": "x", - "type": { "k": "con", "name": "Int" }, - "init": { "t": "lit", "lit": { "kind": "int", "value": 1 } } - } - ], - "body": { - "t": "mut", - "vars": [ - { - "name": "x", - "type": { "k": "con", "name": "Float" }, - "init": { "t": "lit", "lit": { "kind": "float", "bits": "4000000000000000" } } - } - ], - "body": { "t": "var", "name": "x" } - } - } - } - ] -} diff --git a/examples/test_mut_var_captured_by_lambda.ail.json b/examples/test_mut_var_captured_by_lambda.ail.json deleted file mode 100644 index fcce772..0000000 --- a/examples/test_mut_var_captured_by_lambda.ail.json +++ /dev/null @@ -1,42 +0,0 @@ -{ - "schema": "ailang/v0", - "name": "test_mut_var_captured_by_lambda", - "imports": [], - "defs": [ - { - "kind": "fn", - "name": "main", - "type": { - "k": "fn", - "params": [], - "ret": { "k": "con", "name": "Int" }, - "effects": [] - }, - "params": [], - "doc": "Iter mut.4-tidy: a lambda inside a mut block that references the enclosing mut-var `x` is rejected with mut-var-captured-by-lambda. Mut-vars are alloca-resident and lexically scoped; lifting them into a heap-closure env is deferred to a follow-on milestone (ref-types + !Mut effect).", - "body": { - "t": "mut", - "vars": [ - { - "name": "x", - "type": { "k": "con", "name": "Int" }, - "init": { "t": "lit", "lit": { "kind": "int", "value": 0 } } - } - ], - "body": { - "t": "let", - "name": "f", - "value": { - "t": "lam", - "params": [], - "paramTypes": [], - "retType": { "k": "con", "name": "Int" }, - "effects": [], - "body": { "t": "var", "name": "x" } - }, - "body": { "t": "lit", "lit": { "kind": "int", "value": 0 } } - } - } - } - ] -} diff --git a/examples/test_mut_var_unsupported_type.ail.json b/examples/test_mut_var_unsupported_type.ail.json deleted file mode 100644 index 1bd142f..0000000 --- a/examples/test_mut_var_unsupported_type.ail.json +++ /dev/null @@ -1,30 +0,0 @@ -{ - "schema": "ailang/v0", - "name": "test_mut_var_unsupported_type", - "imports": [], - "defs": [ - { - "kind": "fn", - "name": "main", - "type": { - "k": "fn", - "params": [], - "ret": { "k": "con", "name": "Str" }, - "effects": [] - }, - "params": [], - "doc": "Iter mut.2: a Str-typed mut-var fires mut-var-unsupported-type (heap-RC-managed types deferred).", - "body": { - "t": "mut", - "vars": [ - { - "name": "s", - "type": { "k": "con", "name": "Str" }, - "init": { "t": "lit", "lit": { "kind": "str", "value": "hello" } } - } - ], - "body": { "t": "var", "name": "s" } - } - } - ] -}