# 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:

1. The `Show` typeclass with single method `show : (a borrow) -> Str`.
2. Four primitive instances: `Show Int`, `Show Bool`, `Show Str`,
   `Show Float`.
3. A polymorphic free fn `print : forall a. Show a => (a borrow) -> () !IO`
   that routes through `show` and `io/print_str`, replacing the
   ad-hoc per-type print idiom for new code.
4. Two new runtime primitives that let `Show Bool` and `Show Str`
   honour the uniform `(a borrow) -> Str Own` signature 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
  via `ailang_rc_alloc` with `rc_header` at `payload - 8`, exactly
  like `int_to_str`. Always heap-allocates; never returns a
  static-Str pointer. The allocation cost is paid every `show True`
  / `show False` call; the alternative (schema-`if` with 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` —
  reads `i64 len` from offset 0 of the source slab and allocates a
  fresh heap-Str slab of the same length, memcpy's `len + 1` bytes
  (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 the `len` field at offset 0 plus the
  bytes plus the NUL are consulted, never the (absent for
  static-Str) `rc_header` of the source.

Both new symbols install in `crates/ailang-check/src/builtins.rs`
alongside `int_to_str` / `float_to_str`:

- `bool_to_str : (Bool) -> Str` with `ret_mode: Own`.
- `str_clone : (Str borrow) -> Str` with `ret_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:

```jsonc
// 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:

```jsonc
{ "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 **inside** `show x`,
  not in `print`'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|bool|float` effect-ops + migrate example corpus to `print`". |

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`

1. **Typecheck.** `print` resolves to the polymorphic free fn with
   `forall a. Show a => (a borrow) -> () !IO`. Bare-name lookup
   reaches the prelude via `env.module_globals` fall-through
   (prep1 from 23.4-prep). Constraint `Show Int` discharged against
   the registry entry for `instance Show Int`.
2. **Mono (free-fn entry).** Body taken directly from `Def::Fn.body`,
   rigid-var subst `a → Int`. The body walker detects the nested
   `show x` call at concrete `Int` and schedules `show__Int` in the
   same fixpoint round. `show__Int` synthesises from the
   `instance Show Int` body (class-method entry,
   `Registry::entries[(Show, Int)]` lookup). The synthesised
   `show__Int` body is `\x -> int_to_str x`. The synthesised
   `print__Int` body is `\x -> let s = show__Int x in do io/print_str s`.
3. **Codegen.** `print__Int` lowers as any monomorphic fn. The
   nested `show__Int x` is a direct call returning Own heap-Str;
   the let-binder `s` is RC-tracked at scope close per the eob.1
   Str carve-out in `drop_symbol_for_binder`. `do io/print_str s`
   walks `s` as `Position::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`

1. **Typecheck.** `Show IntBox` discharged against the registry
   entry for `instance Show IntBox`.
2. **Mono.** `print__IntBox` synthesises with body
   `\x -> let s = show__IntBox x in do io/print_str s`. `show__IntBox`
   synthesises from the instance body (whatever the user wrote, e.g.
   `\x -> match x with MkIntBox n -> int_to_str n`).
3. **Codegen.** Lowers normally; ADT-pattern-match plus inner
   `int_to_str` call produce a heap-Str; outer let-binder drops at
   scope close.

## Error handling

- **`NoInstance Show <T>`** — fired at typecheck when `print` is
  called on a type without a `Show` instance (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-side
  `instance 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::Internal` from mono** — if `Show`-specialisation
  fails internally (e.g. the body walker fails to schedule
  `show__T` when synthesising `print__T`), this is a caller-contract
  violation per the existing convention in `mono.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` — fixture
    `let s = bool_to_str true in do io/print_str s`; assert IR body
    contains `call ptr @ailang_bool_to_str(i1 1)` and
    `declare ptr @ailang_bool_to_str(i1)`.
  - `str_clone_emits_call_to_ailang_str_clone` — fixture
    `let s = str_clone "hi" in do io/print_str s`; assert IR body
    contains `call ptr @ailang_str_clone(ptr ...)` and
    `declare ptr @ailang_str_clone(ptr)`.

- **E2E RC-stats (two tests in `crates/ailang-codegen/tests/e2e.rs`):**
  - `bool_to_str_drop_balances_rc_stats` — fixture
    `let 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` — fixture
    `let 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 with `false` → `"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 of `int_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 --workspace` green. The
  `__attribute__((weak))` on `ailang_rc_alloc` extern (hs.3 pattern)
  is unchanged — `rc.c` is 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.json` parses + 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 calls `show 1`, `show True`, `show "x"`, and
  `show 1.5`, assert that the post-mono workspace contains
  `show__Int`, `show__Bool`, `show__Str`, `show__Float` as `Def::Fn`
  entries. 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 of
  `crates/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 `%g` rendering 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 that `print` composes with them.

- **Negative E2E (`examples/show_no_instance.ail.json`):**
  ```
  fn main = do print id  -- id : forall a. a -> a, no Show instance
  ```
  Typecheck 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::tests`
  asserts `print__Int` body desugars to
  `\x -> let s = show__Int x in do io/print_str s` post-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:

1. **24.1 has landed:** `runtime/str.c` defines `ailang_bool_to_str`
   and `ailang_str_clone`. `crates/ailang-check/src/builtins.rs`
   installs both signatures with `ret_mode: Own`.
   `crates/ailang-codegen/src/lib.rs::Emitter::lower_app` emits direct
   calls plus extern declarations. RC-stats E2E pins balance.

2. **24.2 has landed:** `examples/prelude.ail.json` contains
   `class Show a where show : (a borrow) -> Str` and four primitive
   instances (Int/Bool/Str/Float). The mono pass synthesises
   `show__Int`, `show__Bool`, `show__Str`, `show__Float`. DESIGN.md
   §"Prelude (built-in) classes" amended to include Show with the
   four instances.

3. **24.3 has landed:** `examples/prelude.ail.json` contains
   `fn print : forall a. Show a => (a borrow) -> () !IO` with the
   explicit-let body shape. The mono pass synthesises `print__Int`,
   `print__Bool`, `print__Str`, `print__Float` for each call site at
   a primitive type, plus `print__<UserType>` for each user-instance
   call site. Positive / user-ADT / negative E2E green. DESIGN.md
   §"Prelude (built-in) classes" lists `print` as the polymorphic
   helper; §"Float semantics" gains the Show Float NaN-spelling
   cross-reference paragraph.

4. **Tests pass:** `cargo test --workspace` green; existing milestone
   23 prelude fixtures (`ne`/`lt`/`le`/`gt`/`ge` E2E) unchanged;
   existing typeclass fixtures (`eq__Int`, `compare__Str`, etc.)
   hash-stable.

5. **Bench regression ratified:** `bench/check.py && bench/compile_check.py && bench/cross_lang.py`
   exit 0 or audit-ratified with journal entry.

6. **Roadmap update:** P1 "Post-22 Prelude — Show + print rewire"
   entry flips to `[x]`. New P2 entry: "Retire `io/print_int` /
   `io/print_bool` / `io/print_float` effect-ops + migrate example
   corpus to `print`". A user runs the bulk text substitution
   `do io/print_<T> x` → `do print x` across `examples/*.ail.json`
   (86 files affected), and the per-type ops are deleted from
   `builtins.rs` / `lib.rs` codegen 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`, `<` / `lt` redundancy.

- **`io/print_str` retirement.** Stays. Load-bearing inside `print`'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 and `show__Str`'s
  `str_clone`-allocation is wasteful).

- **`bool_to_str` as 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-correct
  `bool_to_str` primitive avoids the question. Open commitment:
  if a future codegen iter ratifies phi-of-static-Str drop-elision,
  swap `show__Bool` body to the schema-`if` form and retire
  `bool_to_str`.

- **`str_clone` as rc_header bump.** For heap-Str inputs, `str_clone`
  could 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_clone` can become an rc_inc on heap-Str / no-op-with-fresh-
  alloc on static-Str hybrid. For now: always-allocate, always-copy.

- **`Show Ordering` instance.** The prelude's `Ordering` ADT has
  three constructors (LT/EQ/GT). Shipping `Show Ordering` would
  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
  own `instance Show Ordering` if they want it.

- **`Show` for ADTs via `deriving`.** No auto-derivation. The
  `instance 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.

- **`print` with newline-free variant (`putStr` / `print_no_newline`).**
  `io/print_str` calls `@puts` which appends a newline. Every
  `print x` therefore ends in `\n`. A newline-suppressed variant is
  a separate feature; deferred until a concrete LLM-author pattern
  demands it.

- **Operator routing through `Show` (`show` invoked as `<<` or
  similar).** No commitment. AILang does not have a Haskell-style
  monadic operator surface and adding one to invoke `show`
  implicitly would re-open the ergonomics question Decision 6
  closed in favour of explicit `do` blocks.

## Open commitments (24.1 / 24.2 / 24.3 implementer)

- **`bool_to_str` runtime 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_clone` byte count.** The naive memcpy copies `len + 1`
  bytes (payload + trailing NUL). The implementation may choose to
  recompute the NUL via `strncpy`-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 by `show x` has
  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 `Unit` type / 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 the `IntBox` test), 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 in
  `crates/ailang-check/src/builtins.rs` (two new signature
  installations parallel to the existing `int_to_str` /
  `float_to_str` blocks), +30 LOC in
  `crates/ailang-codegen/src/lib.rs` (two new arms in `lower_app`
  + two extern declarations in the IR header preamble), +2 LOC in
  `crates/ailang-codegen/src/synth.rs::is_static_callee`.

- **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.json` fixtures, ~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 by `bench/compile_check.py`
  post-24.3; audit-ratified or rebaselined.

- **Runtime cost on `print x`:**
  - For `x : Int` — one `int_to_str` allocation (~3–20 bytes per
    Int depending on magnitude) plus one rc_dec at scope close.
  - For `x : Bool` — one `bool_to_str` allocation (5 or 6 bytes)
    plus one rc_dec.
  - For `x : Str` — one `str_clone` allocation + memcpy plus one
    rc_dec. **Strictly worse** than `do io/print_str x`. The
    redundancy is acknowledged; LLM-author has the recourse to
    bypass `print` for Str-typed values.
  - For `x : Float` — one `float_to_str` allocation plus one rc_dec.

## 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.

1. `crates/ailang-check/src/mono.rs::monomorphise_workspace` is 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.

2. `int_to_str : (Int) -> Str` and `float_to_str : (Float) -> Str`
   are installed in `builtins.rs` with `ret_mode: Own`, lowered in
   `Emitter::lower_app` via 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.

3. `io/print_str` lowers to `@puts(ptr)` and accepts any Str
   realisation (static-Str at `@.str_*` LLVM globals OR heap-Str
   allocated by `int_to_str` / `float_to_str` / the new primitives)
   via a consumer ABI of `i64 len` at offset 0 followed by the
   bytes plus a trailing NUL (DESIGN.md §"Str ABI").

4. Effect-op args walk in `Position::Borrow` per eob.1
   (`uniqueness.rs:289` + `linearity.rs`'s effect-op-arg arm); the
   let-binder for the Own heap-Str returned by `show x` fires its
   drop at scope close.

5. `drop_symbol_for_binder` has 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 reach
   `ailang_rc_dec`).

6. `Type::Forall` with `constraints` round-trips bit-stable through
   the Form-A parser + canonicaliser + printer (22b.4a.4.5 work,
   ratified by milestone 23's prelude fixtures `ne`/`lt`/`le`/etc.
   which all carry `Eq` or `Ord` constraints).

7. The Form-A `ClassDef` and `InstanceDef` schema shapes (DESIGN.md
   line 1512–1547) round-trip; the existing `class Eq` / `class Ord`
   defs in `examples/prelude.ail.json` are the load-bearing proof.

8. `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).

9. 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 (the `cmp_max_smoke` proof from 23.4).

10. `NoInstance` diagnostics 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.

11. The canonical-form layer (per
    `docs/specs/0007-canonical-type-names.md`) treats mono
    Defs as post-typecheck artefacts not in
    `CheckedModule.symbols`, so adding `show__T` and `print__T`
    symbols 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).

12. `examples/prelude.ail.json` is the auto-loaded prelude per
    23.1's `check_workspace` extension; new defs added to it are
    visible to every workspace without an explicit `import`.
