diff --git a/docs/journals/2026-05-13-audit-24.md b/docs/journals/2026-05-13-audit-24.md new file mode 100644 index 0000000..1ebe86b --- /dev/null +++ b/docs/journals/2026-05-13-audit-24.md @@ -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 (`.`) + 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` → `.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`. diff --git a/docs/journals/INDEX.md b/docs/journals/INDEX.md index 3ee3f6c..bbccf8d 100644 --- a/docs/journals/INDEX.md +++ b/docs/journals/INDEX.md @@ -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) -> 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