All 176 files in the four accumulating directories now use a zero-padded 4-digit counter prefix that reflects creation order (`NNNN-slug.md`). The counter is assigned per directory in strict git-log creation order; ties broken alphabetically by original name. The old `YYYY-MM-DD-` prefix on docs/specs/ and docs/plans/ files is dropped — the date is recoverable from git log and the counter carries the ordering. A file's counter is stable for the life of the file: never reassigned, never reused, never compacted. Deleted files retire their counter; subsequent files do not fill the gap. This is the property that lets cross-references stay literal — refs use the full filename including the counter (`design/contracts/0007-honesty-rule.md`) so they grep cleanly and resolve directly without a glob step. 313 cross-references updated across .md/.rs/.toml/.c/.json files (test pins, include_str! paths, design-INDEX entries, baseline notes, runtime C comments, inter-contract markdown links incl. bare basename and `../models/foo.md` forms). CLAUDE.md gets a new "File-naming convention" section spelling out the rule and rationale. skills/brainstorm/SKILL.md and skills/planner/SKILL.md updated so new spec/plan creation produces counter-prefixed names from the start. The full test suite (cargo test --workspace) passes.
32 KiB
24 — Show + print rewire — Design Spec
Date: 2026-05-12
Status: Partial — iter 24.1 shipped 2026-05-12 @ f38bad8
(bool_to_str + str_clone runtime + codegen wiring); iters 24.2
and 24.3 deferred on 2026-05-13 pending the retirement of the
MethodNameCollision workaround. The deferral was triggered by a
spec-gap surfaced during 24.2 plan-recon: adding class Show to
the prelude collides with the user-class Show declared by 14
test fixtures under examples/test_22b{1,2,3}_*.ail.json (plus
hardcoded "Show" / "show" assertions in
crates/ail/tests/typeclass_22b{2,3}.rs) through the
workspace-global method-name-collision pre-pass at
crates/ailang-core/src/workspace.rs:547. Three resolution paths
were surfaced; the user picked "defer until
MethodNameCollision retires" per the P2 roadmap entry
"Module-qualified class names + type-driven method dispatch". A
fresh brainstorm re-derives 24.2 + 24.3 from scratch against the
post-retirement architecture; this document remains as historical
context for that re-brainstorm.
Authors: Brummel (orchestrator) + Claude
Goal
Close the prelude story opened by milestone 23 (Eq/Ord) by shipping:
- The
Showtypeclass with single methodshow : (a borrow) -> Str. - Four primitive instances:
Show Int,Show Bool,Show Str,Show Float. - A polymorphic free fn
print : forall a. Show a => (a borrow) -> () !IOthat routes throughshowandio/print_str, replacing the ad-hoc per-type print idiom for new code. - Two new runtime primitives that let
Show BoolandShow Strhonour the uniform(a borrow) -> Str Ownsignature without special-casing the codegen pipeline.
The milestone is the second and final half of the original
"Post-22 Prelude" roadmap entry. The first half (Eq/Ord) shipped as
milestone 23. The two halves were split because Show had a hard
dependency on the heap-Str ABI (int_to_str / float_to_str had
to allocate heap-Str slabs at runtime), which itself was a
substantively separate milestone. That dependency closed in the
heap-Str ABI milestone (hs.1–hs.4 plus eob.1, ratified at
audit-eob on 2026-05-12). int_to_str / float_to_str are now
fully wired and RC-disciplined, removing the only barrier.
Existing per-type effect-ops io/print_int / io/print_bool /
io/print_float STAY in this milestone. The redundancy between
print x and io/print_int x for x : Int is ratified as
transitional, mirroring the same call milestone 23 made for ==
vs. eq. Retiring the per-type effect-ops and migrating the
example corpus (86 fixtures) is queued as a separate P2 follow-up;
this milestone is the architecture-shipping milestone, not the
migration milestone.
Architecture
Three artefacts, in dependency order.
1. Two new runtime primitives
runtime/str.c gains two functions paralleling the existing
ailang_int_to_str and ailang_float_to_str:
ailang_bool_to_str(int1_t b) -> heap_str_ptr— returns a freshly allocated heap-Str slab containing either"true"or"false"(5 or 6 bytes inclusive of trailing NUL). The slab is allocated viaailang_rc_allocwithrc_headeratpayload - 8, exactly likeint_to_str. Always heap-allocates; never returns a static-Str pointer. The allocation cost is paid everyshow True/show Falsecall; the alternative (schema-ifwith two static-Str literals) is deferred as an open commitment because phi-of-static-Str through the drop-elision pipeline is not load-bearing-ratified today.ailang_str_clone(const heap_str_ptr src) -> heap_str_ptr— readsi64 lenfrom offset 0 of the source slab and allocates a fresh heap-Str slab of the same length, memcpy'slen + 1bytes (payload + trailing NUL) from source to destination, and returns the new slab pointer. Works uniformly on both static-Str and heap-Str inputs because the consumer ABI is identical (see DESIGN.md §"Str ABI"); only thelenfield at offset 0 plus the bytes plus the NUL are consulted, never the (absent for static-Str)rc_headerof the source.
Both new symbols install in crates/ailang-check/src/builtins.rs
alongside int_to_str / float_to_str:
bool_to_str : (Bool) -> Strwithret_mode: Own.str_clone : (Str borrow) -> Strwithret_mode: Own.
Both lower in crates/ailang-codegen/src/lib.rs::Emitter::lower_app
via new arms paralleling the existing int_to_str and
float_to_str arms (extern declarations in the IR header preamble,
direct call emission, Position::Consume for the result let-binder
per eob.1's effect-op-Borrow / let-binder-Own discipline).
The is_static_callee whitelist in crates/ailang-codegen/src/synth.rs
extends to include both new names.
2. Show typeclass + four primitive instances
examples/prelude.ail.json gains five new top-level defs:
// class
{ "kind": "class",
"name": "Show",
"param": "a",
"methods": [
{ "name": "show",
"type": {
"k": "fn",
"params": [{ "k": "var", "name": "a" }],
"param_modes": ["borrow"],
"ret": { "k": "con", "name": "Str" },
"effects": []
}
}
]
}
// instance Show Int: body invokes int_to_str
// instance Show Bool: body invokes bool_to_str
// instance Show Str: body invokes str_clone
// instance Show Float: body invokes float_to_str
Each instance body is a single-application lambda
\x -> <primitive_to_str> x. No codegen intercept (mirror of
try_emit_primitive_instance_body from Eq/Ord) is required because
the four primitives are themselves first-class lowered functions —
unlike Eq/Bool/Str whose == bodies needed try_emit_primitive_instance_body
to redirect to the appropriate icmp or @ail_str_eq lowering. The
mono pass synthesises show__Int, show__Bool, show__Str,
show__Float; each body is a direct call to the corresponding
primitive.
Float is included in the Show prelude — unlike Eq/Ord which
explicitly excluded Float because IEEE-754 makes structural equality
and total ordering semantically dubious. Producing a textual
representation of a Float is well-defined modulo the NaN spelling
caveat in DESIGN.md §"Float semantics" (printf("%g", nan) may emit
nan / -nan / NaN); this caveat is acceptable for a
human-readable Show.
3. print polymorphic free fn
examples/prelude.ail.json also gains a sixth new top-level def:
{ "kind": "fn",
"name": "print",
"type": {
"k": "forall",
"vars": ["a"],
"constraints": [{ "class": "Show", "type": { "k": "var", "name": "a" } }],
"body": {
"k": "fn",
"params": [{ "k": "var", "name": "a" }],
"param_modes": ["borrow"],
"ret": { "k": "con", "name": "Unit" },
"effects": ["IO"]
}
},
"params": ["x"],
"body": {
"t": "let",
"name": "s",
"value": { "t": "app", "fn": { "t": "var", "name": "show" }, "args": [{ "t": "var", "name": "x" }] },
"body": { "t": "do", "op": "io/print_str", "args": [{ "t": "var", "name": "s" }] }
}
}
The let-binder for show x is explicit, not implicit. This is
load-bearing for the heap-Str RC discipline: the Own heap-Str
returned by show x (for Int/Bool/Str/Float, every instance allocates)
needs a let-binder so that
drop_symbol_for_binder fires the heap-Str drop at scope close —
the Str carve-out path introduced in eob.1. Without the let-binder,
the heap-Str slab from show x would not be RC-tracked at any
binder and the slab would leak (the same failure mode the eob.1 RED
tests pinned).
print is a free fn, not a class method. Two reasons:
- The body is identical for every
a : Show a— no per-instance variation. The per-instance dispatch happens insideshow x, not inprint's body. A class method with an identical body for every instance would be redundant. - It matches Haskell's Prelude convention (
print :: Show a => a -> IO ()is a free fn, not a class method) and the milestone 23 free-fn pattern (ne/lt/le/gt/ge).
The mono pass (crates/ailang-check/src/mono.rs, unified in 23.4)
handles print exactly as it handles ne: free-fn entry, rigid-var
substitution a → Int (or whichever concrete type), body walker
sees the nested show call at concrete type, schedules show__Int
in the same fixpoint round, the synthesised print__Int body
references show__Int directly.
Components (iterations)
| Iter | Status | Scope |
|---|---|---|
| 24.1 | upcoming | Runtime + codegen for two new primitives. runtime/str.c gains ailang_bool_to_str and ailang_str_clone. crates/ailang-check/src/builtins.rs installs bool_to_str : (Bool) -> Str and str_clone : (Str borrow) -> Str, both with ret_mode: Own. crates/ailang-codegen/src/lib.rs::Emitter::lower_app gains two new arms emitting direct calls to @ailang_bool_to_str and @ailang_str_clone, plus extern declarations in the IR header preamble. crates/ailang-codegen/src/synth.rs::is_static_callee whitelist extends. RC-stats E2E tests pin allocs == 1, frees == 1, live == 0 for both primitives. IR-shape pins for the two extern declarations and the two call emissions. |
| 24.2 | upcoming | class Show + 4 primitive instances in prelude. examples/prelude.ail.json gains class Show a where show : (a borrow) -> Str and four instance Show <T> defs (Int/Bool/Str/Float), each with a single-application lambda body invoking the corresponding *_to_str primitive. Round-trip test confirms prelude.ail.json parses + canonicalises bit-stable. Mono synthesis test confirms show__Int, show__Bool, show__Str, show__Float Def::Fn entries are produced. DESIGN.md §"Prelude (built-in) classes" amended to include Show with the four instances. |
| 24.3 | upcoming | print polymorphic free fn + E2E + DESIGN.md sync. examples/prelude.ail.json gains fn print : forall a. Show a => (a borrow) -> () !IO with body \x -> let s = show x in do io/print_str s. E2E fixtures: (a) positive — do print 42, do print True, do print "hello", do print 3.14 produce expected stdout; (b) user-ADT — data IntBox = MkIntBox Int + instance Show IntBox where show x = match x with MkIntBox n -> int_to_str n + print (MkIntBox 7) produces stdout "7"; (c) negative — print f where f : Int -> Int fires NoInstance Show <fn-type> at typecheck with a Show-aware diagnostic. DESIGN.md §"Prelude (built-in) classes" amended to include print; §"Float semantics" gains a paragraph linking Show Float (via float_to_str) to the existing NaN-spelling caveat. Roadmap P1 "Post-22 Prelude" entry flips to [x]; new P2 entry added: "retire `io/print_int |
The milestone closes with the standard audit pipeline (architect
drift review + three bench scripts).
Data flow
Two trajectories show the routing at work.
print 42 at type Int
- Typecheck.
printresolves to the polymorphic free fn withforall a. Show a => (a borrow) -> () !IO. Bare-name lookup reaches the prelude viaenv.module_globalsfall-through (prep1 from 23.4-prep). ConstraintShow Intdischarged against the registry entry forinstance Show Int. - Mono (free-fn entry). Body taken directly from
Def::Fn.body, rigid-var substa → Int. The body walker detects the nestedshow xcall at concreteIntand schedulesshow__Intin the same fixpoint round.show__Intsynthesises from theinstance Show Intbody (class-method entry,Registry::entries[(Show, Int)]lookup). The synthesisedshow__Intbody is\x -> int_to_str x. The synthesisedprint__Intbody is\x -> let s = show__Int x in do io/print_str s. - Codegen.
print__Intlowers as any monomorphic fn. The nestedshow__Int xis a direct call returning Own heap-Str; the let-bindersis RC-tracked at scope close per the eob.1 Str carve-out indrop_symbol_for_binder.do io/print_str swalkssasPosition::Borrow(eob.1's effect-op arg rule); the let-binder's drop fires after the effect-op call returns, emitting@ailang_rc_dec(s_ptr)which decrements the heap-Str's rc_header to zero and frees the slab. Stdout:"42\n".
print 3.14 at type Float
Identical to the above with a → Float, Show Float, show__Float,
int_to_str replaced by float_to_str. The NaN-spelling caveat
applies if the Float happens to be NaN: the rendered output follows
libc's %g formatting, identical to io/print_float. Stdout for
3.14: depends on libc but typically "3.14\n"; for 0.0 / 0.0:
typically "nan\n" or "-nan\n".
print x at user type IntBox with instance Show IntBox
- Typecheck.
Show IntBoxdischarged against the registry entry forinstance Show IntBox. - Mono.
print__IntBoxsynthesises with body\x -> let s = show__IntBox x in do io/print_str s.show__IntBoxsynthesises from the instance body (whatever the user wrote, e.g.\x -> match x with MkIntBox n -> int_to_str n). - Codegen. Lowers normally; ADT-pattern-match plus inner
int_to_strcall produce a heap-Str; outer let-binder drops at scope close.
Error handling
-
NoInstance Show <T>— fired at typecheck whenprintis called on a type without aShowinstance (most commonly a function type, or a user type the author forgot to give an instance for). Diagnostic must cross-reference DESIGN.md §"Prelude (built-in) classes" so the LLM-author immediately learns which types ship with built-in Show instances. -
NoInstance Show <UserType>— re-uses the existing 22-era diagnostic path; no new code shape. -
OrphanInstance Show <T>— Decision 11 coherence rule unchanged. The four primitive instances ship in the prelude (Show's defining module =Int/Bool/Str/Float's defining module = prelude module; non-orphan by construction). User-sideinstance Show <UserType>MUST live in the user type's defining module or in the prelude module (the latter is not possible for user types since the prelude is read-only), which constrains user instances to the user-type's defining module. -
CheckError::Internalfrom mono — ifShow-specialisation fails internally (e.g. the body walker fails to scheduleshow__Twhen synthesisingprint__T), this is a caller-contract violation per the existing convention inmono.rs::synthesise_mono_fn. Mirrors 23.4's free-fn-entry error handling.
Testing strategy
24.1 — runtime + codegen for bool_to_str + str_clone
-
Unit: each primitive lowers to expected IR. Two new IR-shape pins in
crates/ailang-codegen/src/lib.rs::tests:bool_to_str_emits_call_to_ailang_bool_to_str— fixturelet s = bool_to_str true in do io/print_str s; assert IR body containscall ptr @ailang_bool_to_str(i1 1)anddeclare ptr @ailang_bool_to_str(i1).str_clone_emits_call_to_ailang_str_clone— fixturelet s = str_clone "hi" in do io/print_str s; assert IR body containscall ptr @ailang_str_clone(ptr ...)anddeclare ptr @ailang_str_clone(ptr).
-
E2E RC-stats (two tests in
crates/ailang-codegen/tests/e2e.rs):bool_to_str_drop_balances_rc_stats— fixturelet s = bool_to_str true in do io/print_str s. Stdout"true\n". RC stats:allocs == 1, frees == 1, live == 0.str_clone_drop_balances_rc_stats— fixturelet s = str_clone "hello" in do io/print_str s. Stdout"hello\n". RC stats:allocs == 1, frees == 1, live == 0.
-
E2E stdout-smoke (two tests):
bool_to_str_emits_true_or_false— both branches:let s = bool_to_str true in do io/print_str s→"true\n"; same withfalse→"false\n".str_clone_emits_input_bytes—str_clone "world"→ stdout"world\n". Confirms memcpy semantics, not just the pointer.
-
Cross-realisation test for
str_clone: clone a heap-Str (output ofint_to_str 42) AND a static-Str (literal"abc"). Both produce heap-Str outputs with correct bytes. RC discipline green in both cases. Pins the "uniform consumer ABI" claim from DESIGN.md §"Str ABI". -
Linker / build hygiene:
cargo test --workspacegreen. The__attribute__((weak))onailang_rc_allocextern (hs.3 pattern) is unchanged —rc.cis unconditionally linked post-eob.1, so the weak symbol is a documented no-op.
24.2 — class Show + 4 instances
-
Round-trip:
examples/prelude.ail.jsonparses + canonicalises bit-stable after the four new defs land. The existing prelude hash-stability test from milestone 23 stays green (the prelude's pre-existing defs — Ordering, Eq, Ord, instances, ne/lt/le/gt/ge — are unaffected). -
Mono synthesis (unit test in
crates/ailang-check/src/mono.rs::tests): for a fixture that callsshow 1,show True,show "x", andshow 1.5, assert that the post-mono workspace containsshow__Int,show__Bool,show__Str,show__FloatasDef::Fnentries. Bodies are direct calls to the corresponding primitives. -
Hash stability of existing mono symbols:
eq__Int,compare__Str, and the four other class-method mono symbols from milestone 23 must produce identical bodies (and therefore identical hashes) under the unified pass after the Show defs are added. Pin test in the style ofcrates/ailang-core/src/hash.rs::ct4_migrated_fixtures_have_canonical_form_hashes. -
Round-trip of new fixtures — every fixture authored under 24.2 passes Form-A parser + canonicaliser bit-stable.
24.3 — print polymorphic free fn + E2E
-
Positive E2E (
examples/show_print_smoke.ail.json): a single fixture exercises all four primitives:fn main = do print 42 -- "42\n" print true -- "true\n" print "hello" -- "hello\n" print 3.14 -- "3.14\n" (libc-dependent)Test asserts compile, run, expected stdout (the Float line uses a non-NaN, non-inf value where libc rendering is stable across versions —
3.14's%grendering is"3.14"on every libc this project targets). -
User-ADT E2E (
examples/show_user_adt.ail.json):data IntBox = MkIntBox Int instance Show IntBox where show = \x -> match x with MkIntBox n -> int_to_str n fn main = do print (MkIntBox 7)Stdout
"7\n". Confirms the mono pass handles user-defined Show instances and thatprintcomposes with them. -
Negative E2E (
examples/show_no_instance.ail.json):fn main = do print id -- id : forall a. a -> a, no Show instanceTypecheck rejects with
NoInstance Show <fn-type>. The exact diagnostic wording is named as an Open commitment below ("Negative-test diagnostic wording"); the gold-standard test pins the message verbatim after the implementer drafts it. -
Mono symbol IR-shape pin:
crates/ailang-check/src/mono.rs::testsassertsprint__Intbody desugars to\x -> let s = show__Int x in do io/print_str spost-mono. Confirms the let-binder is preserved (not optimised away — load-bearing for the heap-Str RC discipline). -
Round-trip of new fixtures.
-
Bench regression check at milestone close: the three bench scripts exit 0 or audit-ratified per audit-skill convention.
Acceptance criteria
The milestone closes when:
-
24.1 has landed:
runtime/str.cdefinesailang_bool_to_strandailang_str_clone.crates/ailang-check/src/builtins.rsinstalls both signatures withret_mode: Own.crates/ailang-codegen/src/lib.rs::Emitter::lower_appemits direct calls plus extern declarations. RC-stats E2E pins balance. -
24.2 has landed:
examples/prelude.ail.jsoncontainsclass Show a where show : (a borrow) -> Strand four primitive instances (Int/Bool/Str/Float). The mono pass synthesisesshow__Int,show__Bool,show__Str,show__Float. DESIGN.md §"Prelude (built-in) classes" amended to include Show with the four instances. -
24.3 has landed:
examples/prelude.ail.jsoncontainsfn print : forall a. Show a => (a borrow) -> () !IOwith the explicit-let body shape. The mono pass synthesisesprint__Int,print__Bool,print__Str,print__Floatfor each call site at a primitive type, plusprint__<UserType>for each user-instance call site. Positive / user-ADT / negative E2E green. DESIGN.md §"Prelude (built-in) classes" listsprintas the polymorphic helper; §"Float semantics" gains the Show Float NaN-spelling cross-reference paragraph. -
Tests pass:
cargo test --workspacegreen; existing milestone 23 prelude fixtures (ne/lt/le/gt/geE2E) unchanged; existing typeclass fixtures (eq__Int,compare__Str, etc.) hash-stable. -
Bench regression ratified:
bench/check.py && bench/compile_check.py && bench/cross_lang.pyexit 0 or audit-ratified with journal entry. -
Roadmap update: P1 "Post-22 Prelude — Show + print rewire" entry flips to
[x]. New P2 entry: "Retireio/print_int/io/print_bool/io/print_floateffect-ops + migrate example corpus toprint". A user runs the bulk text substitutiondo io/print_<T> x→do print xacrossexamples/*.ail.json(86 files affected), and the per-type ops are deleted frombuiltins.rs/lib.rscodegen arms / runtime C glue.
Out of scope (deferred, with substantive rationale)
-
Retiring
io/print_int/io/print_bool/io/print_float. Queued as P2 follow-up. The architecture-shipping milestone and the corpus-migration milestone are kept separate so the migration is one mechanical pass against a frozen architecture, not a moving target. Same call milestone 23 made for the==/eq,</ltredundancy. -
io/print_strretirement. Stays. Load-bearing insideprint's body (do io/print_str s); cannot be retired without an alternate primitive output path. Also stays as a directly-callable surface for Str-typed values where the LLM-author wants to bypass Show (e.g. when the Str is already in hand andshow__Str'sstr_clone-allocation is wasteful). -
bool_to_stras schema-if. The body\x -> if x then "true" else "false"is schematically valid but requires phi-of-static-Str to flow cleanly through the drop-elision pipeline (DESIGN.md §"Str ABI" codegen-level static-Str elision). The phi case is not load-bearing-ratified today; the safe always-correctbool_to_strprimitive avoids the question. Open commitment: if a future codegen iter ratifies phi-of-static-Str drop-elision, swapshow__Boolbody to the schema-ifform and retirebool_to_str. -
str_cloneas rc_header bump. For heap-Str inputs,str_clonecould rc_inc the existing slab instead of allocating a fresh copy. This requires the implementation to distinguish heap-Str from static-Str at runtime, which the codebase deliberately does NOT support post-hs.2 (the UINT64_MAX sentinel was dropped in favour of codegen-time elision). Open commitment for a future iter: if runtime tagging is reintroduced for a different reason,str_clonecan become an rc_inc on heap-Str / no-op-with-fresh- alloc on static-Str hybrid. For now: always-allocate, always-copy. -
Show Orderinginstance. The prelude'sOrderingADT has three constructors (LT/EQ/GT). ShippingShow Orderingwould add a user-friendly pretty-print but no concrete demand exists and the design-by-default in this milestone is "instances ship for types LLM authors actually print". User can declare their owninstance Show Orderingif they want it. -
Showfor ADTs viaderiving. No auto-derivation. Theinstance Show <UserType>body is hand-written per instance. Auto-derivation is a substantive feature in its own right (canonical-form choices, recursive descent, list / tuple Show bodies) and out of scope here. -
Higher-kinded
Show(Show [a],Show (Maybe a)). Decision 11 axis 5 explicitly excludes higher-kinded class params. The LLM-natural pattern is per-type instances (Show [Int]as a monomorphic instance, hand-written). Same constraint as Eq/Ord. -
printwith newline-free variant (putStr/print_no_newline).io/print_strcalls@putswhich appends a newline. Everyprint xtherefore ends in\n. A newline-suppressed variant is a separate feature; deferred until a concrete LLM-author pattern demands it. -
Operator routing through
Show(showinvoked as<<or similar). No commitment. AILang does not have a Haskell-style monadic operator surface and adding one to invokeshowimplicitly would re-open the ergonomics question Decision 6 closed in favour of explicitdoblocks.
Open commitments (24.1 / 24.2 / 24.3 implementer)
-
bool_to_strruntime body. The 4-byte slab"true"and 5-byte slab"false"can either be heap-allocated per call (current spec) or cached as module-level static slabs the function returns by-pointer. The cached variant is faster but re-introduces the same "static-Str-without-sentinel" question the project deliberately avoided. Implementer ships the always-heap-allocate variant unless a clear path through the drop-elision pipeline appears. -
str_clonebyte count. The naive memcpy copieslen + 1bytes (payload + trailing NUL). The implementation may choose to recompute the NUL viastrncpy-style code instead of trusting the trailing NUL in the source; either is correct, both are acceptable as long as the result slab is valid UTF-8-or-bytes per the source's contract. -
Negative-test diagnostic wording. Implementer drafts the one-line Show-aware message for
NoInstance Show <T>; orchestrator reviews in spec-compliance phase. -
Implicit-let vs. explicit-let in
print's body. The spec prescribes the explicit-let form\x -> let s = show x in do io/print_str s. If the AILang desugarer would automatically introduce a let-binder for any Term::App in effect-op-arg position, the explicit-let could be dropped from the source form. Implementer verifies the desugarer's behaviour during 24.3 Task 1; if implicit-let is the realised semantics, the prelude source form may be the more compact\x -> do io/print_str (show x). The load-bearing property is "the heap-Str returned byshow xhas a tracked let-binder when entering the effect-op call site"; whether the let-binder is sourced from the prelude form or desugar-introduced is a surface-level choice with identical semantics post-checker. -
Show instance for the
Unittype / Tuples / Lists. Out of scope per "instances for types LLM authors actually print" — but if 24.3's user-ADT E2E surfaces a recurring shape (e.g. printing a Tuple inside theIntBoxtest), the implementer may queue a P3 idea entry without expanding the milestone scope.
Known costs
-
24.1 runtime + codegen: estimated +30 LOC in
runtime/str.c(two new functions, ~15 LOC each), +20 LOC incrates/ailang-check/src/builtins.rs(two new signature installations parallel to the existingint_to_str/float_to_strblocks), +30 LOC incrates/ailang-codegen/src/lib.rs(two new arms inlower_app- two extern declarations in the IR header preamble), +2 LOC in
crates/ailang-codegen/src/synth.rs::is_static_callee.
- two extern declarations in the IR header preamble), +2 LOC in
-
24.2 prelude additions:
examples/prelude.ail.json+40–50 lines (one class + four instances, JSON shape parallel to Eq Int/Bool/Str). DESIGN.md amendment ~10 lines. -
24.3 prelude additions + tests:
examples/prelude.ail.json+30 lines (one polymorphic free fn with constraint signature and let-bound body). Three new E2E.ail.jsonfixtures, ~40 lines JSON each. Diagnostic test ~20 lines Rust. DESIGN.md amendments ~20 lines (Prelude classes + Float semantics cross-reference). -
Net code reduction: none. This milestone is additive. Reduction comes later in the P2 follow-up (retiring three builtin effect-ops + three codegen arms + three runtime glue functions, estimated ~−150 LOC).
-
Compile-time shift: the mono pass gains four new instance-method synthesis targets (
show__Int|Bool|Str|Float) plus one new free-fn target (print) per call site. The shift is bounded — additive per call site, no new fixpoint depth — and is expected to be within bench tolerance. Quantified bybench/compile_check.pypost-24.3; audit-ratified or rebaselined. -
Runtime cost on
print x:- For
x : Int— oneint_to_strallocation (~3–20 bytes per Int depending on magnitude) plus one rc_dec at scope close. - For
x : Bool— onebool_to_strallocation (5 or 6 bytes) plus one rc_dec. - For
x : Str— onestr_cloneallocation + memcpy plus one rc_dec. Strictly worse thando io/print_str x. The redundancy is acknowledged; LLM-author has the recourse to bypassprintfor Str-typed values. - For
x : Float— onefloat_to_strallocation plus one rc_dec.
- For
Load-bearing assumptions about current behaviour
Surfaced explicitly so the Step 7.5 grounding-check agent has a canonical list to ratify. Each assumption is something this spec relies on as currently-true.
-
crates/ailang-check/src/mono.rs::monomorphise_workspaceis the single specialiser for both class methods and polymorphic free fns (per milestone 23.4 unification), and it handles transitive specialisation across nested calls in one fixpoint loop. -
int_to_str : (Int) -> Strandfloat_to_str : (Float) -> Strare installed inbuiltins.rswithret_mode: Own, lowered inEmitter::lower_appvia direct extern calls to@ailang_int_to_str/@ailang_float_to_str, and RC-disciplined end-to-end (allocs == frees, live == 0) per eob.1. -
io/print_strlowers to@puts(ptr)and accepts any Str realisation (static-Str at@.str_*LLVM globals OR heap-Str allocated byint_to_str/float_to_str/ the new primitives) via a consumer ABI ofi64 lenat offset 0 followed by the bytes plus a trailing NUL (DESIGN.md §"Str ABI"). -
Effect-op args walk in
Position::Borrowper eob.1 (uniqueness.rs:289+linearity.rs's effect-op-arg arm); the let-binder for the Own heap-Str returned byshow xfires its drop at scope close. -
drop_symbol_for_binderhas a Str carve-out (eob.1) that emits the correct rc-dec call for a Str let-binder regardless of whether the underlying realisation is heap-Str or static-Str (codegen-time elision ensures static-Str pointers do not reachailang_rc_dec). -
Type::Forallwithconstraintsround-trips bit-stable through the Form-A parser + canonicaliser + printer (22b.4a.4.5 work, ratified by milestone 23's prelude fixturesne/lt/le/etc. which all carryEqorOrdconstraints). -
The Form-A
ClassDefandInstanceDefschema shapes (DESIGN.md line 1512–1547) round-trip; the existingclass Eq/class Orddefs inexamples/prelude.ail.jsonare the load-bearing proof. -
ailang_rc_alloc(size_t)is the heap-Str slab allocator, unconditionally linked post-eob.1 (the__attribute__((weak))on str.c's extern declaration is a documented no-op). -
The mono pass's body walker (
mono.rs::collect_mono_targets) schedules targets for both class-method residuals AND polymorphic free-fn calls with fully-concrete substitutions, in one pass, transitively (thecmp_max_smokeproof from 23.4). -
NoInstancediagnostics already cross-reference the responsible class (per 22 milestone error-handling) and accept additional cross-references via the existing diagnostic-message infrastructure; adding a Show-aware variant is a content edit, not an infrastructure extension. -
The canonical-form layer (per
docs/specs/0007-canonical-type-names.md) treats mono Defs as post-typecheck artefacts not inCheckedModule.symbols, so addingshow__Tandprint__Tsymbols does not affect surface-level hash pins. The four primitive instance type names (Int/Bool/Str/Float) are canonical (Decision 11's mono-symbol naming uses surface name for primitives). -
examples/prelude.ail.jsonis the auto-loaded prelude per 23.1'scheck_workspaceextension; new defs added to it are visible to every workspace without an explicitimport.