71dec143d9
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.
245 lines
12 KiB
Markdown
245 lines
12 KiB
Markdown
# 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`.
|