Five polymorphic prelude free fns (ne/lt/le/gt/ge) plus three E2E fixtures: positive composition over Int/Bool/Str, user-ADT with user-written Eq/Ord on IntBox, and Float-NoInstance with a Float-aware diagnostic addendum cross-referencing DESIGN.md §"Float semantics". Two checker-side fixes were needed alongside the prelude additions, both symmetric to the iter 23.4-prep visibility fix: - linearity::check_module_with_visible — workspace-aware callee-mode resolution so a Borrow-mode user fn forwarding into a prelude poly fn (e.g. `at_most x y = not (gt x y)`) doesn't false-fire consume-while-borrowed. - mono::collect_mono_targets — distinguish rigid forall vars from unbound metavars; rigid-bearing free-fn targets skip (they get re-collected with concrete subst when the enclosing fn itself monomorphises). Without this, a polymorphic body calling another polymorphic free fn produced a spurious __Unit specialisation whose body referenced compare at Unit. Roadmap split: shipped Eq/Ord half checked, Show + print-rewire half remains pending behind the heap-Str ABI prerequisite. DESIGN.md §"Prelude classes" amended. One pre-existing fixture (test_22b1_missing_method) renamed its class method ne → tne to avoid collision with the new prelude `ne`. Bench: check.py + cross_lang.py exit 0; compile_check.py exits 1 with one sub-ms check_ms.local_rec_capture at +26% (tolerance 25%) — prelude-growth-related noise-band fluctuation, ratifiable per spec §248-249 and journal Concerns.
9.8 KiB
iter 23.5 — Prelude free fns + E2E (Eq/Ord milestone close)
Date: 2026-05-12
Started from: 35c6eb5
Status: DONE
Tasks completed: 8 of 8
Summary
Closes milestone 23. Ships the five polymorphic prelude free fns
ne / lt / le / gt / ge (Tasks 1-2), three end-to-end
fixtures covering primitive composition + user-ADT integration +
the Float negative case (Tasks 3-5), the Float-aware NoInstance
diagnostic addendum (Task 5), the DESIGN.md §"Prelude classes"
amendment (Task 6), and the roadmap split between shipped Eq/Ord
and the pending Show + print-rewire half (Task 7). Bench check
runs clean except a noisy sub-millisecond check_ms.local_rec_capture
regression that sits just over the 25% tolerance — ratifiable per
the audit-tolerance rule (the prelude grew by 5 polymorphic
Def::Fns, so the typecheck workload per workspace measurably
shifts).
Two implementer-phase fixes were needed beyond the plan, both
symmetric to existing precedent (iter 23.4-prep). They surfaced
when the natural test fixtures (user-defined at_most x y = not (gt x y) composing prelude poly fns) hit two distinct false-positive
shapes:
-
Linearity check needed workspace-wide visibility for imported polymorphic free fns + class methods. Previously the linearity pass only saw the current module's globals, so a user
at_mostforwarding its borrow params intogt(defined in the prelude) hitcallee_arg_modes("gt", 2) → []→ default-Consume →consume-while-borrowedfalse positive. Fix extendslinearity::check_moduleto acheck_module_with_visibleentry that takes additional Module references whose top-level fns- class methods are registered in globals; the workspace-side driver passes all sibling modules. Pure precedent extension of 23.4-prep, which added class-method visibility for the same reason.
-
Mono pass needed to skip rigid-var-bearing free-fn targets. When a polymorphic free fn's body calls another polymorphic free fn (e.g.
at_most's body callsgt x ywith x, y of type rigida), the synth-channel observer recorded a FreeFnCall whose type args were rigidType::Var { name: "a" }. The existing Unit-default fallback (correct foris_empty(Nil)-shape unobservable metavars) was unsound here: it producedgt__Unitwhose body referencedcompare, which has no Unit instance → "unknown variable: compare" at the synthesised body. Fix discriminates rigid vars (Type::Var without$mprefix) from unbound metavars ($mprefix) — rigids skip (they get re-collected with concrete substitution when the enclosing fn itself monomorphises), metavars keep their Unit-default.
One existing fixture (examples/test_22b1_missing_method.ail.json)
had to rename its class method ne → tne because ne is now a
top-level prelude free fn and the workspace registry's
method-name uniqueness check rejected the collision. The
fixture's docstring already foreshadowed this case ("Class is
TEq rather than Eq to avoid colliding…"); extended to also
cover the ne → tne rename rationale.
Per-task notes
- iter 23.5.1:
nefree fn — addedDef::Fn neto prelude (bodynot (eq x y)); createdcrates/ail/tests/prelude_free_fns.rswith helpersfixture()andmono_symbol_names()+ first test; smoke fixtureexamples/ne_at_int_smoke.ail.json. RED→GREEN. Adjustedmono.modules.iter()→.values()per the actualBTreeMap<String, Module>shape (Boss pre-flight flagged). - iter 23.5.2:
lt/le/gt/gefree fns — fourDef::Fns in prelude (each pattern-matches oncompare x yagainst one of LT/GT plus a wildcard); four extending tests in the sameprelude_free_fns.rs; four smoke fixtures. RED→GREEN per fn. - iter 23.5.3: positive E2E —
examples/eq_ord_polymorphic.ail.jsonwith user-definedat_most a a → Boolcomposing preludenotgt;crates/ail/tests/eq_ord_e2e.rswith helpersfixture_path()+build_and_run()pluseq_ord_polymorphic_runs_end_to_end. Two implementer-phase fixes (linearity + mono rigid-skip) landed during this task to unblock the GREEN. Final stdout pinned at"1\n0\n1"(matchesio/print_int's actual newline-separator behaviour; plan text had"101"based on an incorrect concatenation assumption).
- iter 23.5.4: user-ADT E2E —
examples/eq_ord_user_adt.ail.jsonwithtype IntBox = MkIntBox Int+ user-writteninstance Eq IntBox+instance Ord IntBox+maincallingeqtwice. Two extending tests:eq_ord_user_adt_runs_end_to_end(stdout"1\n0") andeq_ord_user_adt_eq_intbox_hash_stable(record-then-pin). Mono symbol name iseq__cde77856(8-hex-prefix mangling for compound types per DESIGN.md §"Mangling scheme"); body hash pinned at9daaffa7528d2a1c. The Ord-IntBox instance's retType needed qualifying asprelude.Orderingper the canonical-type-names rule. - iter 23.5.5: Float-aware
NoInstanceaddendum — implemented inCheckError::to_diagnostic()(the per-iter spec-§"NoInstance" conversion path; option-a from the plan's structural-choice list). Addendum fires whenclass ∈ {Eq, Ord}ANDat_type == "Float": appends— Float has no Eq/Ord instance by design (partial orderability per IEEE-754); see DESIGN.md §"Float semantics".Newexamples/eq_float_noinstance.ail.json+crates/ail/tests/eq_float_noinstance.rs. RED→GREEN. - iter 23.5.6: DESIGN.md amendment — inserted Milestone-23 amendment paragraph in §"Prelude (built-in) classes". Grep gate RED→GREEN.
- iter 23.5.7: roadmap split — replaced the single Post-22
Prelude entry with two:
[x]Eq/Ord (shipped 23.5) and[ ]Show + print rewire (gated on heap-StrABI). Also disambiguated thedepends on: Post-22 Preludereference at line 73 to point specifically at the Eq/Ord half (Op routing's actual dependency). - iter 23.5.8: bench gate —
bench/check.pyexit 0 (clean),bench/compile_check.pyexit 1 (one sub-mscheck_msregression at the tolerance line — see Concerns),bench/cross_lang.pyexit 0 (clean).
Concerns
bench/compile_check.pyexited 1 with one regression:check_ms.local_rec_capturemeasured +26.36% over baseline (1.1ms → 1.4ms, tolerance 25.0%). This is a sub-millisecond noise-band metric, and the prelude grew by 5 polymorphicDef::Fns in this iter so a measurable typecheck-workload shift is expected per the Boss-noted audit tolerance. Two other check_ms metrics in the first run (borrow_own_demo,bench_list_sum) crossed the line on one sample but stayed inside on the rerun, confirming this is noise-band fluctuation rather than a structural regression. Recommend ratifying at iter close via--update-baseline; flagged as a Boss judgement call.- The mono-pass rigid-var skip (Task 3 inline fix) preserves
the existing Unit-default for unbound metavars exactly (the
is_empty(Nil)shape from iter 23.4.8 is untouched). The discrimination is by name prefix ($m) which is the same conventionSubst::meta_iduses; a future schema refactor that drops the prefix convention would silently break the skip rule. Code comment names this. - The linearity-side
check_module(&Module)standalone entry is now#[cfg(test)]-gated because production code usescheck_module_with_visible(m, &visible_extra)instead. Unit tests still call the no-extra path. Acceptable; flagging in case a future caller wants the standalone form back.
Known debt
- The
eq_ord_user_adtmono-symbol name (eq__cde77856) is pinned by literal string match in the test. If the canonical type-bytes encoding forIntBoxever changes (e.g. via a new schema field onTypeDef), the test fails with a clear type-mangling-regression message. That is the intended drift alarm; no immediate debt.
Files touched
crates/ailang-check/src/lib.rs— Float-awareNoInstanceaddendum into_diagnostic(); linearity call site updated to pass workspace siblings as visible-extra.crates/ailang-check/src/linearity.rs— newcheck_module_with_visible(m, visible_extra)entry;check_module(&Module)made test-only.crates/ailang-check/src/mono.rs— free-fn target collection now distinguishes rigidType::Varfrom$m-prefixed metavars; rigid-bearing targets are skipped; new helpercontains_rigid_var.crates/ailang-core/src/workspace.rs— docstring update on theiter22b1_missing_methodtest naming the newnereason for theTEq/tnerename.crates/ail/tests/prelude_free_fns.rs(new) — five workspace-level tests, one per prelude free fn, plus helpers.crates/ail/tests/eq_ord_e2e.rs(new) — three E2E tests: polymorphic composition, user-ADT integration, mono-symbol hash stability.crates/ail/tests/eq_float_noinstance.rs(new) — Float-aware NoInstance pin.examples/prelude.ail.json— five newDef::Fnentries (ne / lt / le / gt / ge).examples/ne_at_int_smoke.ail.json(new),lt_at_int_smoke.ail.json(new),le_at_str_smoke.ail.json(new),gt_at_bool_smoke.ail.json(new),ge_at_int_smoke.ail.json(new) — five workspace-level smoke fixtures.examples/eq_ord_polymorphic.ail.json(new) — positive E2E: polymorphic helper composing prelude fns at Int / Bool / Str.examples/eq_ord_user_adt.ail.json(new) — user-ADT E2E:IntBoxwith user-written Eq + Ord instances.examples/eq_float_noinstance.ail.json(new) — negative-Float fixture firing the addended diagnostic.examples/test_22b1_missing_method.ail.json—ne → tnerename to avoid collision with the new preludenefree fn.docs/DESIGN.md— Milestone-23 amendment paragraph inserted in §"Prelude (built-in) classes".docs/roadmap.md— Post-22 Prelude entry split into shipped Eq/Ord + pending Show; Op-routing depends-on reference disambiguated.
Stats
bench/orchestrator-stats/2026-05-12-iter-23.5.json