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.
8.1 KiB
iter 23.4 — Mono-Pass Unification
Date: 2026-05-11
Started from: fab1685
Status: DONE
Tasks completed: 11 of 11
Summary
Restructured crates/ailang-check/src/mono.rs::monomorphise_workspace
into a single specialiser over every Type::Forall-quantified
Def::Fn in one fixpoint pass, covering both class-method residuals
and polymorphic free-fn call sites. Removed the codegen-time
specialiser entirely (lower_polymorphic_call, module_polymorphic_fns,
mono_queue, mono_emitted, emit_specialised_fn, plus the dead
helpers apply_subst_to_term, descriptor_for_subst, type_descriptor).
Codegen now sees only monomorphic Def::Fns — no polymorphic call
sites, no codegen-time queue.
The unified pass produces bit-identical mono-symbol bodies for the
six existing primitive Eq/Ord symbols (hash-stability pin Task 1,
crates/ail/tests/mono_hash_stability.rs) and produces new free-fn
mono symbols on demand for fixtures like cmp_max_smoke (Tasks 2,
6, 9). All 73 e2e tests + 18 typeclass tests + 81 codegen tests pass
end-to-end against the new architecture.
Per-task notes
- iter 23.4.1: hash-stability regression pin —
examples/mono_hash_pin_smoke.ail.json+mono_hash_stability.rswith six pinned hashes (eq__Int/Bool/Str,compare__Int/Bool/Str). - iter 23.4.2:
cmp_max_smokefixture + RED workspace-level acceptance gate. - iter 23.4.3:
MonoTargetstruct → enum withClassMethod/FreeFnvariants;mono_target_keywidened with"class"/"free"first component (guaranteed-disjoint keys). - iter 23.4.4: free-fn arm in
collect_mono_targets. NOTE: the plan's Step 4.3 sketch proposed a separate walker overTerm::Appplus a newenv.polymorphic_free_fnsindex; instead I extendedsynthitself with a parallel&mut Vec<FreeFnCall>channel (mirroring the existing&mut Vec<ResidualConstraint>mechanism). One less data flow, one less helper; substitution tracking comes "for free" via fresh metavars + post-synthsubst.apply. Plan's Step 4.4 (addingpolymorphic_free_fnstoEnv) is therefore obsolete; not landed. - iter 23.4.5:
synthesise_mono_fn_for_free_fn+ N-arymono_symbol_n. One substantive deviation from the plan's sketch: I also substitute rigid vars in the BODY via a newcrate::substitute_rigids_in_termhelper. The plan said "passes through unchanged" mirroring the class-method arm — but free-fn bodies (e.g.std_list.length) carry innerTerm::Lams whoseparam_tysreference outer Forall vars; without body substitution, Phase 3's re-synth would fail unification on unbound rigida. - iter 23.4.6:
rewrite_class_method_callsrenamed torewrite_mono_calls; new helperpoly_free_fn_names_for_modulebuilds the per-module set of bare + qualified poly-fn names;collect_residuals_orderedwalks the AST emitting interleaved slots via newinterleave_slotshelper. The walker and slot collector share the same predicate so the cursor invariant survives across the two channels. - iter 23.4.7:
workspace_has_typeclasses→workspace_has_specialisable_targets. NOTE: the plan's expected "FAIL → PASS after generalisation" was incorrect because the auto-injected prelude (iter 23.1) already keeps the old predicate true for every user workspace. The generalisation is principled-correctness only; documented in the predicate docstring. - iter 23.4.8: codegen cleanup — removed
lower_polymorphic_call, both call sites, the Emitter fields (module_polymorphic_fns,mono_queue,mono_emitted),emit_specialised_fn, Pass-1 poly population, and the unused dead helpers (apply_subst_to_term,descriptor_for_subst,type_descriptor).is_static_calleecollapsed to its mono-only path. Two follow-on fixes were required beyond the plan: (a) Unit-default fallback for unpinned forall vars in the mono pass — mirrors pre-iter-23.4 codegen behaviour for sites likeis_empty(Nil)where the elem type is unobservable from the args alone; (b) updated theail emit-ircommand pipeline to includelift_letrecs+monomorphise_workspace(pre-iter-23.4 the codegen-time poly path was masking this dependency —emit-irwas broken for poly fixtures without the explicit mono call now that codegen no longer handles it). - iter 23.4.9: end-to-end verification —
cmp_max_smoke_runs_end_to_endprovescmp_max(3, 7)at Int prints 7; the three existing poly fixtures (polymorphic_id_at_int_and_bool,polymorphic_apply_with_fn_param,poly_rec_capture_demo) still run. - iter 23.4.10:
docs/DESIGN.md§"Monomorphisation (post-typecheck, pre-codegen)" amended to describe the two-arm fixpoint, the dedup key with disjoint"class"/"free"prefixes, the cursor-aligned walker, and the removed codegen-time path. - iter 23.4.11: bench-regression check —
bench/check.py,bench/compile_check.py,bench/cross_lang.pyall exit 0; no metrics regressed.
Concerns
- Plan Step 4.4 was bypassed (no
env.polymorphic_free_fnsfield added). The synth-channel approach is structurally cleaner; the plan's index is unneeded. If a future iter needs an O(1) "is this name a poly free fn" predicate at non-synth call sites, the helperpoly_free_fn_names_for_module(in mono.rs) builds the set on demand. - Plan Steps 5.3 / 7's "body unchanged" / "early-out fires for class-free workspaces" assumptions were both wrong against the actual codebase shape (free-fn bodies carry rigid vars in inner Lams; prelude is auto-injected so no workspace is class-free). Both were caught and adapted during implementation — no spec defect, just plan-draft drift against the live codebase.
Known debt
- None tracked; all out-of-scope items from the plan (five prelude free fns ne/lt/le/gt/ge,
examples/prelude.ail.jsonedits, the negative E2E fixtureseq_ord_polymorphic/eq_ord_user_adt, DESIGN.md §"Decision 11" amendment, Float-NoInstance diagnostic wording, roadmap update) remain owned by iter 23.5 as planned.
Files touched
crates/ailang-check/src/lib.rs— newFreeFnCallstruct;synthsignature gains&mut Vec<FreeFnCall>; Var arm tracks(owner_module, unqualified_name)and pushesFreeFnCallforType::Forall-resolved non-local refs; newsubstitute_rigids_in_termhelper.crates/ailang-check/src/mono.rs—MonoTargetstruct → enum (ClassMethod/FreeFn);mono_target_keywidened; newmono_symbol_n(N-ary); newsynthesise_mono_fn_for_free_fn; newpoly_free_fn_names_for_module; newinterleave_slots; renamerewrite_class_method_calls→rewrite_mono_calls;workspace_has_typeclasses→workspace_has_specialisable_targets; fixpoint loop matches on target variants.crates/ailang-check/src/lift.rs,crates/ailang-check/src/builtins.rs— synth call sites pass new&mut free_fn_callsVec.crates/ailang-codegen/src/lib.rs— deletions per Task 8 (Emitter fields,lower_polymorphic_call,emit_specialised_fn, mono drain loop, Pass-1 poly population,is_static_calleepoly arm).useimport shrunk.crates/ailang-codegen/src/subst.rs—apply_subst_to_termanddescriptor_for_substremoved (dead post-Task-8).crates/ailang-codegen/src/synth.rs—type_descriptorremoved (dead post-Task-8).crates/ail/src/main.rs—emit-ircommand gainslift_letrecs+monomorphise_workspacepre-passes (matchesbuildshape).crates/ail/tests/typeclass_22b3.rs— updated three test sites toMonoTarget::ClassMethodenum form.crates/ail/tests/mono_hash_stability.rs(new) — regression pin for six primitive Eq/Ord mono-symbol body hashes.crates/ail/tests/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, and end-to-endcmp_max_smokeruntime correctness.examples/cmp_max_smoke.ail.json(new) —cmp_max : forall a. Ord a => (a, a) -> acomposition fixture.examples/mono_hash_pin_smoke.ail.json(new) — fixture exercising the six primitive Eq/Ord mono symbols for hash-stability pinning.docs/DESIGN.md— §"Monomorphisation (post-typecheck, pre-codegen)" amended.
Stats
bench/orchestrator-stats/2026-05-11-iter-23.4.json