audit-24: milestone close (Show + print rewire) drift report + 24.tidy queue

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.
This commit is contained in:
2026-05-13 04:13:16 +02:00
parent 0cfb3f6c87
commit 71dec143d9
2 changed files with 245 additions and 0 deletions
+244
View File
@@ -0,0 +1,244 @@
# 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_args` carries canonical (`<owner>.<bare>`)
types post-collection via `normalize_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_types` lookup when
the prefix isn't in the current module's `import_map`.
- (c) FreeFnCall synth pushes one `ResidualConstraint` per declared
forall-constraint with rigid vars substituted by fresh metavars.
Pre-iter, the arm had `constraints: _` (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_us` metric cluster shifts
identity between runs; `bench_list_sum.bump_s` is the 5th
consecutive sighting since `audit-mq.tidy`. Same envelope the
iter-24.3 journal documented as the 8th consecutive observation;
this audit makes it 9.
- `bench/compile_check.py`: exit 1 with 3-5 regressed metrics across
runs; metric identity migrates between `build_O0_ms.*` cluster (run
1) and `check_ms.*` cluster (run 2). All deltas are +5% to +34%
across runs; tolerance is 20-25% so multiple `check_ms.*` deltas
cross threshold sporadically. Same migration pattern.
- `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.tidy` iter 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]` (replace `unwrap_or_default` with explicit error),
`[medium-2]` (extract `normalize_free_fn_type_args` helper). Routes
through `planner``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`.
+1
View File
@@ -49,3 +49,4 @@
- 2026-05-13 — iter mq.tidy: close 4 actionable drift items from audit-mq — T1 extends `refine_multi_candidate_residual`'s rigid-var filter at `lib.rs:2163-2168` from class-only (`dc.class == c`) to class + type-unification (`dc.class == c && constraint_type_matches(&dc.type_, &residual.type_)`) via the existing `constraint_type_matches` helper at `lib.rs:2273`, so `forall a b. prelude.Show a, userlib.Show b => (a, b) -> String`-shape declared-constraint sets correctly discriminate by which typevar the residual is on (high-1; spec §"Constraint-discharge refinement" 130-138). Plan revision noted at the architecture paragraph: synth-time `resolve_method_dispatch` is invoked with `concrete_arg_type: None` and constructs the residual metavar AFTER the dispatch call, so the rigid-var leg there has no residual type to unify against — class-only filter at synth time is semantically correct, not drift; fix lands only at the discharge-time site. T2 extracts the inline `qualifier_is_class_shape` predicate from synth Var-arm at `lib.rs:2570-2575` as a free `pub(crate) fn qualifier_is_class_shape(&Option<String>) -> bool` adjacent to `parse_method_qualifier`; broadened to accept PascalCase single-segment qualifiers (`"Show.show"`) alongside module-qualified ones (`"prelude.Show.show"`), so same-module bare-class call sites are now reachable — symmetric to mq.1's canonical-form rule at the schema level (high-2). Discriminator: class names start with uppercase per PascalCase convention; bare-fn qualifiers (`"std_list"`, lowercase) correctly reject and fall through to the cross-module-fn arm. T3 introduces `pub(crate) fn any_candidate_class_has_instance(...)` using a BTreeMap range-scan (O(|candidates| * log n)) and gates the `class-method-shadowed-by-fn` warning closure at `lib.rs:2479-2502` on it, so class methods with zero registry instances anywhere no longer fire the warning — conservative Boss-Q2-decision tightening of the spec rule (full rule would require the arg type, unavailable at the Var-arm before App-arm unification) (medium-1). T4 annotates four DESIGN.md Data Model schema fragments (`SuperclassRef` 2139, `InstanceDef.class` 2152, `Type::Con.name` 2253, `Constraint.class` 2273) with trailing-comment canonical-form cross-references — Type::Con annotation points at the actual existing section title `§"Type::Con name scoping"` (ct.1 anchor verified via grep, plan's text guess was off) (medium-2). Two trip-wires fixed inline: (a) `parse_method_qualifier` docstring updated to remove the now-incorrect "class qualifier is always qualified per mq.1" claim that the broadened gate directly contradicts — coherence-required not unrequested; (b) in-crate `mq3_class_method_shadowed_by_fn_warning_fires` test built workspace with `Registry::default()` (no instances) and pre-tidy fired anyway because old gate didn't check instances — post-tidy correctly suppresses, fixture repaired by injecting a `clsmod.Show Int` registry entry directly into `ws.registry.entries` (analogous to mq.3's `typeclass_22b3` trip-wire pattern). 5/5 tasks, 548 tests green (was 545 + 3 new `mq_tidy_*` unit tests); `bench/compile_check.py` exit 0 (24/24 stable after one-run noise blip absorbed under tolerance on re-run), `cross_lang.py` exit 0 (25/25 stable), `check.py` exit 0 both runs with metric-identity-migrating noise on `latency.implicit_at_rc.*` max-tail cluster — 6th-consecutive observation of the audit-mq-named noise envelope, baseline pristine. Plan Step 5 expectation `ail check examples/prelude.ail.json` exit 0 was inaccurate (actual: exit 1 "module name 'prelude' is reserved", identical to pre-edit, prelude is loader-auto-injected and never a workspace entry); 548/548 workspace test pass is the right sanity gate → 2026-05-13-iter-mq.tidy.md
- 2026-05-13 — iter 24.2: class Show + 4 primitive instances (Int/Bool/Str/Float) shipped in prelude; 22b user-Show fixtures migrated to TShow/tshow → 2026-05-13-iter-24.2.md
- 2026-05-13 — iter 24.3: fn print polymorphic free fn + 3 E2E fixtures (positive 4-prim smoke / user-ADT IntBox+instance prelude.Show IntBox / negative print f:Int→Int firing Show-aware NoInstance) + IR-shape pin asserting post-mono print__Int.body preserves explicit let-binder + 3 compiler-path repairs (mono.rs canonical-form normalisation of MonoTarget::FreeFn::type_args 2 sites; codegen/lib.rs cross-module reference fallback for post-mono synthesised bodies 3 sites: resolve_top_level_fn / lower_app cross-module arm / synth_with_extras Var arm; check/lib.rs synth FreeFnCall constraint-residual push) + Show-aware NoInstance diagnostic addendum cross-referencing DESIGN.md §Prelude(built-in)classes + DESIGN.md §Prelude(built-in)classes mq24 paragraph flipped to past tense + §Float semantics Show-Float NaN paragraph + roadmap P1 Post-22 Prelude → [x] + new P2 Retire io/print_int|bool|float at top; 556 tests pass (was 552+4 new); milestone 24 structurally closes → 2026-05-13-iter-24.3.md
- 2026-05-13 — audit-24: milestone close (Show + print rewire) — architect drift report surfaces 5 actionable items routing to `24.tidy` (3× [high]: DESIGN.md §"Monomorphisation" doc-anchor missing for three iter-24.3 strengthenings — MonoTarget::FreeFn::type_args canonical-form normalisation, post-mono synthesised-body cross-module-ref import_map-bypass invariant, FreeFnCall constraint-residual push; codegen import_map-fallback path has no unit-level pin — only E2E catches regression; FreeFnCall constraint-residual push covers only dot-qualified branch, bare-name poly-fn refs uncovered; 2× [medium]: unwrap_or_default in method-name lookup masks class-index drift, normalize_type_for_lookup duplicated across two mono.rs sites with manual-lockstep invariant), plus 1× [medium] deferred to roadmap P3 (negative-test coverage single-shape — only f:Int→Int, broader shapes wait for downstream corpus-migration milestone), plus 1× [low] carry-on (bench/architect_sweeps.sh noise on pre-milestone-24 DESIGN.md lines, not new drift). Bench: cross_lang.py exit 0 (25/25 stable); check.py + compile_check.py exit 1 with metric-identity-migrating noise envelope per audit-cma → audit-ms → audit-eob → audit-ct-tidy → audit-mq → audit-mq.tidy → iter-24.2 → iter-24.3 lineage — 9th consecutive observation; conservative-call convention holds baseline pristine across all 9, the metric-migration-between-runs is itself attribution evidence variance not signal; right ratification path is the queued P3 latency-methodology-histogram rework, not --update-baseline → 2026-05-13-audit-24.md