iter remove-mut-var-assign.1: atomic removal of mut/var/assign
mut/var/assign removed from AILang entirely and atomically. Deleted: Term::Mut/Term::Assign/struct MutVar; the three Form-A keywords + parse_mut/parse_assign + grammar EBNF; the 4 mut CheckError variants; the mut_scope_stack synth threading (param dropped from synth + every internal/external/test caller); the two lower_term arms; and every exhaustive no-_ Term::Mut/Term::Assign match arm across 17 source files — cut in lockstep with DESIGN.md, fixtures, the drift trio, carve-out and roadmap so the schema is honest at every commit. No catch-all wildcard introduced (verified). loop/recur + let/if are the surviving forms. The shared codegen alloca machinery survives (loop reuses it): mut_var_allocas renamed binder_allocas (representation-only, loop codegen byte-identical) and the shared Term::Lam escape guard simplified to !loop_stack.is_empty() with the loop half (LoopBinderCapturedByLambda) byte-equivalent. Feature-acceptance applied inverted: the removed feature fails clause 2 (redundant) and clause 3 (IS the iterated-mutable-state bug class). Behaviour preservation is executable: mut_counter/mut_sum_floats still print 55 after the faithful let/if rewrite. The removal is made executable by the new mut_removed_pin.rs (4 must-fail pins). Independent verification: cargo test --workspace 605/0, zero residual mut symbols in any crate source, loop/recur non-regression all green (55 / 500000500000 / infinite-compiles / the lambda_capturing_loop_binder pin), roundtrip_cli PASS. One DONE_WITH_CONCERNS: a 4th recurrence of the recon-undercount class (in-source mod tests + a drift-pin fn + 5 orphaned mut .ail.json carve-outs + a non-enumerated E0599); all resolved within implementer remit, no behaviour change. Milestone-close audit then fieldtest remain. spec docs/specs/2026-05-18-remove-mut-var-assign.md (grounding PASS) plan docs/plans/remove-mut-var-assign.1.md
This commit is contained in:
@@ -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."
|
||||||
|
}
|
||||||
@@ -1513,37 +1513,6 @@ fn walk_term(
|
|||||||
walk_term(source, out, builtins, scope);
|
walk_term(source, out, builtins, scope);
|
||||||
walk_term(body, 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 } => {
|
Term::Loop { binders, body } => {
|
||||||
let mut newly = Vec::new();
|
let mut newly = Vec::new();
|
||||||
for b in binders {
|
for b in binders {
|
||||||
@@ -2765,44 +2734,6 @@ fn rewrite_def(
|
|||||||
changed,
|
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 } => {
|
Term::Loop { binders, body } => {
|
||||||
for b in binders {
|
for b in binders {
|
||||||
rewrite_type(
|
rewrite_type(
|
||||||
|
|||||||
@@ -91,20 +91,9 @@ fn synthesised_print_uses_user_module_show_via_fallback() {
|
|||||||
Term::ReuseAs { source, body } => {
|
Term::ReuseAs { source, body } => {
|
||||||
contains_xmod_show_var(source) || contains_xmod_show_var(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__<T>` 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
|
// loop-recur iter 1: a `Term::Loop` cannot itself host a
|
||||||
// synthesised cross-module reference, but recurse
|
// synthesised cross-module reference, but recurse
|
||||||
// defensively through binder inits / body / recur args,
|
// defensively through binder inits / body / recur args.
|
||||||
// mirroring the `Term::Mut` arm above.
|
|
||||||
Term::Loop { binders, body } => {
|
Term::Loop { binders, body } => {
|
||||||
binders.iter().any(|b| contains_xmod_show_var(&b.init))
|
binders.iter().any(|b| contains_xmod_show_var(&b.init))
|
||||||
|| contains_xmod_show_var(body)
|
|| contains_xmod_show_var(body)
|
||||||
|
|||||||
@@ -187,48 +187,23 @@ fn check_ordering_match_post_migration_is_clean() {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Property (RED — fieldtest mut-local F2): in non-JSON (human) mode,
|
/// Property: in non-JSON (human) mode, the human-stderr formatter
|
||||||
/// the human-stderr formatter prepends the diagnostic code exactly once
|
/// prepends the diagnostic code exactly once as the canonical
|
||||||
/// as the canonical `[code]` bracket prefix (the cli-diag-human format).
|
/// `[code]` bracket prefix (the cli-diag-human format). The
|
||||||
/// The four mut-local `CheckError` variants
|
/// `loop-binder-captured-by-lambda` `CheckError` Display body must
|
||||||
/// (`mut-assign-out-of-scope`, `assign-type-mismatch`,
|
/// NOT also embed `[code] ` inside its `thiserror` Display body,
|
||||||
/// `mut-var-unsupported-type`, `mut-var-captured-by-lambda`) must NOT
|
/// which would render the bracketed code TWICE in the user-visible
|
||||||
/// also embed `[code] ` inside their `thiserror` Display body, which
|
/// stderr line (`error: [code] fn: [code] message`).
|
||||||
/// 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
|
/// This pins the *observable* doubling on the real CLI human path
|
||||||
/// (`crates/ail/src/main.rs` non-JSON `Cmd::Check` arm), not the raw
|
/// (`crates/ail/src/main.rs` non-JSON `Cmd::Check` arm), not the raw
|
||||||
/// Display string of one variant in isolation: it counts occurrences
|
/// Display string of the variant in isolation: it counts occurrences
|
||||||
/// of the literal `[<code>]` token in the rendered stderr and requires
|
/// of the literal `[<code>]` token in the rendered stderr and
|
||||||
/// exactly one. Drops to GREEN once the four Display bodies shed their
|
/// requires exactly one (the formatter supplies it; the Display body
|
||||||
/// leading `[<code>] ` prefix (the formatter's prefix then supplies it
|
/// must not also embed it).
|
||||||
/// once). The non-mut variants are unaffected because they never
|
|
||||||
/// embedded the bracket.
|
|
||||||
#[test]
|
#[test]
|
||||||
fn check_human_mode_renders_mut_diagnostic_code_exactly_once() {
|
fn check_human_mode_renders_loop_binder_diagnostic_code_exactly_once() {
|
||||||
// (fixture filename, kebab diagnostic code) for the four mut-local
|
|
||||||
// negative fixtures shipped by the mut-local milestone.
|
|
||||||
let cases = [
|
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 `[<code>]` exactly once.
|
|
||||||
(
|
(
|
||||||
"test_loop_binder_captured_by_lambda.ail.json",
|
"test_loop_binder_captured_by_lambda.ail.json",
|
||||||
"loop-binder-captured-by-lambda",
|
"loop-binder-captured-by-lambda",
|
||||||
|
|||||||
@@ -2853,20 +2853,17 @@ fn str_clone_cross_realisation_uniform_abi() {
|
|||||||
assert_eq!(live, 0, "no leaked heap-Str slabs allowed; live={live}");
|
assert_eq!(live, 0, "no leaked heap-Str slabs allowed; live={live}");
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Iter mut.3: `examples/mut_counter.ail` exercises `Term::Mut` +
|
/// `examples/mut_counter.ail` sums 1..10 via a tail-recursive
|
||||||
/// `Term::Assign` codegen with an `Int` mut-var. The body
|
/// `sum_helper` and prints the result. Behaviour-preservation gate:
|
||||||
/// `(mut (var sum Int 0) (assign sum (sum_helper 1 10 0)) sum)`
|
/// the fixture's value assertion stays `55` after the let/if rewrite,
|
||||||
/// stores the helper's result into `sum`'s alloca and prints the
|
/// proving the rewrite loses no expressivity.
|
||||||
/// loaded value. End-to-end gate for the entry-block-hoist /
|
|
||||||
/// store / load lowering.
|
|
||||||
#[test]
|
#[test]
|
||||||
fn mut_counter_prints_55() {
|
fn mut_counter_prints_55() {
|
||||||
let stdout = build_and_run("mut_counter.ail");
|
let stdout = build_and_run("mut_counter.ail");
|
||||||
assert_eq!(stdout.trim(), "55", "mut_counter must print 55, got {stdout:?}");
|
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
|
/// Float twin of `mut_counter_prints_55`: `sum_helper` returns the
|
||||||
/// is `Float`, init is `0.0`, the recursive helper returns the
|
|
||||||
/// sum 1.0+...+10.0 = 55.0. The polymorphic `print` routes through
|
/// sum 1.0+...+10.0 = 55.0. The polymorphic `print` routes through
|
||||||
/// `Show Float.show` → `float_to_str`'s libc `%g` formatter, which
|
/// `Show Float.show` → `float_to_str`'s libc `%g` formatter, which
|
||||||
/// strips the trailing `.0` — so the canonical stdout for Float
|
/// strips the trailing `.0` — so the canonical stdout for Float
|
||||||
|
|||||||
@@ -342,7 +342,6 @@ mod tests {
|
|||||||
let mut env = Env::default();
|
let mut env = Env::default();
|
||||||
install(&mut env);
|
install(&mut env);
|
||||||
let mut locals: IndexMap<String, Type> = IndexMap::new();
|
let mut locals: IndexMap<String, Type> = IndexMap::new();
|
||||||
let mut mut_scope_stack: Vec<IndexMap<String, Type>> = Vec::new();
|
|
||||||
let mut effects: BTreeSet<String> = BTreeSet::new();
|
let mut effects: BTreeSet<String> = BTreeSet::new();
|
||||||
let mut subst = Subst::default();
|
let mut subst = Subst::default();
|
||||||
let mut counter: u32 = 0;
|
let mut counter: u32 = 0;
|
||||||
@@ -350,13 +349,12 @@ mod tests {
|
|||||||
let mut free_fn_calls = Vec::new();
|
let mut free_fn_calls = Vec::new();
|
||||||
let mut warnings: Vec<crate::diagnostic::Diagnostic> = Vec::new();
|
let mut warnings: Vec<crate::diagnostic::Diagnostic> = Vec::new();
|
||||||
// loop-recur iter 2: test helper synths one term from top-of-
|
// 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<(String, Type)>> = Vec::new();
|
let mut loop_stack: Vec<Vec<(String, Type)>> = Vec::new();
|
||||||
let ty = crate::synth(
|
let ty = crate::synth(
|
||||||
t,
|
t,
|
||||||
&env,
|
&env,
|
||||||
&mut locals,
|
&mut locals,
|
||||||
&mut mut_scope_stack,
|
|
||||||
&mut loop_stack,
|
&mut loop_stack,
|
||||||
&mut effects,
|
&mut effects,
|
||||||
"<test>",
|
"<test>",
|
||||||
@@ -589,7 +587,6 @@ mod tests {
|
|||||||
let mut env = Env::default();
|
let mut env = Env::default();
|
||||||
install(&mut env);
|
install(&mut env);
|
||||||
let mut locals: IndexMap<String, Type> = IndexMap::new();
|
let mut locals: IndexMap<String, Type> = IndexMap::new();
|
||||||
let mut mut_scope_stack: Vec<IndexMap<String, Type>> = Vec::new();
|
|
||||||
let mut effects: BTreeSet<String> = BTreeSet::new();
|
let mut effects: BTreeSet<String> = BTreeSet::new();
|
||||||
let mut subst = Subst::default();
|
let mut subst = Subst::default();
|
||||||
let mut counter: u32 = 0;
|
let mut counter: u32 = 0;
|
||||||
@@ -597,13 +594,12 @@ mod tests {
|
|||||||
let mut free_fn_calls = Vec::new();
|
let mut free_fn_calls = Vec::new();
|
||||||
let mut warnings: Vec<crate::diagnostic::Diagnostic> = Vec::new();
|
let mut warnings: Vec<crate::diagnostic::Diagnostic> = Vec::new();
|
||||||
// loop-recur iter 2: test helper synths one term from top-of-
|
// 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<(String, Type)>> = Vec::new();
|
let mut loop_stack: Vec<Vec<(String, Type)>> = Vec::new();
|
||||||
let err = crate::synth(
|
let err = crate::synth(
|
||||||
&term,
|
&term,
|
||||||
&env,
|
&env,
|
||||||
&mut locals,
|
&mut locals,
|
||||||
&mut mut_scope_stack,
|
|
||||||
&mut loop_stack,
|
&mut loop_stack,
|
||||||
&mut effects,
|
&mut effects,
|
||||||
"<test>",
|
"<test>",
|
||||||
|
|||||||
+62
-689
File diff suppressed because it is too large
Load Diff
@@ -375,28 +375,6 @@ impl<'a> Lifter<'a> {
|
|||||||
source: Box::new(self.lift_in_term(source, locals, in_def)?),
|
source: Box::new(self.lift_in_term(source, locals, in_def)?),
|
||||||
body: Box::new(self.lift_in_term(body, 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::<Result<Vec<_>>>()?,
|
|
||||||
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 {
|
Term::Loop { binders, body } => Ok(Term::Loop {
|
||||||
binders: binders
|
binders: binders
|
||||||
.iter()
|
.iter()
|
||||||
@@ -739,15 +717,11 @@ impl<'a> Lifter<'a> {
|
|||||||
// (e.g. class-method-shadowed-by-fn) have already been
|
// (e.g. class-method-shadowed-by-fn) have already been
|
||||||
// surfaced upstream by `check_workspace`. Discard here.
|
// surfaced upstream by `check_workspace`. Discard here.
|
||||||
let mut warnings_discarded: Vec<crate::diagnostic::Diagnostic> = Vec::new();
|
let mut warnings_discarded: Vec<crate::diagnostic::Diagnostic> = 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<indexmap::IndexMap<String, crate::Type>> = Vec::new();
|
|
||||||
// loop-recur iter 2: lift's letrec-capture synth re-entry walks
|
// 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
|
// one term at a time from a top-of-body position; fresh empty
|
||||||
// loop-stack is correct (any `Term::Loop` pushes its own frame).
|
// loop-stack is correct (any `Term::Loop` pushes its own frame).
|
||||||
let mut loop_stack: Vec<Vec<(String, crate::Type)>> = Vec::new();
|
let mut loop_stack: Vec<Vec<(String, crate::Type)>> = 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))
|
Ok(subst.apply(&ty))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -778,12 +752,6 @@ fn contains_any_letrec(m: &Module) -> bool {
|
|||||||
Term::Clone { value } => term_has_letrec(value),
|
Term::Clone { value } => term_has_letrec(value),
|
||||||
// Iter 18d.1: structural recursion through both children.
|
// Iter 18d.1: structural recursion through both children.
|
||||||
Term::ReuseAs { source, body } => term_has_letrec(source) || term_has_letrec(body),
|
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 } => {
|
Term::Loop { binders, body } => {
|
||||||
binders.iter().any(|b| term_has_letrec(&b.init)) || term_has_letrec(body)
|
binders.iter().any(|b| term_has_letrec(&b.init)) || term_has_letrec(body)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -592,23 +592,6 @@ impl<'a> Checker<'a> {
|
|||||||
// standard position rules.
|
// standard position rules.
|
||||||
self.walk(body, pos);
|
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 } => {
|
Term::Loop { binders, body } => {
|
||||||
for b in binders {
|
for b in binders {
|
||||||
self.walk(&b.init, Position::Consume);
|
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::Seq { .. } => "seq",
|
||||||
Term::Clone { .. } => "clone",
|
Term::Clone { .. } => "clone",
|
||||||
Term::ReuseAs { .. } => "reuse-as",
|
Term::ReuseAs { .. } => "reuse-as",
|
||||||
Term::Mut { .. } => "mut",
|
|
||||||
Term::Assign { .. } => "assign",
|
|
||||||
Term::Loop { .. } => "loop",
|
Term::Loop { .. } => "loop",
|
||||||
Term::Recur { .. } => "recur",
|
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(source, pname, uniq, def_name, ctors)
|
||||||
|| any_sub_binder_consumed_for(body, 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 } => {
|
Term::Loop { binders, body } => {
|
||||||
binders.iter().any(|b| {
|
binders.iter().any(|b| {
|
||||||
any_sub_binder_consumed_for(&b.init, pname, uniq, def_name, ctors)
|
any_sub_binder_consumed_for(&b.init, pname, uniq, def_name, ctors)
|
||||||
|
|||||||
@@ -713,19 +713,14 @@ pub fn collect_mono_targets(
|
|||||||
// discards warnings — typecheck has already run and surfaced any
|
// discards warnings — typecheck has already run and surfaced any
|
||||||
// shadow warnings; mono is a post-typecheck pass.
|
// shadow warnings; mono is a post-typecheck pass.
|
||||||
let mut warnings_discarded: Vec<crate::diagnostic::Diagnostic> = Vec::new();
|
let mut warnings_discarded: Vec<crate::diagnostic::Diagnostic> = 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<indexmap::IndexMap<String, crate::Type>> = Vec::new();
|
|
||||||
// loop-recur iter 2: mono re-synth begins from top-of-body —
|
// loop-recur iter 2: mono re-synth begins from top-of-body —
|
||||||
// empty loop-stack (any `Term::Loop` pushes its own frame as the
|
// 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<(String, crate::Type)>> = Vec::new();
|
let mut loop_stack: Vec<Vec<(String, crate::Type)>> = Vec::new();
|
||||||
crate::synth(
|
crate::synth(
|
||||||
&f.body,
|
&f.body,
|
||||||
&env,
|
&env,
|
||||||
&mut locals,
|
&mut locals,
|
||||||
&mut mut_scope_stack,
|
|
||||||
&mut loop_stack,
|
&mut loop_stack,
|
||||||
&mut effects,
|
&mut effects,
|
||||||
&f.name,
|
&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(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);
|
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 } => {
|
Term::Loop { binders, body } => {
|
||||||
for b in binders.iter_mut() {
|
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);
|
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
|
// discards warnings — typecheck has already run and surfaced any
|
||||||
// shadow warnings; mono is a post-typecheck pass.
|
// shadow warnings; mono is a post-typecheck pass.
|
||||||
let mut warnings_discarded: Vec<crate::diagnostic::Diagnostic> = Vec::new();
|
let mut warnings_discarded: Vec<crate::diagnostic::Diagnostic> = 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<indexmap::IndexMap<String, crate::Type>> = Vec::new();
|
|
||||||
// loop-recur iter 2: mono re-synth begins from top-of-body —
|
// loop-recur iter 2: mono re-synth begins from top-of-body —
|
||||||
// empty loop-stack (any `Term::Loop` pushes its own frame as the
|
// 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<(String, crate::Type)>> = Vec::new();
|
let mut loop_stack: Vec<Vec<(String, crate::Type)>> = Vec::new();
|
||||||
crate::synth(
|
crate::synth(
|
||||||
&f.body,
|
&f.body,
|
||||||
&env,
|
&env,
|
||||||
&mut locals,
|
&mut locals,
|
||||||
&mut mut_scope_stack,
|
|
||||||
&mut loop_stack,
|
&mut loop_stack,
|
||||||
&mut effects,
|
&mut effects,
|
||||||
&f.name,
|
&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(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);
|
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 } => {
|
Term::Loop { binders, body } => {
|
||||||
for b in binders {
|
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);
|
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);
|
||||||
|
|||||||
@@ -120,19 +120,6 @@ fn walk_term(t: &Term) -> Result<(), CheckError> {
|
|||||||
walk_term(source)?;
|
walk_term(source)?;
|
||||||
walk_term(body)
|
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 } => {
|
Term::Loop { binders, body } => {
|
||||||
for b in binders {
|
for b in binders {
|
||||||
walk_term(&b.init)?;
|
walk_term(&b.init)?;
|
||||||
|
|||||||
@@ -253,18 +253,6 @@ impl<'a> Checker<'a> {
|
|||||||
self.walk(body);
|
self.walk(body);
|
||||||
self.check_reuse_as(source, 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 } => {
|
Term::Loop { binders, body } => {
|
||||||
for b in binders {
|
for b in binders {
|
||||||
self.walk(&b.init);
|
self.walk(&b.init);
|
||||||
|
|||||||
@@ -336,22 +336,6 @@ impl<'a> Walker<'a> {
|
|||||||
self.walk(source, Position::Consume);
|
self.walk(source, Position::Consume);
|
||||||
self.walk(body, pos);
|
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 } => {
|
Term::Loop { binders, body } => {
|
||||||
for b in binders {
|
for b in binders {
|
||||||
self.walk(&b.init, Position::Consume);
|
self.walk(&b.init, Position::Consume);
|
||||||
|
|||||||
@@ -68,16 +68,13 @@ fn infinite_loop_typechecks_clean_no_termination_claim() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Property: a `Term::Lam` whose body captures a `Term::Loop` binder
|
/// Property: a `Term::Lam` whose body captures a `Term::Loop` binder
|
||||||
/// must be rejected at `ail check` with a dedicated diagnostic,
|
/// is rejected at `ail check` with the dedicated
|
||||||
/// symmetric to `mut-var-captured-by-lambda`. Loop binders are
|
/// `loop-binder-captured-by-lambda` diagnostic. Loop binders are
|
||||||
/// alloca-resident and cannot escape their loop frame (iter-3 reuses
|
/// alloca-resident and cannot escape their loop frame (codegen
|
||||||
/// the `mut_var_allocas` registry, `std::mem::take`-emptied at the
|
/// registers them in the `binder_allocas` registry,
|
||||||
/// lambda boundary), exactly the mut-var escape rule. Today the
|
/// `std::mem::take`-emptied at the lambda boundary); the `Term::Lam`
|
||||||
/// escape guard inspects only `mut_scope_stack`; loop binders live
|
/// escape guard rejects the capture at typecheck rather than letting
|
||||||
/// in `locals`, so check passes (exit 0) and codegen panics
|
/// codegen panic on type-correct input.
|
||||||
/// `unreachable!()` at `crates/ailang-codegen/src/lambda.rs:102`.
|
|
||||||
/// This test pins the FIXED contract — RED until the guard also
|
|
||||||
/// rejects loop-binder captures.
|
|
||||||
#[test]
|
#[test]
|
||||||
fn lambda_capturing_loop_binder_emits_loop_binder_captured_by_lambda() {
|
fn lambda_capturing_loop_binder_emits_loop_binder_captured_by_lambda() {
|
||||||
let codes = check_fixture("test_loop_binder_captured_by_lambda.ail.json");
|
let codes = check_fixture("test_loop_binder_captured_by_lambda.ail.json");
|
||||||
|
|||||||
@@ -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<String> {
|
|
||||||
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()]);
|
|
||||||
}
|
|
||||||
@@ -190,17 +190,6 @@ fn walk(t: &Term, out: &mut NonEscapeSet) {
|
|||||||
walk(source, out);
|
walk(source, out);
|
||||||
walk(body, 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 } => {
|
Term::Loop { binders, body } => {
|
||||||
for b in binders {
|
for b in binders {
|
||||||
walk(&b.init, out);
|
walk(&b.init, out);
|
||||||
@@ -389,23 +378,6 @@ fn escapes(t: &Term, tainted: &BTreeSet<String>, in_tail: bool) -> bool {
|
|||||||
}
|
}
|
||||||
escapes(body, tainted, in_tail)
|
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 } => {
|
Term::Loop { binders, body } => {
|
||||||
for b in binders {
|
for b in binders {
|
||||||
if escapes(&b.init, tainted, false) {
|
if escapes(&b.init, tainted, false) {
|
||||||
@@ -526,31 +498,6 @@ fn collect_free_vars(t: &Term, bound: &mut BTreeSet<String>, out: &mut BTreeSet<
|
|||||||
collect_free_vars(source, bound, out);
|
collect_free_vars(source, bound, out);
|
||||||
collect_free_vars(body, bound, out);
|
collect_free_vars(body, bound, out);
|
||||||
}
|
}
|
||||||
// Iter mut.1: a mut-var name binds inside the block. Var
|
|
||||||
// inits before the binding see the outer scope; later inits
|
|
||||||
// and the body see the var as bound. Mirrors `Term::Let`'s
|
|
||||||
// add-then-walk-then-remove pattern, extended over N vars.
|
|
||||||
Term::Mut { vars, body } => {
|
|
||||||
let mut newly: Vec<String> = Vec::new();
|
|
||||||
for v in vars {
|
|
||||||
collect_free_vars(&v.init, bound, out);
|
|
||||||
if bound.insert(v.name.clone()) {
|
|
||||||
newly.push(v.name.clone());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
collect_free_vars(body, bound, out);
|
|
||||||
for n in newly {
|
|
||||||
bound.remove(&n);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// Iter mut.1: the assigned `name` is a free var unless
|
|
||||||
// bound by an enclosing `Term::Mut`.
|
|
||||||
Term::Assign { name, value } => {
|
|
||||||
if !bound.contains(name) {
|
|
||||||
out.insert(name.clone());
|
|
||||||
}
|
|
||||||
collect_free_vars(value, bound, out);
|
|
||||||
}
|
|
||||||
Term::Loop { binders, body } => {
|
Term::Loop { binders, body } => {
|
||||||
let mut newly: Vec<String> = Vec::new();
|
let mut newly: Vec<String> = Vec::new();
|
||||||
for b in binders {
|
for b in binders {
|
||||||
|
|||||||
@@ -85,12 +85,11 @@ impl<'a> Emitter<'a> {
|
|||||||
// hard internal error is right.
|
// hard internal error is right.
|
||||||
// Tuple: (name, outer_ssa, llvm_type, ail_type, optional_fn_sig).
|
// Tuple: (name, outer_ssa, llvm_type, ail_type, optional_fn_sig).
|
||||||
//
|
//
|
||||||
// Iter mut.4-tidy: post-Task-2 the typechecker rejects
|
// The typechecker rejects lambda-captures-of-loop-binder via
|
||||||
// lambda-captures-of-mut-var via
|
// `CheckError::LoopBinderCapturedByLambda`. Reaching the
|
||||||
// `CheckError::MutVarCapturedByLambda`. Reaching the `None`
|
// `None` arm below means typecheck was skipped or a bug let
|
||||||
// arm below means typecheck was skipped or a bug let the AST
|
// the AST through — both are bugs in upstream layers, so
|
||||||
// through — both are bugs in upstream layers, so panic rather
|
// panic rather than return an Internal error.
|
||||||
// than return an Internal error.
|
|
||||||
let mut cap_meta: Vec<(String, String, String, Type, Option<FnSig>)> = Vec::new();
|
let mut cap_meta: Vec<(String, String, String, Type, Option<FnSig>)> = Vec::new();
|
||||||
for c in &captures {
|
for c in &captures {
|
||||||
let (_, outer_ssa, lty, ail_ty) = self
|
let (_, outer_ssa, lty, ail_ty) = self
|
||||||
@@ -100,7 +99,7 @@ impl<'a> Emitter<'a> {
|
|||||||
.find(|(n, _, _, _)| n == c)
|
.find(|(n, _, _, _)| n == c)
|
||||||
.unwrap_or_else(|| {
|
.unwrap_or_else(|| {
|
||||||
unreachable!(
|
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();
|
.clone();
|
||||||
@@ -135,19 +134,19 @@ impl<'a> Emitter<'a> {
|
|||||||
let saved_moved = std::mem::take(&mut self.moved_slots);
|
let saved_moved = std::mem::take(&mut self.moved_slots);
|
||||||
// Iter mut.3: a lambda thunk is its own fn frame for the
|
// Iter mut.3: a lambda thunk is its own fn frame for the
|
||||||
// mut-var bookkeeping introduced in iter mut.3 — the outer
|
// mut-var bookkeeping introduced in iter mut.3 — the outer
|
||||||
// fn's mut-vars are not in scope inside the thunk (a lambda
|
// fn's loop binders are not in scope inside the thunk (a
|
||||||
// body cannot reference enclosing mut-vars; the spec
|
// lambda body cannot reference enclosing loop binders; the
|
||||||
// disallows capture of mut-vars by lambdas via the
|
// typechecker rejects such captures, and the
|
||||||
// `mut_var_allocas` map being thunk-local). Save and reset
|
// `binder_allocas` map is thunk-local). Save and reset the
|
||||||
// the three fields so the thunk emits its own hoisted-
|
// three fields so the thunk emits its own hoisted-alloca
|
||||||
// alloca block at the thunk's entry, not the outer fn's.
|
// block at the thunk's entry, not the outer fn's.
|
||||||
let saved_mut_allocas = std::mem::take(&mut self.mut_var_allocas);
|
let saved_binder_allocas = std::mem::take(&mut self.binder_allocas);
|
||||||
// loop-recur iter 3: a lambda thunk is its own fn frame —
|
// loop-recur iter 3: a lambda thunk is its own fn frame —
|
||||||
// a `recur` cannot target a loop enclosing the lambda
|
// a `recur` cannot target a loop enclosing the lambda
|
||||||
// (iter-2 typecheck already rejects it; codegen resets so
|
// (iter-2 typecheck already rejects it; codegen resets so
|
||||||
// the thunk's own loops work and the outer frames cannot
|
// the thunk's own loops work and the outer frames cannot
|
||||||
// leak in). Save + reset (mem::take empties the Vec),
|
// 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_loop_frames = std::mem::take(&mut self.loop_frames);
|
||||||
let saved_pending_allocas = std::mem::take(&mut self.pending_entry_allocas);
|
let saved_pending_allocas = std::mem::take(&mut self.pending_entry_allocas);
|
||||||
let saved_entry_marker = self.entry_block_end_marker.take();
|
let saved_entry_marker = self.entry_block_end_marker.take();
|
||||||
@@ -233,10 +232,10 @@ impl<'a> Emitter<'a> {
|
|||||||
self.body.push_str("}\n\n");
|
self.body.push_str("}\n\n");
|
||||||
}
|
}
|
||||||
|
|
||||||
// Iter mut.3: splice any hoisted mut-var allocas into the
|
// Splice any hoisted loop-binder allocas into the thunk's
|
||||||
// thunk's entry block at the marker captured during the
|
// entry block at the marker captured during the thunk's
|
||||||
// thunk's `start_block("entry")` above. Empty `pending` is
|
// `start_block("entry")` above. Empty `pending` is a no-op
|
||||||
// a no-op (the thunk had no `Term::Mut` in its body).
|
// (the thunk had no `Term::Loop` in its body).
|
||||||
if !self.pending_entry_allocas.is_empty() {
|
if !self.pending_entry_allocas.is_empty() {
|
||||||
let marker = self.entry_block_end_marker.ok_or_else(|| {
|
let marker = self.entry_block_end_marker.ok_or_else(|| {
|
||||||
CodegenError::Internal(
|
CodegenError::Internal(
|
||||||
@@ -261,8 +260,8 @@ impl<'a> Emitter<'a> {
|
|||||||
self.non_escape = saved_non_escape;
|
self.non_escape = saved_non_escape;
|
||||||
self.moved_slots = saved_moved;
|
self.moved_slots = saved_moved;
|
||||||
self.current_param_modes = saved_param_modes;
|
self.current_param_modes = saved_param_modes;
|
||||||
// Iter mut.3: restore outer fn's mut-var bookkeeping.
|
// Restore outer fn's loop-binder bookkeeping.
|
||||||
self.mut_var_allocas = saved_mut_allocas;
|
self.binder_allocas = saved_binder_allocas;
|
||||||
self.loop_frames = saved_loop_frames;
|
self.loop_frames = saved_loop_frames;
|
||||||
self.pending_entry_allocas = saved_pending_allocas;
|
self.pending_entry_allocas = saved_pending_allocas;
|
||||||
self.entry_block_end_marker = saved_entry_marker;
|
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(source, bound, captures, captures_set, builtins, top_level);
|
||||||
Self::collect_captures(body, bound, captures, captures_set, builtins, top_level);
|
Self::collect_captures(body, bound, captures, captures_set, builtins, top_level);
|
||||||
}
|
}
|
||||||
// Iter mut.1: a mut-var name binds inside the block. Var
|
|
||||||
// inits before the binding see the outer scope; later
|
|
||||||
// inits and the body see the var as bound. Mut-vars
|
|
||||||
// cannot be captured by an inner lambda — the typecheck
|
|
||||||
// pass rejects this via `CheckError::MutVarCapturedByLambda`
|
|
||||||
// (added in mut.4-tidy). For the capture-collection scan
|
|
||||||
// the var bindings are tracked identically to let-bound
|
|
||||||
// names.
|
|
||||||
Term::Mut { vars, body } => {
|
|
||||||
let mut newly_bound: Vec<String> = Vec::new();
|
|
||||||
for v in vars {
|
|
||||||
Self::collect_captures(&v.init, bound, captures, captures_set, builtins, top_level);
|
|
||||||
if bound.insert(v.name.clone()) {
|
|
||||||
newly_bound.push(v.name.clone());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Self::collect_captures(body, bound, captures, captures_set, builtins, top_level);
|
|
||||||
for n in newly_bound {
|
|
||||||
bound.remove(&n);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Term::Assign { name, value, .. } => {
|
|
||||||
// The assigned `name` is a use of a mut-var binding.
|
|
||||||
// If it is bound in scope (it should be, by spec), it
|
|
||||||
// is not a capture. Otherwise — well-formed inputs do
|
|
||||||
// not produce this case (the typecheck pass at mut.2
|
|
||||||
// rejects it via `MutAssignOutOfScope`); we
|
|
||||||
// conservatively treat it as a possible capture so an
|
|
||||||
// ill-formed input still flows through codegen.
|
|
||||||
if !bound.contains(name)
|
|
||||||
&& !builtins.contains(name.as_str())
|
|
||||||
&& !top_level.contains(name)
|
|
||||||
&& captures_set.insert(name.clone())
|
|
||||||
{
|
|
||||||
captures.push(name.clone());
|
|
||||||
}
|
|
||||||
Self::collect_captures(value, bound, captures, captures_set, builtins, top_level);
|
|
||||||
}
|
|
||||||
Term::Loop { binders, body } => {
|
Term::Loop { binders, body } => {
|
||||||
let mut newly_bound: Vec<String> = Vec::new();
|
let mut newly_bound: Vec<String> = Vec::new();
|
||||||
for b in binders {
|
for b in binders {
|
||||||
|
|||||||
@@ -715,36 +715,31 @@ struct Emitter<'a> {
|
|||||||
/// handed off ownership" signal that makes the dec safe.
|
/// handed off ownership" signal that makes the dec safe.
|
||||||
current_param_modes: BTreeMap<String, ParamMode>,
|
current_param_modes: BTreeMap<String, ParamMode>,
|
||||||
/// Per-fn map of name → (alloca SSA name, AIL element type) for
|
/// Per-fn map of name → (alloca SSA name, AIL element type) for
|
||||||
/// the two alloca-resident binding classes. Populated on entry
|
/// alloca-resident loop binders. Populated on entry to a
|
||||||
/// to a `Term::Mut` block (iter mut.3 — mut-vars) AND on entry
|
/// `Term::Loop` (iter loop-recur.3): the `Term::Loop` arm inserts
|
||||||
/// to a `Term::Loop` (iter loop-recur.3 — loop binders ride the
|
/// each binder here and `recur` stores into the slot. Entries are
|
||||||
/// same alloca representation; 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.
|
/// removed (or restored to their prior binding) on block exit.
|
||||||
/// Consulted by the `Term::Var` arm of `lower_term` before
|
/// Consulted by the `Term::Var` arm of `lower_term` before
|
||||||
/// `self.locals` — the single load path serving both classes.
|
/// `self.locals` — the single load path for loop binders. Loop
|
||||||
/// Note: only the mut-var half mirrors a typecheck-side
|
/// binders are tracked typecheck-side in the ordinary `locals`
|
||||||
/// `mut_scope_stack`; loop binders are tracked typecheck-side in
|
/// plus `loop_stack` (positional); this codegen map is a
|
||||||
/// the ordinary `locals` plus `loop_stack` (positional), not in
|
|
||||||
/// `mut_scope_stack` — the shared codegen map is a
|
|
||||||
/// representation detail, not a scope-precedence claim.
|
/// representation detail, not a scope-precedence claim.
|
||||||
mut_var_allocas: BTreeMap<String, (String, Type)>,
|
binder_allocas: BTreeMap<String, (String, Type)>,
|
||||||
/// loop-recur iter 3: stack of enclosing `Term::Loop` codegen
|
/// loop-recur iter 3: stack of enclosing `Term::Loop` codegen
|
||||||
/// frames. Each frame is `(header_block_label, binder_slots)`
|
/// frames. Each frame is `(header_block_label, binder_slots)`
|
||||||
/// where `binder_slots` is `(binder_name, alloca_ssa, llvm_ty)`
|
/// where `binder_slots` is `(binder_name, alloca_ssa, llvm_ty)`
|
||||||
/// in declaration order. Pushed on `Term::Loop` body entry,
|
/// in declaration order. Pushed on `Term::Loop` body entry,
|
||||||
/// popped on exit; `Term::Recur` reads `.last()` for its target
|
/// popped on exit; `Term::Recur` reads `.last()` for its target
|
||||||
/// header + the binder allocas to store into. Saved/reset/
|
/// header + the binder allocas to store into. Saved/reset/
|
||||||
/// restored at the lambda-lowering boundary exactly as the
|
/// restored at the lambda-lowering boundary alongside
|
||||||
/// mut.3 `mut_var_allocas` triple (a `recur` cannot cross a
|
/// `binder_allocas` (a `recur` cannot cross a lambda boundary).
|
||||||
/// lambda boundary).
|
|
||||||
loop_frames: Vec<(String, Vec<(String, String, String)>)>,
|
loop_frames: Vec<(String, Vec<(String, String, String)>)>,
|
||||||
/// Iter mut.3: side buffer for `alloca` instructions emitted
|
/// Side buffer for `alloca` instructions emitted during body
|
||||||
/// during body lowering but hoisted to the fn's entry block.
|
/// lowering but hoisted to the fn's entry block. Flushed once
|
||||||
/// Flushed once into `self.body` at `entry_block_end_marker`
|
/// into `self.body` at `entry_block_end_marker` after
|
||||||
/// after `lower_term` completes. Entry-block placement is
|
/// `lower_term` completes. Entry-block placement is what makes
|
||||||
/// what makes the allocas mem2reg-eligible regardless of how
|
/// the loop-binder allocas mem2reg-eligible regardless of how
|
||||||
/// deeply nested the originating `Term::Mut` block is.
|
/// deeply nested the originating `Term::Loop` is.
|
||||||
pending_entry_allocas: String,
|
pending_entry_allocas: String,
|
||||||
/// Iter mut.3: byte position in `self.body` immediately after
|
/// Iter mut.3: byte position in `self.body` immediately after
|
||||||
/// the `entry:\n` label, captured during `start_block("entry")`
|
/// the `entry:\n` label, captured during `start_block("entry")`
|
||||||
@@ -868,7 +863,7 @@ impl<'a> Emitter<'a> {
|
|||||||
closure_drops: BTreeMap::new(),
|
closure_drops: BTreeMap::new(),
|
||||||
moved_slots: BTreeMap::new(),
|
moved_slots: BTreeMap::new(),
|
||||||
current_param_modes: BTreeMap::new(),
|
current_param_modes: BTreeMap::new(),
|
||||||
mut_var_allocas: BTreeMap::new(),
|
binder_allocas: BTreeMap::new(),
|
||||||
loop_frames: Vec::new(),
|
loop_frames: Vec::new(),
|
||||||
pending_entry_allocas: String::new(),
|
pending_entry_allocas: String::new(),
|
||||||
entry_block_end_marker: None,
|
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
|
// consulted by `lower_match`'s Iter A gate to skip arm-close
|
||||||
// pattern-binder dec when the scrutinee is a non-Own param.
|
// pattern-binder dec when the scrutinee is a non-Own param.
|
||||||
self.current_param_modes.clear();
|
self.current_param_modes.clear();
|
||||||
// Iter mut.3: per-fn-body mut-var bookkeeping. The map is
|
// Per-fn-body loop-binder bookkeeping. The map is
|
||||||
// populated/restored by the `Term::Mut` arm of `lower_term`;
|
// populated/restored by the `Term::Loop` arm of `lower_term`;
|
||||||
// the side buffer collects alloca instructions for hoisting;
|
// the side buffer collects alloca instructions for hoisting;
|
||||||
// the marker is captured by `start_block("entry")` below.
|
// the marker is captured by `start_block("entry")` below.
|
||||||
self.mut_var_allocas.clear();
|
self.binder_allocas.clear();
|
||||||
self.pending_entry_allocas.clear();
|
self.pending_entry_allocas.clear();
|
||||||
self.entry_block_end_marker = None;
|
self.entry_block_end_marker = None;
|
||||||
for (i, pname) in f.params.iter().enumerate() {
|
for (i, pname) in f.params.iter().enumerate() {
|
||||||
@@ -1394,15 +1389,14 @@ impl<'a> Emitter<'a> {
|
|||||||
self.block_terminated = true;
|
self.block_terminated = true;
|
||||||
return Ok(("0".into(), "i8".into()));
|
return Ok(("0".into(), "i8".into()));
|
||||||
}
|
}
|
||||||
// Iter mut.3: mut-vars take precedence over let-bound
|
// Loop binders take precedence over let-bound / param
|
||||||
// / param resolutions, symmetric to the typecheck-side
|
// resolutions. Shadowing across nested loops is
|
||||||
// mut-scope-stack walk in `synth`'s `Term::Var` arm.
|
// handled by the `Term::Loop` arm's push-and-restore
|
||||||
// Shadowing across nested mut blocks is handled by the
|
// on the BTreeMap entry, so a single `get` suffices
|
||||||
// `Term::Mut` arm's push-and-restore on the BTreeMap
|
// here (the entry visible at this point is the
|
||||||
// entry, so a single `get` suffices here (the entry
|
// innermost binding).
|
||||||
// visible at this point is the innermost binding).
|
|
||||||
if let Some((alloca_name, ail_ty)) =
|
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 llvm_ty = llvm_type(&ail_ty)?;
|
||||||
let load_ssa = self.fresh_ssa();
|
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)
|
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 } => {
|
Term::Loop { binders, body } => {
|
||||||
// loop-recur iter 3: loop binders are loop-carried
|
// 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
|
// `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
|
// path resolves them with zero new Var code
|
||||||
// (representation-sharing; Boss calls 1+2). The loop
|
// (representation-sharing; Boss calls 1+2). The loop
|
||||||
// header is a fresh block reached by an
|
// 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"
|
" store {llvm_ty} {init_ssa}, ptr {alloca_name}\n"
|
||||||
));
|
));
|
||||||
let prior = self
|
let prior = self
|
||||||
.mut_var_allocas
|
.binder_allocas
|
||||||
.insert(b.name.clone(), (alloca_name.clone(), b.ty.clone()));
|
.insert(b.name.clone(), (alloca_name.clone(), b.ty.clone()));
|
||||||
saved.push((b.name.clone(), prior));
|
saved.push((b.name.clone(), prior));
|
||||||
frame_slots.push((b.name.clone(), alloca_name, llvm_ty));
|
frame_slots.push((b.name.clone(), alloca_name, llvm_ty));
|
||||||
@@ -1906,15 +1804,15 @@ impl<'a> Emitter<'a> {
|
|||||||
self.start_block(&header);
|
self.start_block(&header);
|
||||||
let body_result = self.lower_term(body);
|
let body_result = self.lower_term(body);
|
||||||
self.loop_frames.pop();
|
self.loop_frames.pop();
|
||||||
// Restore prior bindings unconditionally (mirrors the
|
// Restore prior bindings unconditionally (defensive
|
||||||
// mut.3 `Term::Mut` arm — defensive on the `?` path).
|
// on the `?` path).
|
||||||
for (name, prior) in saved.into_iter().rev() {
|
for (name, prior) in saved.into_iter().rev() {
|
||||||
match prior {
|
match prior {
|
||||||
Some(p) => {
|
Some(p) => {
|
||||||
self.mut_var_allocas.insert(name, p);
|
self.binder_allocas.insert(name, p);
|
||||||
}
|
}
|
||||||
None => {
|
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
|
// loop-recur iter 3: re-enter the innermost
|
||||||
// enclosing loop. Typecheck (iter 2) pinned arity ==
|
// enclosing loop. Typecheck (iter 2) pinned arity ==
|
||||||
// binder count and per-arg type == binder type, so a
|
// binder count and per-arg type == binder type, so a
|
||||||
// miss/mismatch here is an internal error (the
|
// miss/mismatch here is an internal error. ALL args
|
||||||
// `Term::Assign`-arm precedent). ALL args are lowered
|
// are lowered to SSA values BEFORE any store, so a
|
||||||
// to SSA values BEFORE any store, so a recur arg that
|
// recur arg that reads a binder sees its OLD value
|
||||||
// reads a binder sees its OLD value (recur is a
|
// (recur is a simultaneous positional rebind, e.g.
|
||||||
// simultaneous positional rebind, e.g.
|
|
||||||
// `(recur (+ acc i) (+ i 1))`). The back-edge `br` is
|
// `(recur (+ acc i) (+ i 1))`). The back-edge `br` is
|
||||||
// a block terminator: set `block_terminated` at
|
// a block terminator: set `block_terminated` at
|
||||||
// recur's OWN emit site (the parallel setter — Boss
|
// 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
|
// exactly like a tail-app block. recur never falls
|
||||||
// through; its value is never used — return the
|
// through; its value is never used — return the
|
||||||
// canonical Unit/dead SSA (`("0","i8")`, the
|
// canonical Unit/dead SSA (`("0","i8")`, the
|
||||||
// `Term::Assign` / both-if-branches-terminated
|
// both-if-branches-terminated precedent).
|
||||||
// precedent).
|
|
||||||
let (header, slots) = self
|
let (header, slots) = self
|
||||||
.loop_frames
|
.loop_frames
|
||||||
.last()
|
.last()
|
||||||
@@ -3011,7 +2907,7 @@ impl<'a> Emitter<'a> {
|
|||||||
return Ok(ty.clone());
|
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());
|
return Ok(ail_ty.clone());
|
||||||
}
|
}
|
||||||
if let Some((_, _, _, ail)) =
|
if let Some((_, _, _, ail)) =
|
||||||
@@ -3228,19 +3124,10 @@ impl<'a> Emitter<'a> {
|
|||||||
// type. The source is dropped at codegen.
|
// type. The source is dropped at codegen.
|
||||||
self.synth_with_extras(body, extras)
|
self.synth_with_extras(body, extras)
|
||||||
}
|
}
|
||||||
// Iter mut.1: `Term::Mut`'s static type is the body's
|
// A `Term::Loop`'s static type is the body's type.
|
||||||
// static type (spec §"JSON-AST schema"). `Term::Assign`'s
|
// `Term::Recur` does not fall through; a Unit stub is
|
||||||
// is Unit (spec §"Form A surface"). These won't be hit
|
// safe (recur transfers control and never produces a
|
||||||
// along the shipping codegen path because the dispatcher
|
// value at its own position).
|
||||||
// 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.
|
|
||||||
Term::Loop { body, .. } => self.synth_with_extras(body, extras),
|
Term::Loop { body, .. } => self.synth_with_extras(body, extras),
|
||||||
Term::Recur { .. } => Ok(Type::unit()),
|
Term::Recur { .. } => Ok(Type::unit()),
|
||||||
}
|
}
|
||||||
|
|||||||
+10
-119
@@ -513,33 +513,6 @@ pub enum Term {
|
|||||||
source: Box<Term>,
|
source: Box<Term>,
|
||||||
body: Box<Term>,
|
body: Box<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 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<MutVar>,
|
|
||||||
body: Box<Term>,
|
|
||||||
},
|
|
||||||
/// 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<Term>,
|
|
||||||
},
|
|
||||||
/// loop-recur iter 1: a strict iteration block. `binders`
|
/// loop-recur iter 1: a strict iteration block. `binders`
|
||||||
/// declares one or more loop parameters (name, type, init),
|
/// declares one or more loop parameters (name, type, init),
|
||||||
/// evaluated in order on loop entry; `body` is evaluated with
|
/// 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
|
/// the iteration that exits via a non-`recur` branch. Strictly
|
||||||
/// additive: pre-existing fixtures hash bit-identically because
|
/// additive: pre-existing fixtures hash bit-identically because
|
||||||
/// none carry the `"t":"loop"` tag. `binders` has no
|
/// none carry the `"t":"loop"` tag. `binders` has no
|
||||||
/// `skip_serializing_if` (mirrors `Term::Mut.vars` — the field
|
/// `skip_serializing_if` — the field is part of the shape.
|
||||||
/// is part of the shape). Typecheck (loop-recur.2): `synth`
|
/// Typecheck (loop-recur.2): `synth`
|
||||||
/// types each binder init, the loop's type is the body's type;
|
/// types each binder init, the loop's type is the body's type;
|
||||||
/// `recur` arity/type is checked positionally via `loop_stack`
|
/// `recur` arity/type is checked positionally via `loop_stack`
|
||||||
/// and `verify_loop_body` enforces `recur`-in-tail-position; a
|
/// and `verify_loop_body` enforces `recur`-in-tail-position; a
|
||||||
@@ -587,42 +560,12 @@ pub struct Arm {
|
|||||||
pub body: Term,
|
pub body: Term,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Iter mut.1: a mutable binding inside [`Term::Mut`]. Lives as a
|
/// loop-recur iter 1: one binder of a [`Term::Loop`]. The
|
||||||
/// nested field of `Term::Mut`, not as a standalone `Term` variant —
|
/// `(name, type, init)` triple is the Form-A surface vocabulary
|
||||||
/// mut-vars are not first-class values (they cannot escape the
|
/// `(NAME TYPE INIT)`; it is a nested field of `Term::Loop`, not a
|
||||||
/// enclosing `Term::Mut` scope; see
|
/// first-class `Term` (loop binders cannot escape the enclosing
|
||||||
/// `docs/specs/2026-05-15-mut-local.md` §"Why three AST nodes, not
|
/// loop). `recur` rebinds these positionally per iteration. The
|
||||||
/// one fused construct").
|
/// `ty` JSON field is `"type"`, matching the spec schema.
|
||||||
///
|
|
||||||
/// The JSON field for `ty` is `"type"`, mirroring the spec's schema
|
|
||||||
/// (`{ "name": "<id>", "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.
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
pub struct LoopBinder {
|
pub struct LoopBinder {
|
||||||
/// The binder's lexical name. Within the enclosing `Term::Loop`,
|
/// The binder's lexical name. Within the enclosing `Term::Loop`,
|
||||||
@@ -952,61 +895,9 @@ fn is_false(b: &bool) -> bool {
|
|||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
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
|
/// loop-recur iter 1: pin the canonical-bytes shape of a
|
||||||
/// `Term::Loop` with one binder. Mirrors the mut-empty-vars pin:
|
/// `Term::Loop` with one binder: `binders` stays present in
|
||||||
/// `binders` stays present (no `skip_serializing_if`).
|
/// canonical JSON (no `skip_serializing_if`).
|
||||||
#[test]
|
#[test]
|
||||||
fn term_loop_one_binder_serialises_with_explicit_binders_field() {
|
fn term_loop_one_binder_serialises_with_explicit_binders_field() {
|
||||||
let t = Term::Loop {
|
let t = Term::Loop {
|
||||||
|
|||||||
@@ -346,23 +346,6 @@ fn collect_used_in_term(t: &Term, used: &mut BTreeSet<String>) {
|
|||||||
collect_used_in_term(source, used);
|
collect_used_in_term(source, used);
|
||||||
collect_used_in_term(body, 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 } => {
|
Term::Loop { binders, body } => {
|
||||||
for b in binders {
|
for b in binders {
|
||||||
used.insert(b.name.clone());
|
used.insert(b.name.clone());
|
||||||
@@ -568,47 +551,12 @@ impl Desugarer {
|
|||||||
source: Box::new(self.desugar_term(source, scope)),
|
source: Box::new(self.desugar_term(source, scope)),
|
||||||
body: Box::new(self.desugar_term(body, 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<MutVar> = 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 } => {
|
Term::Loop { binders, body } => {
|
||||||
// loop-recur iter 1: structural recursion mirroring
|
// loop-recur iter 1: structural recursion. Each
|
||||||
// the Term::Mut arm. Each binder's init is desugared
|
// binder's init is desugared in scope of the outer env
|
||||||
// in scope of the outer env plus already-declared
|
// plus already-declared binders; the body sees all
|
||||||
// binders; the body sees all binders. The LetBound
|
// binders. The LetBound sentinel keeps generated fresh
|
||||||
// sentinel keeps generated fresh names off binder
|
// names off binder names.
|
||||||
// names.
|
|
||||||
let mut inner = scope.clone();
|
let mut inner = scope.clone();
|
||||||
let new_binders: Vec<LoopBinder> = binders
|
let new_binders: Vec<LoopBinder> = binders
|
||||||
.iter()
|
.iter()
|
||||||
@@ -1254,33 +1202,10 @@ pub fn free_vars_in_term(t: &Term, bound: &BTreeSet<String>, out: &mut BTreeSet<
|
|||||||
free_vars_in_term(source, bound, out);
|
free_vars_in_term(source, bound, out);
|
||||||
free_vars_in_term(body, 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 } => {
|
Term::Loop { binders, body } => {
|
||||||
// loop-recur iter 1: binder names bind inside the loop —
|
// loop-recur iter 1: binder names bind inside the loop —
|
||||||
// each init sees the outer env plus already-declared
|
// 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();
|
let mut b = bound.clone();
|
||||||
for bd in binders {
|
for bd in binders {
|
||||||
free_vars_in_term(&bd.init, &b, out);
|
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)),
|
source: Box::new(subst_var(source, from, to)),
|
||||||
body: Box::new(subst_var(body, 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<MutVar> = 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 } => {
|
Term::Loop { binders, body } => {
|
||||||
// loop-recur iter 1: binders lexically shadow outer
|
// loop-recur iter 1: binders lexically shadow outer
|
||||||
// names, symmetric to the Term::Mut arm — once a binder
|
// names — once a binder named `from` is declared, later
|
||||||
// named `from` is declared, later inits and the body
|
// inits and the body stop being substituted.
|
||||||
// stop being substituted.
|
|
||||||
let mut shadowed = false;
|
let mut shadowed = false;
|
||||||
let new_binders: Vec<LoopBinder> = binders
|
let new_binders: Vec<LoopBinder> = binders
|
||||||
.iter()
|
.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)),
|
source: Box::new(subst_call_with_extras(source, name, lifted, extras)),
|
||||||
body: Box::new(subst_call_with_extras(body, 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 {
|
Term::Loop { binders, body } => Term::Loop {
|
||||||
binders: binders
|
binders: binders
|
||||||
.iter()
|
.iter()
|
||||||
@@ -1735,24 +1590,6 @@ pub fn find_non_callee_use(t: &Term, name: &str) -> Option<Term> {
|
|||||||
Term::ReuseAs { source, body } => {
|
Term::ReuseAs { source, body } => {
|
||||||
find_non_callee_use(source, name).or_else(|| find_non_callee_use(body, name))
|
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
|
Term::Loop { binders, body } => binders
|
||||||
.iter()
|
.iter()
|
||||||
.find_map(|b| find_non_callee_use(&b.init, name))
|
.find_map(|b| find_non_callee_use(&b.init, name))
|
||||||
@@ -1802,11 +1639,6 @@ mod tests {
|
|||||||
}
|
}
|
||||||
Term::Clone { value } => any_nested_ctor(value),
|
Term::Clone { value } => any_nested_ctor(value),
|
||||||
Term::ReuseAs { source, body } => any_nested_ctor(source) || any_nested_ctor(body),
|
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 } => {
|
Term::Loop { binders, body } => {
|
||||||
binders.iter().any(|b| any_nested_ctor(&b.init)) || any_nested_ctor(body)
|
binders.iter().any(|b| any_nested_ctor(&b.init)) || any_nested_ctor(body)
|
||||||
}
|
}
|
||||||
@@ -1836,11 +1668,6 @@ mod tests {
|
|||||||
Term::LetRec { .. } => true,
|
Term::LetRec { .. } => true,
|
||||||
Term::Clone { value } => any_let_rec(value),
|
Term::Clone { value } => any_let_rec(value),
|
||||||
Term::ReuseAs { source, body } => any_let_rec(source) || any_let_rec(body),
|
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 } => {
|
Term::Loop { binders, body } => {
|
||||||
binders.iter().any(|b| any_let_rec(&b.init)) || any_let_rec(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::Clone { value } => any_lit_pattern(value),
|
||||||
Term::ReuseAs { source, body } => any_lit_pattern(source) || any_lit_pattern(body),
|
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 } => {
|
Term::Loop { binders, body } => {
|
||||||
binders.iter().any(|b| any_lit_pattern(&b.init)) || any_lit_pattern(body)
|
binders.iter().any(|b| any_lit_pattern(&b.init)) || any_lit_pattern(body)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1247,18 +1247,6 @@ where
|
|||||||
walk_term_embedded_types(source, f)?;
|
walk_term_embedded_types(source, f)?;
|
||||||
walk_term_embedded_types(body, 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 } => {
|
Term::Loop { binders, body } => {
|
||||||
for b in binders {
|
for b in binders {
|
||||||
walk_type(&b.ty, f)?;
|
walk_type(&b.ty, f)?;
|
||||||
@@ -1394,17 +1382,6 @@ where
|
|||||||
walk_term(source, f)?;
|
walk_term(source, f)?;
|
||||||
walk_term(body, 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 } => {
|
Term::Loop { binders, body } => {
|
||||||
for b in binders {
|
for b in binders {
|
||||||
walk_term(&b.init, f)?;
|
walk_term(&b.init, f)?;
|
||||||
|
|||||||
@@ -3,16 +3,9 @@
|
|||||||
//! under `examples/*.ail.json` after milestone close. The list is
|
//! under `examples/*.ail.json` after milestone close. The list is
|
||||||
//! hardcoded; any change is a deliberate, brainstorm-level decision.
|
//! 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
|
//! - §C4 (a) subject-matter: 7 fixtures from form-a.1 milestone
|
||||||
//! close (canonical-form rejection / unbound / class-def rejection).
|
//! 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
|
//! - §C4 (b) compile-time-embed: retired 2026-05-14 by milestone
|
||||||
//! prelude-decouple; the prelude is now embedded as `prelude.ail`
|
//! prelude-decouple; the prelude is now embedded as `prelude.ail`
|
||||||
//! in `ailang-surface` and parsed at compile time via
|
//! in `ailang-surface` and parsed at compile time via
|
||||||
@@ -21,12 +14,11 @@
|
|||||||
//! four `Recur*` diagnostics (recur-outside-loop /
|
//! four `Recur*` diagnostics (recur-outside-loop /
|
||||||
//! recur-arity-mismatch / recur-type-mismatch /
|
//! recur-arity-mismatch / recur-type-mismatch /
|
||||||
//! recur-not-in-tail-position) — the diagnostic code is the
|
//! 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.
|
//! negative-fixture convention.
|
||||||
//! - loop-recur iter loop-recur.tidy: 1 negative typecheck fixture
|
//! - loop-recur iter loop-recur.tidy: 1 negative typecheck fixture
|
||||||
//! for the lambda-capture-of-loop-binder rejection
|
//! for the lambda-capture-of-loop-binder rejection
|
||||||
//! (loop-binder-captured-by-lambda) — symmetric to the mut.4-tidy
|
//! (loop-binder-captured-by-lambda).
|
||||||
//! `test_mut_var_captured_by_lambda.ail.json` carve-out.
|
|
||||||
|
|
||||||
use std::collections::BTreeSet;
|
use std::collections::BTreeSet;
|
||||||
|
|
||||||
@@ -39,14 +31,6 @@ const EXPECTED: &[&str] = &[
|
|||||||
"test_ct1_bad_qualifier.ail.json",
|
"test_ct1_bad_qualifier.ail.json",
|
||||||
"test_ct1_bare_xmod_rejected.ail.json",
|
"test_ct1_bare_xmod_rejected.ail.json",
|
||||||
"test_ct1_qualified_class_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
|
// Iter loop-recur.tidy — lambda-capture-of-loop-binder rejection
|
||||||
"test_loop_binder_captured_by_lambda.ail.json",
|
"test_loop_binder_captured_by_lambda.ail.json",
|
||||||
// loop-recur iter 2 — negative typecheck fixtures
|
// loop-recur iter 2 — negative typecheck fixtures
|
||||||
|
|||||||
@@ -137,20 +137,6 @@ fn design_md_anchors_every_term_variant() {
|
|||||||
body: Box::new(Term::Var { name: "y".into() }),
|
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""#,
|
r#""t": "loop""#,
|
||||||
Term::Loop {
|
Term::Loop {
|
||||||
@@ -181,8 +167,6 @@ fn design_md_anchors_every_term_variant() {
|
|||||||
Term::Seq { .. } => "seq",
|
Term::Seq { .. } => "seq",
|
||||||
Term::Clone { .. } => "clone",
|
Term::Clone { .. } => "clone",
|
||||||
Term::ReuseAs { .. } => "reuse-as",
|
Term::ReuseAs { .. } => "reuse-as",
|
||||||
Term::Mut { .. } => "mut",
|
|
||||||
Term::Assign { .. } => "assign",
|
|
||||||
Term::Loop { .. } => "loop",
|
Term::Loop { .. } => "loop",
|
||||||
Term::Recur { .. } => "recur",
|
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"
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -47,8 +47,6 @@ enum VariantTag {
|
|||||||
TermSeq,
|
TermSeq,
|
||||||
TermClone,
|
TermClone,
|
||||||
TermReuseAs,
|
TermReuseAs,
|
||||||
TermMut,
|
|
||||||
TermAssign,
|
|
||||||
TermLoop,
|
TermLoop,
|
||||||
TermRecur,
|
TermRecur,
|
||||||
// Pattern
|
// Pattern
|
||||||
@@ -96,8 +94,6 @@ const EXPECTED_VARIANTS: &[VariantTag] = &[
|
|||||||
VariantTag::TermSeq,
|
VariantTag::TermSeq,
|
||||||
VariantTag::TermClone,
|
VariantTag::TermClone,
|
||||||
VariantTag::TermReuseAs,
|
VariantTag::TermReuseAs,
|
||||||
VariantTag::TermMut,
|
|
||||||
VariantTag::TermAssign,
|
|
||||||
VariantTag::TermLoop,
|
VariantTag::TermLoop,
|
||||||
VariantTag::TermRecur,
|
VariantTag::TermRecur,
|
||||||
VariantTag::PatternWild,
|
VariantTag::PatternWild,
|
||||||
@@ -236,18 +232,6 @@ fn visit_term(t: &Term, observed: &mut HashSet<VariantTag>) {
|
|||||||
visit_term(source, observed);
|
visit_term(source, observed);
|
||||||
visit_term(body, 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 } => {
|
Term::Loop { binders, body } => {
|
||||||
observed.insert(VariantTag::TermLoop);
|
observed.insert(VariantTag::TermLoop);
|
||||||
for b in binders {
|
for b in binders {
|
||||||
|
|||||||
@@ -114,20 +114,6 @@ fn spec_mentions_every_term_variant() {
|
|||||||
body: Box::new(Term::Var { name: "y".into() }),
|
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",
|
"(loop",
|
||||||
Term::Loop {
|
Term::Loop {
|
||||||
@@ -160,8 +146,6 @@ fn spec_mentions_every_term_variant() {
|
|||||||
Term::Seq { .. } => "seq",
|
Term::Seq { .. } => "seq",
|
||||||
Term::Clone { .. } => "clone",
|
Term::Clone { .. } => "clone",
|
||||||
Term::ReuseAs { .. } => "reuse-as",
|
Term::ReuseAs { .. } => "reuse-as",
|
||||||
Term::Mut { .. } => "mut",
|
|
||||||
Term::Assign { .. } => "assign",
|
|
||||||
Term::Loop { .. } => "loop",
|
Term::Loop { .. } => "loop",
|
||||||
Term::Recur { .. } => "recur",
|
Term::Recur { .. } => "recur",
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -906,35 +906,6 @@ fn write_term_prec(out: &mut String, t: &Term, level: usize, parent_prec: u8, ow
|
|||||||
out.push_str(&body_buf);
|
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 <name>: <ty>
|
|
||||||
// = <init>; ...; <body> }` 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 } => {
|
Term::Loop { binders, body } => {
|
||||||
out.push_str("loop(");
|
out.push_str("loop(");
|
||||||
for (i, b) in binders.iter().enumerate() {
|
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::Clone { value } => count_free_var(name, value),
|
||||||
Term::ReuseAs { source, body } => count_free_var(name, source) + count_free_var(name, body),
|
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
|
// loop-recur iter 1: a binder named `name` shadows the outer
|
||||||
// binding for later binder inits and the body. Inits before
|
// binding for later binder inits and the body. Inits before
|
||||||
// the shadowing binder still see the outer `name`.
|
// 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)),
|
source: Box::new(subst_var_with_term(source, name, replacement)),
|
||||||
body: Box::new(subst_var_with_term(body, name, replacement)),
|
body: Box::new(subst_var_with_term(body, name, replacement)),
|
||||||
},
|
},
|
||||||
// Iter mut.1: lexical-shadow semantics for mut-vars symmetric
|
// loop-recur iter 1: lexical-shadow semantics — once a binder
|
||||||
// to `count_free_var` above: once a var named `name` is
|
// named `name` is declared, later inits and the body are not
|
||||||
// declared, neither later inits nor the body should be
|
|
||||||
// rewritten.
|
// rewritten.
|
||||||
Term::Mut { vars, body } => {
|
|
||||||
let mut shadowed = false;
|
|
||||||
let new_vars: Vec<ailang_core::ast::MutVar> = 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 } => {
|
Term::Loop { binders, body } => {
|
||||||
let mut shadowed = false;
|
let mut shadowed = false;
|
||||||
let new_binders: Vec<ailang_core::ast::LoopBinder> = binders
|
let new_binders: Vec<ailang_core::ast::LoopBinder> = binders
|
||||||
|
|||||||
@@ -40,7 +40,6 @@
|
|||||||
//! | app-term | tail-app-term | match-term | ctor-term
|
//! | app-term | tail-app-term | match-term | ctor-term
|
||||||
//! | do-term | tail-do-term | seq-term | lam-term | if-term
|
//! | do-term | tail-do-term | seq-term | lam-term | if-term
|
||||||
//! | let-term | let-rec-term | clone-term | reuse-as-term
|
//! | let-term | let-rec-term | clone-term | reuse-as-term
|
||||||
//! | mut-term | assign-term
|
|
||||||
//! var-ref ::= ident ; reserved: true/false → bool-lit
|
//! var-ref ::= ident ; reserved: true/false → bool-lit
|
||||||
//! int-lit ::= integer ; numeric atom
|
//! int-lit ::= integer ; numeric atom
|
||||||
//! str-lit ::= string ; string atom
|
//! str-lit ::= string ; string atom
|
||||||
@@ -67,9 +66,6 @@
|
|||||||
//! "(" "in" term ")" ")"
|
//! "(" "in" term ")" ")"
|
||||||
//! clone-term ::= "(" "clone" term ")" ; Iter 18c.1
|
//! clone-term ::= "(" "clone" term ")" ; Iter 18c.1
|
||||||
//! reuse-as-term ::= "(" "reuse-as" term term ")" ; Iter 18d.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
|
//! pattern ::= pat-var | pat-ctor | pat-lit | pat-wild
|
||||||
//! pat-var ::= ident
|
//! pat-var ::= ident
|
||||||
@@ -1210,8 +1206,6 @@ impl<'a> Parser<'a> {
|
|||||||
"let-rec" => self.parse_let_rec(),
|
"let-rec" => self.parse_let_rec(),
|
||||||
"clone" => self.parse_clone(),
|
"clone" => self.parse_clone(),
|
||||||
"reuse-as" => self.parse_reuse_as(),
|
"reuse-as" => self.parse_reuse_as(),
|
||||||
"mut" => self.parse_mut(),
|
|
||||||
"assign" => self.parse_assign(),
|
|
||||||
"loop" => self.parse_loop(),
|
"loop" => self.parse_loop(),
|
||||||
"recur" => self.parse_recur(),
|
"recur" => self.parse_recur(),
|
||||||
other => {
|
other => {
|
||||||
@@ -1221,8 +1215,8 @@ impl<'a> Parser<'a> {
|
|||||||
message: format!(
|
message: format!(
|
||||||
"unknown term head `{other}`; expected one of \
|
"unknown term head `{other}`; expected one of \
|
||||||
`app`, `tail-app`, `lam`, `let`, `let-rec`, `if`, `match`, `do`, \
|
`app`, `tail-app`, `lam`, `let`, `let-rec`, `if`, `match`, `do`, \
|
||||||
`tail-do`, `seq`, `term-ctor`, `clone`, `reuse-as`, `mut`, \
|
`tail-do`, `seq`, `term-ctor`, `clone`, `reuse-as`, \
|
||||||
`assign`, `loop`, `recur`, `lit-unit`"
|
`loop`, `recur`, `lit-unit`"
|
||||||
),
|
),
|
||||||
pos,
|
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<Term, ParseError> {
|
|
||||||
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<ailang_core::ast::MutVar> = 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<Term> = 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<Term, ParseError> {
|
|
||||||
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+)` —
|
/// loop-recur iter 1: `(loop (NAME TYPE INIT)* BODY_TERM+)` —
|
||||||
/// strict iteration block. Reads zero or more `(NAME TYPE INIT)`
|
/// strict iteration block. Reads zero or more `(NAME TYPE INIT)`
|
||||||
/// binder triples (bare, no leading keyword — unlike `(var ...)`,
|
/// binder triples (bare, no leading keyword), then ≥ 1
|
||||||
/// the loop binder triple has no `var` keyword), then ≥ 1
|
/// trailing terms right-folded into `Term::Seq`. The binder/body
|
||||||
/// trailing terms right-folded into `Term::Seq` (mirrors
|
/// boundary is disambiguated by the
|
||||||
/// `parse_mut`). The binder/body boundary is disambiguated by the
|
|
||||||
/// inner head: a binder's first inner token is an ident NAME
|
/// inner head: a binder's first inner token is an ident NAME
|
||||||
/// (never a term-head keyword), whereas a parenthesised body form
|
/// (never a term-head keyword), whereas a parenthesised body form
|
||||||
/// (`(if …)`, `(recur …)`, `(app …)`, …) opens with a term-head
|
/// (`(if …)`, `(recur …)`, `(app …)`, …) opens with a term-head
|
||||||
@@ -1673,8 +1596,6 @@ impl<'a> Parser<'a> {
|
|||||||
| "let-rec"
|
| "let-rec"
|
||||||
| "clone"
|
| "clone"
|
||||||
| "reuse-as"
|
| "reuse-as"
|
||||||
| "mut"
|
|
||||||
| "assign"
|
|
||||||
| "loop"
|
| "loop"
|
||||||
| "recur"
|
| "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]
|
#[test]
|
||||||
fn parse_loop_and_recur_round_trip_via_term() {
|
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))))";
|
let src = "(loop (acc (con Int) 0) (i (con Int) 1) (if (app > i n) acc (recur (app + acc i) (app + i 1))))";
|
||||||
|
|||||||
@@ -548,57 +548,10 @@ fn write_term(out: &mut String, t: &Term, level: usize) {
|
|||||||
write_term(out, body, level);
|
write_term(out, body, level);
|
||||||
out.push(')');
|
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 } => {
|
Term::Loop { binders, body } => {
|
||||||
// loop-recur iter 1: print as
|
// loop-recur iter 1: print as
|
||||||
// `(loop (NAME TYPE INIT)* STMT* FINAL_EXPR)`
|
// `(loop (NAME TYPE INIT)* STMT* FINAL_EXPR)`
|
||||||
// — inverse of parse_loop's binder read + Seq right-fold,
|
// — inverse of parse_loop's binder read + Seq right-fold.
|
||||||
// mirroring the Term::Mut printer.
|
|
||||||
out.push_str("(loop");
|
out.push_str("(loop");
|
||||||
for b in binders {
|
for b in binders {
|
||||||
out.push_str(" (");
|
out.push_str(" (");
|
||||||
|
|||||||
@@ -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<ailang_core::ast::Term, _> = 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<ailang_core::ast::Term, _> = serde_json::from_str(j);
|
||||||
|
assert!(r.is_err(), "{{\"t\":\"assign\"}} must be an unknown variant, got Ok");
|
||||||
|
}
|
||||||
+6
-54
@@ -2401,26 +2401,6 @@ are real surface forms.
|
|||||||
// lowers as in-place rewrite under `--alloc=rc`.
|
// lowers as in-place rewrite under `--alloc=rc`.
|
||||||
{ "t": "reuse-as", "source": Term, "body": Term }
|
{ "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": "<id>", "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": "<id>",
|
|
||||||
"value": Term }
|
|
||||||
|
|
||||||
// loop-recur iter 1: strict iteration block. `binders` declares
|
// loop-recur iter 1: strict iteration block. `binders` declares
|
||||||
// one or more loop parameters (name, type, init), evaluated in
|
// one or more loop parameters (name, type, init), evaluated in
|
||||||
// order on loop entry; `body` is in scope of all binders. The
|
// 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
|
handler). A `lam` term constructs an anonymous function value; free
|
||||||
variables of its body are captured from the enclosing scope.
|
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):
|
Loop binders are alloca-resident: typecheck binds them in the
|
||||||
typecheck binds mut-vars in a lexical scope stack, codegen lowers
|
ordinary local scope plus a positional `loop_stack`, and codegen
|
||||||
them as entry-block allocas, and `Term::Assign` emits a `store`
|
lowers them as entry-block allocas. Capturing a `loop` binder into a
|
||||||
yielding the canonical Unit SSA. Mut-vars must be of one of the
|
lambda body is rejected at typecheck via
|
||||||
stack-resident scalar types `{Int, Float, Bool, Unit}`; capturing a
|
`CheckError::LoopBinderCapturedByLambda`. See
|
||||||
mut-var into a lambda body is rejected at typecheck via
|
`docs/specs/2026-05-17-loop-recur.md`.
|
||||||
`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`.
|
|
||||||
|
|
||||||
**`Literal`**:
|
**`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
|
arg types (ctor) or the scrutinee's `Type::Con.args` (match). An
|
||||||
unresolved `Type::Var` reaching `llvm_type` is a hard error
|
unresolved `Type::Var` reaching `llvm_type` is a hard error
|
||||||
rather than a silent fallback to `ptr`.
|
rather than a silent fallback to `ptr`.
|
||||||
- **Local mutable state.** `(mut (var <name> <type> <init>) ... <body>)`
|
|
||||||
declares lexically-scoped mutable bindings whose types are restricted
|
|
||||||
to the stack-resident scalars `Int`, `Float`, `Bool`, `Unit`. Inside
|
|
||||||
the block, `(assign <name> <value>)` 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:
|
Pipeline regression smoke tests:
|
||||||
|
|
||||||
- `examples/sum.ail.json` → prints 55 (recursion, arithmetic).
|
- `examples/sum.ail.json` → prints 55 (recursion, arithmetic).
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -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 — 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 — 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 — 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
|
||||||
|
|||||||
+6
-13
@@ -45,9 +45,12 @@ work progresses.
|
|||||||
window, JSON tags fail closed as generic unknown-variant, codegen
|
window, JSON tags fail closed as generic unknown-variant, codegen
|
||||||
alloca machinery kept (loop reuses it) + renamed `binder_allocas`,
|
alloca machinery kept (loop reuses it) + renamed `binder_allocas`,
|
||||||
shared `Term::Lam` escape guard keeps its `loop` half. Explicitly
|
shared `Term::Lam` escape guard keeps its `loop` half. Explicitly
|
||||||
decoupled from Stateful-islands (separate, deferred). Spec written
|
decoupled from Stateful-islands (separate, deferred). Spec
|
||||||
+ grounding-check PASS; awaiting user spec review → planner.
|
approved + planned + implemented (605/0, loop/recur
|
||||||
- context: `docs/specs/2026-05-18-remove-mut-var-assign.md`
|
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 —
|
- [x] **\[milestone\]** Prose `loop` binders — projection redesign —
|
||||||
CLOSED 2026-05-18. Form-B prose now renders loop binders as a
|
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
|
zero-allocation pipe combinator, growing tuple-state types as
|
||||||
pipelines lengthen).
|
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
|
**Sub-milestone sequencing is UNDER RE-THINK with the user
|
||||||
(2026-05-16) — no further sub-milestone is planned or brainstormed
|
(2026-05-16) — no further sub-milestone is planned or brainstormed
|
||||||
until that conversation happens.** Why: the 2026-05-15 decomposition
|
until that conversation happens.** Why: the 2026-05-15 decomposition
|
||||||
|
|||||||
@@ -1,17 +1,4 @@
|
|||||||
; Fieldtest mut-local #1 — factorial 5! via straight-line mut updates.
|
; Print 5! (= 120) via a let-threaded running product.
|
||||||
;
|
|
||||||
; 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
|
|
||||||
|
|
||||||
(module mut-local_1_factorial
|
(module mut-local_1_factorial
|
||||||
|
|
||||||
@@ -19,12 +6,4 @@
|
|||||||
(type (fn-type (params) (ret (con Unit)) (effects IO)))
|
(type (fn-type (params) (ret (con Unit)) (effects IO)))
|
||||||
(params)
|
(params)
|
||||||
(body
|
(body
|
||||||
(app print
|
(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))))))))))
|
||||||
(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)))))
|
|
||||||
|
|||||||
@@ -1,20 +1,5 @@
|
|||||||
; Fieldtest mut-local #2 — classify a temperature into a band using
|
; Classify a temperature into a 0..3 band via a let-bound code.
|
||||||
; nested if-branches that each update a mut-Int "category code".
|
; classify 22 = 2 ("warm"). Expected stdout: 2
|
||||||
;
|
|
||||||
; 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")
|
|
||||||
|
|
||||||
(module mut-local_2_classify_temp
|
(module mut-local_2_classify_temp
|
||||||
|
|
||||||
@@ -22,17 +7,7 @@
|
|||||||
(doc "Return category code 0..3 for temperature t in degrees C.")
|
(doc "Return category code 0..3 for temperature t in degrees C.")
|
||||||
(type (fn-type (params (con Int)) (ret (con Int))))
|
(type (fn-type (params (con Int)) (ret (con Int))))
|
||||||
(params t)
|
(params t)
|
||||||
(body
|
(body (let code 0 (let code (if (app < t 0) 0 (if (app < t 15) 1 (if (app < t 28) 2 3))) code))))
|
||||||
(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)))
|
|
||||||
|
|
||||||
(fn main
|
(fn main
|
||||||
(type (fn-type (params) (ret (con Unit)) (effects IO)))
|
(type (fn-type (params) (ret (con Unit)) (effects IO)))
|
||||||
|
|||||||
@@ -1,23 +1,5 @@
|
|||||||
; Fieldtest mut-local #3 — evaluate the polynomial
|
; Evaluate p(x) = 2x^3 - 3x^2 + 5x - 7 at x = 2.5 by Horner's method
|
||||||
; p(x) = 2 x^3 - 3 x^2 + 5 x - 7
|
; via a let-threaded Float accumulator. Expected stdout: 18
|
||||||
; 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.)
|
|
||||||
|
|
||||||
(module mut-local_3_horner
|
(module mut-local_3_horner
|
||||||
|
|
||||||
@@ -25,10 +7,4 @@
|
|||||||
(type (fn-type (params) (ret (con Unit)) (effects IO)))
|
(type (fn-type (params) (ret (con Unit)) (effects IO)))
|
||||||
(params)
|
(params)
|
||||||
(body
|
(body
|
||||||
(app print
|
(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))))))))
|
||||||
(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)))))
|
|
||||||
|
|||||||
@@ -1,17 +1,5 @@
|
|||||||
; Fieldtest mut-local #4 — Bool mut-var "found-a-factor" flag.
|
; 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).
|
||||||
; 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.
|
|
||||||
; Expected stdout: true
|
; Expected stdout: true
|
||||||
|
|
||||||
(module mut-local_4_has_small_factor
|
(module mut-local_4_has_small_factor
|
||||||
@@ -19,14 +7,7 @@
|
|||||||
(fn has_small_factor
|
(fn has_small_factor
|
||||||
(type (fn-type (params (con Int)) (ret (con Bool))))
|
(type (fn-type (params (con Int)) (ret (con Bool))))
|
||||||
(params n)
|
(params n)
|
||||||
(body
|
(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)))))))
|
||||||
(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)))
|
|
||||||
|
|
||||||
(fn main
|
(fn main
|
||||||
(type (fn-type (params) (ret (con Unit)) (effects IO)))
|
(type (fn-type (params) (ret (con Unit)) (effects IO)))
|
||||||
|
|||||||
@@ -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)))))
|
|
||||||
@@ -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)))))
|
|
||||||
+12
-37
@@ -1,62 +1,37 @@
|
|||||||
(module mut
|
(module mut
|
||||||
|
|
||||||
(fn mut_empty
|
(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))))
|
(type (fn-type (params) (ret (con Int))))
|
||||||
(params)
|
(params)
|
||||||
(body (mut 0)))
|
(body 0))
|
||||||
|
|
||||||
(fn mut_single_var
|
(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))))
|
(type (fn-type (params) (ret (con Int))))
|
||||||
(params)
|
(params)
|
||||||
(body
|
(body (let x 0 (let x (app + x 1) x))))
|
||||||
(mut
|
|
||||||
(var x (con Int) 0)
|
|
||||||
(assign x (app + x 1))
|
|
||||||
x)))
|
|
||||||
|
|
||||||
(fn mut_two_vars
|
(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))))
|
(type (fn-type (params) (ret (con Float))))
|
||||||
(params)
|
(params)
|
||||||
(body
|
(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))))))))
|
||||||
(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)))))
|
|
||||||
|
|
||||||
(fn mut_nested_shadow
|
(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))))
|
(type (fn-type (params) (ret (con Int))))
|
||||||
(params)
|
(params)
|
||||||
(body
|
(body (let x 10 (let x (app + x 1) (let x 100 (let x (app + x 1) x))))))
|
||||||
(mut
|
|
||||||
(var x (con Int) 10)
|
|
||||||
(assign x (app + x 1))
|
|
||||||
(mut
|
|
||||||
(var x (con Int) 100)
|
|
||||||
(assign x (app + x 1))
|
|
||||||
x))))
|
|
||||||
|
|
||||||
(fn mut_returns_bool
|
(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))))
|
(type (fn-type (params) (ret (con Bool))))
|
||||||
(params)
|
(params)
|
||||||
(body
|
(body (let flag false (let flag true flag))))
|
||||||
(mut
|
|
||||||
(var flag (con Bool) false)
|
|
||||||
(assign flag true)
|
|
||||||
flag)))
|
|
||||||
|
|
||||||
(fn mut_returns_unit
|
(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))))
|
(type (fn-type (params) (ret (con Unit))))
|
||||||
(params)
|
(params)
|
||||||
(body
|
(body (let u (lit-unit) (let u (lit-unit) u)))))
|
||||||
(mut
|
|
||||||
(var u (con Unit) (lit-unit))
|
|
||||||
(assign u (lit-unit))
|
|
||||||
u))))
|
|
||||||
|
|||||||
@@ -1,15 +1,11 @@
|
|||||||
(module mut_counter
|
(module mut_counter
|
||||||
|
|
||||||
(fn main
|
(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)))
|
(type (fn-type (params) (ret (con Unit)) (effects IO)))
|
||||||
(params)
|
(params)
|
||||||
(body
|
(body
|
||||||
(app print
|
(app print (app sum_helper 1 10 0))))
|
||||||
(mut
|
|
||||||
(var sum (con Int) 0)
|
|
||||||
(assign sum (app sum_helper 1 10 0))
|
|
||||||
sum))))
|
|
||||||
|
|
||||||
(fn sum_helper
|
(fn sum_helper
|
||||||
(doc "Accumulator-style tail-recursive helper — sum from lo through hi inclusive.")
|
(doc "Accumulator-style tail-recursive helper — sum from lo through hi inclusive.")
|
||||||
|
|||||||
@@ -1,15 +1,11 @@
|
|||||||
(module mut_sum_floats
|
(module mut_sum_floats
|
||||||
|
|
||||||
(fn main
|
(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)))
|
(type (fn-type (params) (ret (con Unit)) (effects IO)))
|
||||||
(params)
|
(params)
|
||||||
(body
|
(body
|
||||||
(app print
|
(app print (app sum_helper 1.0 10.0 0.0))))
|
||||||
(mut
|
|
||||||
(var sum (con Float) 0.0)
|
|
||||||
(assign sum (app sum_helper 1.0 10.0 0.0))
|
|
||||||
sum))))
|
|
||||||
|
|
||||||
(fn sum_helper
|
(fn sum_helper
|
||||||
(doc "Accumulator-style tail-recursive Float helper — sum from lo through hi inclusive.")
|
(doc "Accumulator-style tail-recursive Float helper — sum from lo through hi inclusive.")
|
||||||
|
|||||||
@@ -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" }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
@@ -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 } }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
@@ -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" }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
@@ -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" }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
@@ -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 } }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
@@ -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" }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
Reference in New Issue
Block a user