5 actionable drift items route to 24.tidy (3× [high] + 2× [medium]). 1× [medium] (negative-test coverage breadth) defers to roadmap P3. 1× [low] (bench sweep noise on pre-mq24 DESIGN.md lines) is carry-on. Bench: 9th consecutive observation of metric-identity-migrating noise envelope on latency.*_at_rc.* + bench_list_sum.bump_s + check_ms.* clusters; baseline pristine per conservative-call convention. The right ratification path is the queued P3 latency-histogram methodology rework, not --update-baseline.
12 KiB
audit-24 — milestone close (Show + print rewire)
Date: 2026-05-13
Milestone: Show + print rewire (number-24 family)
Iters covered: 24.1 (f38bad8, shipped 2026-05-12) → 24.2 (3286117) → 24.3 (246b5c7)
Range from previous close: 0dcdaab..0cfb3f6 (audit-ct-tidy → iter-24.3 INDEX)
Status: drift_found — 24.tidy queued
Summary
The Show + print milestone shipped in three iters: 24.1 installed
bool_to_str + str_clone runtime + codegen primitives at heap-Str
ABI level (pre-mq audit lineage); 24.2 added class Show + four
primitive instances (Int/Bool/Str/Float) to the prelude and migrated
the 22b user-Show fixture corpus to TShow/tshow to eliminate
post-prelude.Show ambiguity; 24.3 added fn print with explicit-let
body, three E2E fixtures (positive 4-prim, user-ADT, negative
NoInstance), an IR-shape pin on print__Int.body, a Show-aware
NoInstance diagnostic, plus three compiler-path repairs surfaced by
the user-ADT E2E (canonical-form normalisation of
MonoTarget::FreeFn::type_args, codegen cross-module reference
fallback, FreeFnCall constraint-residual push).
Architect drift review surfaces 7 items: three [high] (missing DESIGN.md
documentation of the three iter-24.3 strengthenings: canonical-form
type_args, post-mono cross-module-ref invariant, FreeFnCall constraint-
residual push; missing codegen-level pin for the import_map-fallback
path; constraint-residual push currently fires only in the dot-qualified
FreeFnCall branch), three [medium] (unwrap_or_default in the
method-name lookup masks class-index drift; normalize_type_for_lookup
duplicated across two mono.rs sites with manual-lockstep invariant;
negative-test coverage is single-shape — only f : Int -> Int, not
user-type-without-instance), and one [low] (bench architect_sweeps.sh
noise on three pre-milestone-24 DESIGN.md lines — not new drift).
Bench: bench/check.py exit 1 with 2 regressed / 1 improved metrics
(metric identities migrate between runs — first run hit
latency.implicit_at_rc.max_us +209%, second run hit
latency.explicit_at_rc.max_us +66% + bench_list_sum.bump_s +11.9%).
bench/compile_check.py exit 1 with 3-5 regressed metrics across
runs (metric identities migrate: first run hit build_O0_ms.*
cluster within tolerance, second run hit check_ms.* cluster with
several +25-34% deltas). bench/cross_lang.py exit 0, 25/25 stable.
9th consecutive audit observation of the documented noise envelope
covering latency.implicit_at_rc.* / latency.explicit_at_rc.* /
bench_list_sum.bump_s / check_ms.* metric-cluster migration
across consecutive runs. Conservative-call convention has held the
baseline pristine across 8 prior audits since audit-cma; this
audit extends the lineage to 9 with the same conservative call —
the metric-migration-between-runs is itself attribution evidence
that the signal is variance, not regression.
Resolution path: 24.tidy iteration covers 5 actionable drift
items (three [high] + two [medium] documentable/refactorable). The
[medium] "negative-test coverage single-shape" item moves to the
roadmap P3 backlog. The [low] bench_sweep noise is carry-on.
Architect drift items
[high-1] Three iter-24.3 strengthenings undocumented in DESIGN.md
Paths: crates/ailang-check/src/mono.rs:687-696 + :1287-1294,
crates/ailang-codegen/src/lib.rs:1980-2000 + :2200-2244 +
:2776-2816, crates/ailang-check/src/lib.rs:2820-2865,
docs/DESIGN.md §"Monomorphisation" or new subsection.
Iter 24.3 silently strengthened three load-bearing mono / codegen / synth invariants without documenting them in DESIGN.md:
- (a)
MonoTarget::FreeFn::type_argscarries canonical (<owner>.<bare>) types post-collection vianormalize_type_for_lookup. Pre-iter invariant was "subst.apply result"; new invariant is "subst.apply result normalised to registry-lookup form". - (b) Post-mono synthesised bodies may carry cross-module references
to modules their source template didn't import. Codegen falls back
to direct
module_user_fns/module_def_ail_typeslookup when the prefix isn't in the current module'simport_map. - (c) FreeFnCall synth pushes one
ResidualConstraintper declared forall-constraint with rigid vars substituted by fresh metavars. Pre-iter, the arm hadconstraints: _(constraints ignored); latent for milestone-23 fixtures because all primitive instances discharged silently.
Future planner / implementer work that touches these surfaces will not see the strengthenings without a doc anchor.
Outcome: fix in 24.tidy (doc-only).
[high-2] Codegen import_map fallback path has no unit-level pin
Path: crates/ailang-codegen/src/lib.rs:2200-2244 + :1980-2000 +
:2776-2816.
The three cross-module-fallback arms (resolve_top_level_fn,
lower_app cross-module arm, synth_with_extras Var arm) introduce
a new dispatch path: try import_map, fall back to direct
module_user_fns / module_def_ail_types lookup. The fallback is
only justified by post-mono synthesised cross-module references.
No codegen-level unit test pins the failure mode; the only test that
would catch a regression is the E2E show_user_adt.ail.json. If a
future refactor tightens resolve_top_level_fn back to import_map-only,
the regression surfaces only at E2E (slow to bisect).
Outcome: fix in 24.tidy — add a unit pin asserting the fallback
resolves for a synthesised prelude.print__T → <user_module>.show__T
pair without going through import_map.
[high-3] FreeFnCall constraint-residual push covers only dot-qualified branch
Path: crates/ailang-check/src/lib.rs:2820-2865.
The constraint-residual push happens only in the dot-qualified
prefix.suffix synth branch (the only branch that populates
free_fn_owner = Some(...)). Bare-name references to polymorphic free
fns that resolve through other branches (locals shadow,
env.module_globals direct hit at the current module, etc.) do not
push residuals for declared constraints. For prelude poly fns this is
fine because the auto-injected prelude call-path always lands in the
dot-qualified branch — but the invariant is implicit and undocumented.
The same "silent unknown variable from codegen" failure mode existed
pre-iter-24.3 for any milestone-23 negative case (ne on a type
without Eq instance, etc.); the iter-24.3 fix covers Show only
because that's what the negative E2E exercised.
Outcome: fix in 24.tidy — generalise the constraint-residual push
to all FreeFnCall synth branches, OR document the invariant
"poly free fns always reach synth via the dot-qualified branch" and
pin it with a unit test.
[medium-1] unwrap_or_default in method-name lookup masks class-index drift
Path: crates/ailang-check/src/lib.rs:2852-2858.
Method-name lookup in the FreeFnCall constraint-residual push uses
unwrap_or_default() (empty string fallback) when class_methods has
no entry for the residual class. A user constraint referencing a class
that does not exist would silently render NoInstance with an empty
method: field. The iter-22 MissingClass pre-pass plausibly rejects
this earlier, but the fallback masks the dependency.
Outcome: fix in 24.tidy — replace with explicit Internal or
expect(...) to surface registry/index drift on regression.
[medium-2] normalize_type_for_lookup duplicated across two sites
Path: crates/ailang-check/src/mono.rs:687-696 + :1287-1294.
The two normalize_type_for_lookup call sites use a literal copy of
the body comment rather than extracting a helper. The two sites must
agree byte-identically (per iter-24.3 journal: "Must agree byte-
identically with that site or Phase 3 rewrite cursor produces a mono-
symbol name that differs from the Phase 2 synthesis name"). A
normalize_free_fn_type_args(env, module_name, metas, subst) helper
would make the lockstep enforceable by construction.
Outcome: fix in 24.tidy — extract helper.
[medium-3] Negative-test coverage single-shape (deferred)
Paths: examples/show_no_instance.ail.json and the broader
NoInstance Show test surface.
The negative E2E covers f : Int -> Int (function type without Show
instance). Other NoInstance shapes — user type without instance,
partial-application residual carrying both a missing constraint AND a
NoInstance, polymorphic call with two unsatisfied constraints — are
not pinned. The Show-aware NoInstance arm fires for any
non-prelude-Show type by construction, so the single fixture is
structurally representative, but coverage breadth is thin.
Outcome: roadmap backlog P3 (not in 24.tidy). Single-fixture
coverage is acceptable for the architecture-shipping milestone;
broader coverage waits for the corpus-migration milestone (the queued
P2 retirement of io/print_int|bool|float) when more downstream
fixtures naturally exercise these shapes.
[low-1] bench/architect_sweeps.sh noise on pre-milestone-24 DESIGN.md lines
Path: bench/architect_sweeps.sh matches on docs/DESIGN.md:50,
:449, :614.
All three matched lines predate milestone 24 (text not touched in the milestone-24 diff). Legitimate references or empirical-addendum headings, not new history anchors. Sweep noise from prior eras.
Outcome: carry-on (not a milestone-24 drift item).
Bench
bench/check.py: exit 1 with metric identities migrating between consecutive runs:- Run 1: 2 regressed (
latency.implicit_at_rc.max_us +209%,bench_list_sum.bump_s +11.9%), 1 improved (bench_list_sum.gc_over_bump). - Run 2: 2 regressed (
latency.explicit_at_rc.max_us +66%,bench_list_sum.bump_s +11.9%again), 1 improved (different metric). - Pattern: the
latency.*_at_rc.max_usmetric cluster shifts identity between runs;bench_list_sum.bump_sis the 5th consecutive sighting sinceaudit-mq.tidy. Same envelope the iter-24.3 journal documented as the 8th consecutive observation; this audit makes it 9.
- Run 1: 2 regressed (
bench/compile_check.py: exit 1 with 3-5 regressed metrics across runs; metric identity migrates betweenbuild_O0_ms.*cluster (run- and
check_ms.*cluster (run 2). All deltas are +5% to +34% across runs; tolerance is 20-25% so multiplecheck_ms.*deltas cross threshold sporadically. Same migration pattern.
- and
bench/cross_lang.py: exit 0; 25/25 stable.
Conservative call: baseline pristine for the 9th consecutive audit.
The metric-identity-migration is itself attribution evidence that the
signal is variance, not regression. If signal were present, the metric
would not migrate between runs at this rate. Ratifying via
--update-baseline would pin a snapshot that does not characterise
the actual distribution (since the noise migrates, any single
snapshot is wrong). The right ratification path is the queued P3
"Latency methodology rework — switch from per-run timing to a
histogram-based approach"; until that ships, conservative-call is the
correct response.
No --update-baseline invocation this audit.
Resolution
24.tidyiter to land 5 drift fixes:[high-1](DESIGN.md documentation of the three iter-24.3 strengthenings),[high-2](codegen import_map-fallback pin),[high-3](FreeFnCall constraint-residual push generalisation OR documented invariant),[medium-1](replaceunwrap_or_defaultwith explicit error),[medium-2](extractnormalize_free_fn_type_argshelper). Routes throughplanner→implement.[medium-3](negative-test coverage single-shape): roadmap backlog P3.[low-1](bench sweep noise): carry-on, not actionable.- Bench: extend lineage to 9th consecutive observation. Pristine baseline. Document for future histogram-methodology landing.
Files touched
docs/journals/2026-05-13-audit-24.md(new, this file)docs/journals/INDEX.md(one new line)
No production code or test edits in this audit; all recommendations
land in 24.tidy.