# iter bugfix-print-leak-show-ret-mode — print() leaks heap-Str under --alloc=rc; Show class method's ret_mode stripped twice

**Date:** 2026-05-14
**Started from:** 301cbc33a0b2fed18f465c6c17041211ff7da35e
**Status:** DONE
**Tasks completed:** 1 of 1

## Summary

Two-layer bug: (i) the `Show` class method declaration in
`examples/prelude.ail.json` omitted the `ret_mode` key, so serde
defaulted it to `ParamMode::Implicit`; (ii) even with `ret_mode: own`
declared, `substitute_rigids` in `crates/ailang-check/src/lib.rs`
was hard-resetting `param_modes` to `vec![]` and `ret_mode` to
`Implicit` in its `Type::Fn` arm — silently stripping the mode
metadata during class-method mono synthesis (`show__Int`, `eq__T`,
`compare__T`, user-instance `eq__<UserType>`, etc.). Codegen's
`is_rc_heap_allocated` App-arm requires the callee's `ret_mode == Own`
to mark the let-binder trackable, so print's `let s = show x` binder
was never `ailang_rc_dec`'d at let-scope-close. The fix declares
`ret_mode: "own"` on the Show class method (parallel to the
`int_to_str`/`bool_to_str`/`float_to_str`/`str_clone`/`str_concat`
builtins which already carry `ret_mode: Own`) and repairs
`substitute_rigids`'s `Type::Fn` arm to clone `param_modes` and
preserve `ret_mode` through rigid substitution. The Form-A sibling
`examples/prelude.ail` is regenerated via `ail render` to stay in
lock-step per spec §C4 (b). Ten pinned mono-body hashes
(`eq__Int/Bool/Str`, `compare__Int/Bool/Str`, `show__Int/Bool/Str/Float`,
plus `eq__IntBox`) are re-pinned to the new GREEN baselines because
the mono bodies now carry the previously-stripped `param_modes` /
`ret_mode` metadata; bodies are semantically identical.

## Per-task notes

- bugfix.1: applied the fix.
  - `examples/prelude.ail.json`: Show class method now has
    `"ret_mode": "own"`.
  - `examples/prelude.ail`: regenerated via
    `cargo run -p ail -- render examples/prelude.ail.json
    > examples/prelude.ail`; round-trips parse-isomorphic.
  - `crates/ailang-check/src/lib.rs` `substitute_rigids` Type::Fn
    arm: pattern-match all five fields explicitly; clone
    `param_modes` and pass `*ret_mode` through (`ParamMode: Copy`).
    Comment cites the bugfix iter id and the cause.
  - `crates/ail/tests/mono_hash_stability.rs`: re-pinned 10 hashes
    across the Eq/Ord and Show test fns; both bodies now report
    `captured: {h}` on assertion failure (the Eq/Ord arm was
    missing the captured-suffix; small in-place tidy folded into
    the re-pin since the assertion-message string was being
    rewritten anyway).
  - `crates/ail/tests/eq_ord_e2e.rs`: re-pinned `eq__IntBox`
    body hash to `3c4cf040cb4e8bb2`.

## Concerns

The debugger's cause analysis (carrier) pointed only at the JSON
omission. The JSON-only fix leaves the test RED — `substitute_rigids`
was silently stripping modes too. Suggest: at the next /audit pass,
verify there are no *other* call sites that build a `Type::Fn` with
explicit `..` field-spread and end up dropping modes. The pattern
`Type::Fn { ..., .. }` is the smell.

## Known debt

None — the fix is local. The wider question of "should `Type::Fn`'s
mode fields be field-init-by-name everywhere to prevent this class
of bug at the compiler level" is out of scope for this minimal fix.

## Files touched

- `examples/prelude.ail.json` — Show class: added `ret_mode: own`.
- `examples/prelude.ail` — regenerated Form-A sibling.
- `crates/ailang-check/src/lib.rs` — `substitute_rigids` Type::Fn
  arm: preserve `param_modes` + `ret_mode`.
- `crates/ail/tests/mono_hash_stability.rs` — re-pinned 10 hashes
  (6 Eq/Ord + 4 Show); added cause comments.
- `crates/ail/tests/eq_ord_e2e.rs` — re-pinned `eq__IntBox` hash.
- `crates/ail/tests/print_no_leak_pin.rs` (from /debug) — RED-pin
  for the leak; now GREEN.
- `examples/print_int_no_leak_pin.ail` (from /debug) — fixture for
  the RED-pin; now GREEN.

## Stats

bench/orchestrator-stats/2026-05-14-iter-bugfix-print-leak-show-ret-mode.json
