Second tranche of the contracts-against-code audit. Two threads, both applied conservatively under the over-correction guards in docs_honesty_pin.rs (the self-labelled tiebreaker in 0008 and the Diverge-reserved anchor in 0010 are deliberate honest content and were left untouched; design rationale that explains a present-state design principle — semantic-locality, the reuse-as-wrapper reasons — was also preserved). Honesty-rule (0007): demote clear change/deletion narration to present tense. - 0008: drop "they were promoted from ... to ... Recorded here so"; strip the "Iter A"/"Iter B" iteration labels (the descriptive titles carry the meaning); drop "(no longer a carve-out)". - 0001: "were rewired to use ... was deleted at the same time" -> present tense. (The substance was already correct: `pretty.rs` holds only the diagnostic helpers; an audit agent had misread the line as "pretty.rs was deleted" — the file exists, the printer code does not.) - 0012: drop "Per the tail-call survey of existing fixtures" and "Migration of existing fixtures is partial" -> present-state description of which corpus fixtures carry the tail marker. Ratifier integrity: a contract that names a test which does not ratify it is itself a form of the dishonesty this ledger forbids. - 0014 named `bench/architect_sweeps.sh`, which sweeps honesty-anchors and ratifies none of the six verification mechanisms; and claim 5 cited `tests/expected/`, which never existed (git log empty). Point claim 5 at the real golden mechanism (`crates/ail/tests/snapshots/` via `ir_snapshot.rs`) and the footer at each mechanism's actual test. - 0015's four constraints are guaranteed by absence (no thunk/`ref`/ `IORef` node, non-recursive `let`); the named uniqueness in-source tests only count RC consumes, never the constraints. State the by-construction guarantee and point the ratifier at `ast.rs` (the single source of truth for which nodes exist). - INDEX ratifying-test column updated for both to match. All ledger pins green (docs_honesty_pin, design_index_pin incl. every_contract_names_a_resolvable_ratifying_test, effect_doc_honesty_pin, carve_out_inventory); architect honesty sweep clean. Deferred, recommend-only: 0016-method-dispatch carries no invariant absent from 0013 (merge candidate), but a contract-file merge touches INDEX, the retired-counter convention, and cross-refs — a structural call left for explicit direction. Minor history phrasing in 0008's Type::Con.name hash-shift paragraph (§"FnDef.suppress") also left.
3.1 KiB
Tail calls
Explicit, verified tail calls
For an LLM author, recursion is the natural iteration form
(\n. if n == 0 then () else loop(n-1) is what I reach for, not
a for-loop). Without a tail-call guarantee, every recursive
program has a silent stack-depth ceiling that no compile-time
diagnostic warns about. That is exactly the class of correctness
hazard the language exists to eliminate.
Solution: explicit, verified tail calls.
- AST.
Term::App { fn, args, tail: bool }andTerm::Do { op, args, tail: bool }(see Data model) gain atailflag, serde-defaulting tofalseso existing fixtures load withtail: falseand their hashes stay bit-identical. - Typecheck. A new pass
verify_tail_positions(fn_body)runs after the main type-check. It walks the body with anis_tail_context: boolthreaded down. The flag istrueat the start and propagates to every sub-term that is the value of the whole expression: the body of everyTerm::Matcharm, both branches ofTerm::If, the right operand ofTerm::Seq, the body ofTerm::Letand the in-clause ofTerm::LetRec, and the body ofTerm::Lam(each Lam opens its own tail scope); the identity nodesTerm::Clone/Term::ReuseAsand aTerm::Loopbody propagate the enclosing flag unchanged. The flag isfalsefor every argument position: args of anyApp/Do/Ctor/Recur/New, the scrutinee ofMatch, the condition ofIf, the left operand ofSeq, and the value of aLet/LetRecbinding. When the walker visits anApp { tail: true }orDo { tail: true }, the flag must betrueat that visit; otherwise emit diagnostictail-call-not-in-tail-position. - Codegen. Emit
musttail call(LLVM IR) for marked calls instead of plaincall. LLVM rejects at IR-verification time if the call cannot physically be a tail call (calling convention mismatch, signature divergence, etc.). The reject surfaces as a hard build error, not a silent runtime surprise. - Form (A). Two new keywords:
tail-app,tail-do. Productions are positional analogues ofapp/dowithtail: trueset on the resulting term. EBNF gains 2 lines; total production count goes from ~28 to ~30, still inside the constraint-1 budget (see authoring surface).
What this does NOT promise. Many recursive calls are not in
tail position because they are arguments to constructor calls (e.g.
Cons (f h) (map f t)). The current pipeline adds annotation + verification;
it does not add a CPS transform or accumulator-form
rewrite. Programs whose recursion is constructor-blocked
will continue to be stack-bounded by recursion depth. The
canonical authoring pattern in such cases is to write the
accumulator-form variant (map_acc, fold_left, etc.)
explicitly. The stdlib ships both forms where
relevant.
In the corpus, only terminal recursions (print_list-style)
carry the tail marker; the constructor-blocked recursions in
map, sort, insert are unmarked — they cannot benefit from
musttail without a source-level rewrite.
Ratified by: crates/ailang-check/src/lib.rs.