iter it.2: structural-guardedness checker + first real Diverge effect
Iteration-discipline milestone, 2 of 3. Strictly additive (nothing tail-related removed; that is it.3). New whole-body pass verify_structural_recursion sibling of verify_tail_positions (DD-1): smaller-set algorithm with implicit candidate inference + unconstrained accumulators (DD-2, foldl=structural), self/mutual via inline ADT-family union-find (DD-3), it.2-only tail==false grandfather. CheckError::NonStructuralRecursion. term_contains_loop (stops at Term::Lam, DD-4) injects Diverge so existing UndeclaredEffect enforces it, no new variant; lam-arrow + LetRec sub-effect sites wired. DESIGN.md Decision 3 synced. Four it.1 loop fixtures gained !Diverge. Two spec-premise boundary defects surfaced + resolved within the additive invariant (corpus clean, check not weakened), recorded as corrected it.3 corpus-migration scope: (1) the "21 tail-app fixtures" grandfather premise under-counts the corpus — no-ADT-candidate counter recursions have no structural position to verify, deferred to it.3; (2) two RC-regression fixtures joined the spec's transitional tail-app grandfather as the other 20 do (RC==GC guards verified still green). cargo test --workspace 622/0; 9 acceptance pins non-vacuous. Specfda9b78, planbc9f512.
This commit is contained in:
@@ -1,8 +1,8 @@
|
||||
(module loop_counter
|
||||
|
||||
(fn main
|
||||
(doc "Iter it.1 — sum 1..10 via an accumulator loop. Expected stdout: 55.")
|
||||
(type (fn-type (params) (ret (con Unit)) (effects IO)))
|
||||
(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
|
||||
|
||||
@@ -1,21 +1,27 @@
|
||||
(module loop_in_lambda_e2e
|
||||
|
||||
(fn apply
|
||||
(doc "apply a fn-of-Int to an Int")
|
||||
(type (fn-type (params (fn-type (params (con Int)) (ret (con Int))) (con Int)) (ret (con Int))))
|
||||
(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.")
|
||||
(type (fn-type (params) (ret (con Unit)) (effects IO)))
|
||||
(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)
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
; 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,13 +1,14 @@
|
||||
(module loop_nested_in_lambda
|
||||
|
||||
(fn make_adder
|
||||
(doc "Iter it.1 — a loop inside a lambda body (lambda-boundary analogue).")
|
||||
(type (fn-type (params (con Int)) (ret (fn-type (params (con Int)) (ret (con Int))))))
|
||||
(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)
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
(module loop_smoke
|
||||
|
||||
(fn count_to
|
||||
(doc "Iter it.1 — single counted loop; counts i up to n and returns n.")
|
||||
(type (fn-type (params (con Int)) (ret (con Int))))
|
||||
(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))
|
||||
|
||||
@@ -59,6 +59,14 @@
|
||||
(case (pat-ctor TLeaf) 0)
|
||||
(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
|
||||
(type
|
||||
(fn-type
|
||||
@@ -69,7 +77,7 @@
|
||||
(if (app == n 0)
|
||||
0
|
||||
(let _v (app pin_aliased t)
|
||||
(app loop (app - n 1) t)))))
|
||||
(tail-app loop (app - n 1) t)))))
|
||||
|
||||
(fn main
|
||||
(type (fn-type (params) (ret (con Unit)) (effects IO)))
|
||||
|
||||
@@ -38,6 +38,14 @@
|
||||
(case (pat-ctor TLeaf) 0)
|
||||
(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
|
||||
(type (fn-type (params (con Int) (con Tree)) (ret (con Int))))
|
||||
(params n t)
|
||||
@@ -45,7 +53,7 @@
|
||||
(if (app == n 0)
|
||||
0
|
||||
(let _v (app pin t)
|
||||
(app loop (app - n 1) t)))))
|
||||
(tail-app loop (app - n 1) t)))))
|
||||
|
||||
(fn main
|
||||
(type (fn-type (params) (ret (con Unit)) (effects IO)))
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
; 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)))))))
|
||||
@@ -0,0 +1,24 @@
|
||||
; 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)))))))
|
||||
@@ -0,0 +1,41 @@
|
||||
; 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)))))))))))
|
||||
@@ -0,0 +1,43 @@
|
||||
; 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)))))))
|
||||
@@ -0,0 +1 @@
|
||||
{"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"}
|
||||
@@ -0,0 +1 @@
|
||||
{"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"}
|
||||
@@ -0,0 +1 @@
|
||||
{"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"}
|
||||
Reference in New Issue
Block a user