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:
@@ -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`.
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user