iter revert: back out the Iteration-discipline milestone (it.1 + it.2)
One forward iteration; main never rewound.1ff7e81(the pre-9973546 commit) is the per-region byte oracle — every reverted source/test file is byte-identical to it; crates/ailang-check/src/lib.rs fully so. Root cause being corrected: the Iteration-discipline milestone was an over-escalation of fieldtest finding F1 (a [friction] item whose own minimal recommendation was a DESIGN.md note). Its totality dichotomy made the maximally-LLM-natural build(d:Int)=Node(1,build(d-1), build(d-1)) inexpressible (it.3 BLOCKED); the only in-thesis escape (A1/it.2b) conceded the language's first documented-unenforced totality precondition — a purity-pillar dilution the user rejected in favour of a full revert + rebuild. Removed: Term::Loop/Term::Recur/LoopBinder; the verify_structural_ recursion guardedness pass + term_contains_loop + Diverge-injection + the transitively-it.2 module_fns plumbing; the five Recur*/ NonStructuralRecursion CheckError variants (+ code() + the 3 dedicated ctx() arms); the it.1 codegen loop-header/phi/back-edge + parallel block_terminated setter; all Loop/Recur walker arms; 16 it.1/it.2 fixtures; 2 pin files; bench/it3-oracle/. Restored: 2 RC fixtures to1ff7e81content. Surgically kept (not in1ff7e81, landed with the milestone but independently sound): feature-acceptance clause 3 in DESIGN.md and skills/brainstorm/SKILL.md, with its worked example de-claimed from "shipped" to hypothetical-illustration form; the F3 P2 todo. bench/orchestrator-stats/2026-05-15-iter-it.{1,2,3}.json kept as historical record (like journals/plans). Sole net addition: an honest F1/F4 documented-idiom note in DESIGN.md (the tail-recursive accumulator fallback; examples/mut_counter.ail), guarded by a doc-presence test — "a documentation note is not a reshape", asserts nothing at the typecheck level. Roadmap: the Iteration-discipline block + blocking-fork section removed; the genuine total-Int-recursion ambition preserved as a deferred P2 milestone sequenced behind a future Nat/refinement-types milestone (not abandoned — correctly sequenced after the type machinery it needs). 2026-05-15-iteration-discipline.md carries a superseded header; it.1/it.2/it.3 journals + plans stay as history. Correctness gate PRISTINE: 164 surviving 1ff7e81-era fixtures ail check/ail run byte-identical to pre-milestone behaviour (verified against a1ff7e81worktree reference compiler, zero drift); cargo test --workspace 600/0; zero residual it.1/it.2 production surface. Spec docs/specs/2026-05-16-iteration-discipline-revert.md (b3853bf), plan docs/plans/2026-05-16-iter-revert.md (abf0013).
This commit is contained in:
@@ -1,41 +0,0 @@
|
|||||||
stem class migration runnable rec_fns note
|
|
||||||
bench_closure_chain a loop main run_loop:loop
|
|
||||||
bench_compute_collatz a loop main collatz_steps:loop,sum_steps_loop:loop
|
|
||||||
bench_compute_intsum a loop main intsum_loop:loop
|
|
||||||
bench_hof_pipeline a plaincall main build_n:structural,fold_with_fn:structural
|
|
||||||
bench_latency_explicit a BLOCKED main build_tree:BLOCKED,cons_n_acc:structural,sum_list_acc:structural,loop:structural BLOCKED:build_tree
|
|
||||||
bench_latency_implicit a BLOCKED main build_tree:BLOCKED,sum_tree:structural,cons_n_acc:structural,sum_list_acc:structural,loop:structural BLOCKED:build_tree
|
|
||||||
bench_list_sum a plaincall main cons_n_acc:structural,sum_acc:structural
|
|
||||||
bench_list_sum_explicit a plaincall main cons_n_acc:structural,sum_acc:structural
|
|
||||||
bench_mono_dispatch a plaincall-or-loop ? (self-rec via loop/letrec only)
|
|
||||||
bench_tree_walk b BLOCKED main build_tree:BLOCKED,sum_tree:structural BLOCKED:build_tree
|
|
||||||
borrow_own_demo c already-structural main list_length:structural,sum_list:structural
|
|
||||||
ct_1_ordering_signum a plaincall main drain:structural
|
|
||||||
escape_local_demo b loop main count:loop
|
|
||||||
fieldtest/floats_1_newton_sqrt a loop main newton_iter:loop
|
|
||||||
fieldtest/floats_2_average_int_list a plaincall main sum_acc:structural,count_acc:structural
|
|
||||||
fieldtest/forma_1_factorial a loop main fact_acc:loop
|
|
||||||
gc_stress b loop+structural main build:loop,sum_list:structural
|
|
||||||
list c already-structural main sum_list:structural
|
|
||||||
list_map c already-structural main map_int:structural,print_list:structural
|
|
||||||
list_map_poly a plaincall main map:structural,print_list:structural
|
|
||||||
mut_counter a loop main sum_helper:loop
|
|
||||||
mut_sum_floats a loop main sum_helper:loop
|
|
||||||
pat_extract_partial_drop c already-structural main sum_list:structural
|
|
||||||
rc_drop_iterative_long_list a plaincall main cons_n_acc:structural
|
|
||||||
rc_let_alias_implicit_param a BLOCKED main build:BLOCKED,loop:structural BLOCKED:build
|
|
||||||
rc_let_owned_app_leak b BLOCKED main build:BLOCKED BLOCKED:build
|
|
||||||
rc_list_drop c already-structural main sum_list:structural
|
|
||||||
rc_pin_recurse_implicit a BLOCKED main build:BLOCKED,loop:structural BLOCKED:build
|
|
||||||
rc_tail_sum_explicit_leak a plaincall main cons_n_acc:structural,sum_acc:structural
|
|
||||||
reuse_as_demo c already-structural main map_inc:structural,sum_list:structural
|
|
||||||
sort a plaincall main insert:structural,sort:structural,print_list:structural
|
|
||||||
std_either_list c already-structural checkonly lefts:structural,rights:structural,partition_eithers:structural
|
|
||||||
std_list a plaincall checkonly fold_left:structural,fold_right:structural,append:structural,map:structural,filter:structural,take:structural,drop:structural
|
|
||||||
std_list_stress b loop main build:loop
|
|
||||||
struct_rec_foldl_sum c already-structural checkonly go:structural
|
|
||||||
struct_rec_list_len c already-structural checkonly len:structural
|
|
||||||
struct_rec_sum_e2e c already-structural main sum:structural
|
|
||||||
struct_rec_tree_forest c already-structural checkonly forest_size:structural
|
|
||||||
sum b loop main sum:loop
|
|
||||||
test_mono_recursive_fn_bug a loop main loop:loop
|
|
||||||
|
@@ -1,3 +0,0 @@
|
|||||||
49995000
|
|
||||||
4999950000
|
|
||||||
124999750000
|
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
849666
|
|
||||||
10753840
|
|
||||||
62134795
|
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
3500003500000
|
|
||||||
350000035000000
|
|
||||||
8750000175000000
|
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
5000050000
|
|
||||||
500000500000
|
|
||||||
4500001500000
|
|
||||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -1,3 +0,0 @@
|
|||||||
4999950000
|
|
||||||
499999500000
|
|
||||||
4499998500000
|
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
4999950000
|
|
||||||
499999500000
|
|
||||||
4499998500000
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
-2551317978420243992
|
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
65535
|
|
||||||
262143
|
|
||||||
1048575
|
|
||||||
@@ -1,2 +0,0 @@
|
|||||||
3
|
|
||||||
6
|
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
-1
|
|
||||||
0
|
|
||||||
1
|
|
||||||
-1
|
|
||||||
1
|
|
||||||
@@ -1,4 +0,0 @@
|
|||||||
42
|
|
||||||
42
|
|
||||||
5
|
|
||||||
0
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
1.41421
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
3.2
|
|
||||||
@@ -1,2 +0,0 @@
|
|||||||
120
|
|
||||||
3628800
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
1275
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
42
|
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
2
|
|
||||||
4
|
|
||||||
6
|
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
2
|
|
||||||
3
|
|
||||||
4
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
55
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
55
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
6
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
1
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
0
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
1
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
15
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
0
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
4950
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
9
|
|
||||||
@@ -1,11 +0,0 @@
|
|||||||
1
|
|
||||||
1
|
|
||||||
2
|
|
||||||
3
|
|
||||||
3
|
|
||||||
4
|
|
||||||
5
|
|
||||||
5
|
|
||||||
5
|
|
||||||
6
|
|
||||||
9
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
ok (53 symbols across 6 modules)
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
ok (38 symbols across 3 modules)
|
|
||||||
@@ -1,2 +0,0 @@
|
|||||||
500500
|
|
||||||
500500
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
ok (22 symbols across 2 modules)
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
ok (22 symbols across 2 modules)
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
15
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
ok (24 symbols across 2 modules)
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
55
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
15
|
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
{
|
||||||
|
"iter_id": "revert",
|
||||||
|
"date": "2026-05-16",
|
||||||
|
"mode": "standard",
|
||||||
|
"outcome": "DONE",
|
||||||
|
"tasks_total": 11,
|
||||||
|
"tasks_completed": 11,
|
||||||
|
"reloops_per_task": {
|
||||||
|
"1": 0,
|
||||||
|
"2": 0,
|
||||||
|
"3": 0,
|
||||||
|
"4": 0,
|
||||||
|
"5": 0,
|
||||||
|
"6": 0,
|
||||||
|
"7": 0,
|
||||||
|
"8": 0,
|
||||||
|
"9": 0,
|
||||||
|
"10": 0,
|
||||||
|
"11": 0
|
||||||
|
},
|
||||||
|
"review_loops_spec": 0,
|
||||||
|
"review_loops_quality": 0,
|
||||||
|
"blocked_reason": null,
|
||||||
|
"notes": {
|
||||||
|
"self_caught_implementer_corrections": 1,
|
||||||
|
"self_caught_detail": "Task 4: a scripted line-range delete also removed verify_tail_positions' match/fn closing braces; caught by the disciplined full-file oracle diff in the implementer phase and fixed forward within the same task (not a spec/quality re-loop).",
|
||||||
|
"stop_gates_cleared": ["task3-step1-helper-provenance", "task8-step1-fixture-split"],
|
||||||
|
"concerns": 2,
|
||||||
|
"concern_detail": "Task 3 plan file-map under-enumerated the module_fns signature threading (resolved via oracle authority, no bounce); Task 9 Step-6 single-line grep heuristic returned 0 due to verbatim-text line-wrap (substantive clause-3 retention positively verified otherwise).",
|
||||||
|
"behavioural_equivalence": "164 surviving fixtures, fail=0, zero check/run drift vs 1ff7e81 — pristine",
|
||||||
|
"workspace_tests": "600 passed, 0 failed",
|
||||||
|
"main_head_moved": false
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1544,26 +1544,6 @@ fn walk_term(
|
|||||||
}
|
}
|
||||||
walk_term(value, out, builtins, scope);
|
walk_term(value, out, builtins, scope);
|
||||||
}
|
}
|
||||||
// Iter it.1: loop binder names bind inside the body and later
|
|
||||||
// binder inits, mirroring `Term::Mut`. Recur args are uses.
|
|
||||||
Term::Loop { binders, body } => {
|
|
||||||
let mut newly = Vec::new();
|
|
||||||
for b in binders {
|
|
||||||
walk_term(&b.init, out, builtins, scope);
|
|
||||||
if scope.insert(b.name.clone()) {
|
|
||||||
newly.push(b.name.clone());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
walk_term(body, out, builtins, scope);
|
|
||||||
for n in newly {
|
|
||||||
scope.remove(&n);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Term::Recur { args } => {
|
|
||||||
for a in args {
|
|
||||||
walk_term(a, out, builtins, scope);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2805,47 +2785,6 @@ fn rewrite_def(
|
|||||||
changed,
|
changed,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
// Iter it.1: rewrite types embedded in each
|
|
||||||
// `LoopBinder.ty` (loop binders carry full Type
|
|
||||||
// annotations like mut-vars) and recurse into each
|
|
||||||
// binder's `init` and the body. `Term::Recur` has no
|
|
||||||
// embedded type.
|
|
||||||
Term::Loop { binders, body } => {
|
|
||||||
for b in binders {
|
|
||||||
rewrite_type(
|
|
||||||
&mut b.ty,
|
|
||||||
owning_module,
|
|
||||||
local_types,
|
|
||||||
import_names,
|
|
||||||
changed,
|
|
||||||
);
|
|
||||||
rewrite_term(
|
|
||||||
&mut b.init,
|
|
||||||
owning_module,
|
|
||||||
local_types,
|
|
||||||
import_names,
|
|
||||||
changed,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
rewrite_term(
|
|
||||||
body,
|
|
||||||
owning_module,
|
|
||||||
local_types,
|
|
||||||
import_names,
|
|
||||||
changed,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
Term::Recur { args } => {
|
|
||||||
for a in args {
|
|
||||||
rewrite_term(
|
|
||||||
a,
|
|
||||||
owning_module,
|
|
||||||
local_types,
|
|
||||||
import_names,
|
|
||||||
changed,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -101,13 +101,6 @@ fn synthesised_print_uses_user_module_show_via_fallback() {
|
|||||||
|| contains_xmod_show_var(body)
|
|| contains_xmod_show_var(body)
|
||||||
}
|
}
|
||||||
Term::Assign { value, .. } => contains_xmod_show_var(value),
|
Term::Assign { value, .. } => contains_xmod_show_var(value),
|
||||||
// Iter it.1: a `Term::Loop` cannot itself host a synth'd
|
|
||||||
// cross-module reference, but recurse defensively.
|
|
||||||
Term::Loop { binders, body } => {
|
|
||||||
binders.iter().any(|b| contains_xmod_show_var(&b.init))
|
|
||||||
|| contains_xmod_show_var(body)
|
|
||||||
}
|
|
||||||
Term::Recur { args } => args.iter().any(contains_xmod_show_var),
|
|
||||||
Term::Lit { .. } => false,
|
Term::Lit { .. } => false,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2828,56 +2828,6 @@ fn mut_counter_prints_55() {
|
|||||||
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 it.1: `examples/loop_counter.ail` exercises `Term::Loop` +
|
|
||||||
/// `Term::Recur` codegen — the loop-header block with one phi per
|
|
||||||
/// binder and `recur` as a back-edge `br`. The body
|
|
||||||
/// `(loop ((var acc Int 0) (var i Int 1)) (if (> i 10) acc (recur ...)))`
|
|
||||||
/// sums 1..10 and prints 55. End-to-end gate for the loop-header /
|
|
||||||
/// phi / recur-back-edge lowering.
|
|
||||||
#[test]
|
|
||||||
fn loop_counter_runs_and_prints_55() {
|
|
||||||
let stdout = build_and_run("loop_counter.ail");
|
|
||||||
assert_eq!(stdout.trim(), "55", "loop_counter must print 55, got {stdout:?}");
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Iter it.1: a `Term::Loop` inside a `Term::Lam` body, invoked via
|
|
||||||
/// a returned closure. Protects the lambda-boundary invariant: the
|
|
||||||
/// closure thunk scopes its own loop header / phis (the `loop_frames`
|
|
||||||
/// save+reset+restore across the lambda boundary, mut.3 analogue) —
|
|
||||||
/// a `recur` inside the lambda must back-edge to the lambda's own
|
|
||||||
/// loop header, not the outer fn's. The lambda computes x*x by
|
|
||||||
/// summing x exactly x times; apply 7 prints 49.
|
|
||||||
#[test]
|
|
||||||
fn loop_in_lambda_runs_and_prints_49() {
|
|
||||||
let stdout = build_and_run("loop_in_lambda_e2e.ail");
|
|
||||||
assert_eq!(stdout.trim(), "49", "loop_in_lambda must print 49, got {stdout:?}");
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Iter it.2 Phase-3: an it.2-clean structural recursion that runs.
|
|
||||||
/// `struct_rec_sum_e2e` sums [1,2,3,4,5] via plain non-tail
|
|
||||||
/// recursion on the Cons tail — the it.2 guardedness check
|
|
||||||
/// classifies it pure + total (no `!Diverge`, no `tail-app`). This
|
|
||||||
/// gate proves the structural-recursion "total" verdict is
|
|
||||||
/// behaviourally sound, not merely a typecheck assertion: a plain
|
|
||||||
/// (musttail-free) structurally-decreasing recursive call lowers
|
|
||||||
/// and runs to the correct value.
|
|
||||||
#[test]
|
|
||||||
fn struct_rec_sum_runs_and_prints_15() {
|
|
||||||
let stdout = build_and_run("struct_rec_sum_e2e.ail");
|
|
||||||
assert_eq!(stdout.trim(), "15", "struct_rec_sum must print 15, got {stdout:?}");
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Iter it.2 Phase-3: the `Diverge`-path twin. `loop_needs_diverge`
|
|
||||||
/// declares `!Diverge` (loop-bearing per DD-4) and runs the it.1
|
|
||||||
/// accumulator loop to 55 — proving the Diverge-effect injection is
|
|
||||||
/// purely a typecheck-layer obligation with no codegen impact (the
|
|
||||||
/// loop lowers and runs identically to its pre-it.2 it.1 form).
|
|
||||||
#[test]
|
|
||||||
fn loop_needs_diverge_runs_and_prints_55() {
|
|
||||||
let stdout = build_and_run("loop_needs_diverge.ail");
|
|
||||||
assert_eq!(stdout.trim(), "55", "loop_needs_diverge must print 55, got {stdout:?}");
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Iter mut.3: Float twin of `mut_counter_prints_55`. The mut-var
|
/// Iter mut.3: Float twin of `mut_counter_prints_55`. The mut-var
|
||||||
/// is `Float`, init is `0.0`, the recursive 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
|
||||||
|
|||||||
@@ -343,7 +343,6 @@ mod tests {
|
|||||||
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 mut_scope_stack: Vec<IndexMap<String, Type>> = Vec::new();
|
||||||
let mut loop_stack: Vec<Vec<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;
|
||||||
@@ -355,7 +354,6 @@ mod tests {
|
|||||||
&env,
|
&env,
|
||||||
&mut locals,
|
&mut locals,
|
||||||
&mut mut_scope_stack,
|
&mut mut_scope_stack,
|
||||||
&mut loop_stack,
|
|
||||||
&mut effects,
|
&mut effects,
|
||||||
"<test>",
|
"<test>",
|
||||||
&mut subst,
|
&mut subst,
|
||||||
@@ -588,7 +586,6 @@ mod tests {
|
|||||||
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 mut_scope_stack: Vec<IndexMap<String, Type>> = Vec::new();
|
||||||
let mut loop_stack: Vec<Vec<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;
|
||||||
@@ -600,7 +597,6 @@ mod tests {
|
|||||||
&env,
|
&env,
|
||||||
&mut locals,
|
&mut locals,
|
||||||
&mut mut_scope_stack,
|
&mut mut_scope_stack,
|
||||||
&mut loop_stack,
|
|
||||||
&mut effects,
|
&mut effects,
|
||||||
"<test>",
|
"<test>",
|
||||||
&mut subst,
|
&mut subst,
|
||||||
|
|||||||
+33
-1024
File diff suppressed because it is too large
Load Diff
@@ -397,25 +397,6 @@ impl<'a> Lifter<'a> {
|
|||||||
name: name.clone(),
|
name: name.clone(),
|
||||||
value: Box::new(self.lift_in_term(value, locals, in_def)?),
|
value: Box::new(self.lift_in_term(value, locals, in_def)?),
|
||||||
}),
|
}),
|
||||||
Term::Loop { binders, body } => Ok(Term::Loop {
|
|
||||||
binders: binders
|
|
||||||
.iter()
|
|
||||||
.map(|b| {
|
|
||||||
Ok(ailang_core::ast::LoopBinder {
|
|
||||||
name: b.name.clone(),
|
|
||||||
ty: b.ty.clone(),
|
|
||||||
init: Box::new(self.lift_in_term(&b.init, locals, in_def)?),
|
|
||||||
})
|
|
||||||
})
|
|
||||||
.collect::<Result<Vec<_>>>()?,
|
|
||||||
body: Box::new(self.lift_in_term(body, locals, in_def)?),
|
|
||||||
}),
|
|
||||||
Term::Recur { args } => Ok(Term::Recur {
|
|
||||||
args: args
|
|
||||||
.iter()
|
|
||||||
.map(|a| self.lift_in_term(a, locals, in_def))
|
|
||||||
.collect::<Result<Vec<_>>>()?,
|
|
||||||
}),
|
|
||||||
Term::LetRec { name, ty, params, body, in_term } => {
|
Term::LetRec { name, ty, params, body, in_term } => {
|
||||||
// Iter 16b.3: post-order traversal — lift any inner
|
// Iter 16b.3: post-order traversal — lift any inner
|
||||||
// LetRecs first. Within the body's scope, `name` and
|
// LetRecs first. Within the body's scope, `name` and
|
||||||
@@ -743,11 +724,7 @@ impl<'a> Lifter<'a> {
|
|||||||
// term at a time, beginning from a top-of-body position; fresh
|
// term at a time, beginning from a top-of-body position; fresh
|
||||||
// empty mut-scope stack is correct.
|
// empty mut-scope stack is correct.
|
||||||
let mut mut_scope_stack: Vec<indexmap::IndexMap<String, crate::Type>> = Vec::new();
|
let mut mut_scope_stack: Vec<indexmap::IndexMap<String, crate::Type>> = Vec::new();
|
||||||
// Iter it.1 (DD-1): lift's letrec-capture re-entry walks one
|
let ty = synth(t, &self.env, locals, &mut mut_scope_stack, &mut effects, in_def, &mut subst, &mut counter, &mut residuals, &mut free_fn_calls, &mut warnings_discarded)?;
|
||||||
// term from a top-of-body position; fresh empty loop_stack is
|
|
||||||
// correct (a loop captured into a letrec carries its own).
|
|
||||||
let mut loop_stack: Vec<Vec<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)?;
|
|
||||||
Ok(subst.apply(&ty))
|
Ok(subst.apply(&ty))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -784,10 +761,6 @@ fn contains_any_letrec(m: &Module) -> bool {
|
|||||||
vars.iter().any(|v| term_has_letrec(&v.init)) || term_has_letrec(body)
|
vars.iter().any(|v| term_has_letrec(&v.init)) || term_has_letrec(body)
|
||||||
}
|
}
|
||||||
Term::Assign { value, .. } => term_has_letrec(value),
|
Term::Assign { value, .. } => term_has_letrec(value),
|
||||||
Term::Loop { binders, body } => {
|
|
||||||
binders.iter().any(|b| term_has_letrec(&b.init)) || term_has_letrec(body)
|
|
||||||
}
|
|
||||||
Term::Recur { args } => args.iter().any(term_has_letrec),
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
for def in &m.defs {
|
for def in &m.defs {
|
||||||
|
|||||||
@@ -609,17 +609,6 @@ impl<'a> Checker<'a> {
|
|||||||
// a tracked binder). Walk the `value` normally.
|
// a tracked binder). Walk the `value` normally.
|
||||||
self.walk(value, Position::Consume);
|
self.walk(value, Position::Consume);
|
||||||
}
|
}
|
||||||
Term::Loop { binders, body } => {
|
|
||||||
for b in binders {
|
|
||||||
self.walk(&b.init, Position::Consume);
|
|
||||||
}
|
|
||||||
self.walk(body, pos);
|
|
||||||
}
|
|
||||||
Term::Recur { args } => {
|
|
||||||
for a in args {
|
|
||||||
self.walk(a, Position::Consume);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -806,8 +795,6 @@ fn make_reuse_as_source_not_bare_var(def: &str, source: &Term, body: &Term) -> D
|
|||||||
Term::ReuseAs { .. } => "reuse-as",
|
Term::ReuseAs { .. } => "reuse-as",
|
||||||
Term::Mut { .. } => "mut",
|
Term::Mut { .. } => "mut",
|
||||||
Term::Assign { .. } => "assign",
|
Term::Assign { .. } => "assign",
|
||||||
Term::Loop { .. } => "loop",
|
|
||||||
Term::Recur { .. } => "recur",
|
|
||||||
};
|
};
|
||||||
let replacement = term_to_form_a(body);
|
let replacement = term_to_form_a(body);
|
||||||
Diagnostic::error(
|
Diagnostic::error(
|
||||||
@@ -947,17 +934,6 @@ fn any_sub_binder_consumed_for(
|
|||||||
Term::Assign { value, .. } => {
|
Term::Assign { value, .. } => {
|
||||||
any_sub_binder_consumed_for(value, pname, uniq, def_name, ctors)
|
any_sub_binder_consumed_for(value, pname, uniq, def_name, ctors)
|
||||||
}
|
}
|
||||||
// Iter it.1: loop binders are scalar like mut-vars; recurse
|
|
||||||
// into children defensively so a genuine consume-of-`pname`
|
|
||||||
// inside a binder init / body / recur arg still surfaces.
|
|
||||||
Term::Loop { binders, body } => {
|
|
||||||
binders.iter().any(|b| {
|
|
||||||
any_sub_binder_consumed_for(&b.init, pname, uniq, def_name, ctors)
|
|
||||||
}) || any_sub_binder_consumed_for(body, pname, uniq, def_name, ctors)
|
|
||||||
}
|
|
||||||
Term::Recur { args } => args
|
|
||||||
.iter()
|
|
||||||
.any(|a| any_sub_binder_consumed_for(a, pname, uniq, def_name, ctors)),
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -713,16 +713,11 @@ pub fn collect_mono_targets(
|
|||||||
// mut-scope, since any `Term::Mut` will push its own frame as the
|
// mut-scope, since any `Term::Mut` will push its own frame as the
|
||||||
// walk descends.
|
// walk descends.
|
||||||
let mut mut_scope_stack: Vec<indexmap::IndexMap<String, crate::Type>> = Vec::new();
|
let mut mut_scope_stack: Vec<indexmap::IndexMap<String, crate::Type>> = Vec::new();
|
||||||
// Iter it.1 (DD-1): mono re-synth from top-of-body — empty
|
|
||||||
// loop_stack; any `Term::Loop` pushes its own frame as the walk
|
|
||||||
// descends.
|
|
||||||
let mut loop_stack: Vec<Vec<crate::Type>> = Vec::new();
|
|
||||||
crate::synth(
|
crate::synth(
|
||||||
&f.body,
|
&f.body,
|
||||||
&env,
|
&env,
|
||||||
&mut locals,
|
&mut locals,
|
||||||
&mut mut_scope_stack,
|
&mut mut_scope_stack,
|
||||||
&mut loop_stack,
|
|
||||||
&mut effects,
|
&mut effects,
|
||||||
&f.name,
|
&f.name,
|
||||||
&mut subst,
|
&mut subst,
|
||||||
@@ -1244,17 +1239,6 @@ fn rewrite_mono_calls(
|
|||||||
Term::Assign { value, .. } => {
|
Term::Assign { value, .. } => {
|
||||||
rewrite_mono_calls(value, method_to_candidate_classes, poly_free_fns, poly_free_fn_ccounts, caller_module, ordered_targets, cursor, locals);
|
rewrite_mono_calls(value, method_to_candidate_classes, poly_free_fns, poly_free_fn_ccounts, caller_module, ordered_targets, cursor, locals);
|
||||||
}
|
}
|
||||||
Term::Loop { binders, body } => {
|
|
||||||
for b in binders.iter_mut() {
|
|
||||||
rewrite_mono_calls(&mut b.init, method_to_candidate_classes, poly_free_fns, poly_free_fn_ccounts, caller_module, ordered_targets, cursor, locals);
|
|
||||||
}
|
|
||||||
rewrite_mono_calls(body, method_to_candidate_classes, poly_free_fns, poly_free_fn_ccounts, caller_module, ordered_targets, cursor, locals);
|
|
||||||
}
|
|
||||||
Term::Recur { args } => {
|
|
||||||
for a in args.iter_mut() {
|
|
||||||
rewrite_mono_calls(a, method_to_candidate_classes, poly_free_fns, poly_free_fn_ccounts, caller_module, ordered_targets, cursor, locals);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Term::Lit { .. } => {}
|
Term::Lit { .. } => {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1359,14 +1343,11 @@ pub(crate) fn collect_residuals_ordered(
|
|||||||
// mut-scope, since any `Term::Mut` will push its own frame as the
|
// mut-scope, since any `Term::Mut` will push its own frame as the
|
||||||
// walk descends.
|
// walk descends.
|
||||||
let mut mut_scope_stack: Vec<indexmap::IndexMap<String, crate::Type>> = Vec::new();
|
let mut mut_scope_stack: Vec<indexmap::IndexMap<String, crate::Type>> = Vec::new();
|
||||||
// Iter it.1 (DD-1): empty loop_stack at top-of-body re-synth.
|
|
||||||
let mut loop_stack: Vec<Vec<crate::Type>> = Vec::new();
|
|
||||||
crate::synth(
|
crate::synth(
|
||||||
&f.body,
|
&f.body,
|
||||||
&env,
|
&env,
|
||||||
&mut locals,
|
&mut locals,
|
||||||
&mut mut_scope_stack,
|
&mut mut_scope_stack,
|
||||||
&mut loop_stack,
|
|
||||||
&mut effects,
|
&mut effects,
|
||||||
&f.name,
|
&f.name,
|
||||||
&mut subst,
|
&mut subst,
|
||||||
@@ -1632,17 +1613,6 @@ fn interleave_slots(
|
|||||||
Term::Assign { value, .. } => {
|
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);
|
interleave_slots(value, method_to_candidate_classes, poly_free_fns, poly_free_fn_ccounts, class_slots, free_fn_slots, class_cur, free_cur, locals, out);
|
||||||
}
|
}
|
||||||
Term::Loop { binders, body } => {
|
|
||||||
for b in binders {
|
|
||||||
interleave_slots(&b.init, method_to_candidate_classes, poly_free_fns, poly_free_fn_ccounts, class_slots, free_fn_slots, class_cur, free_cur, locals, out);
|
|
||||||
}
|
|
||||||
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::Recur { args } => {
|
|
||||||
for a in args {
|
|
||||||
interleave_slots(a, method_to_candidate_classes, poly_free_fns, poly_free_fn_ccounts, class_slots, free_fn_slots, class_cur, free_cur, locals, out);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Term::Lit { .. } => {}
|
Term::Lit { .. } => {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -133,18 +133,6 @@ fn walk_term(t: &Term) -> Result<(), CheckError> {
|
|||||||
walk_term(body)
|
walk_term(body)
|
||||||
}
|
}
|
||||||
Term::Assign { value, .. } => walk_term(value),
|
Term::Assign { value, .. } => walk_term(value),
|
||||||
Term::Loop { binders, body } => {
|
|
||||||
for b in binders {
|
|
||||||
walk_term(&b.init)?;
|
|
||||||
}
|
|
||||||
walk_term(body)
|
|
||||||
}
|
|
||||||
Term::Recur { args } => {
|
|
||||||
for a in args {
|
|
||||||
walk_term(a)?;
|
|
||||||
}
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -265,17 +265,6 @@ impl<'a> Checker<'a> {
|
|||||||
self.walk(body);
|
self.walk(body);
|
||||||
}
|
}
|
||||||
Term::Assign { value, .. } => self.walk(value),
|
Term::Assign { value, .. } => self.walk(value),
|
||||||
Term::Loop { binders, body } => {
|
|
||||||
for b in binders {
|
|
||||||
self.walk(&b.init);
|
|
||||||
}
|
|
||||||
self.walk(body);
|
|
||||||
}
|
|
||||||
Term::Recur { args } => {
|
|
||||||
for a in args {
|
|
||||||
self.walk(a);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -352,17 +352,6 @@ impl<'a> Walker<'a> {
|
|||||||
Term::Assign { value, .. } => {
|
Term::Assign { value, .. } => {
|
||||||
self.walk(value, Position::Consume);
|
self.walk(value, Position::Consume);
|
||||||
}
|
}
|
||||||
Term::Loop { binders, body } => {
|
|
||||||
for b in binders {
|
|
||||||
self.walk(&b.init, Position::Consume);
|
|
||||||
}
|
|
||||||
self.walk(body, pos);
|
|
||||||
}
|
|
||||||
Term::Recur { args } => {
|
|
||||||
for a in args {
|
|
||||||
self.walk(a, Position::Consume);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,60 +0,0 @@
|
|||||||
//! Iter it.1 (Task 5): pin tests that the four `recur` negative
|
|
||||||
//! fixtures produce their exact diagnostic codes, and that the
|
|
||||||
//! positive `loop_smoke` fixture typechecks clean.
|
|
||||||
//!
|
|
||||||
//! Spec: `docs/specs/2026-05-15-iteration-discipline.md`. The four
|
|
||||||
//! negatives live as canonical `.ail.json` (diagnostic code is the
|
|
||||||
//! load-bearing assertion, not the surface form), mirroring the
|
|
||||||
//! `mut_typecheck_pin` carve-out precedent.
|
|
||||||
|
|
||||||
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 loop_smoke_typechecks_clean() {
|
|
||||||
let codes = check_fixture("loop_smoke.ail");
|
|
||||||
assert!(codes.is_empty(), "expected zero diagnostics, got {codes:?}");
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn recur_outside_loop_is_rejected() {
|
|
||||||
let codes = check_fixture("test_recur_outside_loop.ail.json");
|
|
||||||
assert_eq!(codes, vec!["recur-outside-loop".to_string()]);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn recur_arity_mismatch_is_rejected() {
|
|
||||||
let codes = check_fixture("test_recur_arity_mismatch.ail.json");
|
|
||||||
assert_eq!(codes, vec!["recur-arity-mismatch".to_string()]);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn recur_type_mismatch_is_rejected() {
|
|
||||||
let codes = check_fixture("test_recur_type_mismatch.ail.json");
|
|
||||||
assert_eq!(codes, vec!["recur-type-mismatch".to_string()]);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn recur_not_in_tail_position_is_rejected() {
|
|
||||||
let codes = check_fixture("test_recur_not_in_tail_position.ail.json");
|
|
||||||
assert_eq!(codes, vec!["recur-not-in-tail-position".to_string()]);
|
|
||||||
}
|
|
||||||
@@ -1,129 +0,0 @@
|
|||||||
//! Iter it.2: pin tests for the structural-recursion guardedness
|
|
||||||
//! pass + the it.2-only `tail:true` grandfather + the first real
|
|
||||||
//! `Diverge` effect injection.
|
|
||||||
//!
|
|
||||||
//! Spec: `docs/specs/2026-05-15-iteration-discipline.md`. Mirrors the
|
|
||||||
//! it.1 `loop_recur_pin.rs` harness verbatim (same inline
|
|
||||||
//! `check_fixture` helper, no `mod common`).
|
|
||||||
|
|
||||||
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 structural_list_len_is_clean() {
|
|
||||||
// Plain non-tail recursion on a Cons sub-component typechecks
|
|
||||||
// clean: structurally guarded ⇒ pure + total, no diagnostics.
|
|
||||||
assert!(
|
|
||||||
check_fixture("struct_rec_list_len.ail").is_empty(),
|
|
||||||
"expected zero diagnostics for structural list length"
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn foldl_accumulator_is_structural_and_clean() {
|
|
||||||
// A foldl-shape accumulator walk (structural on the tail,
|
|
||||||
// unconstrained accumulator) classifies as structural recursion
|
|
||||||
// and stays clean + Diverge-free (spec D1).
|
|
||||||
assert!(
|
|
||||||
check_fixture("struct_rec_foldl_sum.ail").is_empty(),
|
|
||||||
"expected zero diagnostics for foldl-shape accumulator walk"
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn non_structural_self_call_is_rejected() {
|
|
||||||
// A self-call passing the un-decreased parameter at every
|
|
||||||
// candidate position, not `tail`-marked, fires
|
|
||||||
// `non-structural-recursion`.
|
|
||||||
assert!(
|
|
||||||
check_fixture("test_non_structural_recursion.ail.json")
|
|
||||||
.contains(&"non-structural-recursion".to_string()),
|
|
||||||
"expected non-structural-recursion diagnostic"
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn tree_forest_mutual_is_clean() {
|
|
||||||
// Mutual tree/forest recursion over one ADT family (the
|
|
||||||
// cross-reference edge unions {Tree, Forest}) classifies as a
|
|
||||||
// mutual structural group: clean, Diverge-free.
|
|
||||||
assert!(
|
|
||||||
check_fixture("struct_rec_tree_forest.ail").is_empty(),
|
|
||||||
"expected zero diagnostics for same-family mutual recursion"
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn mutual_cross_family_is_rejected() {
|
|
||||||
// Mutual recursion whose members' structural params lie in two
|
|
||||||
// unrelated ADT families is not a valid mutual structural group
|
|
||||||
// (DD-3): the cross-call is an unguarded recursive call.
|
|
||||||
assert!(
|
|
||||||
check_fixture("test_mutual_cross_family.ail.json")
|
|
||||||
.contains(&"non-structural-recursion".to_string()),
|
|
||||||
"expected non-structural-recursion for cross-family mutual recursion"
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn loop_fn_declaring_diverge_is_clean() {
|
|
||||||
// A loop-bearing fn that declares `!Diverge` in its effect row
|
|
||||||
// reconciles clean (DD-4 / D2).
|
|
||||||
assert!(
|
|
||||||
check_fixture("loop_needs_diverge.ail").is_empty(),
|
|
||||||
"expected zero diagnostics for loop fn declaring !Diverge"
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn loop_fn_missing_diverge_is_rejected() {
|
|
||||||
// A loop-bearing fn missing `!Diverge` raises the existing
|
|
||||||
// `undeclared-effect` (no new diagnostic variant; DD-4).
|
|
||||||
assert!(
|
|
||||||
check_fixture("test_loop_missing_diverge.ail.json")
|
|
||||||
.contains(&"undeclared-effect".to_string()),
|
|
||||||
"expected undeclared-effect for loop fn missing !Diverge"
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn structural_recursion_is_diverge_free() {
|
|
||||||
// Structural recursion injects no effect: a structural list walk
|
|
||||||
// with no `loop` and no declared `!Diverge` stays clean.
|
|
||||||
assert!(
|
|
||||||
check_fixture("struct_rec_list_len.ail").is_empty(),
|
|
||||||
"structural recursion must be Diverge-free"
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn non_structural_recursion_code_is_registered() {
|
|
||||||
// A CheckError::NonStructuralRecursion must map to the kebab code.
|
|
||||||
// This step only pins the code() arm exists and returns the exact
|
|
||||||
// string; fixture-level behaviour is pinned in Task 2+.
|
|
||||||
use ailang_check::CheckError;
|
|
||||||
let e = CheckError::NonStructuralRecursion {
|
|
||||||
callee: "f".into(),
|
|
||||||
arg: "n".into(),
|
|
||||||
};
|
|
||||||
assert_eq!(e.code(), "non-structural-recursion");
|
|
||||||
}
|
|
||||||
@@ -201,17 +201,6 @@ fn walk(t: &Term, out: &mut NonEscapeSet) {
|
|||||||
walk(body, out);
|
walk(body, out);
|
||||||
}
|
}
|
||||||
Term::Assign { value, .. } => walk(value, out),
|
Term::Assign { value, .. } => walk(value, out),
|
||||||
Term::Loop { binders, body } => {
|
|
||||||
for b in binders {
|
|
||||||
walk(&b.init, out);
|
|
||||||
}
|
|
||||||
walk(body, out);
|
|
||||||
}
|
|
||||||
Term::Recur { args } => {
|
|
||||||
for a in args {
|
|
||||||
walk(a, out);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Term::Lit { .. } | Term::Var { .. } => {}
|
Term::Lit { .. } | Term::Var { .. } => {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -406,15 +395,6 @@ fn escapes(t: &Term, tainted: &BTreeSet<String>, in_tail: bool) -> bool {
|
|||||||
escapes(body, tainted, in_tail)
|
escapes(body, tainted, in_tail)
|
||||||
}
|
}
|
||||||
Term::Assign { value, .. } => escapes(value, tainted, false),
|
Term::Assign { value, .. } => escapes(value, tainted, false),
|
||||||
Term::Loop { binders, body } => {
|
|
||||||
for b in binders {
|
|
||||||
if escapes(&b.init, tainted, false) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
escapes(body, tainted, in_tail)
|
|
||||||
}
|
|
||||||
Term::Recur { args } => args.iter().any(|a| escapes(a, tainted, false)),
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -544,26 +524,6 @@ fn collect_free_vars(t: &Term, bound: &mut BTreeSet<String>, out: &mut BTreeSet<
|
|||||||
}
|
}
|
||||||
collect_free_vars(value, bound, out);
|
collect_free_vars(value, bound, out);
|
||||||
}
|
}
|
||||||
// Iter it.1: loop binder names bind inside the body and later
|
|
||||||
// binder inits, mirroring `Term::Mut`. Recur args are uses.
|
|
||||||
Term::Loop { binders, body } => {
|
|
||||||
let mut newly: Vec<String> = Vec::new();
|
|
||||||
for b in binders {
|
|
||||||
collect_free_vars(&b.init, bound, out);
|
|
||||||
if bound.insert(b.name.clone()) {
|
|
||||||
newly.push(b.name.clone());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
collect_free_vars(body, bound, out);
|
|
||||||
for n in newly {
|
|
||||||
bound.remove(&n);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Term::Recur { args } => {
|
|
||||||
for a in args {
|
|
||||||
collect_free_vars(a, bound, out);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -144,12 +144,6 @@ impl<'a> Emitter<'a> {
|
|||||||
let saved_mut_allocas = std::mem::take(&mut self.mut_var_allocas);
|
let saved_mut_allocas = std::mem::take(&mut self.mut_var_allocas);
|
||||||
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();
|
||||||
// Iter it.1: a `loop` inside a lambda body scopes its header /
|
|
||||||
// phis to the closure's own body, not the outer fn's. Save and
|
|
||||||
// reset `loop_frames` (the exact mut.3 analogue to
|
|
||||||
// `mut_var_allocas` above) so a `Term::Recur` inside the thunk
|
|
||||||
// cannot back-edge to an outer fn's loop header.
|
|
||||||
let saved_loop_frames = std::mem::take(&mut self.loop_frames);
|
|
||||||
// Iter 18d.4 fix: a lambda thunk is its own fn frame for
|
// Iter 18d.4 fix: a lambda thunk is its own fn frame for
|
||||||
// param-mode lookup. The outer fn's params are not in scope
|
// param-mode lookup. The outer fn's params are not in scope
|
||||||
// inside the thunk; the thunk's own params are pushed below
|
// inside the thunk; the thunk's own params are pushed below
|
||||||
@@ -264,7 +258,6 @@ impl<'a> Emitter<'a> {
|
|||||||
self.mut_var_allocas = saved_mut_allocas;
|
self.mut_var_allocas = saved_mut_allocas;
|
||||||
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;
|
||||||
self.loop_frames = saved_loop_frames;
|
|
||||||
|
|
||||||
// 3. Emit allocation + capture filling + closure-pair packing
|
// 3. Emit allocation + capture filling + closure-pair packing
|
||||||
// in the OUTER body. Captures use 8 bytes each; closure-pair
|
// in the OUTER body. Captures use 8 bytes each; closure-pair
|
||||||
@@ -512,27 +505,6 @@ impl<'a> Emitter<'a> {
|
|||||||
}
|
}
|
||||||
Self::collect_captures(value, bound, captures, captures_set, builtins, top_level);
|
Self::collect_captures(value, bound, captures, captures_set, builtins, top_level);
|
||||||
}
|
}
|
||||||
// Iter it.1: loop binder names bind inside the body and
|
|
||||||
// later binder inits — they are NOT captures, mirroring
|
|
||||||
// the `Term::Mut` arm above. Recur args are uses.
|
|
||||||
Term::Loop { binders, body } => {
|
|
||||||
let mut newly_bound: Vec<String> = Vec::new();
|
|
||||||
for b in binders {
|
|
||||||
Self::collect_captures(&b.init, bound, captures, captures_set, builtins, top_level);
|
|
||||||
if bound.insert(b.name.clone()) {
|
|
||||||
newly_bound.push(b.name.clone());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Self::collect_captures(body, bound, captures, captures_set, builtins, top_level);
|
|
||||||
for n in newly_bound {
|
|
||||||
bound.remove(&n);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Term::Recur { args } => {
|
|
||||||
for a in args {
|
|
||||||
Self::collect_captures(a, bound, captures, captures_set, builtins, top_level);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -735,28 +735,6 @@ struct Emitter<'a> {
|
|||||||
/// at fn-body emission. Used to splice `pending_entry_allocas`
|
/// at fn-body emission. Used to splice `pending_entry_allocas`
|
||||||
/// into the entry block once body lowering completes.
|
/// into the entry block once body lowering completes.
|
||||||
entry_block_end_marker: Option<usize>,
|
entry_block_end_marker: Option<usize>,
|
||||||
/// Iter it.1: stack of in-flight `Term::Loop` frames. Innermost
|
|
||||||
/// last. `Term::Recur` consults the innermost frame for the
|
|
||||||
/// header label and records its back-edge incoming values; the
|
|
||||||
/// `Term::Loop` arm patches each phi's incoming list once the
|
|
||||||
/// body (and all its recur sites) has been lowered. Saved /
|
|
||||||
/// reset / restored across the lambda boundary (a `loop` inside
|
|
||||||
/// a closure scopes its header to the closure's own body), the
|
|
||||||
/// exact mut.3 analogue to `mut_var_allocas`.
|
|
||||||
loop_frames: Vec<LoopFrame>,
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Iter it.1: one in-flight `Term::Loop` being lowered. `phis`
|
|
||||||
/// carries, per binder, `(phi_ssa, llvm_ty, placeholder)` — the
|
|
||||||
/// placeholder is a unique token emitted in place of the phi's
|
|
||||||
/// back-edge incoming list and string-replaced once `recur_edges`
|
|
||||||
/// is complete. `recur_edges` collects `(pred_block_label,
|
|
||||||
/// [arg_ssa,...])` one entry per `Term::Recur` reached in the body.
|
|
||||||
#[derive(Debug, Clone)]
|
|
||||||
struct LoopFrame {
|
|
||||||
header: String,
|
|
||||||
phis: Vec<(String, String, String)>,
|
|
||||||
recur_edges: Vec<(String, Vec<String>)>,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
@@ -877,7 +855,6 @@ impl<'a> Emitter<'a> {
|
|||||||
mut_var_allocas: BTreeMap::new(),
|
mut_var_allocas: BTreeMap::new(),
|
||||||
pending_entry_allocas: String::new(),
|
pending_entry_allocas: String::new(),
|
||||||
entry_block_end_marker: None,
|
entry_block_end_marker: None,
|
||||||
loop_frames: Vec::new(),
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1083,7 +1060,6 @@ impl<'a> Emitter<'a> {
|
|||||||
self.mut_var_allocas.clear();
|
self.mut_var_allocas.clear();
|
||||||
self.pending_entry_allocas.clear();
|
self.pending_entry_allocas.clear();
|
||||||
self.entry_block_end_marker = None;
|
self.entry_block_end_marker = None;
|
||||||
self.loop_frames.clear();
|
|
||||||
for (i, pname) in f.params.iter().enumerate() {
|
for (i, pname) in f.params.iter().enumerate() {
|
||||||
let mode = param_modes.get(i).copied().unwrap_or(ParamMode::Implicit);
|
let mode = param_modes.get(i).copied().unwrap_or(ParamMode::Implicit);
|
||||||
self.current_param_modes.insert(pname.clone(), mode);
|
self.current_param_modes.insert(pname.clone(), mode);
|
||||||
@@ -1861,141 +1837,6 @@ impl<'a> Emitter<'a> {
|
|||||||
));
|
));
|
||||||
Ok(("0".into(), "i8".into()))
|
Ok(("0".into(), "i8".into()))
|
||||||
}
|
}
|
||||||
// Iter it.1: a `Term::Loop` lowers to a header block with
|
|
||||||
// one `phi` per binder. Binder inits flow in from the
|
|
||||||
// predecessor block; every enclosed `Term::Recur` adds a
|
|
||||||
// back-edge incoming value. The phi's back-edge incoming
|
|
||||||
// list is emitted as a unique placeholder token and
|
|
||||||
// string-replaced once the body (and all its recur sites)
|
|
||||||
// has been lowered — phi instructions must syntactically
|
|
||||||
// precede the body, but the recur predecessors are only
|
|
||||||
// known after lowering it.
|
|
||||||
Term::Loop { binders, body } => {
|
|
||||||
// 1. Lower each binder init in the CURRENT block.
|
|
||||||
let pred = self.current_block.clone();
|
|
||||||
// Per binder: (name, ail_ty, init_ssa, llvm_ty).
|
|
||||||
let mut init_ssa: Vec<(String, Type, String, String)> = Vec::new();
|
|
||||||
for b in binders {
|
|
||||||
let lty = llvm_type(&b.ty)?;
|
|
||||||
let (v, vty) = self.lower_term(&b.init)?;
|
|
||||||
if vty != lty {
|
|
||||||
return Err(CodegenError::Internal(format!(
|
|
||||||
"Term::Loop binder `{}`: init LLVM type {vty} != declared {lty}",
|
|
||||||
b.name
|
|
||||||
)));
|
|
||||||
}
|
|
||||||
init_ssa.push((b.name.clone(), b.ty.clone(), v, lty));
|
|
||||||
}
|
|
||||||
let id = self.fresh_id();
|
|
||||||
let header = format!("loop.header.{id}");
|
|
||||||
self.body.push_str(&format!(" br label %{header}\n"));
|
|
||||||
self.start_block(&header);
|
|
||||||
|
|
||||||
// 2. One phi per binder. Each phi's back-edge incoming
|
|
||||||
// list is a placeholder, patched in step 4. Bind the
|
|
||||||
// phi SSA into `self.locals` so a `Term::Var` for the
|
|
||||||
// binder name inside the body resolves to it (a loop
|
|
||||||
// binder shadows like a let binding).
|
|
||||||
let mut phis: Vec<(String, String, String)> = Vec::new();
|
|
||||||
let mut saved_locals: Vec<(String, Option<(String, String, String, Type)>)> =
|
|
||||||
Vec::new();
|
|
||||||
for (idx, (name, ail_ty, iv, lty)) in init_ssa.iter().enumerate() {
|
|
||||||
let p = self.fresh_ssa();
|
|
||||||
let placeholder = format!("/*RECUR_EDGES.{id}.{idx}*/");
|
|
||||||
self.body.push_str(&format!(
|
|
||||||
" {p} = phi {lty} [ {iv}, %{pred} ]{placeholder}\n"
|
|
||||||
));
|
|
||||||
let prior = self
|
|
||||||
.locals
|
|
||||||
.iter()
|
|
||||||
.rposition(|(n, _, _, _)| n == name)
|
|
||||||
.map(|pos| self.locals.remove(pos));
|
|
||||||
self.locals.push((
|
|
||||||
name.clone(),
|
|
||||||
p.clone(),
|
|
||||||
lty.clone(),
|
|
||||||
ail_ty.clone(),
|
|
||||||
));
|
|
||||||
saved_locals.push((name.clone(), prior));
|
|
||||||
phis.push((p.clone(), lty.clone(), placeholder));
|
|
||||||
}
|
|
||||||
|
|
||||||
// 3. Lower the body inside this loop frame.
|
|
||||||
self.loop_frames.push(LoopFrame {
|
|
||||||
header: header.clone(),
|
|
||||||
phis: phis.clone(),
|
|
||||||
recur_edges: Vec::new(),
|
|
||||||
});
|
|
||||||
let body_result = self.lower_term(body);
|
|
||||||
let frame = self
|
|
||||||
.loop_frames
|
|
||||||
.pop()
|
|
||||||
.expect("loop frame pushed above must still be on the stack");
|
|
||||||
|
|
||||||
// Restore the shadowed locals unconditionally so an
|
|
||||||
// error during body lowering does not leak the binder
|
|
||||||
// bindings into the outer scope.
|
|
||||||
for (name, prior) in saved_locals.into_iter().rev() {
|
|
||||||
if let Some(pos) = self.locals.iter().rposition(|(n, _, _, _)| n == &name) {
|
|
||||||
self.locals.remove(pos);
|
|
||||||
}
|
|
||||||
if let Some(p) = prior {
|
|
||||||
self.locals.push(p);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
let (body_v, body_ty) = body_result?;
|
|
||||||
|
|
||||||
// 4. Patch each phi placeholder with the collected
|
|
||||||
// back-edge incomings (one `[ arg, %pred ]` per
|
|
||||||
// recur site). A loop whose only exit is `recur`
|
|
||||||
// has zero non-recur predecessors after the entry
|
|
||||||
// edge — that is fine; the phi still lists the
|
|
||||||
// entry edge plus the recur edges.
|
|
||||||
for (binder_idx, (_p, _lty, placeholder)) in frame.phis.iter().enumerate() {
|
|
||||||
let mut edges = String::new();
|
|
||||||
for (pred_lbl, args) in &frame.recur_edges {
|
|
||||||
let arg = args.get(binder_idx).ok_or_else(|| {
|
|
||||||
CodegenError::Internal(format!(
|
|
||||||
"recur edge from %{pred_lbl} missing arg #{binder_idx} \
|
|
||||||
— typecheck guarantees arity"
|
|
||||||
))
|
|
||||||
})?;
|
|
||||||
edges.push_str(&format!(", [ {arg}, %{pred_lbl} ]"));
|
|
||||||
}
|
|
||||||
self.body = self.body.replacen(placeholder.as_str(), &edges, 1);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 5. The loop's value is the body value on the
|
|
||||||
// non-recur exit path. If every path recurred, the
|
|
||||||
// body block is terminated and the value is unused.
|
|
||||||
Ok((body_v, body_ty))
|
|
||||||
}
|
|
||||||
// Iter it.1: a `Term::Recur` is a back-edge `br` to the
|
|
||||||
// innermost enclosing loop header. It records its argument
|
|
||||||
// SSA values + the current block label so the loop arm can
|
|
||||||
// patch the header phis. `block_terminated = true` is a
|
|
||||||
// NEW, parallel setter (it.1 additive constraint — it does
|
|
||||||
// NOT touch the seven existing tail-driven setters).
|
|
||||||
Term::Recur { args } => {
|
|
||||||
let mut arg_ssa: Vec<String> = Vec::with_capacity(args.len());
|
|
||||||
for a in args {
|
|
||||||
let (v, _ty) = self.lower_term(a)?;
|
|
||||||
arg_ssa.push(v);
|
|
||||||
}
|
|
||||||
let from = self.current_block.clone();
|
|
||||||
let frame = self.loop_frames.last_mut().ok_or_else(|| {
|
|
||||||
CodegenError::Internal(
|
|
||||||
"Term::Recur without an enclosing loop frame — \
|
|
||||||
typecheck (RecurOutsideLoop) guarantees this cannot happen"
|
|
||||||
.into(),
|
|
||||||
)
|
|
||||||
})?;
|
|
||||||
let header = frame.header.clone();
|
|
||||||
frame.recur_edges.push((from, arg_ssa));
|
|
||||||
self.body.push_str(&format!(" br label %{header}\n"));
|
|
||||||
self.block_terminated = true;
|
|
||||||
Ok(("0".into(), "i8".into()))
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -3252,11 +3093,6 @@ impl<'a> Emitter<'a> {
|
|||||||
// is exhaustive on Term so the arms must exist.
|
// is exhaustive on Term so the arms must exist.
|
||||||
Term::Mut { body, .. } => self.synth_with_extras(body, extras),
|
Term::Mut { body, .. } => self.synth_with_extras(body, extras),
|
||||||
Term::Assign { .. } => Ok(Type::unit()),
|
Term::Assign { .. } => Ok(Type::unit()),
|
||||||
// Iter it.1: a loop's static type is its body's static
|
|
||||||
// type (spec §"Data model"); `Term::Recur` does not fall
|
|
||||||
// through (its synth value is never consumed).
|
|
||||||
Term::Loop { binders: _, body } => self.synth_with_extras(body, extras),
|
|
||||||
Term::Recur { .. } => Ok(Type::unit()),
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -284,8 +284,6 @@ Parenthesised forms:
|
|||||||
(mut (var NAME TYPE INIT)* BODY-TERM+) ; local mutable-state block (Iter mut.1)
|
(mut (var NAME TYPE INIT)* BODY-TERM+) ; local mutable-state block (Iter mut.1)
|
||||||
(var NAME TYPE INIT) ; mut-var declaration; only inside (mut ...)
|
(var NAME TYPE INIT) ; mut-var declaration; only inside (mut ...)
|
||||||
(assign NAME VALUE-TERM) ; mut-var update; only inside (mut ...)
|
(assign NAME VALUE-TERM) ; mut-var update; only inside (mut ...)
|
||||||
(loop ((var NAME TYPE INIT)*) BODY-TERM+) ; named loop head (Iter it.1)
|
|
||||||
(recur TERM*) ; backward jump to nearest enclosing (loop ...)
|
|
||||||
```
|
```
|
||||||
|
|
||||||
Notes:
|
Notes:
|
||||||
@@ -324,23 +322,6 @@ Notes:
|
|||||||
pass (iter mut.2) rejects it with `mut-assign-out-of-scope`. The
|
pass (iter mut.2) rejects it with `mut-assign-out-of-scope`. The
|
||||||
expression's static type is Unit. See spec
|
expression's static type is Unit. See spec
|
||||||
`docs/specs/2026-05-15-mut-local.md`.
|
`docs/specs/2026-05-15-mut-local.md`.
|
||||||
- `loop` introduces one or more named, typed, initialised binders
|
|
||||||
(the same `(var NAME TYPE INIT)` shape `mut` uses) wrapped in an
|
|
||||||
explicit binder list `(...)`, followed by one or more body terms
|
|
||||||
right-folded into `Term::Seq` exactly as `mut` does. Each binder's
|
|
||||||
`INIT` is evaluated in scope of the outer environment plus the
|
|
||||||
already-declared binders; the body is in scope of all binders. The
|
|
||||||
loop's static type is the body's static type.
|
|
||||||
- `recur` re-enters the lexically nearest enclosing `loop`,
|
|
||||||
rebinding its binders positionally. It must appear in tail
|
|
||||||
position of that loop's body. Diagnostics: `recur-outside-loop`
|
|
||||||
(no enclosing loop), `recur-arity-mismatch` (arg count ≠ binder
|
|
||||||
count), `recur-type-mismatch` (arg type ≠ binder type),
|
|
||||||
`recur-not-in-tail-position` (recur reached outside the loop
|
|
||||||
body's tail context). `loop`/`recur` are additive as of iter it.1;
|
|
||||||
the structural-recursion restriction and the `Diverge` effect land
|
|
||||||
in it.2. See spec
|
|
||||||
`docs/specs/2026-05-15-iteration-discipline.md`.
|
|
||||||
|
|
||||||
## Patterns
|
## Patterns
|
||||||
|
|
||||||
|
|||||||
@@ -537,19 +537,6 @@ pub enum Term {
|
|||||||
name: String,
|
name: String,
|
||||||
value: Box<Term>,
|
value: Box<Term>,
|
||||||
},
|
},
|
||||||
/// Named loop head. The only repetition form besides structural
|
|
||||||
/// recursion. Iter it.1. `recur` re-enters the lexically
|
|
||||||
/// nearest enclosing `Loop`, rebinding `binders` positionally.
|
|
||||||
Loop {
|
|
||||||
binders: Vec<LoopBinder>,
|
|
||||||
body: Box<Term>,
|
|
||||||
},
|
|
||||||
/// Backward jump to the lexically nearest enclosing `Loop`.
|
|
||||||
/// Must be in tail position of that loop's body. Iter it.1.
|
|
||||||
Recur {
|
|
||||||
#[serde(default)]
|
|
||||||
args: Vec<Term>,
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// One arm of a [`Term::Match`].
|
/// One arm of a [`Term::Match`].
|
||||||
@@ -593,17 +580,6 @@ pub struct MutVar {
|
|||||||
pub init: Term,
|
pub init: Term,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// One named, typed, initialised binder of a [`Term::Loop`].
|
|
||||||
/// Mirrors [`MutVar`] (DD-2): no `PartialEq` — codegen maps the
|
|
||||||
/// binder name to an SSA value, never compares binders.
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
||||||
pub struct LoopBinder {
|
|
||||||
pub name: String,
|
|
||||||
#[serde(rename = "type")]
|
|
||||||
pub ty: Type,
|
|
||||||
pub init: Box<Term>,
|
|
||||||
}
|
|
||||||
|
|
||||||
/// A match pattern.
|
/// A match pattern.
|
||||||
///
|
///
|
||||||
/// The JSON discriminator is the `p` field. Patterns are linear: each
|
/// The JSON discriminator is the `p` field. Patterns are linear: each
|
||||||
@@ -970,41 +946,4 @@ mod tests {
|
|||||||
other => panic!("variant mismatch: {other:?}"),
|
other => panic!("variant mismatch: {other:?}"),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Iter it.1: round-trip a `Term::Loop` through JSON. Pins the
|
|
||||||
/// canonical-form shape and the `LoopBinder.ty` serde rename
|
|
||||||
/// (`"type"`); a future rename or a `skip_serializing_if` on the
|
|
||||||
/// binder fields would break the content-addressed identity this
|
|
||||||
/// pin protects.
|
|
||||||
#[test]
|
|
||||||
fn term_loop_round_trips_through_json() {
|
|
||||||
let t = Term::Loop {
|
|
||||||
binders: vec![LoopBinder {
|
|
||||||
name: "i".into(),
|
|
||||||
ty: Type::int(),
|
|
||||||
init: Box::new(Term::Lit {
|
|
||||||
lit: Literal::Int { value: 0 },
|
|
||||||
}),
|
|
||||||
}],
|
|
||||||
body: Box::new(Term::Recur {
|
|
||||||
args: vec![Term::Var { name: "i".into() }],
|
|
||||||
}),
|
|
||||||
};
|
|
||||||
let j = serde_json::to_string(&t).expect("serialise");
|
|
||||||
assert!(j.contains(r#""t":"loop""#));
|
|
||||||
assert!(j.contains(r#""type":"#)); // LoopBinder.ty serde rename
|
|
||||||
let back: Term = serde_json::from_str(&j).expect("deserialise");
|
|
||||||
assert_eq!(serde_json::to_string(&back).expect("re-serialise"), j);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Iter it.1: a zero-arg `Term::Recur` round-trips. `args` carries
|
|
||||||
/// `#[serde(default)]` so an absent `args` key still deserialises;
|
|
||||||
/// the empty vector serialises explicitly.
|
|
||||||
#[test]
|
|
||||||
fn term_recur_empty_args_round_trips() {
|
|
||||||
let t = Term::Recur { args: vec![] };
|
|
||||||
let j = serde_json::to_string(&t).expect("serialise");
|
|
||||||
let back: Term = serde_json::from_str(&j).expect("deserialise");
|
|
||||||
assert_eq!(serde_json::to_string(&back).expect("re-serialise"), j);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -363,18 +363,6 @@ fn collect_used_in_term(t: &Term, used: &mut BTreeSet<String>) {
|
|||||||
used.insert(name.clone());
|
used.insert(name.clone());
|
||||||
collect_used_in_term(value, used);
|
collect_used_in_term(value, used);
|
||||||
}
|
}
|
||||||
Term::Loop { binders, body } => {
|
|
||||||
for b in binders {
|
|
||||||
used.insert(b.name.clone());
|
|
||||||
collect_used_in_term(&b.init, used);
|
|
||||||
}
|
|
||||||
collect_used_in_term(body, used);
|
|
||||||
}
|
|
||||||
Term::Recur { args } => {
|
|
||||||
for a in args {
|
|
||||||
collect_used_in_term(a, used);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -602,32 +590,6 @@ impl Desugarer {
|
|||||||
name: name.clone(),
|
name: name.clone(),
|
||||||
value: Box::new(self.desugar_term(value, scope)),
|
value: Box::new(self.desugar_term(value, scope)),
|
||||||
},
|
},
|
||||||
Term::Loop { binders, body } => {
|
|
||||||
// Iter it.1: loop binders introduce source-level names
|
|
||||||
// like mut-vars; extend the scope per-binder with a
|
|
||||||
// `LetBound` sentinel so a generated fresh name cannot
|
|
||||||
// collide. Binder inits see earlier binders.
|
|
||||||
let mut inner = scope.clone();
|
|
||||||
let new_binders: Vec<LoopBinder> = binders
|
|
||||||
.iter()
|
|
||||||
.map(|b| {
|
|
||||||
let init = self.desugar_term(&b.init, &inner);
|
|
||||||
inner.insert(b.name.clone(), ScopeEntry::LetBound);
|
|
||||||
LoopBinder {
|
|
||||||
name: b.name.clone(),
|
|
||||||
ty: b.ty.clone(),
|
|
||||||
init: Box::new(init),
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.collect();
|
|
||||||
Term::Loop {
|
|
||||||
binders: new_binders,
|
|
||||||
body: Box::new(self.desugar_term(body, &inner)),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Term::Recur { args } => Term::Recur {
|
|
||||||
args: args.iter().map(|a| self.desugar_term(a, scope)).collect(),
|
|
||||||
},
|
|
||||||
Term::LetRec { name, ty, params, body, in_term } => {
|
Term::LetRec { name, ty, params, body, in_term } => {
|
||||||
// Iter 16b.1: lift to a synthetic top-level fn (no-capture).
|
// Iter 16b.1: lift to a synthetic top-level fn (no-capture).
|
||||||
// Iter 16b.2: extend the lift to the path-1 safe subset —
|
// Iter 16b.2: extend the lift to the path-1 safe subset —
|
||||||
@@ -1272,23 +1234,6 @@ pub fn free_vars_in_term(t: &Term, bound: &BTreeSet<String>, out: &mut BTreeSet<
|
|||||||
}
|
}
|
||||||
free_vars_in_term(value, bound, out);
|
free_vars_in_term(value, bound, out);
|
||||||
}
|
}
|
||||||
Term::Loop { binders, body } => {
|
|
||||||
// Iter it.1: a loop binder name binds inside the body.
|
|
||||||
// Each binder's `init` is evaluated in scope of the OUTER
|
|
||||||
// environment plus the already-declared binders; the body
|
|
||||||
// is in scope of all binders. Mirrors `Term::Mut`.
|
|
||||||
let mut b = bound.clone();
|
|
||||||
for binder in binders {
|
|
||||||
free_vars_in_term(&binder.init, &b, out);
|
|
||||||
b.insert(binder.name.clone());
|
|
||||||
}
|
|
||||||
free_vars_in_term(body, &b, out);
|
|
||||||
}
|
|
||||||
Term::Recur { args } => {
|
|
||||||
for a in args {
|
|
||||||
free_vars_in_term(a, bound, out);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1482,43 +1427,6 @@ pub fn subst_var(t: &Term, from: &str, to: &str) -> Term {
|
|||||||
name: name.clone(),
|
name: name.clone(),
|
||||||
value: Box::new(subst_var(value, from, to)),
|
value: Box::new(subst_var(value, from, to)),
|
||||||
},
|
},
|
||||||
Term::Loop { binders, body } => {
|
|
||||||
// Iter it.1: loop binders lexically shadow outer names,
|
|
||||||
// exactly like mut-vars (above). Once a binder named
|
|
||||||
// `from` is declared, later inits AND the body stop
|
|
||||||
// substituting.
|
|
||||||
let mut shadowed = false;
|
|
||||||
let new_binders: Vec<LoopBinder> = binders
|
|
||||||
.iter()
|
|
||||||
.map(|b| {
|
|
||||||
let init = if shadowed {
|
|
||||||
(*b.init).clone()
|
|
||||||
} else {
|
|
||||||
subst_var(&b.init, from, to)
|
|
||||||
};
|
|
||||||
if b.name == from {
|
|
||||||
shadowed = true;
|
|
||||||
}
|
|
||||||
LoopBinder {
|
|
||||||
name: b.name.clone(),
|
|
||||||
ty: b.ty.clone(),
|
|
||||||
init: Box::new(init),
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.collect();
|
|
||||||
let body_rw = if shadowed {
|
|
||||||
(**body).clone()
|
|
||||||
} else {
|
|
||||||
subst_var(body, from, to)
|
|
||||||
};
|
|
||||||
Term::Loop {
|
|
||||||
binders: new_binders,
|
|
||||||
body: Box::new(body_rw),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Term::Recur { args } => Term::Recur {
|
|
||||||
args: args.iter().map(|a| subst_var(a, from, to)).collect(),
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1660,23 +1568,6 @@ pub fn subst_call_with_extras(t: &Term, name: &str, lifted: &str, extras: &[Stri
|
|||||||
name: assign_name.clone(),
|
name: assign_name.clone(),
|
||||||
value: Box::new(subst_call_with_extras(value, name, lifted, extras)),
|
value: Box::new(subst_call_with_extras(value, name, lifted, extras)),
|
||||||
},
|
},
|
||||||
Term::Loop { binders, body } => Term::Loop {
|
|
||||||
binders: binders
|
|
||||||
.iter()
|
|
||||||
.map(|b| LoopBinder {
|
|
||||||
name: b.name.clone(),
|
|
||||||
ty: b.ty.clone(),
|
|
||||||
init: Box::new(subst_call_with_extras(&b.init, name, lifted, extras)),
|
|
||||||
})
|
|
||||||
.collect(),
|
|
||||||
body: Box::new(subst_call_with_extras(body, name, lifted, extras)),
|
|
||||||
},
|
|
||||||
Term::Recur { args } => Term::Recur {
|
|
||||||
args: args
|
|
||||||
.iter()
|
|
||||||
.map(|a| subst_call_with_extras(a, name, lifted, extras))
|
|
||||||
.collect(),
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1749,14 +1640,6 @@ pub fn find_non_callee_use(t: &Term, name: &str) -> Option<Term> {
|
|||||||
find_non_callee_use(value, name)
|
find_non_callee_use(value, name)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// Iter it.1: scan each binder's init plus the body. Loop
|
|
||||||
// binders never appear in callee position, so any matching
|
|
||||||
// `Term::Var` inside a loop is non-callee by construction.
|
|
||||||
Term::Loop { binders, body } => binders
|
|
||||||
.iter()
|
|
||||||
.find_map(|b| find_non_callee_use(&b.init, name))
|
|
||||||
.or_else(|| find_non_callee_use(body, name)),
|
|
||||||
Term::Recur { args } => args.iter().find_map(|a| find_non_callee_use(a, name)),
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1804,10 +1687,6 @@ mod tests {
|
|||||||
vars.iter().any(|v| any_nested_ctor(&v.init)) || any_nested_ctor(body)
|
vars.iter().any(|v| any_nested_ctor(&v.init)) || any_nested_ctor(body)
|
||||||
}
|
}
|
||||||
Term::Assign { value, .. } => any_nested_ctor(value),
|
Term::Assign { value, .. } => any_nested_ctor(value),
|
||||||
Term::Loop { binders, body } => {
|
|
||||||
binders.iter().any(|b| any_nested_ctor(&b.init)) || any_nested_ctor(body)
|
|
||||||
}
|
|
||||||
Term::Recur { args } => args.iter().any(any_nested_ctor),
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1838,10 +1717,6 @@ mod tests {
|
|||||||
vars.iter().any(|v| any_let_rec(&v.init)) || any_let_rec(body)
|
vars.iter().any(|v| any_let_rec(&v.init)) || any_let_rec(body)
|
||||||
}
|
}
|
||||||
Term::Assign { value, .. } => any_let_rec(value),
|
Term::Assign { value, .. } => any_let_rec(value),
|
||||||
Term::Loop { binders, body } => {
|
|
||||||
binders.iter().any(|b| any_let_rec(&b.init)) || any_let_rec(body)
|
|
||||||
}
|
|
||||||
Term::Recur { args } => args.iter().any(any_let_rec),
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2963,10 +2838,6 @@ mod tests {
|
|||||||
vars.iter().any(|v| any_lit_pattern(&v.init)) || any_lit_pattern(body)
|
vars.iter().any(|v| any_lit_pattern(&v.init)) || any_lit_pattern(body)
|
||||||
}
|
}
|
||||||
Term::Assign { value, .. } => any_lit_pattern(value),
|
Term::Assign { value, .. } => any_lit_pattern(value),
|
||||||
Term::Loop { binders, body } => {
|
|
||||||
binders.iter().any(|b| any_lit_pattern(&b.init)) || any_lit_pattern(body)
|
|
||||||
}
|
|
||||||
Term::Recur { args } => args.iter().any(any_lit_pattern),
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1259,22 +1259,6 @@ where
|
|||||||
walk_term_embedded_types(body, f)
|
walk_term_embedded_types(body, f)
|
||||||
}
|
}
|
||||||
Term::Assign { value, .. } => walk_term_embedded_types(value, f),
|
Term::Assign { value, .. } => walk_term_embedded_types(value, f),
|
||||||
// Iter it.1: each `LoopBinder.ty` is an embedded type — walk
|
|
||||||
// it, then recurse into each binder's `init` and the body.
|
|
||||||
// `Term::Recur` carries no embedded type.
|
|
||||||
Term::Loop { binders, body } => {
|
|
||||||
for b in binders {
|
|
||||||
walk_type(&b.ty, f)?;
|
|
||||||
walk_term_embedded_types(&b.init, f)?;
|
|
||||||
}
|
|
||||||
walk_term_embedded_types(body, f)
|
|
||||||
}
|
|
||||||
Term::Recur { args } => {
|
|
||||||
for a in args {
|
|
||||||
walk_term_embedded_types(a, f)?;
|
|
||||||
}
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1408,18 +1392,6 @@ where
|
|||||||
walk_term(body, f)
|
walk_term(body, f)
|
||||||
}
|
}
|
||||||
Term::Assign { value, .. } => walk_term(value, f),
|
Term::Assign { value, .. } => walk_term(value, f),
|
||||||
Term::Loop { binders, body } => {
|
|
||||||
for b in binders {
|
|
||||||
walk_term(&b.init, f)?;
|
|
||||||
}
|
|
||||||
walk_term(body, f)
|
|
||||||
}
|
|
||||||
Term::Recur { args } => {
|
|
||||||
for a in args {
|
|
||||||
walk_term(a, f)?;
|
|
||||||
}
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
//! 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.
|
||||||
//!
|
//!
|
||||||
//! Twenty carve-outs post iter it.2 (2026-05-15):
|
//! Twelve carve-outs post iter mut.2 (2026-05-15):
|
||||||
//! - §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 /
|
//! - Iter mut.2: 5 negative typecheck fixtures for the new mut /
|
||||||
@@ -13,14 +13,6 @@
|
|||||||
//! fixture convention. The positive nested-shadow case is a
|
//! fixture convention. The positive nested-shadow case is a
|
||||||
//! .ail.json carve-out for symmetry with its negative siblings
|
//! .ail.json carve-out for symmetry with its negative siblings
|
||||||
//! (the driver tests all five from one helper).
|
//! (the driver tests all five from one helper).
|
||||||
//! - Iter mut.4-tidy: 1 lambda-capture-of-mut-var negative fixture.
|
|
||||||
//! - Iter it.1: 4 recur negative typecheck fixtures.
|
|
||||||
//! - Iter it.2: 3 negative fixtures — self non-structural recursion,
|
|
||||||
//! mutual cross-family recursion (both fire `non-structural-
|
|
||||||
//! recursion`), and a loop-bearing fn missing `!Diverge` (fires
|
|
||||||
//! the existing `undeclared-effect`, no new variant per DD-4). The
|
|
||||||
//! diagnostic code is the load-bearing assertion, not the surface
|
|
||||||
//! form.
|
|
||||||
//! - §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
|
||||||
@@ -45,15 +37,6 @@ const EXPECTED: &[&str] = &[
|
|||||||
"test_mut_var_unsupported_type.ail.json",
|
"test_mut_var_unsupported_type.ail.json",
|
||||||
// Iter mut.4-tidy — lambda-capture-of-mut-var rejection
|
// Iter mut.4-tidy — lambda-capture-of-mut-var rejection
|
||||||
"test_mut_var_captured_by_lambda.ail.json",
|
"test_mut_var_captured_by_lambda.ail.json",
|
||||||
// Iter it.1 — recur negative typecheck fixtures
|
|
||||||
"test_recur_outside_loop.ail.json",
|
|
||||||
"test_recur_arity_mismatch.ail.json",
|
|
||||||
"test_recur_type_mismatch.ail.json",
|
|
||||||
"test_recur_not_in_tail_position.ail.json",
|
|
||||||
// Iter it.2 — non-structural-recursion negative fixtures
|
|
||||||
"test_non_structural_recursion.ail.json",
|
|
||||||
"test_mutual_cross_family.ail.json",
|
|
||||||
"test_loop_missing_diverge.ail.json",
|
|
||||||
];
|
];
|
||||||
|
|
||||||
fn examples_dir() -> std::path::PathBuf {
|
fn examples_dir() -> std::path::PathBuf {
|
||||||
|
|||||||
@@ -151,17 +151,6 @@ fn design_md_anchors_every_term_variant() {
|
|||||||
value: Box::new(Term::Lit { lit: Literal::Unit }),
|
value: Box::new(Term::Lit { lit: Literal::Unit }),
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
(
|
|
||||||
r#""t": "loop""#,
|
|
||||||
Term::Loop {
|
|
||||||
binders: Vec::new(),
|
|
||||||
body: Box::new(Term::Lit { lit: Literal::Unit }),
|
|
||||||
},
|
|
||||||
),
|
|
||||||
(
|
|
||||||
r#""t": "recur""#,
|
|
||||||
Term::Recur { args: Vec::new() },
|
|
||||||
),
|
|
||||||
];
|
];
|
||||||
|
|
||||||
for (anchor, term) in exemplars {
|
for (anchor, term) in exemplars {
|
||||||
@@ -183,8 +172,6 @@ fn design_md_anchors_every_term_variant() {
|
|||||||
Term::ReuseAs { .. } => "reuse-as",
|
Term::ReuseAs { .. } => "reuse-as",
|
||||||
Term::Mut { .. } => "mut",
|
Term::Mut { .. } => "mut",
|
||||||
Term::Assign { .. } => "assign",
|
Term::Assign { .. } => "assign",
|
||||||
Term::Loop { .. } => "loop",
|
|
||||||
Term::Recur { .. } => "recur",
|
|
||||||
};
|
};
|
||||||
assert!(
|
assert!(
|
||||||
data_model_section().contains(anchor),
|
data_model_section().contains(anchor),
|
||||||
@@ -443,3 +430,20 @@ fn data_model_section_is_bounded() {
|
|||||||
section.len()
|
section.len()
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[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"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|||||||
@@ -49,8 +49,6 @@ enum VariantTag {
|
|||||||
TermReuseAs,
|
TermReuseAs,
|
||||||
TermMut,
|
TermMut,
|
||||||
TermAssign,
|
TermAssign,
|
||||||
TermLoop,
|
|
||||||
TermRecur,
|
|
||||||
// Pattern
|
// Pattern
|
||||||
PatternWild,
|
PatternWild,
|
||||||
PatternVar,
|
PatternVar,
|
||||||
@@ -98,8 +96,6 @@ const EXPECTED_VARIANTS: &[VariantTag] = &[
|
|||||||
VariantTag::TermReuseAs,
|
VariantTag::TermReuseAs,
|
||||||
VariantTag::TermMut,
|
VariantTag::TermMut,
|
||||||
VariantTag::TermAssign,
|
VariantTag::TermAssign,
|
||||||
VariantTag::TermLoop,
|
|
||||||
VariantTag::TermRecur,
|
|
||||||
VariantTag::PatternWild,
|
VariantTag::PatternWild,
|
||||||
VariantTag::PatternVar,
|
VariantTag::PatternVar,
|
||||||
VariantTag::PatternLit,
|
VariantTag::PatternLit,
|
||||||
@@ -248,20 +244,6 @@ fn visit_term(t: &Term, observed: &mut HashSet<VariantTag>) {
|
|||||||
observed.insert(VariantTag::TermAssign);
|
observed.insert(VariantTag::TermAssign);
|
||||||
visit_term(value, observed);
|
visit_term(value, observed);
|
||||||
}
|
}
|
||||||
Term::Loop { binders, body } => {
|
|
||||||
observed.insert(VariantTag::TermLoop);
|
|
||||||
for b in binders {
|
|
||||||
visit_type(&b.ty, observed);
|
|
||||||
visit_term(&b.init, observed);
|
|
||||||
}
|
|
||||||
visit_term(body, observed);
|
|
||||||
}
|
|
||||||
Term::Recur { args } => {
|
|
||||||
observed.insert(VariantTag::TermRecur);
|
|
||||||
for a in args {
|
|
||||||
visit_term(a, observed);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -128,17 +128,6 @@ fn spec_mentions_every_term_variant() {
|
|||||||
value: Box::new(Term::Lit { lit: Literal::Unit }),
|
value: Box::new(Term::Lit { lit: Literal::Unit }),
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
(
|
|
||||||
"(loop",
|
|
||||||
Term::Loop {
|
|
||||||
binders: Vec::new(),
|
|
||||||
body: Box::new(Term::Lit { lit: Literal::Unit }),
|
|
||||||
},
|
|
||||||
),
|
|
||||||
(
|
|
||||||
"(recur",
|
|
||||||
Term::Recur { args: Vec::new() },
|
|
||||||
),
|
|
||||||
];
|
];
|
||||||
|
|
||||||
for (anchor, term) in exemplars {
|
for (anchor, term) in exemplars {
|
||||||
@@ -162,8 +151,6 @@ fn spec_mentions_every_term_variant() {
|
|||||||
Term::ReuseAs { .. } => "reuse-as",
|
Term::ReuseAs { .. } => "reuse-as",
|
||||||
Term::Mut { .. } => "mut",
|
Term::Mut { .. } => "mut",
|
||||||
Term::Assign { .. } => "assign",
|
Term::Assign { .. } => "assign",
|
||||||
Term::Loop { .. } => "loop",
|
|
||||||
Term::Recur { .. } => "recur",
|
|
||||||
};
|
};
|
||||||
assert!(
|
assert!(
|
||||||
FORM_A_SPEC.contains(anchor),
|
FORM_A_SPEC.contains(anchor),
|
||||||
|
|||||||
@@ -935,37 +935,6 @@ fn write_term_prec(out: &mut String, t: &Term, level: usize, parent_prec: u8, ow
|
|||||||
out.push_str(" := ");
|
out.push_str(" := ");
|
||||||
write_term(out, value, level, owning_module);
|
write_term(out, value, level, owning_module);
|
||||||
}
|
}
|
||||||
Term::Loop { binders, body } => {
|
|
||||||
// Iter it.1: prose-side minimal-correctness rendering for
|
|
||||||
// the `(loop ...)` head, mirroring the `mut` block shape
|
|
||||||
// above. The prose surface for loops is not yet fully
|
|
||||||
// designed; a follow-on prose iter refines it once the
|
|
||||||
// LLM-author signal arrives.
|
|
||||||
out.push_str("loop {\n");
|
|
||||||
for b in binders {
|
|
||||||
indent(out, level + 1);
|
|
||||||
out.push_str("var ");
|
|
||||||
out.push_str(&b.name);
|
|
||||||
out.push_str(" = ");
|
|
||||||
write_term(out, &b.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::Recur { args } => {
|
|
||||||
out.push_str("recur(");
|
|
||||||
for (i, a) in args.iter().enumerate() {
|
|
||||||
if i > 0 {
|
|
||||||
out.push_str(", ");
|
|
||||||
}
|
|
||||||
write_term(out, a, level, owning_module);
|
|
||||||
}
|
|
||||||
out.push(')');
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1164,26 +1133,6 @@ fn count_free_var(name: &str, t: &Term) -> usize {
|
|||||||
let n_use = if assign_name == name { 1 } else { 0 };
|
let n_use = if assign_name == name { 1 } else { 0 };
|
||||||
n_use + count_free_var(name, value)
|
n_use + count_free_var(name, value)
|
||||||
}
|
}
|
||||||
// Iter it.1: a loop binder named `name` shadows the outer
|
|
||||||
// binding for both later binder inits and the body, exactly
|
|
||||||
// like `Term::Mut`.
|
|
||||||
Term::Loop { binders, body } => {
|
|
||||||
let mut total = 0usize;
|
|
||||||
let mut shadowed = false;
|
|
||||||
for b in binders {
|
|
||||||
if !shadowed {
|
|
||||||
total += count_free_var(name, &b.init);
|
|
||||||
}
|
|
||||||
if b.name == name {
|
|
||||||
shadowed = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if !shadowed {
|
|
||||||
total += count_free_var(name, body);
|
|
||||||
}
|
|
||||||
total
|
|
||||||
}
|
|
||||||
Term::Recur { args } => args.iter().map(|a| count_free_var(name, a)).sum(),
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1336,39 +1285,6 @@ fn subst_var_with_term(t: &Term, name: &str, replacement: &Term) -> Term {
|
|||||||
name: assign_name.clone(),
|
name: assign_name.clone(),
|
||||||
value: Box::new(subst_var_with_term(value, name, replacement)),
|
value: Box::new(subst_var_with_term(value, name, replacement)),
|
||||||
},
|
},
|
||||||
Term::Loop { binders, body } => {
|
|
||||||
let mut shadowed = false;
|
|
||||||
let new_binders: Vec<ailang_core::ast::LoopBinder> = binders
|
|
||||||
.iter()
|
|
||||||
.map(|b| {
|
|
||||||
let init = if shadowed {
|
|
||||||
(*b.init).clone()
|
|
||||||
} else {
|
|
||||||
subst_var_with_term(&b.init, name, replacement)
|
|
||||||
};
|
|
||||||
if b.name == name {
|
|
||||||
shadowed = true;
|
|
||||||
}
|
|
||||||
ailang_core::ast::LoopBinder {
|
|
||||||
name: b.name.clone(),
|
|
||||||
ty: b.ty.clone(),
|
|
||||||
init: Box::new(init),
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.collect();
|
|
||||||
let body_rw = if shadowed {
|
|
||||||
(**body).clone()
|
|
||||||
} else {
|
|
||||||
subst_var_with_term(body, name, replacement)
|
|
||||||
};
|
|
||||||
Term::Loop {
|
|
||||||
binders: new_binders,
|
|
||||||
body: Box::new(body_rw),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Term::Recur { args } => Term::Recur {
|
|
||||||
args: args.iter().map(|a| subst_var_with_term(a, name, replacement)).collect(),
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2185,43 +2101,6 @@ mod tests {
|
|||||||
assert_eq!(render_term(&t), "tail not(x)");
|
assert_eq!(render_term(&t), "tail not(x)");
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Iter it.1: prose-projection lockstep for `Term::Loop` /
|
|
||||||
/// `Term::Recur`. The property protected: the prose renderer
|
|
||||||
/// projects a loop's binders + body and a recur's args without
|
|
||||||
/// dropping or reordering them (free-var / subst arms are
|
|
||||||
/// exercised by the existing prose suite). Prose is a one-way
|
|
||||||
/// projection (no Form-B parser exists — see CLAUDE.md), so an
|
|
||||||
/// AST-equality round-trip is not expressible; this render
|
|
||||||
/// assertion is the feasible lockstep, using the same
|
|
||||||
/// `render_term` harness the neighbouring render tests use.
|
|
||||||
#[test]
|
|
||||||
fn loop_and_recur_project_to_prose() {
|
|
||||||
let t = Term::Loop {
|
|
||||||
binders: vec![ailang_core::ast::LoopBinder {
|
|
||||||
name: "i".into(),
|
|
||||||
ty: ailang_core::ast::Type::Con {
|
|
||||||
name: "Int".into(),
|
|
||||||
args: vec![],
|
|
||||||
},
|
|
||||||
init: Box::new(Term::Lit {
|
|
||||||
lit: ailang_core::ast::Literal::Int { value: 0 },
|
|
||||||
}),
|
|
||||||
}],
|
|
||||||
body: Box::new(Term::Recur {
|
|
||||||
args: vec![ivar("i")],
|
|
||||||
}),
|
|
||||||
};
|
|
||||||
let rendered = render_term(&t);
|
|
||||||
assert!(
|
|
||||||
rendered.contains("loop {") && rendered.contains("var i = 0"),
|
|
||||||
"loop head not projected, got:\n{rendered}"
|
|
||||||
);
|
|
||||||
assert!(
|
|
||||||
rendered.contains("recur(i)"),
|
|
||||||
"recur not projected, got:\n{rendered}"
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// ---- Polish 4: long doc-string wrap ----
|
// ---- Polish 4: long doc-string wrap ----
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
|||||||
@@ -40,7 +40,7 @@
|
|||||||
//! | 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 | loop-term | recur-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
|
||||||
@@ -68,10 +68,8 @@
|
|||||||
//! 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
|
//! mut-term ::= "(" "mut" var-decl* term+ ")" ; Iter mut.1
|
||||||
//! var-decl ::= "(" "var" ident type term ")" ; legal only inside mut-term / loop-term
|
//! var-decl ::= "(" "var" ident type term ")" ; legal only inside mut-term
|
||||||
//! assign-term ::= "(" "assign" ident term ")" ; Iter mut.1; legal only inside mut-term
|
//! assign-term ::= "(" "assign" ident term ")" ; Iter mut.1; legal only inside mut-term
|
||||||
//! loop-term ::= "(" "loop" "(" var-decl* ")" term+ ")" ; Iter it.1
|
|
||||||
//! recur-term ::= "(" "recur" term* ")" ; Iter it.1; tail-position-only in loop
|
|
||||||
//!
|
//!
|
||||||
//! pattern ::= pat-var | pat-ctor | pat-lit | pat-wild
|
//! pattern ::= pat-var | pat-ctor | pat-lit | pat-wild
|
||||||
//! pat-var ::= ident
|
//! pat-var ::= ident
|
||||||
@@ -1214,8 +1212,6 @@ impl<'a> Parser<'a> {
|
|||||||
"reuse-as" => self.parse_reuse_as(),
|
"reuse-as" => self.parse_reuse_as(),
|
||||||
"mut" => self.parse_mut(),
|
"mut" => self.parse_mut(),
|
||||||
"assign" => self.parse_assign(),
|
"assign" => self.parse_assign(),
|
||||||
"loop" => self.parse_loop(),
|
|
||||||
"recur" => self.parse_recur(),
|
|
||||||
other => {
|
other => {
|
||||||
let pos = self.peek().map(|t| t.span.start).unwrap_or(0);
|
let pos = self.peek().map(|t| t.span.start).unwrap_or(0);
|
||||||
Err(ParseError::Production {
|
Err(ParseError::Production {
|
||||||
@@ -1224,7 +1220,7 @@ impl<'a> Parser<'a> {
|
|||||||
"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`, `mut`, \
|
||||||
`assign`, `loop`, `recur`, `lit-unit`"
|
`assign`, `lit-unit`"
|
||||||
),
|
),
|
||||||
pos,
|
pos,
|
||||||
})
|
})
|
||||||
@@ -1646,80 +1642,6 @@ impl<'a> Parser<'a> {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Iter it.1: `(loop ((var NAME TYPE INIT)*) BODY-TERM+)` — named
|
|
||||||
/// loop head. The binder list is an explicit parenthesised group
|
|
||||||
/// holding zero or more `(var NAME TYPE INIT)` entries (same entry
|
|
||||||
/// shape `parse_mut` reads); the trailing ≥ 1 body terms are
|
|
||||||
/// right-folded into `Term::Seq` exactly as `parse_mut` does. The
|
|
||||||
/// recur arity / type / tail-position rules are enforced at
|
|
||||||
/// typecheck (it.1 Task 5); the parser accepts the shape.
|
|
||||||
fn parse_loop(&mut self) -> Result<Term, ParseError> {
|
|
||||||
let head_pos = self.peek().map(|t| t.span.start).unwrap_or(0);
|
|
||||||
self.expect_lparen("loop-term")?;
|
|
||||||
self.expect_keyword("loop")?;
|
|
||||||
|
|
||||||
// The binder list is an explicit parenthesised group of
|
|
||||||
// (var NAME TYPE INIT) entries.
|
|
||||||
self.expect_lparen("loop-binder-list")?;
|
|
||||||
let mut binders: Vec<ailang_core::ast::LoopBinder> = Vec::new();
|
|
||||||
while matches!(self.peek_head_ident(), Some("var")) {
|
|
||||||
self.expect_lparen("loop-binder")?;
|
|
||||||
self.expect_keyword("var")?;
|
|
||||||
let name = self.expect_ident("loop-binder-name")?;
|
|
||||||
let ty = self.parse_type()?;
|
|
||||||
let init = self.parse_term()?;
|
|
||||||
self.expect_rparen("loop-binder")?;
|
|
||||||
binders.push(ailang_core::ast::LoopBinder {
|
|
||||||
name,
|
|
||||||
ty,
|
|
||||||
init: Box::new(init),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
self.expect_rparen("loop-binder-list")?;
|
|
||||||
|
|
||||||
// Then read ≥ 1 trailing body terms, right-folded into Seq.
|
|
||||||
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: "loop-term",
|
|
||||||
message: "(loop ...) requires at least one body expression after the binder list"
|
|
||||||
.into(),
|
|
||||||
pos: head_pos,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
self.expect_rparen("loop-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::Loop {
|
|
||||||
binders,
|
|
||||||
body: Box::new(body),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Iter it.1: `(recur TERM*)` — backward jump to the lexically
|
|
||||||
/// nearest enclosing `(loop ...)`. The parser accepts any arg
|
|
||||||
/// count; the enclosing-loop / arity / type / tail-position rules
|
|
||||||
/// are enforced at typecheck (it.1 Task 5).
|
|
||||||
fn parse_recur(&mut self) -> Result<Term, ParseError> {
|
|
||||||
self.expect_lparen("recur-term")?;
|
|
||||||
self.expect_keyword("recur")?;
|
|
||||||
let mut args: Vec<Term> = Vec::new();
|
|
||||||
while !matches!(self.peek(), Some(Token { tok: Tok::RParen, .. })) {
|
|
||||||
args.push(self.parse_term()?);
|
|
||||||
}
|
|
||||||
self.expect_rparen("recur-term")?;
|
|
||||||
Ok(Term::Recur { args })
|
|
||||||
}
|
|
||||||
|
|
||||||
// ---- patterns -------------------------------------------------------
|
// ---- patterns -------------------------------------------------------
|
||||||
|
|
||||||
fn parse_pattern(&mut self) -> Result<Pattern, ParseError> {
|
fn parse_pattern(&mut self) -> Result<Pattern, ParseError> {
|
||||||
@@ -2661,29 +2583,4 @@ mod tests {
|
|||||||
"diagnostic should mention missing body, got: {msg}"
|
"diagnostic should mention missing body, got: {msg}"
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Iter it.1: `(loop ((var i Int 0)) (recur i))` parses to a
|
|
||||||
/// `Term::Loop` with one binder and a `Term::Recur` body. The
|
|
||||||
/// binder list is explicitly parenthesised (unlike `mut`).
|
|
||||||
#[test]
|
|
||||||
fn parses_loop_with_one_binder_and_recur_body() {
|
|
||||||
let src = "(loop ((var i (con Int) 0)) (recur i))";
|
|
||||||
let t = parse_term(src).expect("loop parses");
|
|
||||||
match t {
|
|
||||||
Term::Loop { binders, body } => {
|
|
||||||
assert_eq!(binders.len(), 1);
|
|
||||||
assert_eq!(binders[0].name, "i");
|
|
||||||
assert!(matches!(*body, Term::Recur { .. }));
|
|
||||||
}
|
|
||||||
other => panic!("expected Term::Loop, got {other:?}"),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Iter it.1: `(recur)` with no args parses to an empty-args
|
|
||||||
/// `Term::Recur` (arity/scope validity is a typecheck concern).
|
|
||||||
#[test]
|
|
||||||
fn parses_recur_zero_args() {
|
|
||||||
let t = parse_term("(recur)").expect("recur parses");
|
|
||||||
assert!(matches!(t, Term::Recur { args } if args.is_empty()));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -594,54 +594,6 @@ fn write_term(out: &mut String, t: &Term, level: usize) {
|
|||||||
write_term(out, value, level);
|
write_term(out, value, level);
|
||||||
out.push(')');
|
out.push(')');
|
||||||
}
|
}
|
||||||
Term::Loop { binders, body } => {
|
|
||||||
// Iter it.1: print as
|
|
||||||
// `(loop ((var NAME TYPE INIT)*) STMT* FINAL_EXPR)`
|
|
||||||
// The binder list is explicitly parenthesised (unlike
|
|
||||||
// `mut`); the body's `Term::Seq` right-spine is walked the
|
|
||||||
// same way the `Term::Mut` arm above walks it.
|
|
||||||
out.push_str("(loop (");
|
|
||||||
for (k, b) in binders.iter().enumerate() {
|
|
||||||
if k > 0 {
|
|
||||||
out.push(' ');
|
|
||||||
}
|
|
||||||
out.push_str("(var ");
|
|
||||||
out.push_str(&b.name);
|
|
||||||
out.push(' ');
|
|
||||||
write_type(out, &b.ty);
|
|
||||||
out.push(' ');
|
|
||||||
write_term(out, &b.init, level);
|
|
||||||
out.push(')');
|
|
||||||
}
|
|
||||||
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::Recur { args } => {
|
|
||||||
// Iter it.1: print as `(recur ARG*)`. Legal only in tail
|
|
||||||
// position of an enclosing `Term::Loop` body; the
|
|
||||||
// typechecker (it.1 Task 5) enforces that rule.
|
|
||||||
out.push_str("(recur");
|
|
||||||
for a in args {
|
|
||||||
out.push(' ');
|
|
||||||
write_term(out, a, level);
|
|
||||||
}
|
|
||||||
out.push(')');
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+15
-49
@@ -102,12 +102,17 @@ A proposed feature ships only if all three hold:
|
|||||||
the bug class is structurally impossible rather than merely
|
the bug class is structurally impossible rather than merely
|
||||||
discouraged. A documentation note is not a reshape; the
|
discouraged. A documentation note is not a reshape; the
|
||||||
discriminator is whether the wrong code fails to typecheck, not
|
discriminator is whether the wrong code fails to typecheck, not
|
||||||
whether a guideline advises against it. The canonical worked
|
whether a guideline advises against it. Worked example: a bare
|
||||||
example is the iteration story: a bare `while` over mutable state
|
`while` over mutable state would pass clauses 1 and 2 yet fail
|
||||||
passes 1 and 2 yet fails 3 (it reinstates iterated-mutable-state
|
clause 3 (it reinstates iterated-mutable-state reasoning the pure
|
||||||
reasoning); structural recursion over an acyclic ADT and a named
|
core exists to remove); a hypothetical "all repetition is either
|
||||||
loop/recur point pass all three (the bug class is closed by
|
structurally-decreasing recursion over an acyclic ADT or an
|
||||||
construction, not by advice).
|
explicit named loop" iteration story would, *if it could be built
|
||||||
|
without a documented-unenforced precondition*, pass all three —
|
||||||
|
and the fact that the 2026-05 attempt could not (it forced a
|
||||||
|
silent-divergence precondition; see
|
||||||
|
`docs/specs/2026-05-16-iteration-discipline-revert.md`) is itself
|
||||||
|
the clause-3 mechanism working as intended.
|
||||||
|
|
||||||
This is the positive complement to the CLAUDE.md rule that
|
This is the positive complement to the CLAUDE.md rule that
|
||||||
implementation effort is not a rationale: cost is not a reason *for* a
|
implementation effort is not a rationale: cost is not a reason *for* a
|
||||||
@@ -164,16 +169,8 @@ Advantages:
|
|||||||
|
|
||||||
The default is total, pure functions. Effects are declared as a set in the
|
The default is total, pure functions. Effects are declared as a set in the
|
||||||
function type: `(Int) -> Int ![IO]`. The effect set is row-polymorphic
|
function type: `(Int) -> Int ![IO]`. The effect set is row-polymorphic
|
||||||
(`![IO | r]`). Two effects are wired up: `IO` (observable side effects,
|
(`![IO | r]`). In the MVP only the effects `IO` and `Diverge` (for infinite
|
||||||
raised by `do`-operations), and `Diverge` (non-termination). As of iter
|
loops) are wired up.
|
||||||
it.2 (2026-05-15) `Diverge` is no longer nominal: it is the effect carried
|
|
||||||
by any function whose body contains a `loop` (or that calls a
|
|
||||||
`Diverge`-declaring function), surfaced through the existing
|
|
||||||
declared-vs-raised reconciliation (an undeclared `Diverge` is the existing
|
|
||||||
`UndeclaredEffect`, no new diagnostic). Structural recursion is pure and
|
|
||||||
total and carries no effect — the author who wants a `!Diverge`-free
|
|
||||||
signature is structurally pulled toward structural recursion and pays
|
|
||||||
`!Diverge` only when genuinely writing an unbounded `loop`.
|
|
||||||
|
|
||||||
This is the most important LLM property: when I read a function, I can trust
|
This is the most important LLM property: when I read a function, I can trust
|
||||||
its signature without reading the body.
|
its signature without reading the body.
|
||||||
@@ -2418,22 +2415,6 @@ are real surface forms.
|
|||||||
{ "t": "assign",
|
{ "t": "assign",
|
||||||
"name": "<id>",
|
"name": "<id>",
|
||||||
"value": Term }
|
"value": Term }
|
||||||
|
|
||||||
// Iter it.1: named loop head. `binders` declares one or more
|
|
||||||
// lexically-scoped, typed, initialised binders (initialised in
|
|
||||||
// order); `body` is a single Term in scope of all binders. `recur`
|
|
||||||
// re-enters the lexically nearest enclosing loop, rebinding the
|
|
||||||
// binders positionally; it must be in tail position of that loop's
|
|
||||||
// body. The loop's static type is `body`'s static type.
|
|
||||||
{ "t": "loop",
|
|
||||||
"binders": [ { "name": "<id>", "type": Type, "init": Term }, ... ],
|
|
||||||
"body": Term }
|
|
||||||
|
|
||||||
// Iter it.1: backward jump to the lexically nearest enclosing loop;
|
|
||||||
// tail-position-only; binds that loop's binders. The `args` array is
|
|
||||||
// always present in canonical JSON (even when empty).
|
|
||||||
{ "t": "recur",
|
|
||||||
"args": [ Term, ... ] }
|
|
||||||
```
|
```
|
||||||
|
|
||||||
In the MVP, `do` is only a direct call to a built-in effect op (no
|
In the MVP, `do` is only a direct call to a built-in effect op (no
|
||||||
@@ -2449,23 +2430,6 @@ mut-var into a lambda body is rejected at typecheck via
|
|||||||
`CheckError::MutVarCapturedByLambda` (iter mut.4-tidy). See
|
`CheckError::MutVarCapturedByLambda` (iter mut.4-tidy). See
|
||||||
`docs/specs/2026-05-15-mut-local.md`.
|
`docs/specs/2026-05-15-mut-local.md`.
|
||||||
|
|
||||||
`loop`/`recur` are additive as of iter it.1 (2026-05-15): they
|
|
||||||
parse, print, prose-project, round-trip, typecheck (binder typing,
|
|
||||||
recur arity/type unification, recur tail-position) and codegen
|
|
||||||
(loop-header block with one phi per binder, `recur` as a back-edge
|
|
||||||
`br`) without removing or modifying the existing `tail-app`/`tail-do`
|
|
||||||
paths. As of iter it.2 (2026-05-15) the structural-recursion
|
|
||||||
guardedness restriction and the `Diverge` effect are in effect:
|
|
||||||
a non-structural recursion-by-call is the compile error
|
|
||||||
`NonStructuralRecursion` (directing the author to `(loop …)` /
|
|
||||||
`recur`), and any fn whose body contains a `Term::Loop` (or calls a
|
|
||||||
`Diverge`-declaring callee) must declare `!Diverge` in its effect
|
|
||||||
row — structural recursion stays pure, total, and effect-free. A
|
|
||||||
transitional grandfather exempts still-`tail: true`-marked recursive
|
|
||||||
calls so the corpus type-checks unchanged through it.2.
|
|
||||||
`tail-app`/`tail-do` are retired in it.3. See
|
|
||||||
`docs/specs/2026-05-15-iteration-discipline.md`.
|
|
||||||
|
|
||||||
**`Literal`**:
|
**`Literal`**:
|
||||||
|
|
||||||
```jsonc
|
```jsonc
|
||||||
@@ -2917,6 +2881,8 @@ What **is** supported (and used as the smoke test for the pipeline):
|
|||||||
effect), and introduce composable transducers (`Stateful a b` +
|
effect), and introduce composable transducers (`Stateful a b` +
|
||||||
`pipe`). Spec: `docs/specs/2026-05-15-mut-local.md`.
|
`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,174 @@
|
|||||||
|
# iter revert — Iteration-discipline milestone surgical forward revert
|
||||||
|
|
||||||
|
**Date:** 2026-05-16
|
||||||
|
**Started from:** abf00131c10ff9287b419439c565480c96fa861b
|
||||||
|
**Status:** DONE
|
||||||
|
**Tasks completed:** 11 of 11
|
||||||
|
|
||||||
|
## Summary
|
||||||
|
|
||||||
|
The Iteration-discipline milestone (it.1 `96db54d` + it.2 `a4be1e5`)
|
||||||
|
is fully backed out by a single forward iteration, with `1ff7e81`
|
||||||
|
(the last commit before milestone-start `9973546`) as the per-region
|
||||||
|
byte oracle. `main` was never rewound — every restore is a forward
|
||||||
|
working-tree edit, net-new it.1/it.2 surface is `rm`'d, DESIGN.md is
|
||||||
|
hunk-edited in place (clause 3 / criterion section KEPT, never
|
||||||
|
wholesale-restored). Feature-acceptance clause 3 and the F3 todo are
|
||||||
|
retained; the clause-3 worked example is de-claimed from "shipped" to
|
||||||
|
hypothetical-illustration form (in DESIGN.md and brainstorm SKILL.md
|
||||||
|
in lockstep); one honest F1/F4 documentation note is added (the only
|
||||||
|
net addition) and guarded by a doc-presence test. The correctness
|
||||||
|
gate (Task 11) proved compiler behaviour **pristine**: all 164
|
||||||
|
surviving `1ff7e81`-era fixtures `ail check`/`ail run` byte-identical
|
||||||
|
to their pre-milestone behaviour; `cargo test --workspace` 600/0;
|
||||||
|
zero residual it.1/it.2 production surface. AILang's iteration story
|
||||||
|
is exactly the pre-`9973546` state (structural / tail recursion;
|
||||||
|
`tail-app` intact); the genuine total-recursion ambition is preserved
|
||||||
|
on the roadmap as a deferred P2 milestone sequenced behind a future
|
||||||
|
`Nat`/refinement-types milestone.
|
||||||
|
|
||||||
|
## Per-task notes
|
||||||
|
|
||||||
|
- revert.1: AST node family + drift anchors + DESIGN.md §Data-model —
|
||||||
|
removed `Term::Loop`/`Term::Recur`/`struct LoopBinder` + 2 serde
|
||||||
|
units (ast.rs byte-identical to oracle); loop/recur exemplars +
|
||||||
|
match arms removed from design_schema_drift.rs / spec_drift.rs /
|
||||||
|
schema_coverage.rs (byte-identical); DESIGN.md §Data-model
|
||||||
|
loop/recur JSON + it.1/it.2 prose removed (region byte-identical).
|
||||||
|
- revert.2: `CheckError` surface — 5 variants + doc comments, 5
|
||||||
|
`code()` arms, 3 dedicated `ctx()` arms deleted; `_ =>` catch-all
|
||||||
|
preserved; diagnostic.rs confirmed genuine no-op.
|
||||||
|
- revert.3: it.2 guardedness pass + Diverge-injection — STOP-gate
|
||||||
|
cleared (all 6 named helpers it.2-net-new at oracle → all delete,
|
||||||
|
no ambiguity). Removed the entire 687-line guardedness cluster
|
||||||
|
(verify_structural_recursion, term_contains_loop, adt_families,
|
||||||
|
referenced_con_names, mutual_structural_group, adt_param_positions,
|
||||||
|
RecCall, TypeUnionFind, MutualGroup, et al.) **plus** the
|
||||||
|
transitively-it.2 `module_fns` plumbing through
|
||||||
|
check_module/check_def/check_fn signatures (oracle proved net-new;
|
||||||
|
resolved via the plan's Oracle convention + recon-by-name mandate —
|
||||||
|
see Concerns). 3 Diverge-injection sites restored byte-identical;
|
||||||
|
lib.rs `"Diverge"` count 0 == oracle.
|
||||||
|
- revert.4: it.1 check surface — synth Loop/Recur arms +
|
||||||
|
verify_loop_body deleted; loop_stack param + ~30 threaded calls + 9
|
||||||
|
local decls removed; verify_tail_positions survives, it.1 arms
|
||||||
|
removed, byte-identical to oracle; substitute_rigids_in_term +
|
||||||
|
post-mono name arms cleaned. Result:
|
||||||
|
`crates/ailang-check/src/lib.rs` FULLY byte-identical to `1ff7e81`.
|
||||||
|
- revert.5: 9 remaining walker files (lift/mono/linearity/uniqueness/
|
||||||
|
reuse_shape/pre_desugar_validation/builtins + core desugar/
|
||||||
|
workspace) restored to oracle, all byte-identical; pre-restore diff
|
||||||
|
classification proved every hunk it.1/it.2-only.
|
||||||
|
- revert.6: surface + prose + form-A grammar — parse.rs/print.rs/
|
||||||
|
prose lib.rs/form_a.md restored to oracle byte-identical; each
|
||||||
|
touched only by 96db54d (it.1) since 1ff7e81; surface+prose build
|
||||||
|
clean.
|
||||||
|
- revert.7: codegen — lib.rs/escape.rs/lambda.rs + ail/main.rs
|
||||||
|
restored to oracle byte-identical; `block_terminated = true` count
|
||||||
|
== 8 == oracle (it.1 parallel setter gone; the 4 tail-driven + 4
|
||||||
|
control-flow setters survive). `cargo build --workspace` clean
|
||||||
|
(first full-build gate, lockstep clustering worked as designed).
|
||||||
|
- revert.8: STOP-gate re-verify cleared (live `git diff --stat`
|
||||||
|
matched plan's net-new/modified/absent classification exactly).
|
||||||
|
Deleted 2 pin files + 16 fixtures + bench/it3-oracle via plain
|
||||||
|
`rm` (no git-index touch). 2 RC fixtures restored byte-identical.
|
||||||
|
e2e.rs + codegen_import_map_fallback_pin.rs + carve_out_inventory.rs
|
||||||
|
restored byte-identical (carve = "Twelve carve-outs", last entry
|
||||||
|
test_mut_var_captured_by_lambda.ail.json). it.{1,2,3} stats json
|
||||||
|
KEPT. cargo test --workspace 599/0.
|
||||||
|
- revert.9: DESIGN.md Decision 3 → 1ff7e81 placeholder (byte-identical,
|
||||||
|
hunk-only); clause-3 worked example de-claimed to hypothetical form
|
||||||
|
(criterion section + clause-3 discriminator KEPT); brainstorm
|
||||||
|
SKILL.md de-claimed in lockstep; F1/F4 note added verbatim;
|
||||||
|
doc-presence test added verbatim, PASSES.
|
||||||
|
- revert.10: roadmap Iteration-discipline [~] block + blocking-fork
|
||||||
|
sub-section removed; deferred "Iteration-totality story" entry
|
||||||
|
added verbatim at top of P2 (per plan + spec sequencing); F3 +
|
||||||
|
Stateful-islands untouched; superseded header prepended to the old
|
||||||
|
spec; Step-4 STOP-check clean (no residual A1 amendment).
|
||||||
|
- revert.11 (correctness gate): 1ff7e81 ref compiler built in
|
||||||
|
throwaway worktree; 164-fixture check/run oracle captured;
|
||||||
|
post-revert compiler diffed byte-for-byte — fail=0, ZERO DRIFT,
|
||||||
|
behaviour PRISTINE. cargo test --workspace 600/0. Residual
|
||||||
|
it.1/it.2 production-surface grep clean. Worktree removed; main
|
||||||
|
HEAD never moved.
|
||||||
|
|
||||||
|
## Concerns
|
||||||
|
|
||||||
|
- revert.3: the plan's Task-3 file-map enumerated the guardedness
|
||||||
|
helpers + call site but did NOT explicitly list the `module_fns`
|
||||||
|
signature threading through check_module/check_def/check_fn. The
|
||||||
|
`1ff7e81` oracle unambiguously proved that threading it.2-net-new;
|
||||||
|
leaving it would have been dead it.2 surface referencing deleted
|
||||||
|
helpers. Resolved correctly via the plan's own "Oracle convention"
|
||||||
|
+ "re-locate by symbol name, oracle is authority on every byte"
|
||||||
|
mandate + the spec §Data-flow ("restored to pure pre-it.1 role").
|
||||||
|
This is the recon-map-drifts-but-oracle-is-authority mechanism
|
||||||
|
working as designed, not a plan defect — no bounce. Flagged so the
|
||||||
|
Boss is aware the plan file-map under-enumerated one signature
|
||||||
|
edge; the end-state is verified correct (lib.rs byte-identical to
|
||||||
|
oracle).
|
||||||
|
- revert.9: the plan's Task-9 Step-6 single-line grep heuristic
|
||||||
|
(`grep -c 'clauses 1 and 2 yet fail clause 3' ...`) returned 0
|
||||||
|
because the verbatim-applied replacement text line-wraps ("yet
|
||||||
|
fail\n clause 3"). The substantive acceptance criterion
|
||||||
|
(clause-3 block retained, only de-claimed) is positively verified
|
||||||
|
multiline + by the intact `## Feature-acceptance criterion` section
|
||||||
|
and "criterion 3 is the discriminator" sentence. Not a defect;
|
||||||
|
flagged so the Boss does not misread the heuristic.
|
||||||
|
|
||||||
|
## Known debt
|
||||||
|
|
||||||
|
(none — the revert is byte-pristine against `1ff7e81` for all
|
||||||
|
production surface; behaviour is byte-identical over the surviving
|
||||||
|
corpus.)
|
||||||
|
|
||||||
|
## Blocked detail
|
||||||
|
|
||||||
|
(none — DONE.)
|
||||||
|
|
||||||
|
## Files touched
|
||||||
|
|
||||||
|
90 working-tree paths (Boss commits all as one cohesive revert iter):
|
||||||
|
|
||||||
|
- Deleted (59): `crates/ailang-check/tests/loop_recur_pin.rs`,
|
||||||
|
`crates/ailang-check/tests/structural_recursion_pin.rs`; 16 it.1/it.2
|
||||||
|
fixtures under `examples/` (loop_*.ail, struct_rec_*.ail,
|
||||||
|
test_recur_*.ail.json, test_loop_missing_diverge / test_mutual_cross_family
|
||||||
|
/ test_non_structural_recursion .ail.json); `bench/it3-oracle/`
|
||||||
|
(MANIFEST.tsv + 40 .out).
|
||||||
|
- Modified — source revert to 1ff7e81 (byte-identical):
|
||||||
|
`crates/ailang-core/src/ast.rs`, `desugar.rs`, `workspace.rs`;
|
||||||
|
`crates/ailang-check/src/lib.rs`, `lift.rs`, `mono.rs`,
|
||||||
|
`linearity.rs`, `uniqueness.rs`, `reuse_shape.rs`,
|
||||||
|
`pre_desugar_validation.rs`, `builtins.rs`;
|
||||||
|
`crates/ailang-codegen/src/lib.rs`, `escape.rs`, `lambda.rs`;
|
||||||
|
`crates/ail/src/main.rs`; `crates/ailang-surface/src/parse.rs`,
|
||||||
|
`print.rs`; `crates/ailang-prose/src/lib.rs`;
|
||||||
|
`crates/ailang-core/specs/form_a.md`.
|
||||||
|
- Modified — tests revert to 1ff7e81 (byte-identical):
|
||||||
|
`crates/ailang-core/tests/spec_drift.rs`, `schema_coverage.rs`,
|
||||||
|
`carve_out_inventory.rs`; `crates/ail/tests/e2e.rs`,
|
||||||
|
`codegen_import_map_fallback_pin.rs`.
|
||||||
|
- Modified — net-add / hunk-edit:
|
||||||
|
`crates/ailang-core/tests/design_schema_drift.rs` (loop/recur
|
||||||
|
anchors removed + new doc-presence test added),
|
||||||
|
`docs/DESIGN.md` (Decision 3 placeholder + clause-3 de-claim +
|
||||||
|
F1/F4 note; hunk-edited, clause 3 kept), `docs/roadmap.md`
|
||||||
|
(Iteration-discipline block removed, deferred P2 entry added),
|
||||||
|
`docs/specs/2026-05-15-iteration-discipline.md` (superseded
|
||||||
|
header), `skills/brainstorm/SKILL.md` (worked-example de-claim).
|
||||||
|
- Modified — fixtures restored to 1ff7e81:
|
||||||
|
`examples/rc_pin_recurse_implicit.ail`,
|
||||||
|
`examples/rc_let_alias_implicit_param.ail`.
|
||||||
|
- KEPT (no-op, Boss decision):
|
||||||
|
`bench/orchestrator-stats/2026-05-15-iter-it.{1,2,3}.json`.
|
||||||
|
|
||||||
|
E2E coverage: none (revert iteration — the Task-11 behavioural-
|
||||||
|
equivalence sweep over all 164 surviving fixtures is a stronger
|
||||||
|
correctness proof than new E2E; the doc-presence test guards the
|
||||||
|
only net-new artefact).
|
||||||
|
|
||||||
|
## Stats
|
||||||
|
|
||||||
|
bench/orchestrator-stats/2026-05-16-iter-revert.json
|
||||||
@@ -78,3 +78,4 @@
|
|||||||
- 2026-05-15 — iter it.1: iteration-discipline milestone (1 of 3) — `Term::Loop` / `Term::Recur` / `LoopBinder` added as strictly-additive first-class AST nodes end-to-end: Form-A `parse_loop`/`parse_recur` + print + prose render/free-var/subst lockstep + canonical-JSON serde/round-trip + schema/spec-drift/schema-coverage/carve-out lockstep (13→17) + typecheck (binder typing + recur arity/type unification via `loop_stack: &mut Vec<Vec<Type>>` threaded exactly as mut.2's `mut_scope_stack`; recur-tail-position via a new private `verify_loop_body`, `verify_tail_positions` public signature unchanged) + codegen (loop-header block, one phi per binder, recur back-edge `br` with a NEW parallel `block_terminated` setter, lambda-boundary `loop_frames` save/restore mirroring mut.3). Four new `CheckError` variants `Recur{OutsideLoop,ArityMismatch,TypeMismatch,NotInTailPosition}` (bracket-`[code]`-free Display per the F2 convention). Strictly additive verified — zero deletions touch `tail-app`/`tail-do`, `verify_tail_positions`' tail-app role, or the seven existing codegen `block_terminated` sites (the 32 within-iter deletions are exclusively DD-3 stub-then-fill replacements). `Term::Recur` synth returns a fresh metavar (resolves the plan's flagged `Type::unit()` open risk: recur appears in `if` branches that must unify with the sibling type). `loop_counter.ail`→`55`, `loop_in_lambda_e2e.ail`→`49`, four negatives fire exact codes, `tail-app` fixtures byte-identical, zero IR/prose snapshot drift; `cargo test --workspace` green. Two mut.1-class plan-pseudo-vs-reality substitutions recorded (prose round-trip asserting AST-equality is impossible — no Form-B parser by design; the diagnostic.rs doc-list premise was false) → 2026-05-15-iter-it.1.md
|
- 2026-05-15 — iter it.1: iteration-discipline milestone (1 of 3) — `Term::Loop` / `Term::Recur` / `LoopBinder` added as strictly-additive first-class AST nodes end-to-end: Form-A `parse_loop`/`parse_recur` + print + prose render/free-var/subst lockstep + canonical-JSON serde/round-trip + schema/spec-drift/schema-coverage/carve-out lockstep (13→17) + typecheck (binder typing + recur arity/type unification via `loop_stack: &mut Vec<Vec<Type>>` threaded exactly as mut.2's `mut_scope_stack`; recur-tail-position via a new private `verify_loop_body`, `verify_tail_positions` public signature unchanged) + codegen (loop-header block, one phi per binder, recur back-edge `br` with a NEW parallel `block_terminated` setter, lambda-boundary `loop_frames` save/restore mirroring mut.3). Four new `CheckError` variants `Recur{OutsideLoop,ArityMismatch,TypeMismatch,NotInTailPosition}` (bracket-`[code]`-free Display per the F2 convention). Strictly additive verified — zero deletions touch `tail-app`/`tail-do`, `verify_tail_positions`' tail-app role, or the seven existing codegen `block_terminated` sites (the 32 within-iter deletions are exclusively DD-3 stub-then-fill replacements). `Term::Recur` synth returns a fresh metavar (resolves the plan's flagged `Type::unit()` open risk: recur appears in `if` branches that must unify with the sibling type). `loop_counter.ail`→`55`, `loop_in_lambda_e2e.ail`→`49`, four negatives fire exact codes, `tail-app` fixtures byte-identical, zero IR/prose snapshot drift; `cargo test --workspace` green. Two mut.1-class plan-pseudo-vs-reality substitutions recorded (prose round-trip asserting AST-equality is impossible — no Form-B parser by design; the diagnostic.rs doc-list premise was false) → 2026-05-15-iter-it.1.md
|
||||||
- 2026-05-15 — iter it.2: iteration-discipline milestone (2 of 3) — structural-recursion guardedness checker + first real `Diverge` effect, strictly additive (nothing `tail`-related removed; that is it.3). New whole-body pass `verify_structural_recursion` runs as a sibling of `verify_tail_positions` in `check_fn`'s post-synth region (DD-1): the `smaller`-set algorithm with implicit candidate-position inference + unconstrained accumulator positions (DD-2 — foldl-shape accumulator classifies as structural recursion, pure+total), self/mutual identification with an inline ADT-family connected-components union-find (DD-3), and the it.2-only `tail==false` grandfather. `CheckError::NonStructuralRecursion {callee,arg}` (bracket-`[code]`-free Display per F2). `term_contains_loop` (stops at `Term::Lam` boundaries, DD-4) injects `"Diverge"` into the raised effect set so the existing `UndeclaredEffect` machinery enforces it with no new diagnostic variant; lam-arrow + `Term::LetRec` sub-effect sites wired (loop behind a lam edge carries `!Diverge` on the lam's arrow, propagating via the free callee-effect path, not leaking to the enclosing fn — exactly as `!IO` scopes). DESIGN.md Decision 3 + §Data-model hook synced present-tense. Four it.1 loop fixtures gained `!Diverge`. Two spec-premise boundary defects surfaced and resolved inside the task's invariants (corpus clean, check not weakened): (§1) the spec's "21 tail-app fixtures" grandfather premise under-counts the corpus — no-ADT-candidate counter recursions (~18, e.g. `build_tree(depth:Int)`) have no structural position to verify and are deferred to it.3 migration; (§2) two RC-regression fixtures (`rc_pin_recurse_implicit`, `rc_let_alias_implicit_param`) hold an ADT param constant while decrementing an Int — genuinely non-structural, joined the spec's transitional `tail-app` grandfather exactly as the other 20 corpus fixtures do (their RC==GC regression guards verified still green; the regression lives in the unchanged `pin`/`pin_aliased` bodies). Both recorded as corrected it.3 corpus-migration scope. `cargo test --workspace` 622 green / 0 red; all 9 acceptance pins non-vacuous (negatives `.contains(code)`); struct_rec_sum→15, loop_needs_diverge→55, the it.1 55/49 e2e still green → 2026-05-15-iter-it.2.md
|
- 2026-05-15 — iter it.2: iteration-discipline milestone (2 of 3) — structural-recursion guardedness checker + first real `Diverge` effect, strictly additive (nothing `tail`-related removed; that is it.3). New whole-body pass `verify_structural_recursion` runs as a sibling of `verify_tail_positions` in `check_fn`'s post-synth region (DD-1): the `smaller`-set algorithm with implicit candidate-position inference + unconstrained accumulator positions (DD-2 — foldl-shape accumulator classifies as structural recursion, pure+total), self/mutual identification with an inline ADT-family connected-components union-find (DD-3), and the it.2-only `tail==false` grandfather. `CheckError::NonStructuralRecursion {callee,arg}` (bracket-`[code]`-free Display per F2). `term_contains_loop` (stops at `Term::Lam` boundaries, DD-4) injects `"Diverge"` into the raised effect set so the existing `UndeclaredEffect` machinery enforces it with no new diagnostic variant; lam-arrow + `Term::LetRec` sub-effect sites wired (loop behind a lam edge carries `!Diverge` on the lam's arrow, propagating via the free callee-effect path, not leaking to the enclosing fn — exactly as `!IO` scopes). DESIGN.md Decision 3 + §Data-model hook synced present-tense. Four it.1 loop fixtures gained `!Diverge`. Two spec-premise boundary defects surfaced and resolved inside the task's invariants (corpus clean, check not weakened): (§1) the spec's "21 tail-app fixtures" grandfather premise under-counts the corpus — no-ADT-candidate counter recursions (~18, e.g. `build_tree(depth:Int)`) have no structural position to verify and are deferred to it.3 migration; (§2) two RC-regression fixtures (`rc_pin_recurse_implicit`, `rc_let_alias_implicit_param`) hold an ADT param constant while decrementing an Int — genuinely non-structural, joined the spec's transitional `tail-app` grandfather exactly as the other 20 corpus fixtures do (their RC==GC regression guards verified still green; the regression lives in the unchanged `pin`/`pin_aliased` bodies). Both recorded as corrected it.3 corpus-migration scope. `cargo test --workspace` 622 green / 0 red; all 9 acceptance pins non-vacuous (negatives `.contains(code)`); struct_rec_sum→15, loop_needs_diverge→55, the it.1 55/49 e2e still green → 2026-05-15-iter-it.2.md
|
||||||
- 2026-05-15 — iter it.3 (BLOCKED, bounce-back): destructive terminal iteration of iteration-discipline. Task 1 (non-destructive: pre-migration oracle + the spec-delegated class-(b) live sweep) ran complete — 40 recursive corpus fixtures classified + oracled under `bench/it3-oracle/`; zero production code changed; HEAD clean at c992eb9. The mandated Task-1.3 sweep surfaced a **fundamental milestone-design flaw**, not a migration nuisance: 6 fixtures contain `build(d: Int) = if d==0 then Leaf else Node(1, build(d-1), build(d-1))` — a terminating, maximally-LLM-natural, **non-structural (Int param, no ADT-candidate) non-tail BRANCHING (double) recursion** that the milestone's totality dichotomy ("structural-over-ADT OR loop/recur, else error") makes **inexpressible**: not ADT-structural, and not `recur`-able (branching ≠ single tail back-edge). Post-it.3 it is a hard `NonStructuralRecursion` with no expressible alternative — the milestone as-specified would fail its own feature-acceptance criterion (clause 1: the LLM reaches for `build(d-1),build(d-1)`; clause 2: the milestone would *remove* expressivity). The orchestrator correctly refused a 4th plan-patch and bounced (`feedback_spec_over_plan_patches`). Boss verdict: the totality story overlooked a second canonical total-by-construction scheme — well-founded recursion on a strictly-decreasing non-negative Int measure. Resolution requires a new additive iteration **it.2b** (widen the it.2 guardedness checker to accept Int-bounded recursion incl. branching) BEFORE it.3 re-dispatches; the purity sub-fork (Int≠Nat: total only for non-negative entry) touches the user-co-designed purity pillar → user design decision, recommended option A1 (accept with a documented non-negative-entry obligation, analogous to array-bounds). Notify sent; loop stopped. The RC-RSS/18g.1 load-bearing risk (it.3 Task 5.5/5b) was NOT reached and must NOT be treated as resolved by the milestone-close audit → 2026-05-15-iter-it.3.md
|
- 2026-05-15 — iter it.3 (BLOCKED, bounce-back): destructive terminal iteration of iteration-discipline. Task 1 (non-destructive: pre-migration oracle + the spec-delegated class-(b) live sweep) ran complete — 40 recursive corpus fixtures classified + oracled under `bench/it3-oracle/`; zero production code changed; HEAD clean at c992eb9. The mandated Task-1.3 sweep surfaced a **fundamental milestone-design flaw**, not a migration nuisance: 6 fixtures contain `build(d: Int) = if d==0 then Leaf else Node(1, build(d-1), build(d-1))` — a terminating, maximally-LLM-natural, **non-structural (Int param, no ADT-candidate) non-tail BRANCHING (double) recursion** that the milestone's totality dichotomy ("structural-over-ADT OR loop/recur, else error") makes **inexpressible**: not ADT-structural, and not `recur`-able (branching ≠ single tail back-edge). Post-it.3 it is a hard `NonStructuralRecursion` with no expressible alternative — the milestone as-specified would fail its own feature-acceptance criterion (clause 1: the LLM reaches for `build(d-1),build(d-1)`; clause 2: the milestone would *remove* expressivity). The orchestrator correctly refused a 4th plan-patch and bounced (`feedback_spec_over_plan_patches`). Boss verdict: the totality story overlooked a second canonical total-by-construction scheme — well-founded recursion on a strictly-decreasing non-negative Int measure. Resolution requires a new additive iteration **it.2b** (widen the it.2 guardedness checker to accept Int-bounded recursion incl. branching) BEFORE it.3 re-dispatches; the purity sub-fork (Int≠Nat: total only for non-negative entry) touches the user-co-designed purity pillar → user design decision, recommended option A1 (accept with a documented non-negative-entry obligation, analogous to array-bounds). Notify sent; loop stopped. The RC-RSS/18g.1 load-bearing risk (it.3 Task 5.5/5b) was NOT reached and must NOT be treated as resolved by the milestone-close audit → 2026-05-15-iter-it.3.md
|
||||||
|
- 2026-05-16 — iter revert: Iteration-discipline milestone (it.1 `96db54d` + it.2 `a4be1e5`) fully backed out by one **forward** iteration (`main` sacrosanct — never rewound; `1ff7e81`, the pre-`9973546` commit, is the per-region byte oracle). Root cause: the milestone was an over-escalation of fieldtest finding F1 (a `[friction]` item whose own minimal recommendation was a DESIGN.md note); its totality dichotomy made the maximally-LLM-natural `build(d:Int)=Node(1,build(d-1),build(d-1))` inexpressible (it.3 BLOCKED), and the only in-thesis escape (A1/it.2b) conceded the language's first documented-unenforced totality precondition — a purity-pillar dilution the user rejected. `Term::Loop`/`Term::Recur`/`LoopBinder`, the `verify_structural_recursion` guardedness pass + `term_contains_loop` + `Diverge`-injection + the transitively-it.2 `module_fns` plumbing, the five `Recur*`/`NonStructuralRecursion` `CheckError` variants (+ `code()` + 3 dedicated `ctx()` arms), the it.1 codegen loop-header/phi/back-edge + parallel `block_terminated` setter, and all walker arms are removed — `crates/ailang-check/src/lib.rs` and every reverted source/test file byte-identical to `1ff7e81`. Surgical keeps: feature-acceptance **clause 3** (DESIGN.md + brainstorm SKILL.md, worked example de-claimed "shipped"→hypothetical) and the F3 P2 todo. Sole net addition: an honest F1/F4 documented-idiom note (tail-recursive accumulator fallback; `examples/mut_counter.ail`) guarded by a doc-presence test. 16 it.1/it.2 fixtures + 2 pin files + `bench/it3-oracle/` deleted; 2 RC fixtures restored to `1ff7e81`; `bench/orchestrator-stats/2026-05-15-iter-it.{1,2,3}.json` kept (historical record). Roadmap: Iteration-discipline block + blocking-fork section removed; the genuine total-Int-recursion ambition preserved as a deferred P2 milestone sequenced behind a future `Nat`/refinement-types milestone (not abandoned — correctly sequenced). Correctness gate PRISTINE: 164 surviving `1ff7e81`-era fixtures `ail check`/`ail run` byte-identical to pre-milestone behaviour (1ff7e81 worktree reference compiler, zero drift); `cargo test --workspace` 600/0; zero residual it.1/it.2 production surface. The old it.* journals/plans + the superseded-headered `2026-05-15-iteration-discipline.md` stay as historical record → 2026-05-16-iter-revert.md
|
||||||
|
|||||||
+17
-137
@@ -40,143 +40,6 @@ clean. Pick the next milestone from P2.)_
|
|||||||
|
|
||||||
## P1 — Next
|
## P1 — Next
|
||||||
|
|
||||||
- [~] **\[milestone\]** Iteration discipline — structural recursion +
|
|
||||||
named `loop`/`recur`; recursion-by-call restricted; `tail-app`
|
|
||||||
retired. Spec `docs/specs/2026-05-15-iteration-discipline.md`
|
|
||||||
(approved fda9b78; scope-corrected 2018178/10a0595). **Status
|
|
||||||
2026-05-15: it.1 SHIPPED (96db54d — loop/recur additive), it.2
|
|
||||||
SHIPPED (a4be1e5 — guardedness checker + first real Diverge
|
|
||||||
effect), it.3 BLOCKED on a surfaced fundamental design fork (see
|
|
||||||
"The blocking fork" below — needs a user design decision + a new
|
|
||||||
iteration it.2b before it.3 can re-dispatch).** The iteration
|
|
||||||
story of the language, decided in principle 2026-05-15. Two
|
|
||||||
constructs survive, one is removed:
|
|
||||||
- **Structural recursion stays — and becomes total-by-construction.**
|
|
||||||
A recursive call is permitted iff it is structurally guarded:
|
|
||||||
each recursive call is on a constructor-smaller component of an
|
|
||||||
algebraic argument. Decision 10 already guarantees ADTs are
|
|
||||||
acyclic, so a structurally-guarded recursion terminates *by the
|
|
||||||
invariant the language already holds* — permitting it costs
|
|
||||||
nothing on the provability pillar and it stays locally checkable.
|
|
||||||
This is the most LLM-natural correct pattern (tree / AST / JSON /
|
|
||||||
list `match`-and-recurse, saturated in every FP corpus) and must
|
|
||||||
not be amputated.
|
|
||||||
- **All other repetition must go through a named `(loop ...)` head +
|
|
||||||
`recur`.** `recur` is the single explicit backward jump; it
|
|
||||||
targets a lexically-enclosing named loop, never an implicit fn
|
|
||||||
head (Clojure permits the implicit form — AILang does not, for
|
|
||||||
the same no-outward-inference reason that motivated explicit
|
|
||||||
`tail-app`). Accumulator loops, counted iteration, streaming,
|
|
||||||
state machines — none are structurally decreasing, so all are
|
|
||||||
forced into the explicit named-loop form.
|
|
||||||
- **`tail-app` is retired.** `Term::App { tail: bool }` and the
|
|
||||||
`musttail`-lowering surface go away: a call is either structural
|
|
||||||
recursion (a plain call, checked total) or it is `loop`/`recur`
|
|
||||||
(the only loop). No marked-tail-call concept survives.
|
|
||||||
|
|
||||||
**Motivation.** Discharges fieldtest finding F1 — the most
|
|
||||||
consequential mut-local finding: mut-local's own motivation
|
|
||||||
("removes friction from accumulators that today need tail-recursive
|
|
||||||
helpers") was structurally unmet, because without a loop the only
|
|
||||||
iteration mechanism remained the very tail-rec helper. `loop`/`recur`
|
|
||||||
closes that honestly without introducing an imperative construct
|
|
||||||
(it is pure: no mutable loop variable, no effect, composes into pure
|
|
||||||
contexts freely). It also closes the residual "forgot to mark tail"
|
|
||||||
trap *completely* — a recursive call that is not structurally
|
|
||||||
guarded is a compile error that names itself, not a silent
|
|
||||||
stack-overflow. And it unifies with Decision 10: acyclic values
|
|
||||||
*and* an acyclic implicit call graph; the only cycles anywhere in a
|
|
||||||
program are explicit, named, typed `loop` points. Both surviving
|
|
||||||
constructs pass all three feature-acceptance clauses including the
|
|
||||||
2026-05-15 clause 3 (structural recursion and named loop/recur are
|
|
||||||
LLM-natural *and* their bug class is closed by construction; a bare
|
|
||||||
`while` is exactly what clause 3 rejects).
|
|
||||||
|
|
||||||
**Original open spec questions — RESOLVED in it.2 (a4be1e5).**
|
|
||||||
(1) *mutual* structural recursion → the conservative same-ADT-family
|
|
||||||
rule (connected-components union-find over the type-reference
|
|
||||||
graph; admits tree/forest, even/odd-over-Nat, mutual JSON; a
|
|
||||||
general size-measure ordering stays out of scope). (2) the
|
|
||||||
`foldl`-shaped accumulator-carrying structural walk → **counts as
|
|
||||||
structural recursion** (plain, pure, total — only the structural
|
|
||||||
argument must decrease; accumulator positions are unconstrained).
|
|
||||||
Both shipped and pinned.
|
|
||||||
|
|
||||||
**The blocking fork (it.3-surfaced 2026-05-15 — needs a USER
|
|
||||||
design decision; touches the purity pillar the user co-designed).**
|
|
||||||
The milestone's totality thesis recognised exactly ONE
|
|
||||||
total-by-construction recursion scheme: structural-over-an-acyclic-
|
|
||||||
ADT (Decision 10). The it.3 mandated corpus sweep proved this is
|
|
||||||
*incomplete*. Six corpus fixtures contain the single most
|
|
||||||
LLM-natural tree-builder shape:
|
|
||||||
`build(d: Int) = if d==0 then Leaf else Node(1, build(d-1), build(d-1))`.
|
|
||||||
It is provably terminating, but: **not** ADT-structural (`d` is
|
|
||||||
`Int`, no constructor-smaller component) and **not** `loop`/`recur`-
|
|
||||||
able (it is *branching/double* recursion; `recur` is a single
|
|
||||||
tail-position back-edge — a balanced-tree build is not a tail
|
|
||||||
loop; the only `loop` form is a hand-rolled explicit-stack
|
|
||||||
worklist, which no LLM writes). So the milestone *as-specified*
|
|
||||||
makes a trivially-total, maximally-natural function
|
|
||||||
**inexpressible** — failing its own feature-acceptance criterion
|
|
||||||
(clause 1: the LLM reaches for `build(d-1),build(d-1)`; clause 2:
|
|
||||||
the milestone would *remove* expressivity). This is a real
|
|
||||||
design-thesis gap, not a migration nuisance: there is a SECOND
|
|
||||||
canonical total-by-construction scheme the design overlooked —
|
|
||||||
**well-founded recursion on a strictly-decreasing non-negative
|
|
||||||
integer measure** (`f(0)=base; f(n)=…f(n-1)…`, branching allowed).
|
|
||||||
|
|
||||||
**Decision the user must make** (it trades against the purity
|
|
||||||
pillar — Int ≠ ℕ; AILang has no `Nat`/refinement types, so
|
|
||||||
Int-bounded recursion is total only for *non-negative entry*):
|
|
||||||
- **A1 (RECOMMENDED).** Make the totality story a *trichotomy*:
|
|
||||||
ADT-structural | Int-bounded-well-founded | explicit `loop`/`recur`.
|
|
||||||
Accept Int-bounded recursion (recursive call on `p - <pos-lit>`
|
|
||||||
with a non-recursing base guard, branching allowed) as a
|
|
||||||
*second total-by-construction class*, with one documented
|
|
||||||
obligation: non-negative entry (a negative seed diverges — the
|
|
||||||
same precondition class as array-bounds the language already
|
|
||||||
lives with). Restores the entire `f(n-1)` recursion family (the
|
|
||||||
second-most-common shape in any corpus); keeps all 6 fixtures
|
|
||||||
verbatim; the trichotomy is *more* complete, not less principled.
|
|
||||||
Rationale is the language's own gate: a milestone that makes
|
|
||||||
`build(d)` inexpressible cannot pass the feature-acceptance
|
|
||||||
criterion. *Why this is a user call:* it concedes "total by
|
|
||||||
construction, sign nothing" → "total by construction modulo one
|
|
||||||
documented non-negative-entry obligation" — a dilution of a
|
|
||||||
purity pillar the user personally weighted in the original
|
|
||||||
Socratic design dialogue.
|
|
||||||
- **A2.** Introduce a `Nat` type; accept Int-bounded recursion
|
|
||||||
only on `Nat` — preserves zero-precondition purity, but `Nat`
|
|
||||||
is its own sub-milestone and an LLM writes `depth: Int` unless
|
|
||||||
the prelude makes `Nat` the natural default (clause-1 friction).
|
|
||||||
- **A3 / B / C (not recommended).** Strict-dichotomy +
|
|
||||||
explicit-worklist (fails clause 1 — no LLM hand-rolls a stack
|
|
||||||
for tree-build); a narrow carve-out predicate (the patch smell);
|
|
||||||
reshaping the LLM-natural fixtures (forbidden —
|
|
||||||
`feedback_dont_adapt_tests_to_bugs`).
|
|
||||||
|
|
||||||
**If A1 (or A2): new iteration it.2b** — additive widening of the
|
|
||||||
it.2 guardedness checker to also accept Int-bounded (incl.
|
|
||||||
branching) recursion as total; no removal. After it.2b the 6
|
|
||||||
fixtures check clean with zero migration; it.3 then re-dispatches
|
|
||||||
from c992eb9 (the `bench/it3-oracle/` 40-fixture behavioural
|
|
||||||
oracle is preserved and reused; nothing in it.3 was destructive).
|
|
||||||
The it.3 RC-RSS/18g.1 load-bearing risk (Task 5.5/5b) is still
|
|
||||||
UNTESTED — must not be treated as resolved by the milestone-close
|
|
||||||
audit.
|
|
||||||
|
|
||||||
**Relation to Stateful islands.** This entry owns the iteration
|
|
||||||
story and *supersedes* the "while-loops legal" scope bullet of the
|
|
||||||
Stateful-islands milestone below. The genuinely-escaping
|
|
||||||
mutable-state path (`ref a` / `MutArray a` / `Stateful a b` /
|
|
||||||
`pipe`) is unaffected and stays in Stateful-islands.
|
|
||||||
|
|
||||||
- context: spec `docs/specs/2026-05-15-iteration-discipline.md`;
|
|
||||||
per-iter journals 2026-05-15-iter-it.1 (shipped), -it.2
|
|
||||||
(shipped), -it.3 (BLOCKED — the design fork above). Next action
|
|
||||||
is a USER decision on the blocking fork (A1 recommended), then
|
|
||||||
iteration it.2b, then it.3 re-dispatch from c992eb9.
|
|
||||||
|
|
||||||
- [x] **\[milestone\]** Heap-`Str` ABI — runtime infrastructure for
|
- [x] **\[milestone\]** Heap-`Str` ABI — runtime infrastructure for
|
||||||
malloc-backed, refcounted `Str` values alongside the existing
|
malloc-backed, refcounted `Str` values alongside the existing
|
||||||
static `@.str_*` globals. Today the `Str` path is static-only
|
static `@.str_*` globals. Today the `Str` path is static-only
|
||||||
@@ -213,6 +76,23 @@ clean. Pick the next milestone from P2.)_
|
|||||||
|
|
||||||
## P2 — Medium-term
|
## P2 — Medium-term
|
||||||
|
|
||||||
|
- [ ] **\[milestone\]** Iteration-totality story — structural +
|
||||||
|
Int-bounded total recursion with *enforced* non-negativity.
|
||||||
|
AILang's iteration story stays as-is (structural / tail recursion;
|
||||||
|
`tail-app` intact). The genuine ambition — make `f(n-1)`-family
|
||||||
|
recursion (incl. branching tree builders) total *by construction*
|
||||||
|
with the non-negative-entry precondition **enforced**, not merely
|
||||||
|
documented — is deferred here because doing it without a purity-
|
||||||
|
pillar concession requires refinement/`Nat` type machinery the
|
||||||
|
language has not built (Decision 4 keeps refinements opaque, no
|
||||||
|
SMT). Not abandoned; correctly sequenced after the type machinery.
|
||||||
|
- depends on: a future `Nat`/refinement-types milestone (no spec
|
||||||
|
yet).
|
||||||
|
- context: `docs/specs/2026-05-16-iteration-discipline-revert.md`
|
||||||
|
(why the 2026-05 attempt was reverted) and
|
||||||
|
`docs/journals/2026-05-15-iter-it.3.md` (the branching-builder
|
||||||
|
counter-example that surfaced the gap).
|
||||||
|
|
||||||
- [x] **\[milestone\]** Retire `io/print_int` / `io/print_bool` /
|
- [x] **\[milestone\]** Retire `io/print_int` / `io/print_bool` /
|
||||||
`io/print_float` effect-ops + migrate example corpus to `print`.
|
`io/print_float` effect-ops + migrate example corpus to `print`.
|
||||||
Shipped 2026-05-14 as iter rpe.1.
|
Shipped 2026-05-14 as iter rpe.1.
|
||||||
|
|||||||
@@ -1,3 +1,11 @@
|
|||||||
|
> **SUPERSEDED 2026-05-16 by
|
||||||
|
> `docs/specs/2026-05-16-iteration-discipline-revert.md`.** This
|
||||||
|
> milestone was reverted in full (it.1 + it.2 backed out forward;
|
||||||
|
> it.3 never ran). It is retained as a historical record of the
|
||||||
|
> attempt and the design-thesis gap it surfaced; it is **not** the
|
||||||
|
> current iteration-story design. Do not plan or implement from this
|
||||||
|
> file.
|
||||||
|
|
||||||
# Iteration discipline — Design Spec
|
# Iteration discipline — Design Spec
|
||||||
|
|
||||||
**Date:** 2026-05-15
|
**Date:** 2026-05-15
|
||||||
|
|||||||
@@ -1,12 +0,0 @@
|
|||||||
(module loop_counter
|
|
||||||
|
|
||||||
(fn main
|
|
||||||
(doc "Iter it.1 — sum 1..10 via an accumulator loop. Iter it.2: loop-bearing ⇒ !Diverge added (D2). Expected stdout: 55.")
|
|
||||||
(type (fn-type (params) (ret (con Unit)) (effects IO Diverge)))
|
|
||||||
(params)
|
|
||||||
(body
|
|
||||||
(app print
|
|
||||||
(loop ((var acc (con Int) 0) (var i (con Int) 1))
|
|
||||||
(if (app > i 10)
|
|
||||||
acc
|
|
||||||
(recur (app + acc i) (app + i 1))))))))
|
|
||||||
@@ -1,30 +0,0 @@
|
|||||||
(module loop_in_lambda_e2e
|
|
||||||
|
|
||||||
(fn apply
|
|
||||||
(doc "apply a fn-of-Int to an Int. Iter it.2: the supplied closure is loop-bearing, so its arrow carries !Diverge — the higher-order param type and apply's own effect row must reflect that (effects are part of the function type).")
|
|
||||||
(type (fn-type (params (fn-type (params (con Int)) (ret (con Int)) (effects Diverge)) (con Int)) (ret (con Int)) (effects Diverge)))
|
|
||||||
(params f x)
|
|
||||||
(body (app f x)))
|
|
||||||
|
|
||||||
(fn main
|
|
||||||
(doc "Iter it.1 — a loop inside a lambda body, invoked via a closure. The lambda computes x*x by summing x exactly x times through an accumulator loop; apply 7 prints 49. Codegen-soundness gate for the lambda-boundary loop_frames save/restore. Iter it.2: !Diverge propagates out through apply (callee-effect propagation).")
|
|
||||||
(type (fn-type (params) (ret (con Unit)) (effects IO Diverge)))
|
|
||||||
(params)
|
|
||||||
(body
|
|
||||||
(app print
|
|
||||||
(app apply
|
|
||||||
; Iter it.2: the loop lives in THIS lambda's body, so the
|
|
||||||
; lambda's arrow carries !Diverge (DD-4 lam boundary). The
|
|
||||||
; loop does not leak Diverge to `main` — `main`'s body does
|
|
||||||
; not syntactically contain the loop (it stops at the lam
|
|
||||||
; edge, exactly as !IO does).
|
|
||||||
(lam
|
|
||||||
(params (typed x (con Int)))
|
|
||||||
(ret (con Int))
|
|
||||||
(effects Diverge)
|
|
||||||
(body
|
|
||||||
(loop ((var acc (con Int) 0) (var k (con Int) 0))
|
|
||||||
(if (app == k x)
|
|
||||||
acc
|
|
||||||
(recur (app + acc x) (app + k 1))))))
|
|
||||||
7)))))
|
|
||||||
@@ -1,18 +0,0 @@
|
|||||||
; Iter it.2 positive (DD-4 / D2): a fn whose body contains a
|
|
||||||
; `(loop …)` must declare `!Diverge`. This one does — its effect row
|
|
||||||
; carries `Diverge` (and `IO`, since it `print`s the result), so the
|
|
||||||
; existing declared-vs-raised reconciliation accepts it. Modelled on
|
|
||||||
; `loop_counter.ail` (it.1) with `Diverge` added to the effect row.
|
|
||||||
|
|
||||||
(module loop_needs_diverge
|
|
||||||
|
|
||||||
(fn main
|
|
||||||
(doc "Sum 1..10 via an accumulator loop; loop-bearing ⇒ !Diverge. Expected stdout: 55.")
|
|
||||||
(type (fn-type (params) (ret (con Unit)) (effects IO Diverge)))
|
|
||||||
(params)
|
|
||||||
(body
|
|
||||||
(app print
|
|
||||||
(loop ((var acc (con Int) 0) (var i (con Int) 1))
|
|
||||||
(if (app > i 10)
|
|
||||||
acc
|
|
||||||
(recur (app + acc i) (app + i 1))))))))
|
|
||||||
@@ -1,16 +0,0 @@
|
|||||||
(module loop_nested_in_lambda
|
|
||||||
|
|
||||||
(fn make_adder
|
|
||||||
(doc "Iter it.1 — a loop inside a lambda body (lambda-boundary analogue). Iter it.2: the returned closure is loop-bearing, so its arrow carries !Diverge (DD-4 lam boundary); make_adder's return type reflects that. make_adder's own body does not contain the loop (it stops at the lam edge), so make_adder itself stays effect-free.")
|
|
||||||
(type (fn-type (params (con Int)) (ret (fn-type (params (con Int)) (ret (con Int)) (effects Diverge)))))
|
|
||||||
(params base)
|
|
||||||
(body
|
|
||||||
(lam
|
|
||||||
(params (typed x (con Int)))
|
|
||||||
(ret (con Int))
|
|
||||||
(effects Diverge)
|
|
||||||
(body
|
|
||||||
(loop ((var acc (con Int) base) (var k (con Int) 0))
|
|
||||||
(if (app == k x)
|
|
||||||
acc
|
|
||||||
(recur (app + acc 1) (app + k 1)))))))))
|
|
||||||
@@ -1,11 +0,0 @@
|
|||||||
(module loop_smoke
|
|
||||||
|
|
||||||
(fn count_to
|
|
||||||
(doc "Iter it.1 — single counted loop; counts i up to n and returns n. Iter it.2: loop-bearing ⇒ !Diverge (D2).")
|
|
||||||
(type (fn-type (params (con Int)) (ret (con Int)) (effects Diverge)))
|
|
||||||
(params n)
|
|
||||||
(body
|
|
||||||
(loop ((var i (con Int) 0))
|
|
||||||
(if (app == i n)
|
|
||||||
i
|
|
||||||
(recur (app + i 1)))))))
|
|
||||||
@@ -59,14 +59,6 @@
|
|||||||
(case (pat-ctor TLeaf) 0)
|
(case (pat-ctor TLeaf) 0)
|
||||||
(case (pat-ctor TNode v l r) 1)))))
|
(case (pat-ctor TNode v l r) 1)))))
|
||||||
|
|
||||||
; Iter it.2: integer-counter recursion holding the ADT param `t`
|
|
||||||
; constant — non-structural by the it.2 guardedness check (D2). The
|
|
||||||
; recursive call is in tail position, so it joins the transitional
|
|
||||||
; `tail-app` grandfather (spec it.2 "Transitional grandfather") as
|
|
||||||
; the rest of the corpus does until it.3 migrates such recursions
|
|
||||||
; to `(loop …)`. The 18g let-alias-mode regression this fixture
|
|
||||||
; guards lives in `pin_aliased`'s match arm-close, unaffected by
|
|
||||||
; this tail marking.
|
|
||||||
(fn loop
|
(fn loop
|
||||||
(type
|
(type
|
||||||
(fn-type
|
(fn-type
|
||||||
@@ -77,7 +69,7 @@
|
|||||||
(if (app == n 0)
|
(if (app == n 0)
|
||||||
0
|
0
|
||||||
(let _v (app pin_aliased t)
|
(let _v (app pin_aliased t)
|
||||||
(tail-app loop (app - n 1) t)))))
|
(app loop (app - n 1) t)))))
|
||||||
|
|
||||||
(fn main
|
(fn main
|
||||||
(type (fn-type (params) (ret (con Unit)) (effects IO)))
|
(type (fn-type (params) (ret (con Unit)) (effects IO)))
|
||||||
|
|||||||
@@ -38,14 +38,6 @@
|
|||||||
(case (pat-ctor TLeaf) 0)
|
(case (pat-ctor TLeaf) 0)
|
||||||
(case (pat-ctor TNode v l r) 1))))
|
(case (pat-ctor TNode v l r) 1))))
|
||||||
|
|
||||||
; Iter it.2: this is integer-counter recursion holding the ADT
|
|
||||||
; param `t` constant — non-structural by the it.2 guardedness
|
|
||||||
; check (D2). The recursive call is in tail position, so it joins
|
|
||||||
; the transitional `tail-app` grandfather (spec it.2 "Transitional
|
|
||||||
; grandfather") exactly as the rest of the corpus does until it.3
|
|
||||||
; migrates such recursions to `(loop …)`. The 18d.4 regression this
|
|
||||||
; fixture guards lives in `pin`'s match arm-close, unaffected by
|
|
||||||
; this tail marking.
|
|
||||||
(fn loop
|
(fn loop
|
||||||
(type (fn-type (params (con Int) (con Tree)) (ret (con Int))))
|
(type (fn-type (params (con Int) (con Tree)) (ret (con Int))))
|
||||||
(params n t)
|
(params n t)
|
||||||
@@ -53,7 +45,7 @@
|
|||||||
(if (app == n 0)
|
(if (app == n 0)
|
||||||
0
|
0
|
||||||
(let _v (app pin t)
|
(let _v (app pin t)
|
||||||
(tail-app loop (app - n 1) t)))))
|
(app loop (app - n 1) t)))))
|
||||||
|
|
||||||
(fn main
|
(fn main
|
||||||
(type (fn-type (params) (ret (con Unit)) (effects IO)))
|
(type (fn-type (params) (ret (con Unit)) (effects IO)))
|
||||||
|
|||||||
@@ -1,26 +0,0 @@
|
|||||||
; Iter it.2 positive (spec D1): foldl-shape accumulator. `go` recurses
|
|
||||||
; on the Cons-tail `t` at position 0 (structurally guarded) while
|
|
||||||
; threading an unconstrained accumulator `acc` at position 1. The
|
|
||||||
; accumulator position is never examined by the guardedness check, so
|
|
||||||
; this classifies as structural recursion: pure, total, Diverge-free,
|
|
||||||
; with NO `tail-app` marker. This is the single most LLM-natural
|
|
||||||
; iteration shape and must stay structural (spec D1).
|
|
||||||
|
|
||||||
(module struct_rec_foldl_sum
|
|
||||||
|
|
||||||
(data IntList
|
|
||||||
(ctor INil)
|
|
||||||
(ctor ICons (con Int) (con IntList)))
|
|
||||||
|
|
||||||
(fn go
|
|
||||||
(doc "Sum via a foldl-shape accumulator; structural on the tail.")
|
|
||||||
(type
|
|
||||||
(fn-type
|
|
||||||
(params (con IntList) (con Int))
|
|
||||||
(ret (con Int))))
|
|
||||||
(params xs acc)
|
|
||||||
(body
|
|
||||||
(match xs
|
|
||||||
(case (pat-ctor INil) acc)
|
|
||||||
(case (pat-ctor ICons h t)
|
|
||||||
(app go t (app + acc h)))))))
|
|
||||||
@@ -1,24 +0,0 @@
|
|||||||
; Iter it.2 positive: structural list length. `len` recurses on the
|
|
||||||
; Cons-tail `t` (a constructor sub-component of `xs`) via a plain,
|
|
||||||
; non-tail `(app len t)`. Structurally guarded at position 0, pure,
|
|
||||||
; total, Diverge-free. No `tail-app` marker — this is the structural
|
|
||||||
; recursion form the it.2 guardedness check must accept on its own.
|
|
||||||
|
|
||||||
(module struct_rec_list_len
|
|
||||||
|
|
||||||
(data IntList
|
|
||||||
(ctor INil)
|
|
||||||
(ctor ICons (con Int) (con IntList)))
|
|
||||||
|
|
||||||
(fn len
|
|
||||||
(doc "Length of an IntList via structural recursion on the tail.")
|
|
||||||
(type
|
|
||||||
(fn-type
|
|
||||||
(params (con IntList))
|
|
||||||
(ret (con Int))))
|
|
||||||
(params xs)
|
|
||||||
(body
|
|
||||||
(match xs
|
|
||||||
(case (pat-ctor INil) 0)
|
|
||||||
(case (pat-ctor ICons h t)
|
|
||||||
(app + 1 (app len t)))))))
|
|
||||||
@@ -1,41 +0,0 @@
|
|||||||
; Iter it.2 Phase-3 e2e: an it.2-clean structural recursion that
|
|
||||||
; actually RUNS. `sum` recurses on the Cons-tail `t` via a plain,
|
|
||||||
; non-tail `(app sum t)` — structurally guarded ⇒ the it.2 check
|
|
||||||
; classifies it pure + total, so it carries NO `!Diverge` and uses
|
|
||||||
; NO `tail-app` marker. This proves the structural-recursion
|
|
||||||
; classification is behaviourally sound end-to-end (the "total"
|
|
||||||
; verdict is not just a typecheck assertion): build [1,2,3,4,5],
|
|
||||||
; sum it structurally, print 15.
|
|
||||||
|
|
||||||
(module struct_rec_sum_e2e
|
|
||||||
|
|
||||||
(data IntList
|
|
||||||
(ctor INil)
|
|
||||||
(ctor ICons (con Int) (con IntList)))
|
|
||||||
|
|
||||||
(fn sum
|
|
||||||
(doc "Structural sum: plain non-tail recursion on the Cons tail. No !Diverge, no tail-app.")
|
|
||||||
(type
|
|
||||||
(fn-type
|
|
||||||
(params (con IntList))
|
|
||||||
(ret (con Int))))
|
|
||||||
(params xs)
|
|
||||||
(body
|
|
||||||
(match xs
|
|
||||||
(case (pat-ctor INil) 0)
|
|
||||||
(case (pat-ctor ICons h t)
|
|
||||||
(app + h (app sum t))))))
|
|
||||||
|
|
||||||
(fn main
|
|
||||||
(doc "Build [1,2,3,4,5] and print its structural sum. Expected stdout: 15.")
|
|
||||||
(type (fn-type (params) (ret (con Unit)) (effects IO)))
|
|
||||||
(params)
|
|
||||||
(body
|
|
||||||
(app print
|
|
||||||
(app sum
|
|
||||||
(term-ctor IntList ICons 1
|
|
||||||
(term-ctor IntList ICons 2
|
|
||||||
(term-ctor IntList ICons 3
|
|
||||||
(term-ctor IntList ICons 4
|
|
||||||
(term-ctor IntList ICons 5
|
|
||||||
(term-ctor IntList INil)))))))))))
|
|
||||||
@@ -1,43 +0,0 @@
|
|||||||
; Iter it.2 positive (spec D1, DD-3): mutual structural recursion
|
|
||||||
; over one ADT family. `Tree` references `Forest` (the `Node` field)
|
|
||||||
; and `Forest` references `Tree` and `Forest` (the `Cons` fields), so
|
|
||||||
; the union-find of the ADT type-reference graph puts {Tree, Forest}
|
|
||||||
; in a single connected component. `tree_size` recurses into
|
|
||||||
; `forest_size` on the `Node`-bound `f` (structurally smaller); each
|
|
||||||
; `forest_size` self/cross call passes a `Cons`-bound sub-component.
|
|
||||||
; The whole mutual group is structural: pure, total, Diverge-free,
|
|
||||||
; no `tail-app` markers.
|
|
||||||
|
|
||||||
(module struct_rec_tree_forest
|
|
||||||
|
|
||||||
(data Tree
|
|
||||||
(ctor Node (con Int) (con Forest)))
|
|
||||||
|
|
||||||
(data Forest
|
|
||||||
(ctor FNil)
|
|
||||||
(ctor FCons (con Tree) (con Forest)))
|
|
||||||
|
|
||||||
(fn tree_size
|
|
||||||
(doc "Node count of a tree; recurses into forest_size on the Node forest.")
|
|
||||||
(type
|
|
||||||
(fn-type
|
|
||||||
(params (con Tree))
|
|
||||||
(ret (con Int))))
|
|
||||||
(params tr)
|
|
||||||
(body
|
|
||||||
(match tr
|
|
||||||
(case (pat-ctor Node v f)
|
|
||||||
(app + 1 (app forest_size f))))))
|
|
||||||
|
|
||||||
(fn forest_size
|
|
||||||
(doc "Node count of a forest; mutual with tree_size, self on the tail.")
|
|
||||||
(type
|
|
||||||
(fn-type
|
|
||||||
(params (con Forest))
|
|
||||||
(ret (con Int))))
|
|
||||||
(params fo)
|
|
||||||
(body
|
|
||||||
(match fo
|
|
||||||
(case (pat-ctor FNil) 0)
|
|
||||||
(case (pat-ctor FCons t rest)
|
|
||||||
(app + (app tree_size t) (app forest_size rest)))))))
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
{"defs":[{"body":{"args":[{"binders":[{"init":{"lit":{"kind":"int","value":0},"t":"lit"},"name":"acc","type":{"k":"con","name":"Int"}},{"init":{"lit":{"kind":"int","value":1},"t":"lit"},"name":"i","type":{"k":"con","name":"Int"}}],"body":{"cond":{"args":[{"name":"i","t":"var"},{"lit":{"kind":"int","value":10},"t":"lit"}],"fn":{"name":">","t":"var"},"t":"app"},"else":{"args":[{"args":[{"name":"acc","t":"var"},{"name":"i","t":"var"}],"fn":{"name":"+","t":"var"},"t":"app"},{"args":[{"name":"i","t":"var"},{"lit":{"kind":"int","value":1},"t":"lit"}],"fn":{"name":"+","t":"var"},"t":"app"}],"t":"recur"},"t":"if","then":{"name":"acc","t":"var"}},"t":"loop"}],"fn":{"name":"print","t":"var"},"t":"app"},"doc":"Loop-bearing fn that omits !Diverge from its effect row — must fire undeclared-effect.","kind":"fn","name":"main","params":[],"type":{"effects":["IO"],"k":"fn","params":[],"ret":{"k":"con","name":"Unit"}}}],"imports":[],"name":"test_loop_missing_diverge","schema":"ailang/v0"}
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
{"defs":[{"ctors":[{"fields":[],"name":"LNil"},{"fields":[{"k":"con","name":"Int"},{"k":"con","name":"MyList"}],"name":"LCons"}],"kind":"type","name":"MyList"},{"ctors":[{"fields":[],"name":"TLeaf"},{"fields":[{"k":"con","name":"MyTree"},{"k":"con","name":"MyTree"}],"name":"TBranch"}],"kind":"type","name":"MyTree"},{"body":{"arms":[{"body":{"lit":{"kind":"int","value":0},"t":"lit"},"pat":{"ctor":"LNil","fields":[],"p":"ctor"}},{"body":{"args":[{"name":"h","t":"var"},{"args":[{"args":[{"name":"t","t":"var"}],"fn":{"name":"mk_tree","t":"var"},"t":"app"}],"fn":{"name":"g","t":"var"},"t":"app"}],"fn":{"name":"+","t":"var"},"t":"app"},"pat":{"ctor":"LCons","fields":[{"name":"h","p":"var"},{"name":"t","p":"var"}],"p":"ctor"}}],"scrutinee":{"name":"xs","t":"var"},"t":"match"},"doc":"f recurses into g passing a List sub-component, but g's structural param is an unrelated Tree family.","kind":"fn","name":"f","params":["xs"],"type":{"effects":[],"k":"fn","params":[{"k":"con","name":"MyList"}],"ret":{"k":"con","name":"Int"}}},{"body":{"args":[],"ctor":"TLeaf","t":"ctor","type":"MyTree"},"kind":"fn","name":"mk_tree","params":["xs"],"type":{"effects":[],"k":"fn","params":[{"k":"con","name":"MyList"}],"ret":{"k":"con","name":"MyTree"}}},{"body":{"arms":[{"body":{"lit":{"kind":"int","value":0},"t":"lit"},"pat":{"ctor":"TLeaf","fields":[],"p":"ctor"}},{"body":{"args":[{"args":[{"name":"l","t":"var"}],"fn":{"name":"g","t":"var"},"t":"app"},{"args":[{"args":[],"ctor":"LNil","t":"ctor","type":"MyList"}],"fn":{"name":"f","t":"var"},"t":"app"}],"fn":{"name":"+","t":"var"},"t":"app"},"pat":{"ctor":"TBranch","fields":[{"name":"l","p":"var"},{"name":"r","p":"var"}],"p":"ctor"}}],"scrutinee":{"name":"tr","t":"var"},"t":"match"},"doc":"g recurses into f, but its own structural param is a Tree (different ADT family).","kind":"fn","name":"g","params":["tr"],"type":{"effects":[],"k":"fn","params":[{"k":"con","name":"MyTree"}],"ret":{"k":"con","name":"Int"}}}],"imports":[],"name":"test_mutual_cross_family","schema":"ailang/v0"}
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
{"defs":[{"ctors":[{"fields":[],"name":"INil"},{"fields":[{"k":"con","name":"Int"},{"k":"con","name":"IntList"}],"name":"ICons"}],"kind":"type","name":"IntList"},{"body":{"arms":[{"body":{"lit":{"kind":"int","value":0},"t":"lit"},"pat":{"ctor":"INil","fields":[],"p":"ctor"}},{"body":{"args":[{"name":"h","t":"var"},{"args":[{"name":"xs","t":"var"}],"fn":{"name":"f","t":"var"},"t":"app"}],"fn":{"name":"+","t":"var"},"t":"app"},"pat":{"ctor":"ICons","fields":[{"name":"h","p":"var"},{"name":"t","p":"var"}],"p":"ctor"}}],"scrutinee":{"name":"xs","t":"var"},"t":"match"},"doc":"Non-structural: recurses on the SAME xs, not a sub-component. Not tail-marked.","kind":"fn","name":"f","params":["xs"],"type":{"effects":[],"k":"fn","params":[{"k":"con","name":"IntList"}],"ret":{"k":"con","name":"Int"}}}],"imports":[],"name":"test_non_structural_recursion","schema":"ailang/v0"}
|
|
||||||
@@ -1,60 +0,0 @@
|
|||||||
{
|
|
||||||
"schema": "ailang/v0",
|
|
||||||
"name": "test_recur_arity_mismatch",
|
|
||||||
"imports": [],
|
|
||||||
"defs": [
|
|
||||||
{
|
|
||||||
"kind": "fn",
|
|
||||||
"name": "main",
|
|
||||||
"type": {
|
|
||||||
"k": "fn",
|
|
||||||
"params": [],
|
|
||||||
"ret": {
|
|
||||||
"k": "con",
|
|
||||||
"name": "Int"
|
|
||||||
},
|
|
||||||
"effects": []
|
|
||||||
},
|
|
||||||
"params": [],
|
|
||||||
"doc": "Iter it.1: recur passing 2 args to a 1-binder loop fires recur-arity-mismatch.",
|
|
||||||
"body": {
|
|
||||||
"t": "loop",
|
|
||||||
"binders": [
|
|
||||||
{
|
|
||||||
"name": "i",
|
|
||||||
"type": {
|
|
||||||
"k": "con",
|
|
||||||
"name": "Int"
|
|
||||||
},
|
|
||||||
"init": {
|
|
||||||
"t": "lit",
|
|
||||||
"lit": {
|
|
||||||
"kind": "int",
|
|
||||||
"value": 0
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"body": {
|
|
||||||
"t": "recur",
|
|
||||||
"args": [
|
|
||||||
{
|
|
||||||
"t": "lit",
|
|
||||||
"lit": {
|
|
||||||
"kind": "int",
|
|
||||||
"value": 1
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"t": "lit",
|
|
||||||
"lit": {
|
|
||||||
"kind": "int",
|
|
||||||
"value": 2
|
|
||||||
}
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
@@ -1,60 +0,0 @@
|
|||||||
{
|
|
||||||
"schema": "ailang/v0",
|
|
||||||
"name": "test_recur_not_in_tail_position",
|
|
||||||
"imports": [],
|
|
||||||
"defs": [
|
|
||||||
{
|
|
||||||
"kind": "fn",
|
|
||||||
"name": "main",
|
|
||||||
"type": {
|
|
||||||
"k": "fn",
|
|
||||||
"params": [],
|
|
||||||
"ret": {
|
|
||||||
"k": "con",
|
|
||||||
"name": "Int"
|
|
||||||
},
|
|
||||||
"effects": []
|
|
||||||
},
|
|
||||||
"params": [],
|
|
||||||
"doc": "Iter it.1: recur in the lhs of a seq (non-tail) fires recur-not-in-tail-position.",
|
|
||||||
"body": {
|
|
||||||
"t": "loop",
|
|
||||||
"binders": [
|
|
||||||
{
|
|
||||||
"name": "i",
|
|
||||||
"type": {
|
|
||||||
"k": "con",
|
|
||||||
"name": "Int"
|
|
||||||
},
|
|
||||||
"init": {
|
|
||||||
"t": "lit",
|
|
||||||
"lit": {
|
|
||||||
"kind": "int",
|
|
||||||
"value": 0
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"body": {
|
|
||||||
"t": "seq",
|
|
||||||
"lhs": {
|
|
||||||
"t": "recur",
|
|
||||||
"args": [
|
|
||||||
{
|
|
||||||
"t": "var",
|
|
||||||
"name": "i"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"rhs": {
|
|
||||||
"t": "lit",
|
|
||||||
"lit": {
|
|
||||||
"kind": "int",
|
|
||||||
"value": 0
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
@@ -1,34 +0,0 @@
|
|||||||
{
|
|
||||||
"schema": "ailang/v0",
|
|
||||||
"name": "test_recur_outside_loop",
|
|
||||||
"imports": [],
|
|
||||||
"defs": [
|
|
||||||
{
|
|
||||||
"kind": "fn",
|
|
||||||
"name": "main",
|
|
||||||
"type": {
|
|
||||||
"k": "fn",
|
|
||||||
"params": [],
|
|
||||||
"ret": {
|
|
||||||
"k": "con",
|
|
||||||
"name": "Int"
|
|
||||||
},
|
|
||||||
"effects": []
|
|
||||||
},
|
|
||||||
"params": [],
|
|
||||||
"doc": "Iter it.1: a recur with no enclosing loop fires recur-outside-loop.",
|
|
||||||
"body": {
|
|
||||||
"t": "recur",
|
|
||||||
"args": [
|
|
||||||
{
|
|
||||||
"t": "lit",
|
|
||||||
"lit": {
|
|
||||||
"kind": "int",
|
|
||||||
"value": 1
|
|
||||||
}
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
@@ -1,53 +0,0 @@
|
|||||||
{
|
|
||||||
"schema": "ailang/v0",
|
|
||||||
"name": "test_recur_type_mismatch",
|
|
||||||
"imports": [],
|
|
||||||
"defs": [
|
|
||||||
{
|
|
||||||
"kind": "fn",
|
|
||||||
"name": "main",
|
|
||||||
"type": {
|
|
||||||
"k": "fn",
|
|
||||||
"params": [],
|
|
||||||
"ret": {
|
|
||||||
"k": "con",
|
|
||||||
"name": "Int"
|
|
||||||
},
|
|
||||||
"effects": []
|
|
||||||
},
|
|
||||||
"params": [],
|
|
||||||
"doc": "Iter it.1: recur passing a Bool to an Int binder fires recur-type-mismatch.",
|
|
||||||
"body": {
|
|
||||||
"t": "loop",
|
|
||||||
"binders": [
|
|
||||||
{
|
|
||||||
"name": "i",
|
|
||||||
"type": {
|
|
||||||
"k": "con",
|
|
||||||
"name": "Int"
|
|
||||||
},
|
|
||||||
"init": {
|
|
||||||
"t": "lit",
|
|
||||||
"lit": {
|
|
||||||
"kind": "int",
|
|
||||||
"value": 0
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"body": {
|
|
||||||
"t": "recur",
|
|
||||||
"args": [
|
|
||||||
{
|
|
||||||
"t": "lit",
|
|
||||||
"lit": {
|
|
||||||
"kind": "bool",
|
|
||||||
"value": true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
@@ -104,7 +104,7 @@ A feature ships only if all three hold:
|
|||||||
Aesthetic appeal does not count. Human ergonomics do not count. If
|
Aesthetic appeal does not count. Human ergonomics do not count. If
|
||||||
the answer to any of the three is "I'm not sure", the feature is
|
the answer to any of the three is "I'm not sure", the feature is
|
||||||
probably not ready. The full criterion (with the canonical worked
|
probably not ready. The full criterion (with the canonical worked
|
||||||
example — the iteration story) lives in `docs/DESIGN.md`
|
example — see the criterion text) lives in `docs/DESIGN.md`
|
||||||
("Feature-acceptance criterion"); this skill is the gate that
|
("Feature-acceptance criterion"); this skill is the gate that
|
||||||
applies it during spec writing.
|
applies it during spec writing.
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user