check: mode-strict-because suppression (iter 19b)

Closes the 19a/19a.1/19b arc. Corpus signal from 19a.1 (5/65
fixtures fire over-strict-mode, all deliberate RC codegen-test
fixtures) justified shipping the suppress mechanism end-to-end.

Schema: Suppress { code, because } on FnDef. Pre-19b hashes
bit-identical via skip_serializing_if. Typechecker drops matching
diagnostics; empty 'because' is Error severity; wrong codes are
silent no-ops (open-set registry).

Form-A: (suppress (code "...") (because "...")) clause, parser
+ printer round-trip clean. Form-B: '// @suppress <code>: <reason>'
above the doc string, lossless contract metadata.

5 RC fixtures migrated (.ail.json + .ailx + .prose.txt). Corpus
signal: 5/65 -> 0/65. Lint still fires on any future fn that's
accidentally over-strict without an authored reason.

Test counts: ailang-check 55->61, ailang-core 26->28, ailang-surface
21->26, ailang-prose 49->52, e2e 70 unchanged.

Known debt: .ailx comment headers lost on regen (ail render's
contract excludes comments); parse_suppress_attr accepts
duplicate code/because attrs without diagnose (bounded by canonical
print order).
This commit is contained in:
2026-05-08 19:36:04 +02:00
parent 9f3f10ce6e
commit 50b68267fe
30 changed files with 942 additions and 304 deletions
+10 -76
View File
@@ -1,93 +1,27 @@
; Iter 18e fixture: `(drop-iterative)` opt-in annotation drives the
; codegen to emit `drop_<m>_IntList` with a worklist body in place
; of the recursive cascade. Without the annotation, freeing a long
; list overflows Linux's default 8MB stack at ~1M cells (one frame
; per recursive `drop_<m>_IntList(tail)` call). With it, the chain
; pops through the heap-allocated worklist in O(N) time and O(1)
; stack.
;
; The fixture builds a 100,000-cell IntList tail-recursively, sums
; it into 4_999_950_000 (= N*(N-1)/2 for N=100_000), then `main`
; returns. At process exit the build's `xs` binder is consumed by
; sum_acc (via tail-call), so the iterative drop fires inside
; sum_acc's `Cons` arm — specifically, on every recursive step the
; tail t is bound, then t is consumed by the next iteration's
; tail-app sum_acc, leaving no live cells at termination.
;
; Wait — under the actual emission shape: sum_acc's xs param is
; (own (con IntList)); 18d.4 emits an Own-param drop at fn return.
; On the inductive Cons arm, the fn returns via tail-call into the
; next iteration (musttail), and the Own-param dec on xs at the
; outer fn would normally fire — but tail-call elision means the
; outer frame is gone before any post-tail-call code can run. The
; canonical case for the iterative-drop test is therefore the
; let-close-drop on `xs` in `main` (or the outer-let's drop after
; sum's tail returns). For 18e the load-bearing property is "long
; chains drop without overflowing the stack"; that fires whenever
; ANY drop call against the head IntList runs end-to-end.
;
; To force the issue: `main` builds the list, sums it, prints the
; sum, AND then `let xs = build n in let _ = sum xs in xs` does NOT
; exist as a pattern in AILang. We instead route the list through a
; `head` fn that takes (own IntList) and returns the head Int —
; which transfers ownership and forces the Own-param drop on xs at
; head's return. Because head's body (a match returning Int) cannot
; be tail-called, the drop is emitted in head's epilogue and runs
; in full before head returns to main.
;
; Expected stdout: `1` (the head of [1, 2, ..., 1_000_000]).
; Why 1M and not 100K: at 100K the recursive cascade fits in the 8MB
; default Linux stack (~64B/frame ≈ 6.4MB at 100K), so the test would
; "pass" even without the iterative drop — false-negative on the
; whole point of 18e. At 1M it overflows clean (SIGSEGV in the
; recursive variant; clean exit-0 in the iterative variant).
(module rc_drop_iterative_long_list
(data IntList
(ctor INil)
(ctor ICons (con Int) (con IntList))
(drop-iterative))
(fn cons_n_acc
(doc "Tail-recursive list builder. acc-prepended.")
(type
(fn-type
(params (con Int) (con IntList))
(ret (con IntList))))
(type (fn-type (params (con Int) (con IntList)) (ret (con IntList))))
(params n acc)
(body
(if (app == n 0)
acc
(tail-app cons_n_acc
(app - n 1)
(term-ctor IntList ICons n acc)))))
(body (if (app == n 0) acc (tail-app cons_n_acc (app - n 1) (term-ctor IntList ICons n acc)))))
(fn cons_n
(doc "Build [1, 2, ..., n] :: IntList. Order is reverse of build but immaterial for head/sum.")
(type
(fn-type
(params (con Int))
(ret (con IntList))))
(type (fn-type (params (con Int)) (ret (con IntList))))
(params n)
(body
(app cons_n_acc n (term-ctor IntList INil))))
(body (app cons_n_acc n (term-ctor IntList INil))))
(fn head_or_zero
(doc "Take ownership of an IntList; return its head if ICons, else 0. The (own ...) signature signals 18d.4 to emit an Own-param drop at fn return — which under the (drop-iterative) annotation is the iterative-worklist body, freeing the entire chain via the heap-allocated worklist instead of recursive cascade.")
(type
(fn-type
(params (own (con IntList)))
(ret (con Int))))
(suppress (code "over-strict-mode") (because "Iter 18e test: forces (drop-iterative) cascade via Own-param drop at fn return"))
(type (fn-type (params (own (con IntList))) (ret (con Int))))
(params xs)
(body
(match xs
(case (pat-ctor INil) 0)
(case (pat-ctor ICons h t) h))))
(body (match xs
(case (pat-ctor INil) 0)
(case (pat-ctor ICons h t) h))))
(fn main
(type (fn-type (params) (ret (con Unit)) (effects IO)))
(params)
(body
(do io/print_int
(app head_or_zero (app cons_n 1000000))))))
(body (do io/print_int (app head_or_zero (app cons_n 1000000))))))