Files
AILang/docs/plans/0063-iter-rpe.1.md
T
Brummel 832375f2ac convention: counter-prefix file naming across docs/specs/, docs/plans/, design/contracts/, design/models/
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.
2026-05-28 13:31:31 +02:00

772 lines
29 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# rpe.1 — Retire per-type print effect-ops — Implementation Plan
> **Parent spec:** `docs/specs/0027-retire-per-type-print-effects.md`
>
> **For agentic workers:** REQUIRED SUB-SKILL: use `skills/implement`
> to run this plan. Steps use `- [ ]` checkboxes for tracking.
**Goal:** Delete `io/print_int`, `io/print_bool`, `io/print_float`
from the compiler and migrate every corpus call site to the
polymorphic `print` helper shipped in iter 24.3.
**Architecture:** Four-site lockstep deletion (check builtins,
codegen lower_effect_op, codegen synth.rs effect_op_ret table,
plus three doc-comment touch-ups), preceded by exhaustive corpus
migration so that the deletion does not strand any live caller.
A RED test pins the deletion gate; per-task `cargo test --workspace`
gates each step. Bench-latency drift is expected per spec §C4 (a)
and is ratified at iter close.
**Tech Stack:** `ailang-check`, `ailang-codegen`, `ailang-surface`,
`ailang-prose`, `ailang-core`, `ail` CLI; the snapshot-regenerated
`.prose.txt` artefacts; `bench/run.sh` latency harness baselines.
---
## Errata vs. spec (incorporated below; spec is unchanged)
Recon surfaced four spec-vs-code mismatches; this plan absorbs
them without amending the spec:
1. **Spec §C2 says "three install_io_print_<T>_signature tests
(lines ~593633)".** Only `install_io_print_float_signature`
exists (builtins.rs:600). The plan deletes the one that
exists, not three.
2. **Spec §D1 says "98 examples/*.ail files".** Actual: 92 `.ail`
+ 6 `.prose.txt` snapshot siblings = 98. The plan migrates
both, but a `.prose.txt` is regenerated, not text-substituted.
3. **`crates/ailang-codegen/src/synth.rs:215`** carries a parallel
effect-op `ret` table (`builtin_effect_op_ret`) the spec §C2
does not name. Lockstep delete with the codegen arms.
4. **Five test bodies + four doc-comment sites** carry
`io/print_int` as incidental scaffolding (`crates/ailang-check/src/lib.rs`,
`crates/ailang-core/tests/spec_drift.rs`, `design_schema_drift.rs`,
`crates/ailang-surface/{src/lex.rs, src/parse.rs}`,
`crates/ailang-prose/src/lib.rs`, `crates/ail/src/main.rs`,
`crates/ailang-core/specs/form_a.md`, `runtime/str.c`
comment). Plan covers them in Task 5 and Task 6.
The spec's §B/§C1/§D1 invariants are unaffected.
---
## Files this plan creates or modifies
**Create:**
- `crates/ailang-check/tests/no_per_type_print_ops.rs` — RED test
(Task 1). Model on `crates/ailang-check/tests/show_dispatch_pin.rs`.
**Modify — corpus (Task 2 + Task 3):**
- `examples/<name>.ail` × 92 — bulk substitution. Full list at
`/tmp/migration_targets.txt` (built by Task 2 Step 1; see Task 2
for the canonical query the LLM re-runs).
- `examples/<name>.prose.txt` × 6 — regenerate via `ail prose`.
Files: `bench_list_sum.prose.txt`, `ordering_match.prose.txt`,
`rc_app_let_partial_drop_leak.prose.txt`,
`rc_match_arm_partial_drop_leak.prose.txt`,
`rc_own_param_drop.prose.txt`,
`test_22b3_default_e2e.prose.txt`.
**Modify — compiler core (Task 4):**
- `crates/ailang-check/src/builtins.rs`:
- line 5 — module-doc-comment swap `io/print_int``io/print_str`.
- line 40 — `EffectOpSig.ret` doc swap.
- lines 269-276 — delete `env.effect_ops.insert("io/print_int", ...)`.
- lines 277-284 — delete `env.effect_ops.insert("io/print_bool", ...)`.
- lines 293-302 — delete floats-iter comment + `io/print_float` insert.
- line 349 — delete `io/print_int` row in `list()`.
- line 350 — delete `io/print_bool` row.
- line 352 — delete `io/print_float` row. (Line 351 — `io/print_str` — survives.)
- lines 593-635 — delete `install_io_print_float_signature` test fn (incl. its 7-line doc-comment).
- `crates/ailang-codegen/src/lib.rs`:
- lines 2302-2323 — delete `"io/print_int" => { ... }` arm.
- lines 2324-2345 — delete `"io/print_float" => { ... }` arm.
- lines 2373-2411 — delete `"io/print_bool" => { ... }` arm.
- lines 3580-3617 — delete `lowers_io_print_float` test (incl. doc-comment).
- format-string interns (`"fmt_int"`, `"%lld\n"`, `"fmt_float"`, `"%g\n"`, `"fmt_true"`, `"fmt_false"`) orphan and may be deleted in the same edit if they are no longer referenced. Verify by `grep` post-edit.
- `crates/ailang-codegen/src/synth.rs:215` — remove `"io/print_int" | "io/print_bool" | "io/print_float"` from the `match name` arm; keep `"io/print_str"`.
**Modify — incidental-scaffolding test bodies (Task 5):**
- `crates/ailang-check/src/lib.rs`:
- lines 3871-3877 (undeclared-effect-diagnostic test) — swap `op: "io/print_int"``op: "io/print_str"`; if the arg type was `Int`, change it to a `Str` literal that types as Str.
- lines 5047-5051 (tail-call-in-tail-position test) — same shape.
- `crates/ailang-core/tests/spec_drift.rs` lines 64-71 — swap to `io/print_str` with a `Str` arg.
- `crates/ailang-core/tests/design_schema_drift.rs` lines 87-94 — same.
- `crates/ailang-surface/src/lex.rs`:
- lines 18-20 — module-doc-comment swap.
- line 320 — `tokenize("+ == <= io/print_int std_list.map")` swap to `io/print_str`.
- line 328 — corresponding `assert_eq!` vector swap.
- `crates/ailang-prose/src/lib.rs` lines 1585-1593 — round-trip test asserting `render_term(&t) == "do io/print_int(5)"`: swap to `io/print_str` with a Str arg + matching assert.
**Modify — doc-comment touch-ups (Task 6):**
- `crates/ailang-surface/src/parse.rs:1377` — diagnostic example swap.
- `crates/ail/src/main.rs:30-31, 1419-1420` — CLI doc-comments swap.
- `runtime/str.c:117-118` — re-anchor on `%g` directly.
- `crates/ailang-core/specs/form_a.md:433-434` — surface-spec example: migrate to either `(do io/print_str s)` with a `Str` literal, or `(app print xs)` — pick the latter to advertise the post-iter idiom.
**Modify — DESIGN.md (Task 7):**
- `docs/DESIGN.md`:
- line 331 — Decision 11 example swap.
- lines 1990-1992 — past-tense rewrite ("retired 2026-05-14 in iter rpe.1").
- lines 2024-2025 — Heap-Str primitives output sentence rewrite per spec §C3.
- line 2331 — effect-op invocation comment example swap.
- lines 2565-2577 — Float NaN-rendering paragraph rewrite (anchor on `float_to_str`).
- line 2679 — "What is supported (effect ops)" list, keep only `io/print_str`.
- line 2699 — "(IO effect ops)" list, keep only `io/print_str`.
**Modify — incidental E2E test comments (Task 8):**
- `crates/ail/tests/eq_ord_e2e.rs:66, 76` — update comments to name `print` semantics.
- `crates/ail/tests/e2e.rs:2674-2683` (`int_arg_to_effect_op_does_not_rc_track`) — fixture migrates in Task 2; doc-comment migrates here.
- `crates/ail/tests/floats_e2e.rs:7` — module doc-comment swap.
**Modify — bench baselines (Task 9, conditional):**
- `bench/baseline.json` "latency" block (lines 70-92) — ratify if drift.
- `bench/baseline.json" throughput block (lines 1-69, 93) — ratify if drift.
- `bench/baseline_cross_lang.json` — ratify if drift.
- `bench/baseline_compile.json` — expected exit-0; ratify only if unexpected drift.
**Modify — roadmap (Task 10):**
- `docs/roadmap.md` P2 section lines 80-90 — strike entry with `- [x]` + date.
---
## Task 1: RED test — `no_per_type_print_ops.rs`
**Files:**
- Create: `crates/ailang-check/tests/no_per_type_print_ops.rs`
- [ ] **Step 1: Write the failing test**
```rust
//! Hard gate for iter rpe.1: the per-type print effect-ops
//! `io/print_int`, `io/print_bool`, `io/print_float` are retired
//! and must NOT appear in the builtin registry. The polymorphic
//! `print` helper (prelude, iter 24.3) is the canonical replacement;
//! `io/print_str` survives as the byte-channel primitive.
use ailang_check::build_check_env;
use ailang_surface::load_workspace;
use std::path::PathBuf;
fn fixture_dir() -> PathBuf {
let mut d = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
d.pop();
d.pop();
d.join("examples")
}
#[test]
fn per_type_print_effect_ops_are_retired() {
// Any workspace will do; we only inspect the post-install
// builtin registry. `hello.ail` is the smallest surviving
// fixture (uses `io/print_str` only).
let ws_path = fixture_dir().join("hello.ail");
let ws = load_workspace(&ws_path).expect("workspace loads");
let env = build_check_env(&ws);
for retired in ["io/print_int", "io/print_bool", "io/print_float"] {
assert!(
!env.effect_ops.contains_key(retired),
"{retired} must not appear in builtin registry post-rpe.1; \
use polymorphic `print` (Show-based) instead"
);
}
// Survivor pin: io/print_str stays.
assert!(
env.effect_ops.contains_key("io/print_str"),
"io/print_str must survive — it is the canonical byte-channel \
primitive and used internally by `print`"
);
}
```
- [ ] **Step 2: Verify the test fails RED**
Run: `cargo test --workspace -p ailang-check --test no_per_type_print_ops`
Expected: FAIL on the first `assert!` because `io/print_int`
still appears in `env.effect_ops` post-Task-2.
This gate flips to GREEN at Task 4 close.
---
## Task 2: Corpus `.ail` migration — 92 fixtures
**Files:**
- Modify: 92 `examples/<name>.ail` files (full list deterministic
from `grep -rl "io/print_int\|io/print_bool\|io/print_float"
examples/ | grep "\.ail$"`).
- [ ] **Step 1: Build the canonical target list**
Run:
```bash
grep -rl "io/print_int\|io/print_bool\|io/print_float" examples/ \
| grep "\.ail$" > /tmp/rpe1_targets.txt && \
wc -l /tmp/rpe1_targets.txt
```
Expected: `92 /tmp/rpe1_targets.txt`.
- [ ] **Step 2: Bulk-substitute each fixture**
For each path in `/tmp/rpe1_targets.txt`, transform every
occurrence of:
```
(do io/print_int <ARG>)
```
to:
```
(app print <ARG>)
```
And the same for `io/print_bool`, `io/print_float`.
The transformation is regular: parenthesis-balance is preserved
(both `do` and `app` consume their parentheses identically),
the head is replaced (`io/print_<T>``print`), and the leading
keyword is replaced (`do``app`). The argument expression is
copied verbatim.
Implementation: read each file, apply the regex
`\(do io/print_(int|bool|float) ``(app print `, write back.
Or process file-by-file with Edit-tool calls; either is fine.
- [ ] **Step 3: Verify each migrated fixture parses + round-trips**
Run:
```bash
cargo run -p ail -- check examples/<each-fixture> 2>&1
```
Expected: exit 0. The pre-iter compiler still knows
`io/print_int|bool|float`, so existing fixtures stay green; the
migrated form (using polymorphic `print`) ALSO type-checks because
`print` is in the prelude (iter 24.3). Both paths are green
simultaneously in this iter window.
Pragmatic shortcut: run `cargo test --workspace` at Step 4 instead
of per-fixture `ail check`.
- [ ] **Step 4: `cargo test --workspace` green**
Run: `cargo test --workspace`
Expected: 0 failures. The migrated fixtures' E2E test wrappers
still produce the same stdout (output bytes identical between
`(do io/print_<T> x)` and `(app print x)` per spec §"Data flow").
If any fixture has a stdout-comparison test that now produces
different bytes, that is a real divergence and the spec's
"output bytes identical" claim has a counterexample — STOP
and investigate before continuing.
---
## Task 3: Prose snapshot regeneration — 6 `.prose.txt` files
**Files:**
- Modify: `examples/bench_list_sum.prose.txt`,
`examples/ordering_match.prose.txt`,
`examples/rc_app_let_partial_drop_leak.prose.txt`,
`examples/rc_match_arm_partial_drop_leak.prose.txt`,
`examples/rc_own_param_drop.prose.txt`,
`examples/test_22b3_default_e2e.prose.txt`.
- [ ] **Step 1: Regenerate each prose snapshot**
For each fixture above, run:
```bash
cargo run -p ail -- prose examples/<stem>.ail \
> examples/<stem>.prose.txt
```
The `ail prose` subcommand emits Form-B; this file becomes the
committed snapshot used by `crates/ailang-prose/tests/snapshot.rs`.
- [ ] **Step 2: `cargo test --workspace -p ailang-prose` green**
Run: `cargo test --workspace -p ailang-prose`
Expected: 0 failures. The snapshot test will compare the
regenerated `.prose.txt` to the prose-renderer's current output
of the migrated fixture; they must match because we generated
them with the very command the test uses.
- [ ] **Step 3: `cargo test --workspace` green**
Run: `cargo test --workspace`
Expected: 0 failures (no regression in any other crate).
---
## Task 4: Compiler-side deletion (lockstep)
**Files:**
- Modify: `crates/ailang-check/src/builtins.rs`
(lines 5, 40, 269-302, 349, 350, 352, 593-635)
- Modify: `crates/ailang-codegen/src/lib.rs`
(lines 2302-2323, 2324-2345, 2373-2411, 3580-3617)
- Modify: `crates/ailang-codegen/src/synth.rs:215`
- [ ] **Step 1: Delete the three `env.effect_ops.insert` blocks**
In `crates/ailang-check/src/builtins.rs`, delete:
- lines 269-276 (`io/print_int` block)
- lines 277-284 (`io/print_bool` block)
- lines 293-302 (floats-iter comment + `io/print_float` block)
Keep the surviving `io/print_str` block (between or after; verify
by reading the file post-edit).
- [ ] **Step 2: Delete the three `list()` rows**
In `crates/ailang-check/src/builtins.rs`, delete:
- line 349 (`io/print_int` row)
- line 350 (`io/print_bool` row)
- line 352 (`io/print_float` row)
Keep line 351 (`io/print_str` row). Verify by re-reading.
- [ ] **Step 3: Delete the `install_io_print_float_signature` test**
In `crates/ailang-check/src/builtins.rs`, delete lines 593-635
(test fn + its 7-line doc-comment). This is the ONE test that
exists matching the spec's "three install_io_print_<T>_signature"
description — see Errata #1 above.
- [ ] **Step 4: Swap the module-doc-comments**
In `crates/ailang-check/src/builtins.rs`:
- line 5 — swap `io/print_int` reference to `io/print_str`.
- line 40 — same swap in `EffectOpSig.ret` doc.
- [ ] **Step 5: Delete the three codegen `lower_effect_op` arms**
In `crates/ailang-codegen/src/lib.rs`:
- lines 2302-2323 — delete `"io/print_int" => { ... }` arm.
- lines 2324-2345 — delete `"io/print_float" => { ... }` arm.
- lines 2373-2411 — delete `"io/print_bool" => { ... }` arm.
Keep the `"io/print_str"` arm (lines 2346-2372). Verify by
re-reading.
After deletion, search the file for orphaned format-string
interns (`"fmt_int"`, `"%lld\n"`, `"fmt_float"`, `"%g\n"`,
`"fmt_true"`, `"fmt_false"`). If they are no longer referenced
anywhere in the crate, delete the corresponding `out.push_str`
or `intern` calls too. If they ARE still referenced (e.g.
`"%g\n"` shared with another path), leave them.
- [ ] **Step 6: Delete the `lowers_io_print_float` test**
In `crates/ailang-codegen/src/lib.rs`, delete lines 3580-3617
(test + doc-comment).
- [ ] **Step 7: Update `synth.rs` builtin_effect_op_ret**
In `crates/ailang-codegen/src/synth.rs:215`, remove
`"io/print_int" | "io/print_bool" | "io/print_float"` from the
match arm pattern. Keep `"io/print_str"`. The match arm body
(returning `Type::unit()`) stays unchanged for the surviving
op.
- [ ] **Step 8: RED test flips GREEN**
Run: `cargo test --workspace -p ailang-check --test no_per_type_print_ops`
Expected: PASS. The Task-1 RED test is now satisfied because
the three retired ops no longer appear in `env.effect_ops`.
- [ ] **Step 9: `cargo test --workspace` green**
Run: `cargo test --workspace`
Expected: 0 failures. Any failure here means a stale `io/print_<T>`
reference survived in a fixture or test body — those are addressed
in Task 5; expect Task-5 sites to fail and proceed to Task 5.
---
## Task 5: In-tree test-body migration
**Files:**
- Modify: `crates/ailang-check/src/lib.rs:3871-3877, 5047-5051`
- Modify: `crates/ailang-core/tests/spec_drift.rs:64-71`
- Modify: `crates/ailang-core/tests/design_schema_drift.rs:87-94`
- Modify: `crates/ailang-surface/src/lex.rs:320, 328`
- Modify: `crates/ailang-prose/src/lib.rs:1585-1593`
- Modify: `crates/ail/tests/e2e.rs` — six IR-shape tests at lines 1408,
1628, 1710, 1800, 1936, 2013 (Cat B test-harness patch, see below).
**Cat B test-harness patch (added 2026-05-14 after the rpe.1 BLOCKED
orchestrator run):** Six IR-shape tests in `crates/ail/tests/e2e.rs`
build their own `lifted_ws` via a `lift_letrecs` loop and lower it
via `lower_workspace_with_alloc`, skipping `monomorphise_workspace`.
Pre-migration this was fine — the fixtures used `(do io/print_int x)`,
a directly-recognised effect-op. Post-migration they use `(app print x)`
(polymorphic free fn), which requires mono to resolve to
`print__Int`. Without the mono pass, lowering errors with
`UnknownVar("print")`. The fix is identical at all six sites: replace
the home-rolled `desugar+lift` loop with a single call to
`ailang_check::monomorphise_workspace(&ws)?` and lower the
resulting `MonomorphisedWorkspace`. Sites to patch:
- `reuse_as_demo_under_rc_uses_inplace_rewrite` (line 1408)
- `alloc_rc_borrow_only_recursive_list_drop` (line 1628)
- `alloc_rc_partial_drop_skips_moved_keeps_wildcarded` (line 1710)
- `alloc_rc_own_param_dec_at_fn_return` (line 1800)
- `iter18e_drop_iterative_emits_worklist_body_no_self_recursion` (line 1936)
- `iter18e_no_annotation_keeps_recursive_drop_body` (line 2013)
- [ ] **Step 1: Migrate `ailang-check/src/lib.rs:3871-3877`**
The site is an undeclared-effect-diagnostic test. The op name
is incidental — the test asserts "calling an unknown effect-op
produces diagnostic X". Swap `op: "io/print_int"``op:
"io/print_str"` AND change the arg from an `Int` literal to a
`Str` literal so the swapped op type-checks. Then change the
expected diagnostic's `effect_op` field if it's pinned by name.
If the test is asserting "unknown effect-op", the spirit is
preserved by inventing a never-registered name like
`"io/print_unknown"` and the test continues to assert "diagnostic
produced". Use whichever shape is closest to the original intent
— inspect the test body to decide.
- [ ] **Step 2: Migrate `ailang-check/src/lib.rs:5047-5051`**
Tail-call-in-tail-position test. Same shape as Step 1; the op
is incidental. Swap to `io/print_str` + `Str` arg.
- [ ] **Step 3: Migrate `spec_drift.rs:64-71`**
JSON-shape drift test on `Term::Do`. The op identity is the
serialised payload's key, not the load-bearing assertion.
Swap to `io/print_str` + `Str` arg.
- [ ] **Step 4: Migrate `design_schema_drift.rs:87-94`**
Same shape as Step 3.
- [ ] **Step 5: Migrate `lex.rs:320, 328`**
`tokenize("+ == <= io/print_int std_list.map")` exercises that
the lexer treats `io/print_int` as a single identifier-with-slash
token. The op name is incidental; the assertion is on the token
shape. Swap to `io/print_str`. Update the `assert_eq!` vector
at line 328 to match.
- [ ] **Step 6: Migrate `prose/src/lib.rs:1585-1593`**
`do_renders_with_keyword_and_op` round-trip test. Asserts
`render_term(&t) == "do io/print_int(5)"`. Swap to
`render_term(&t) == "do io/print_str(\"5\")"` (with `Str` literal
arg) OR pick a different surviving op if `io/print_str` makes
the test awkward.
- [ ] **Step 7: `cargo test --workspace` green**
Run: `cargo test --workspace`
Expected: 0 failures. Task 5 closes the test-body migration set.
---
## Task 6: Documentation-comment touch-ups
**Files:**
- Modify: `crates/ailang-surface/src/lex.rs:18-20`
- Modify: `crates/ailang-surface/src/parse.rs:1377`
- Modify: `crates/ail/src/main.rs:30-31, 1419-1420`
- Modify: `runtime/str.c:117-118`
- Modify: `crates/ailang-core/specs/form_a.md:433-434`
- [ ] **Step 1: Swap `lex.rs:18-20` module-doc-comment**
Replace the `io/print_int` example with `io/print_str` in the
qualified-name-tokenisation explanation.
- [ ] **Step 2: Swap `parse.rs:1377` diagnostic example**
`expect_ident("effect op (e.g. `io/print_int`)")` → swap to
`io/print_str`.
- [ ] **Step 3: Swap `main.rs:30-31` and `:1419-1420`**
`main.rs:30-31`: `builtins` subcommand doc — list a single
`io/print_str` (drop the per-type ops).
`main.rs:1419-1420`: `walk_term` Do-arm format-string comment
— swap to `io/print_str`.
- [ ] **Step 4: Re-anchor `runtime/str.c:117-118`**
Original: `matches io/print_float's libc rendering for IEEE
consistency`. Rewrite to: `matches the same %g format used
throughout the runtime for Float text rendering` (or similar —
the anchor is the format, not the now-retired effect-op).
- [ ] **Step 5: Migrate `form_a.md:433-434`**
The surface-spec example shows `(do io/print_int n)`. Migrate
to `(app print n)` to advertise the post-iter canonical idiom.
- [ ] **Step 6: `cargo test --workspace` green**
Run: `cargo test --workspace`
Expected: 0 failures. (Doc-comment changes can break tests if
they're embedded in doc-tests; verify.)
---
## Task 7: DESIGN.md sweep
**Files:**
- Modify: `docs/DESIGN.md` lines 331, 1990-1992, 2024-2025, 2331,
2565-2577, 2679, 2699.
- [ ] **Step 1: Line 331 — Decision 11 example**
Original (excerpt): "names like `io/print_int`". Swap to
`io/print_str`.
- [ ] **Step 2: Lines 1990-1992 — Polymorphic print past-tense rewrite**
Original:
> Routing through `print` replaces the ad-hoc `io/print_int|bool|float`
> idiom for new code; retiring the per-type effect-ops is queued
> as a P2 follow-up.
Rewrite:
> Routing through `print` replaces the ad-hoc per-type print
> effect-ops; the `io/print_int`, `io/print_bool`, `io/print_float`
> primitives were retired 2026-05-14 in iter rpe.1, leaving
> `io/print_str` as the only surviving direct-output effect-op.
- [ ] **Step 3: Lines 2024-2025 — Heap-Str primitives output sentence**
Original:
> Primitive output goes through `io/print_int` / `io/print_bool` /
> `io/print_str` directly.
Rewrite:
> Primitive output for `Str` values goes through `io/print_str`
> directly; values of other primitive types route through the
> polymorphic `print` helper (§"Polymorphic print"), which feeds
> the heap-Str result of `show x` into `io/print_str`. The
> per-type effect-ops `io/print_int`, `io/print_bool`,
> `io/print_float` were retired in iter rpe.1 (2026-05-14).
- [ ] **Step 4: Line 2331 — Effect-op invocation comment example**
Original: `// Effect-op invocation. \`op\` is "<eff>/<op>"
(e.g. "io/print_int").`
Rewrite: `// Effect-op invocation. \`op\` is "<eff>/<op>"
(e.g. "io/print_str").`
- [ ] **Step 5: Lines 2565-2577 — Float NaN-rendering paragraph**
The paragraph anchors NaN textual semantics on `io/print_float`
and contrasts with `do print x`. After iter rpe.1, both ends of
that contrast reach `%g` via `float_to_str` + `io/print_str`.
Rewrite the paragraph to anchor on `float_to_str` (the canonical
heap-Str producer for Float) rather than the now-retired
`io/print_float`. The `%g` semantics are unchanged; this is a
re-anchoring, not a semantic change.
- [ ] **Step 6: Line 2679 — "What is supported (effect ops)"**
Original: `(\`io/print_int\`, \`io/print_bool\`, \`io/print_str\`)`
Rewrite: `(\`io/print_str\`)`. Add a sentence: `Per-type print
ops (\`io/print_int\`, \`io/print_bool\`, \`io/print_float\`)
were retired in iter rpe.1; polymorphic \`print\` is the canonical
output path.`
- [ ] **Step 7: Line 2699 — "(IO effect ops)" list**
Original: `the IO effect ops (\`io/print_int|bool|str|float\`)`
Rewrite: `the IO effect op \`io/print_str\``.
- [ ] **Step 8: `cargo test --workspace` green**
Run: `cargo test --workspace`
Expected: 0 failures. The `design_schema_drift` test (mentioned
in Task 5) reads DESIGN.md; verify it still passes after these
swaps.
---
## Task 8: Incidental E2E test-comment touch-ups
**Files:**
- Modify: `crates/ail/tests/eq_ord_e2e.rs:66, 76`
- Modify: `crates/ail/tests/e2e.rs:2674-2683`
- Modify: `crates/ail/tests/floats_e2e.rs:7`
- [ ] **Step 1: Update `eq_ord_e2e.rs` comments**
Lines 66 and 76 reference `io/print_int` emitting `<int>\n`.
The asserted stdout is unchanged (post-migration `(app print x)`
at `x : Int` also emits `<int>\n`). Update the comments to
reference `print` semantics. The test body itself does not
change (it was migrated in Task 2 via the fixture migration).
- [ ] **Step 2: Update `e2e.rs:2674-2683` (int_arg_to_effect_op test)**
The test pins the RC-tracking property "Int args passed to an
effect-op are not consumed". The fixture `int_to_print_int_borrow.ail`
migrates in Task 2. The test name and doc-comment lose
grounding once `io/print_int` is gone.
Two options:
- Update the doc-comment to explain that the test now pins
"Int args passed to `print` (the polymorphic helper) are not
consumed" — the property is the same.
- Rename the test if Boss prefers (e.g.
`int_arg_to_print_does_not_rc_track`). Renaming changes a
test identity; lean toward keeping the name and updating the
doc.
Pick keep-the-name-and-update-the-doc. The test pins the
property, not the identifier.
- [ ] **Step 3: Update `floats_e2e.rs:7` module doc-comment**
Lists `io/print_float` as a covered op. Swap to `print` / Show
Float.
- [ ] **Step 4: `cargo test --workspace` green**
Run: `cargo test --workspace`
Expected: 0 failures.
---
## Task 9: Bench run + conditional ratification
**Files:**
- Conditional Modify: `bench/baseline.json`, `bench/baseline_cross_lang.json`,
`bench/baseline_compile.json`.
- [ ] **Step 1: Run `bench/check.py`**
Run: `python3 bench/check.py`
Expected exit code: `0` (compile-time-symmetric). If `1`
(drift), investigate before ratifying — the spec did not
anticipate a check-perf change.
- [ ] **Step 2: Run `bench/compile_check.py`**
Run: `python3 bench/compile_check.py`
Expected exit code: `0`. Same rationale.
- [ ] **Step 3: Run `bench/cross_lang.py`**
Run: `python3 bench/cross_lang.py`
Expected exit code: `0` OR `1` (legitimate drift if any bench
fixture's hot loop now allocates a heap-Str per print).
- [ ] **Step 4: Conditional ratification — if any script returned `1`**
For each script that returned `1`:
1. Inspect the per-fixture report to identify which fixtures
moved.
2. Run `python3 <script> --update-baseline` to write the new
baseline.
3. Note in the per-iter journal: which baseline was updated,
the magnitude of the drift, and the language reason
(post-iter `print` heap-alloc-per-iteration is the canonical
output path; old baseline reflected the retired direct-printf
path).
- [ ] **Step 5: All scripts green at final state**
After any ratification, re-run all three scripts; all must
return `0`.
---
## Task 10: Roadmap strike + per-iter journal + stats
**Files:**
- Modify: `docs/roadmap.md` P2 lines 80-90 (entry strike).
- Create: `docs/journals/2026-05-14-iter-rpe.1.md` (per-iter
journal).
- Create: `bench/orchestrator-stats/2026-05-14-iter-rpe.1.json`
(stats file per `implement/SKILL.md`).
- [ ] **Step 1: Strike the roadmap entry**
In `docs/roadmap.md` lines 80-90 (the
`Retire io/print_int / io/print_bool / io/print_float` P2
entry), change `- [ ]` → `- [x]` and append a one-line tail
naming this iter:
```markdown
- [x] **\[milestone\]** Retire `io/print_int` / `io/print_bool` /
`io/print_float` effect-ops + migrate example corpus to `print`.
Shipped 2026-05-14 as iter rpe.1.
- context: per-iter journal `docs/journals/2026-05-14-iter-rpe.1.md`.
```
- [ ] **Step 2: Write the per-iter journal**
Standard journal shape: `# iter rpe.1 — Retire per-type print
effect-ops`, sections: Summary / Decisions / Files touched /
Verification / Concerns / Known debt.
The journal records the §C4 (a) decision (migrate all,
including bench-latency fixtures) and any bench-baseline
ratifications from Task 9.
- [ ] **Step 3: Write the stats file**
JSON file at `bench/orchestrator-stats/2026-05-14-iter-rpe.1.json`
recording: tasks completed, retries per task, blocked count,
duration, `cargo test --workspace` final count.
- [ ] **Step 4: Final `cargo test --workspace` + clippy + doc**
Run, in parallel:
- `cargo test --workspace`
- `cargo clippy --workspace --all-targets -- -D warnings`
- `cargo doc --workspace --no-deps 2>&1 | grep -c '^warning:'`
Expected: tests green; clippy zero (the iter-clippy-sweep
baseline holds); doc zero (the rustdoc-sweep baseline holds).
Any clippy / rustdoc regression introduced by this iter is
itself a Task-N debt; fix in-task before declaring DONE.
---
## Self-review
1. **Spec coverage:** every §C/§D/§G/§I-named element has a
task — RED test (Task 1, spec §G2), corpus migration (Task 2,
spec §C1 + §D1), prose snapshot regen (Task 3, §D1 extension
via Errata #2), four-site deletion (Task 4, spec §C2 + Errata
#3), test-body migration (Task 5, Errata #4), doc-comment
touch-ups (Task 6, Errata #4), DESIGN.md sweep (Task 7, spec
§C3), E2E-comment touch-ups (Task 8, partial Errata #4), bench
ratification (Task 9, spec §C4 (a) + §G3), roadmap strike +
journal + stats (Task 10, spec §I task 7).
2. **Placeholder scan:** zero "TBD" / "TODO" / "similar to Task".
3. **Type consistency:** `env.effect_ops` (the BTreeMap on `Env`),
`lower_effect_op` (the codegen dispatcher), `build_check_env`
(entry point used by the RED test) all match real symbol names
in the workspace.
4. **Step granularity:** every numbered step is 25 minutes.
Task 2 Step 2 is "92 file edits" but they are mechanical and
the LLM can stream them with one Edit-tool call per file.
5. **No commit steps:** zero `git commit` calls in any task
template. The orchestrator-agent (or implement-skill) leaves
the working tree dirty; Boss commits.