Commit Graph

7 Commits

Author SHA1 Message Date
Brummel 76b21c00eb feat(lang): eliminate the Implicit ownership default — totality + the drop-soundness it demasks (#55)
Deletes `ParamMode::Implicit`. `ParamMode` is now `{Own, Borrow}`:
every fn-type slot on every signature carries an explicit `own` or
`borrow`, no defaulted position survives anywhere (model 0008 §2,
spec 0062). The parser rejects a bare fn-type slot; `borrow-return`
and `borrow-over-value` reject at the signature; the corpus is
migrated to minimal-ownership modes (consumed ⇒ own, read-only-heap
⇒ borrow, value ⇒ trivial-own). The documented `Implicit`-ret-mode
leak is fixed: an owned heap return now drops exactly once (live=0,
acceptance criterion 5).

This was the easy half. Removing the default ACTIVATED a family of
drop paths that `Implicit` had silently skipped — the pre-cutover
language was leaking (and in places mis-dropping) here rather than
crashing, because an Implicit scrutinee turned the drop off. Making
the modes explicit (Own) turned those paths on and exposed two
latent-bug clusters, all fixed RED-first as part of this cutover:

Drop-soundness family (four legs):
  A. lit-sub-pattern double-free — the desugar re-matched the same
     owned scrutinee in the lit fall-through; fixed by grouping
     consecutive same-ctor arms into one match (bind fields once),
     in ailang-core desugar.
  B. Cons-husk leak on non-tail arm bodies — the lit-sub-pattern
     desugar rebound the owned scrutinee via `Let $mp = xs`, which
     bumped consume_count and suppressed the existing fn-return
     partial_drop. Fixed by not rebinding a bare-Var scrutinee
     (one husk-freeing mechanism, not two).
  C. polymorphic `drop_<T>` rc_dec'd monomorphised value fields —
     the per-ADT drop fn was emitted once from the polymorphic
     TypeDef, defaulting type-var fields to ptr and rc_dec'ing
     inline Ints (segfault). Fixed with per-monomorph drop
     functions (new ailang-codegen::dropmono): the drop set is
     collected from the lowered MIR, value-type fields are skipped,
     heap fields still freed once; monomorphic-concrete ADTs keep
     their byte-identical un-suffixed drop symbol.
  D. static Str literal passed to an `(own Str)` param — the
     literal lowers to a header-less rodata constant; the callee's
     now-active rc_dec read its length field as a refcount and
     freed a static address (segfault). Fixed with the missing
     fourth StrRep::Static→Heap promotion in lower_to_mir's App arm,
     gated on Own mode (borrow args stay static, no regression).

over-strict-mode lint over-fired: it suggested `(borrow V)` for
value-typed params (which `borrow-over-value` rejects — own is the
only legal mode there) and fired on `(intrinsic)` bodies (whose
consumption the linearity walk cannot observe). Tightened to skip
both; contract 0008 updated to the narrowed firing scope.

Irreversible step — canonical-form hash reset (model 0008 §6,
acceptance criterion 6). Every signature now carries explicit modes,
so the hashable canonical JSON changed for every module. RATIFY:
the corpus-wide hash-pin reset (hash_pin, prelude_module_hash_pin,
mono_hash_stability, eq_ord_e2e, embed_export_hash_stable, the
ct4/iter*/loop_recur schema-extension pins) and the list ir_snapshot
golden were regenerated once, deliberately, as the intended one-time
consequence of removing the mode elision from the canonical form —
not a regression. Each regenerated hash verified deterministic across
two runs.

Also fixes a pre-existing latent failure surfaced by the verification
gate, unrelated to this cutover: the `every_contract_names_a_resolvable_
ratifying_test` resolver (design_index_pin) could not resolve the
" + " dual-link ratifying-test form (`uniqueness.rs + linearity.rs`)
that the #57 audit-close (dfdc65f) introduced — it shipped red on that
commit. Resolver taught the dual-link form, mirroring its sibling.

Verification: cargo test --workspace = 731 passed, 0 failed (twice,
stable); e2e 102 passed, no binary exits non-zero (corpus crash-free);
grep-clean for Implicit/fn_implicit/mode_eq across crates; every drop
fix confirmed via emitted IR + AILANG_RC_STATS balance on the head==K,
head!=K, and Nil paths. Three BLOCKEDs en route (the unsound first
husk-dec attempt, the over-strict derivation premise, the leg-B fix
direction) were each treated as a real design/spec gap and rediagnosed,
not patched over.

Supersedes #54 (return-position-only leak patch). Precondition #57
(linearity hardening) was already met. Spec docs/specs/0062, plan
docs/plans/0121.

closes #55
2026-06-02 00:03:46 +02:00
Brummel caa3618c3e iter intrinsic-bodies.2-migration-lock (DONE 4/4): prelude → (intrinsic) + bijection pin (refs #9)
Second and final iteration of the intrinsic-bodies milestone. Migrates
the 13 authored prelude dummy bodies to (intrinsic), rebaselines the two
hash pins that move, and locks the registry to the source with a
bijection. Behaviour-preserving: the codegen intercepts emit identical
IR; the prelude placeholder bodies were already dead (intercepted by
name before lower_term, since raw-buf.1).

What landed (4 tasks):

  Task 1 — examples/prelude.ail: 13 authored dummy bodies → (intrinsic).
    7 Eq/Ord instance methods (eq Int/Bool/Str/Unit, compare
    Int/Bool/Str) keep their lambda shell (params/ret = the method's
    local signature, Design X); the inner (body false)/(body true)/
    (body (term-ctor Ordering EQ)) becomes (intrinsic). 6 float_* fns
    ((body false) → (intrinsic) in the fn body slot). The honesty-rule
    infraction the milestone exists to fix is now closed: no prelude
    body lies about what runs.

  Task 2 — hash rebaselines (the prelude bytes changed):
    prelude module hash  af372f28c726f29f → 2ea61ef21ebc1913
    eq__Int       86ed4988438924d3 → cc7b99b63d1e44ae
    eq__Bool      d62d4d8c51f433f8 → fd0412f127986512
    eq__Str       3d32f377c66b03e0 → fa269285754a52da
    compare__Int  6d6c20520766368b → 0a02bd9effc9746c
    compare__Bool 02b64e8fadc913eb → d0dc108dacf4e543
    compare__Str  9645929d53cd3cc9 → d1419595dc52a456
    The 4 show__* mono pins did NOT move (Show bodies are real).

  Task 3 — intercepts.rs: registry_contains_all_legacy_arms (one-
    directional, hardcoded 18-name list) replaced by
    intercepts_bijection_with_intrinsic_markers. The in-source test
    walks prelude + kernel_stub pre-mono for Term::Intrinsic markers,
    recovers each marker's codegen symbol (mono_symbol(method, type) for
    instance methods; the fn name for top-level float_*/answer), and
    asserts a bijection over the intrinsic-backed class: (A) every
    marker resolves to an INTERCEPTS entry, (B) every INTERCEPTS entry
    not on the optimisation-only allowlist has a marker, plus a guard
    that the allowlist names are themselves registered. The allowlist is
    the 5 *__Int icmp-family entries, which intercept the monomorphised
    specialisation of the real-bodied polymorphic lt/le/gt/ge/ne — an
    optimisation, not a compiler-supplied body, so no marker. The pin
    passing independently confirms Task 1 missed no prelude site.

  Task 4 — confirmed no codegen path lowers an intercepted body (already
    true since raw-buf.1: try_emit_primitive_instance_body matches by
    name before f.body is inspected). Refreshed the now-accurate comment
    at lib.rs:1310-1319. design/contracts/0007-honesty-rule.md needed NO
    edit — it is a general present-tense rule and does not name the
    prelude dummies (confirmed by grep), so editing it would have been
    invented work.

Verification:
  cargo test --workspace → 669 passed, 0 failed (post-.1 baseline held;
    one test replaced net-0).
  Behaviour-preservation ratifiers GREEN (the safety net):
    eq_primitives_smoke_compiles_and_runs,
    compare_primitives_smoke_prints_1_2_3_thrice (e2e.rs);
    float_compare_smoke_prints_true_true_false (float_compare_smoke_e2e.rs);
    eq_ord_polymorphic_runs_end_to_end (eq_ord_e2e.rs).
  Bijection pin GREEN. Both hash pins GREEN at the new baselines.
  bench/check.py + compile_check.py → 0 regressed.

Plan-text imprecision (no outcome impact, noted for the record): Task 1
Step 5 wrote `--test e2e` for all four ratifiers, but
float_compare_smoke_prints_true_true_false and
eq_ord_polymorphic_runs_end_to_end live in their own test targets
(float_compare_smoke_e2e.rs, eq_ord_e2e.rs). The verification-filter
self-review checked that the fn names exist, not that they sit in the
named target — a sharper form of the filter-zero discipline. The
implementer ran each in its real target; all four green.

Milestone intrinsic-bodies is now feature-complete (.1 mechanism +
.2 migration+lock). Audit + close follow.
2026-05-29 17:46:50 +02:00
Brummel 5170b6abd1 iter operator-routing-eq-ord.1 (DONE 13/13): drop comparator builtins, route through Eq/Ord
Closes Gitea #1. Realises the "P2 follow-up" called out in
examples/prelude.ail:9 — removes the surface comparator names
`==` / `!=` / `<` / `<=` / `>` / `>=` from the language entirely,
routes the LLM-author's `(app eq …)` / `(app compare …)` /
`(app ne|lt|le|gt|ge …)` through prelude.Eq / prelude.Ord class-
dispatch, ships six named Float-comparison fns
(`float_eq`/`float_ne`/`float_lt`/`float_le`/`float_gt`/`float_ge`)
so Float keeps comparability without an Eq/Ord instance, and emits
primitive instance bodies with `alwaysinline` so the -O0 IR shape
stays at one instruction per comparison.

Plan-task journal (13 tasks, single atomic iter per Approach A):

  Task 1 — Bootstrap: 4 new fixtures (eq_user_adt_smoke.ail,
    eq_float_must_fail.ail, float_compare_smoke.ail,
    operator_unbound_check.ail) + 5 new E2E/pin tests as RED
    starting state. All 5 confirmed RED at start: north-star
    fixed `NoInstance Eq Unit` (preserved by Unit-eq opening line
    intentionally added in plan-self-review); float_compare unknown
    `float_eq`; operator-name typechecked (== still polymorphic);
    must-fail diagnostic lacked `float_eq`; alwaysinline absent
    from IR.

  Task 2 — Codegen alwaysinline + intercept arms: introduced
    `intercept_emit_wants_alwaysinline` allowlist + appended
    ` alwaysinline` between `)` and `{` of the `define` line in
    `emit_fn`; added 9 new intercept arms to
    `try_emit_primitive_instance_body` — `eq__Int`/`eq__Bool`/
    `eq__Unit` + the six `float_*` arms.

  Task 3 — Prelude reshape: `instance Eq Unit` added; Eq Int/Bool
    bodies become placeholder-`false` (intercept overrides); six
    `float_*` free fns added; line-9 P2-follow-up comment removed.
    Lockstep hash re-pins: prelude_module_hash_pin (3abe0d3fa3c11c99
    → new), mono_hash_stability six body-hash literals
    (eq__Int/Bool/Str + compare__Int/Bool/Str all shifted because
    placeholder body changes the canonical hash). IR-snapshot
    regen (hello/list/max3/sum/ws_main) rolled forward into this
    task at orchestrator's pragmatic call — prelude shift forces
    the snapshots immediately.

  Task 4 — Fixture migration: 58 .ail fixtures rewritten
    (`(app == …)` → `(app eq …)`, etc.; Float-typed operand sites
    to `(app float_eq …)` / `(app float_lt …)`). eq_ord_user_adt.ail:21
    inner `==` → `eq` migration with lockstep eq_ord_e2e.rs:134
    body-hash re-pin (3c4cf040cb4e8bb2 → new). Two prose snapshots
    accepted; deps test + 5 hash_pin literals updated as cascading
    consequences.

  Task 5 — Test-scaffold migration: all 8 in-source
    `#[cfg(test)] mod tests` AST-literal sites threaded
    (desugar.rs:2414, check/lib.rs:5656/6092/6220, codegen/lib.rs
    sites). 7 obsolete in-source tests deleted (5 eq_typechecks +
    2 lower_eq ADT/Fn rejection — coverage moves to E2E
    eq_user_adt_smoke + eq_float_must_fail). 2 additional letrec
    tests deleted (single-module check env can't resolve eq/ge
    without prelude auto-import; covered by workspace E2E).

  Task 6 — Lit-pattern desugar: build_eq emits
    `Term::Var { name: "eq" }` instead of `"=="` at desugar.rs:1109;
    doc-comment rewritten to describe class-dispatch.

  Task 7 — Dead-machinery deletion sweep (compile-gated): typchecker
    builtins (install + list comparator entries deleted); codegen
    builtin_binop_typed (10 comparator arms deleted from synth.rs,
    table reduced to arithmetic core); lower_eq fn entirely
    deleted; `==` short-circuit in lower_app deleted;
    `is_static_callee` `==` clause deleted;
    `is_arithmetic_or_comparison_op` renamed to `is_arithmetic_op`
    + caller-update at codegen/lib.rs:2152 + :2529. Dead
    `poly_a_a_to_bool` helpers also removed. Workspace build green
    after compile gate — every caller of the deleted surface was
    migrated by Tasks 4-6.

  Task 8 — Float-aware NoInstance diagnostic: check/lib.rs:856-880
    addendum extended to name `float_eq` / `float_lt` as the
    explicit alternative for Eq/Ord at Float;
    eq_float_noinstance.rs:32-44 assertion extended.

  Task 9 — IR snapshot regen: subsumed by Task 3 (prelude shift
    forced immediate snapshot regen; deferring to Task 9 would
    have left the workspace red between tasks).

  Task 10 — Prose-projection cleanup: 6 comparator arms deleted
    from binop_info; 3 in-source mod-tests updated/deleted
    (comparator-infix rendering would be dishonest now that the
    operators are no longer language identifiers).

  Task 11 — Contract updates: 5 design files rewritten to the
    class-dispatch present —
      float-semantics.md (arithmetic guarantees retained on
        +/-/*/; comparison guarantees transferred from ==/!=/<
        to float_eq/float_ne/float_lt/etc.);
      prelude-classes.md (Eq Unit added to instance list;
        new paragraph on six float_* fns; Float-no-Eq/Ord clause
        gains `→ use float_eq` cross-reference);
      str-abi.md (clause "REMAIN primitive operators" rewritten
        to describe class-method dispatch);
      scope-boundaries.md (multiple operator-name and
        Pattern::Lit-desugar clauses rewritten);
      authoring-surface.md (==, <= dropped from operator-example
        list).

  Task 12 — Initial acceptance gate: workspace 638/0 GREEN;
    bench/compile_check + cross_lang 0 regressed; bench/check.py
    flagged 4 regressions on bench_closure_chain (+29% bump_s,
    +47% rc_s, +29% bump_rss_kb, +48% rc_rss_kb). Orchestrator
    initially classified as DONE-with-concerns; Boss reclassified
    as PARTIAL-via-acceptance-#8-fail after independent re-run,
    extended iter scope to Task 13.

  Task 13 — Direct icmp intercept arms (Boss-extension):
    try_emit_primitive_instance_body gains direct-icmp arms for
    lt__Int / le__Int / gt__Int / ge__Int / ne__Int, bypassing
    the compare__Int → Ordering → match indirection that allocated
    one Ordering ctor per call in tight loops. Each new arm
    inherits `alwaysinline` via the existing allowlist (extended
    accordingly). New IR pin test `ord_int_intercept_ir_pin.rs`
    + smoke fixture `ord_int_intercept_smoke.ail` ratify the
    optimization — opt -O2 -S confirms zero `call
    @ail_prelude_lt__Int` in optimized IR; the icmp folds directly
    at every use site. Bool variants (lt__Bool/etc.) deliberately
    NOT added — no bench/example calls Ord at Bool; spec-extension
    permits skipping if unreachable at bench level. Family can be
    extended symmetrically when first Bool-ordered perf workload
    appears.

Bench-gate post-Task-13: bench_closure_chain bump_s -6.05%,
rc_s -2.67%, bump_rss_kb -0.77%, rc_rss_kb +0.46% — all four
previously-regressed metrics back inside tolerance. Full bench
corpus: 36 metrics, 0 regressed, 0 improved beyond tolerance,
36 stable. bench/check.py exit 0 ✓; bench/compile_check.py exit
0 ✓; bench/cross_lang.py exit 0 ✓.

Workspace: 640 passed, 0 failed across all binaries (delta vs.
milestone start: +5 new E2E/pin tests added in Task 1 + 1 IR pin
added in Task 13 − 9 in-source mod tests deleted as obsolete net
≈ −3). The eq_user_adt_smoke fixture's Unit-opening line was
the deliberate RED-first device added in planner self-review so
the north-star wouldn't accidentally GREEN at start (user-ADT-Eq
on Point with hand-written instance was already operable today;
the milestone's actual delivery is operator-name death + Eq Unit
+ Float-named-fns + cleanup of the two-pathy primitive
comparator machinery).

Net delta: 96 files changed, 1760 insertions, 1101 deletions;
12 net-new files (6 new fixtures, 5 new E2E/pin tests, 1 stats
file). main passes-test-count: 640 (was 633 pre-iter, accounting
for the deletions).

Spec-vs-acceptance addendum: spec §Testing strategy anticipated
the bench-gate-regression case with two recovery paths
(`alwaysinline` investigation or "spec needs revisiting toward α").
Task 13 is the third path — direct intercept arms for `lt`/etc.
that bypass the compare→match path entirely. The spec's contingency
clause was thus generous enough to absorb Task 13 without spec
revision, but a future iter that hits a class-method primitive
where the body shape introduces a similar codegen cost (Ordering
allocation, RC tax, deferred-init) should expect a parallel
extension. The pattern is "primitive instance whose canonical body
indirects through other class methods that allocate" → add a
direct-emit intercept arm + alwaysinline + IR-shape pin.

Concerns absorbed:
  - Codegen lower_app / resolve_top_level_fn gained a
    prelude-fallback lookup so bare monomorphic prelude fns
    (`float_eq` etc.) resolve from non-prelude modules without
    explicit `prelude.float_eq` qualifier. Mirrors the
    typechecker's implicit prelude import — not in plan but
    necessary infra for the named-fn surface to be callable.
  - Recon's "≈10 fixtures" estimate was 6× too low — 58 actual.
    Mechanical migration; no design impact.
  - Recon caught 4 in-source AST-literal sites the spec missed
    (lib.rs:5656 `>=` site + three codegen/lib.rs sites);
    Task 5 covered all.
  - Recon caught 2 contract files outside the spec's update set
    that directly contradicted the milestone (str-abi.md +
    scope-boundaries.md "REMAIN primitive operators" /
    "Pattern::Lit desugar to ==" clauses); Task 11 covered all 5.

Stats file:
`bench/orchestrator-stats/2026-05-21-iter-operator-routing-eq-ord.1.json`.

closes #1
2026-05-21 01:16:21 +02:00
Brummel feb941363a bugfix: print leak — propagate ret_mode through rigid substitution
`(app print x)` under --alloc=rc leaked the heap-Str allocated by
`show x` in print's body: the let-binder `s` in
`(let s (app show x) (do io/print_str s))` was never flagged
trackable, so the drop site at scope-close emitted no
`ailang_rc_dec(s)`. Minimal repro `(body (app print 42))` produced
`allocs=1 frees=0 live=1`.

Root cause is two-layer:

1. examples/prelude.ail.json declared the Show class method `show`
   without an explicit `ret_mode` on the return type, so serde
   defaulted to ParamMode::Implicit. Fix: add `"ret_mode": "own"`
   on the Show.show method, parallel to how the heap-Str-producing
   builtins (int_to_str, bool_to_str, float_to_str, str_clone,
   str_concat) declare it. examples/prelude.ail regenerated via
   `ail render` to stay parse-isomorphic with the JSON.

2. crates/ailang-check/src/lib.rs `substitute_rigids` was silently
   stripping `param_modes` and `ret_mode` whenever it rebuilt a
   `Type::Fn`, so even after the prelude fix, the mono-synthesised
   `show__Int / Bool / Str / Float` lost the `Own` annotation
   during rigid substitution. Fix: preserve both fields through
   the substitution.

RED pin: crates/ail/tests/print_no_leak_pin.rs (test
`alloc_rc_print_int_does_not_leak_show_result_str`) + fixture
examples/print_int_no_leak_pin.ail asserts allocs == frees and
live == 0 for `(body (app print 42))` under --alloc=rc.

Ten mono-body hash pins re-recorded (eq__Int/Bool/Str,
compare__Int/Bool/Str, show__Int/Bool/Str/Float, eq__IntBox) —
the bodies are semantically identical; the canonical JSON now
carries the previously-stripped mode metadata, so the body hash
moves. Re-pinning is bookkeeping for the intentional drift, not
a workaround.

Surfaced 2026-05-14 during the rpe.1 BLOCKED orchestrator run
(Cat A). Existed in latent form since iter 24.3 (when Show + print
shipped); only became user-observable when `(app print ...)` joined
the corpus. cargo test --workspace: 565 / 0 / 3.

Open follow-up flagged for next /audit: grep for `Type::Fn { ..., .. }`
field-spread sites — any other shape that drops modes during
rebuild is a latent instance of the same bug class. Out of scope
for this minimal fix.
2026-05-14 01:51:50 +02:00
Brummel 77b28ad64d iter form-a.1 (Tasks 1-5): additive phase + relocation
First half of the form-a-default-authoring milestone-close iter
(Boss-decided strategy C, big-bang). All five tasks DONE; cargo
test --workspace green at every per-task boundary.

T1 — Add three new tests:
- parse_is_deterministic_over_every_ail_fixture (round_trip.rs)
- cli_parse_then_render_then_parse_is_idempotent (roundtrip_cli.rs)
- carve_out_inventory.rs (new file; #[ignore]'d until T8 deletion)

T2 — Bulk-render the 99 missing examples/<stem>.ail files via
`ail render`. Corpus 58 .ail (pre-iter) -> 157 .ail. Eight .ail.json
carve-outs (7 §C4(a) subject-matter + 1 §C4(b) prelude) preserved.
One re-loop triggered: load_workspace prefers .ail siblings since
ext-cli.1, so the newly-rendered imports broke seven Group-A entries
whose JSON entry-paths now resolved imports to fresh .ail. Repair:
pre-emptive forward-pull of five T3 migrations + 4 transient
#[ignore]s on workspace.rs mod tests (cleanly relocated in T5).

T3 — 14 Group-A test files migrated from ailang_core::load_workspace
to ailang_surface::load_workspace + .ail paths. Carve-out lines
preserved verbatim (7 sites in typeclass_22b2.rs / typeclass_22b3.rs).

T4 — 12 Group-B test files: bulk regex flip on build_and_run /
build_and_run_with_alloc / build_and_run_with_rc_stats call sites
(~70 e2e.rs invocations + 11 subprocess sites). Four files
mis-classified Group-A as Group-B in plan recon (mono_hash_stability,
prelude_free_fns, print_mono_body_shape, show_mono_synthesis); two
files mis-classified Group-B as Group-A (mono_recursive_fn,
mono_xmod_qualified_ref). Migrated per actual shape, not plan label.

T5 — Relocated #[cfg(test)] mod tests from production source to
integration test crates with ailang-surface dev-dependency:
- crates/ailang-core/tests/hash_pin.rs (10 tests from hash.rs)
- crates/ailang-core/tests/workspace_pin.rs (10 non-carve-out tests
  from workspace.rs)
- crates/ailang-codegen/tests/eq_primitives_pin.rs (3 tests from
  codegen/src/lib.rs:3717-3799)
- ailang-prose/tests/snapshot.rs migrated (helper + 8 fixtures) to
  .ail + ailang_surface::load_module
Carve-out tests in workspace.rs mod tests preserved in-place
(3× 22b2 + 3× ct1 = 6 tests). Tempdir-based loader-mechanism tests
(3 sites) also preserved — they don't consume examples/.

Tests: 560 passed, 0 failed, 4 ignored (was 558 + 3 T1 new -
1 transit carve_out_inventory #[ignore] = 560 active).

Tasks 6-12 (bench-driver suffix flips, e2e diff-test rewrite + 4
additional raw-JSON-inspect handlers, .ail.json deletion, retiring
obsolete roundtrip tests + schema_coverage corpus flip, §C3
DESIGN.md restatement, §A4 doctrine edits, WhatsNew + roadmap
strike) ship in the next dispatch on this iter ID.

Known debt inherited to T6-12: 4 raw-JSON-inspect tests in e2e.rs
(borrow_own_demo / reuse_as_demo / render_parse_round_trip_canonical
/ ail_run_accepts_ail_source_with_same_stdout_as_ail_json dual-form
smoke) need rewrite or #[ignore] before T8 deletion; recorded in
journal Concerns + Known debt sections.
2026-05-13 11:12:48 +02:00
Brummel 3286117605 iter 24.2: class Show + 4 primitive instances + 22b TShow migration
Ships class Show a where show : (a borrow) -> Str in the prelude
plus primitive Show Int / Bool / Str / Float instances. Each
instance body is a single-application lambda invoking the
corresponding runtime primitive (int_to_str, bool_to_str,
str_clone, float_to_str) — first prelude instance bodies to call
runtime primitives directly. Float is included in Show (unlike
Eq/Ord) because textual representation is well-defined modulo the
NaN-spelling caveat at DESIGN.md §Float semantics.

The 22b typeclass test corpus is migrated preemptively: 21 fixture
files and 6 consumer files (typeclass_22b{2,3}.rs +
hash.rs ct4 pin + workspace.rs iter22b1 tests + the
instance_present.prose.txt snapshot) rename class Show / show
to class TShow / tshow, analogous to the existing TEq/TOrd
convention. Migration runs before the prelude additions so the
workspace stays green throughout.

Three new tests pin the post-mono shape: show_mono_synthesis.rs
(existence of show__Int/Bool/Str/Float as Def::Fn entries in the
prelude post-mono module), show_dispatch_pin.rs (bare show and
tshow both Step-2 singletons workspace-globally),
mono_hash_stability.rs::primitive_show_mono_symbol_hashes_stay_bit_identical
(body hashes pinned for the 4 new mono symbols).

DESIGN.md §Prelude (built-in) classes drops Show from the
deferred-features list and appends a milestone-24 paragraph
naming the class signature + the 4 instances + the body shape.
print rewire stays deferred to iter 24.3.

Tests: 552 passed (was 548 + 4 new). bench/compile_check +
bench/cross_lang exit 0. bench/check exits 1 on the recurring
noise envelope per the conservative-call lineage.
2026-05-13 03:31:40 +02:00
Brummel a1692a4859 iter 23.4: mono-pass unification — class methods + polymorphic free fns in one fixpoint
Restructure ailang-check/src/mono.rs::monomorphise_workspace into a
single specialiser over every Type::Forall-quantified Def::Fn, covering
both class-method residuals AND polymorphic free-fn call sites in one
fixpoint pass. Remove the codegen-time specialiser (lower_polymorphic_call,
module_polymorphic_fns, mono_queue, mono_emitted, emit_specialised_fn,
plus the now-dead helpers apply_subst_to_term, descriptor_for_subst,
type_descriptor) entirely. Codegen sees only monomorphic Def::Fns after
this iter — no polymorphic call sites, no codegen-time queue.

Architecture changes:
- MonoTarget: struct → enum with ClassMethod / FreeFn variants; dedup
  keys are guaranteed-disjoint via 'class'/'free' first component.
- collect_mono_targets: new FreeFnCall channel through synth (cleaner
  than the plan's separate-walker proposal — one less data flow,
  substitution tracking comes 'for free' via fresh metavars +
  post-synth subst.apply). Plan Step 4.4 (polymorphic_free_fns env
  field) consequently obsolete.
- synthesise_mono_fn_for_free_fn: new free-fn entry point; substitutes
  rigid vars in BOTH the type AND the body (body substitution required
  because free-fn bodies carry inner Lams whose param_tys reference
  outer Forall vars — adapted from plan's 'body unchanged' sketch).
- mono_symbol_n: N-ary extension of mono_symbol; bit-stable for the
  single-type-var case (hash-stability pin Task 1 fires zero times
  through all eight refactor tasks).
- rewrite_class_method_calls → rewrite_mono_calls; interleaved-slot
  collector preserves the positional-cursor invariant across both
  channels.
- workspace_has_typeclasses → workspace_has_specialisable_targets
  (principled correctness; old predicate happened to already be true
  for every user workspace because the prelude is auto-injected).

Codegen cleanup:
- Two call sites of lower_polymorphic_call deleted (codegen/src/lib.rs
  lines ~1939-1945, ~1965-1973).
- lower_polymorphic_call body, module_polymorphic_fns field +
  population + Emitter wiring, mono_queue / mono_emitted, drain loop,
  emit_specialised_fn — all removed.
- is_static_callee collapsed to module_user_fns-only.
- subst.rs apply_subst_to_term + descriptor_for_subst removed; synth.rs
  type_descriptor removed (all dead post-removal).
- ail emit-ir command pipeline gains lift_letrecs + monomorphise_workspace
  pre-passes (pre-iter-23.4 the codegen-time poly path was masking
  this dependency — emit-ir is now consistent with build).
- Net codegen reduction: -285 lines lib.rs, -93 lines subst.rs.

Tests:
- mono_hash_stability.rs (new): regression pin for six primitive Eq/Ord
  mono-symbol body hashes; stayed bit-stable through all eight refactor
  tasks.
- mono_unification.rs (new): six tests covering variant-key disjointness,
  free-fn target emission, free-fn synthesis with rigid substitution,
  rewrite walker on free fns, identity for poly-free-fn-only workspaces,
  end-to-end cmp_max_smoke runtime correctness.
- typeclass_22b3.rs: three test sites updated to MonoTarget::ClassMethod
  enum form.
- 73 e2e + 18 typeclass + 81 codegen tests all green workspace-wide.

DESIGN.md §'Monomorphisation (post-typecheck, pre-codegen)' amended:
two-arm fixpoint described, disjoint-keyed dedup, cursor-aligned
walker, removed codegen-time path.

Bench check (all exit 0, 112 metrics stable): bench/check.py +
compile_check.py + cross_lang.py.

Plan parent: docs/plans/2026-05-11-iter-23.4.md (fab1685).
Spec parent: docs/specs/2026-05-11-23-eq-ord-prelude.md (841d65d).
Per-iter journal documents two adapted deviations from the plan
(synth-channel vs separate walker; body substitution; emit-ir fix);
no spec defects.
2026-05-11 23:59:45 +02:00