iter rpe.1: retire per-type print effect-ops
Single iter shipping the post-milestone-24 follow-up named in
docs/specs/2026-05-14-retire-per-type-print-effects.md. After this
iter the only surviving direct-output effect-op is `io/print_str`;
all per-type print primitives are replaced by the polymorphic
`print` helper (prelude, iter 24.3).
Components:
- 92 examples/*.ail fixtures migrated (do io/print_<T> x) →
(app print x); 6 .prose.txt snapshots regenerated via `ail prose`.
- Four-site lockstep compiler deletion: crates/ailang-check/src/builtins.rs
(3 effect_ops.insert blocks + 3 list() rows + the
install_io_print_float_signature test + module + EffectOpSig
doc-comments); crates/ailang-codegen/src/lib.rs lower_app
(3 arms + lowers_io_print_float test); crates/ailang-codegen/src/synth.rs
builtin_effect_op_ret match-arm pattern. Dead `intern_string`
helper removed as a follow-up.
- Five incidental test-body migrations (ailang-check x2, ailang-core
spec_drift + design_schema_drift, ailang-surface/src/lex.rs,
ailang-prose/src/lib.rs round-trip test).
- Cat B test-harness patch: six IR-shape tests in
crates/ail/tests/e2e.rs gained a monomorphise_workspace call
before lower_workspace_with_alloc so they follow the same
pipeline as `ail build` (the home-rolled desugar+lift loop
stayed because mono's precondition is "already lifted"; mono
inserts after lift).
- Six doc-comment touch-ups (lex.rs module doc, parse.rs
diagnostic example, ail/src/main.rs x2, runtime/str.c %g anchor,
crates/ailang-core/specs/form_a.md surface-spec example).
- DESIGN.md seven-site sweep (Decision 11 example, Polymorphic
print past-tense, Heap-Str output sentence, effect-op
invocation comment, Float NaN paragraph re-anchored on
float_to_str, two "What is supported" lists).
- Three E2E test-comment polish + four IR-snapshot refresh + one
canonical-hash pin update (plan-unanticipated downstream
consequences of the corpus migration).
- bench/{check,compile_check,cross_lang}.py: all exit 0; no
ratification needed.
- Roadmap entry struck through; per-iter journal at
docs/journals/2026-05-14-iter-rpe.1.md.
Tests 564/0/3. cargo clippy and cargo doc: zero warnings.
Two upstream codegen bugs surfaced during the first BLOCKED
attempt and were fixed in separate iters before this retry:
- 1fb225e bugfix: mono cursor misalignment at poly-free-fn Var
with class-constrained Forall.
- feb9413 bugfix: print leak — propagate ret_mode through rigid
substitution + prelude Show.show ret_mode.
Known debt (carried forward for next /audit):
- Emitter.strings field is functionally dead post-iter (orphan
after intern_string removal); cycles over empty map harmlessly.
This commit is contained in:
+31
-22
@@ -328,7 +328,7 @@ Every other maximal token is classified post-hoc:
|
||||
- Otherwise → ident.
|
||||
|
||||
Consequence: operators like `+`, `==`, `<=`, `**`, qualified
|
||||
names like `io/print_int`, and cross-module references like
|
||||
names like `io/print_str`, and cross-module references like
|
||||
`std_list.map` are all single ident tokens with no special lex
|
||||
rule. The only reserved tokens are `(`, `)`, and whitespace.
|
||||
Bool literals (`true`, `false`) and unit (`(lit-unit)`) are
|
||||
@@ -1987,9 +1987,10 @@ a => a -> () !IO` shipped in iter 24.3 with body
|
||||
`\x -> let s = show x in do io/print_str s` (explicit let-binder for
|
||||
heap-Str RC discipline per eob.1 Str carve-out). The let-binder is
|
||||
structurally pinned by `crates/ail/tests/print_mono_body_shape.rs`.
|
||||
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.
|
||||
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.
|
||||
|
||||
### Heap-Str primitives
|
||||
|
||||
@@ -2021,8 +2022,12 @@ bodies dispatch into them. `str_concat` IS directly observable
|
||||
because the LLM-author calls it explicitly when authoring an
|
||||
instance body that wants to combine fragments.
|
||||
|
||||
Primitive output goes through `io/print_int` / `io/print_bool` /
|
||||
`io/print_str` directly.
|
||||
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).
|
||||
|
||||
`==`, `<`, `<=`, `>`, `>=` REMAIN primitive operators (unchanged
|
||||
from the original draft). Class methods are accessed by name (`eq x y`,
|
||||
@@ -2328,7 +2333,7 @@ are real surface forms.
|
||||
|
||||
{ "t": "if", "cond": Term, "then": Term, "else": Term }
|
||||
|
||||
// Effect-op invocation. `op` is "<eff>/<op>" (e.g. "io/print_int").
|
||||
// Effect-op invocation. `op` is "<eff>/<op>" (e.g. "io/print_str").
|
||||
// `tail` triggers musttail (omitted when false).
|
||||
{ "t": "do", "op": "<eff>/<op>", "args": [Term...], "tail": false }
|
||||
|
||||
@@ -2562,23 +2567,24 @@ no `f32` variant. The runtime / codegen contract:
|
||||
pattern is conformant; `0.0 / 0.0` may produce
|
||||
`0x7ff8000000000000` on one target and a different qNaN on
|
||||
another.
|
||||
- The textual rendering of NaN by `io/print_float`. The libc
|
||||
`printf("%g", nan)` glue used by the runtime is permitted to
|
||||
emit `nan` / `-nan` / `NaN` etc. depending on libc version and
|
||||
the NaN's sign bit; AILang does not normalise this, since the
|
||||
prose / surface-print paths render NaN as the explicit `"NaN"`
|
||||
spelling and `io/print_float` is for human-readable output, not
|
||||
round-trip.
|
||||
- The textual rendering of NaN through `float_to_str` (the runtime
|
||||
C helper that backs `instance Show Float` and, post-iter-rpe.1,
|
||||
every Float-typed `print` call). The libc `printf("%g", nan)`
|
||||
glue used by `float_to_str` is permitted to emit `nan` / `-nan`
|
||||
/ `NaN` etc. depending on libc version and the NaN's sign bit;
|
||||
AILang does not normalise this, since the prose / surface-print
|
||||
paths render NaN as the explicit `"NaN"` spelling and Float
|
||||
rendering is for human-readable output, not round-trip.
|
||||
|
||||
The same libc-`%g` rendering applies to `show 1.5` / `show nan` /
|
||||
`show inf` via `instance Show Float` (which calls `float_to_str`
|
||||
internally — see §"Prelude (built-in) classes" for the Show ship).
|
||||
The NaN-spelling caveat above is observable via both
|
||||
`do io/print_float x` and `do print x` at the same `x`; the rendering
|
||||
is libc-version-dependent and target-libc-specific. AILang does NOT
|
||||
canonicalise Float textual representation; the LLM-author who needs
|
||||
deterministic Float rendering for cross-platform test fixtures should
|
||||
bypass `show` / `print` and emit a custom formatter.
|
||||
The NaN-spelling caveat above is observable via `do print x` for
|
||||
Float-typed `x`; the rendering is libc-version-dependent and
|
||||
target-libc-specific. AILang does NOT canonicalise Float textual
|
||||
representation; the LLM-author who needs deterministic Float
|
||||
rendering for cross-platform test fixtures should bypass `show` /
|
||||
`print` and emit a custom formatter.
|
||||
|
||||
These are the Rust / Swift / standard-LLVM defaults — not
|
||||
research-grade reproducibility guarantees. The stronger guarantee
|
||||
@@ -2676,7 +2682,10 @@ What **is** supported (and used as the smoke test for the pipeline):
|
||||
- Int, Bool, Unit, **Str**, **Float** as primitive types.
|
||||
- `if`, `let`, function calls, recursion.
|
||||
- Effects on function signatures, with `do op(args)` for direct effect
|
||||
ops (`io/print_int`, `io/print_bool`, `io/print_str`).
|
||||
ops (`io/print_str`). The per-type print ops
|
||||
`io/print_int`, `io/print_bool`, `io/print_float` were retired in
|
||||
iter rpe.1; the polymorphic `print` (§"Polymorphic print") is the
|
||||
canonical output path for non-Str values.
|
||||
- **Builtins.** Arithmetic operators (`+`, `-`, `*`, `/`) of type
|
||||
`forall a. (a, a) -> a` (codegen-restricted to `{Int, Float}`);
|
||||
`%` of type `(Int, Int) -> Int` (Int-only — `fmod` semantics for
|
||||
@@ -2696,7 +2705,7 @@ What **is** supported (and used as the smoke test for the pipeline):
|
||||
`fcmp uno`); Float bit-pattern constants `nan : Float`,
|
||||
`inf : Float`, `neg_inf : Float` (resolved as bare values, lower
|
||||
to direct hex-float `double` SSA constants at use site); the IO
|
||||
effect ops (`io/print_int|bool|str|float`); **`==` : forall a.
|
||||
effect op `io/print_str`; **`==` : forall a.
|
||||
(a, a) -> Bool**; and **`__unreachable__ : forall a. a`**.
|
||||
- **`==` is polymorphic.** The typechecker accepts
|
||||
`==` at any type whose two sides agree (the rigid `a` of the
|
||||
|
||||
@@ -0,0 +1,175 @@
|
||||
# iter rpe.1 — Retire per-type print effect-ops
|
||||
|
||||
**Date:** 2026-05-14
|
||||
**Started from:** 8b455bee4ca2b09bcadc56d95e0d79dcef75d284
|
||||
**Status:** DONE
|
||||
**Tasks completed:** 10 of 10
|
||||
|
||||
## Summary
|
||||
|
||||
The retire-per-type-print-effect-ops milestone shipped in one iter,
|
||||
covering: a RED test pinning the deletion gate (`crates/ailang-check/
|
||||
tests/no_per_type_print_ops.rs`); 92 `examples/*.ail` fixture
|
||||
migrations from `(do io/print_<T> x)` → `(app print x)` plus 6
|
||||
`.prose.txt` snapshot regenerations; four-site lockstep compiler
|
||||
deletion (`crates/ailang-check/src/builtins.rs` × 5 regions,
|
||||
`crates/ailang-codegen/src/lib.rs::lower_app` × 3 arms + 1 test,
|
||||
`crates/ailang-codegen/src/synth.rs::builtin_effect_op_ret` × 1
|
||||
match arm), with the dead `intern_string` helper removed as a
|
||||
clean follow-up; six test-body migrations (`ailang-check` × 2,
|
||||
`ailang-core` tests × 2, `ailang-surface/src/lex.rs`, `ailang-prose`);
|
||||
the Cat B test-harness patch in `crates/ail/tests/e2e.rs` (6
|
||||
IR-shape tests gained a `monomorphise_workspace` call so they
|
||||
follow the same desugar → lift → mono → lower pipeline as `ail
|
||||
build`); 6 doc-comment touch-ups; 7-site DESIGN.md sweep; 3 E2E
|
||||
test-comment polish; bench-run-and-no-ratification (all 3 scripts
|
||||
exit 0 against existing baselines, drift envelope within
|
||||
tolerance); plus IR-snapshot refresh (4 `.ll` files: `sum`,
|
||||
`list`, `max3`, `ws_main`) and 1 canonical-hash pin update
|
||||
(`ordering_match::main`) as plan-unanticipated downstream
|
||||
consequences of the corpus migration. Tests 564/564 green
|
||||
post-iter (was 563 at start; +1 from the new RED test).
|
||||
|
||||
## Per-task notes
|
||||
|
||||
- iter rpe.1.1: RED test `no_per_type_print_ops.rs` — asserts
|
||||
`io/print_int|bool|float` absent from `env.effect_ops`,
|
||||
`io/print_str` present.
|
||||
- iter rpe.1.2: 92 `.ail` corpus migrated (89 single-line +
|
||||
3 multi-line shapes); 5 ancillary comment touch-ups in
|
||||
`borrow_own_demo`, `eq_demo`, `int_to_print_int_borrow`,
|
||||
`floats_1_newton_sqrt`, `floats_3_safe_division` to satisfy
|
||||
the §H acceptance criterion `grep -rl ... examples/ == 0`.
|
||||
- iter rpe.1.3: 6 `.prose.txt` snapshots regenerated via
|
||||
`cargo run -p ail -- prose`.
|
||||
- iter rpe.1.4: builtins.rs deletes (3 effect_ops.insert blocks
|
||||
+ 3 `list()` rows + 1 unit test + 2 doc-comment swaps);
|
||||
codegen lib.rs deletes (3 `lower_app` arms + 1 test);
|
||||
synth.rs `builtin_effect_op_ret` match-arm narrowed.
|
||||
`intern_string` removed as orphan helper. `Emitter.strings`
|
||||
field left in place (still consumed by emit-time loop;
|
||||
cycles over empty map; non-functional). RED test flips
|
||||
GREEN.
|
||||
- iter rpe.1.5: 6 test-body sites swapped to `io/print_str` +
|
||||
`Str` args; Cat B harness patch at all 6 e2e.rs sites
|
||||
inserts `ailang_check::monomorphise_workspace(&lifted_ws)`
|
||||
between the existing lift loop and the lower call (the plan
|
||||
said "replace the desugar+lift loop with mono", but mono's
|
||||
precondition is "already lifted" — interpreting per spirit
|
||||
and matching the `ail build` pipeline). E2E 85/85 green
|
||||
after this step.
|
||||
- iter rpe.1.6: doc-comment swaps in 6 sites (`lex.rs`,
|
||||
`parse.rs`, `main.rs` × 2, `runtime/str.c`, `form_a.md`).
|
||||
- iter rpe.1.7: DESIGN.md 7 sites swept; the iter's
|
||||
past-tense paragraph at line 1990-1992 anchors the
|
||||
milestone close.
|
||||
- iter rpe.1.8: E2E test-comment polish at `eq_ord_e2e.rs` × 2,
|
||||
`floats_e2e.rs`. Plus three plan-unanticipated sites:
|
||||
`int_arg_to_effect_op_does_not_rc_track` test assertions
|
||||
shifted from `0/0/0` to `1/1/0` (post-iter `print n` for
|
||||
Int now goes via Show Int → int_to_str → io/print_str,
|
||||
exactly one heap-Str slab cycle per spec §E); 4 IR
|
||||
snapshots refreshed via `UPDATE_SNAPSHOTS=1` because the
|
||||
format-string globals (`@.str_<module>_fmt_int_0` /
|
||||
`%lld\n`) no longer appear in IR; 1 canonical-hash pin
|
||||
update for `ordering_match::main` (body migration changes
|
||||
the def's canonical-form hash).
|
||||
- iter rpe.1.9: bench run — all 3 scripts exit 0 against
|
||||
existing baselines. No ratification needed; spec §C4 (a)
|
||||
anticipated possible latency drift but actual fell within
|
||||
tolerance. Largest single deltas: `latency.explicit_at_rc.
|
||||
max_us` +154.84% (a tail metric in the noise-class
|
||||
envelope observed across the last ~12 audits); `throughput.
|
||||
bench_list_sum.bump_s` +11.85% (REGRESSION, single bench,
|
||||
3rd-iter-recurring; tracked as background noise per
|
||||
audit-24 lineage). Baseline pristine for the 13th
|
||||
consecutive audit-equivalent.
|
||||
- iter rpe.1.10: roadmap entry struck `[x]`; journal written;
|
||||
stats written.
|
||||
|
||||
## Concerns
|
||||
|
||||
- Cat B harness patch deviates from plan-literal: plan said
|
||||
"replace the desugar+lift loop with monomorphise_workspace";
|
||||
I inserted mono after the existing lift loop. Mono's
|
||||
documented precondition is "already lifted", so the literal
|
||||
reading would have broken mono's contract. Spirit-reading
|
||||
was unambiguous: match `ail build`'s pipeline.
|
||||
- Plan Task 8 under-scoped: it named 3 E2E-comment sites,
|
||||
but the corpus migration also forced (a) one assertion
|
||||
update on `int_arg_to_effect_op_does_not_rc_track` (allocs
|
||||
0→1, frees 0→1, fully documented in spec §E), (b) IR
|
||||
snapshot refresh × 4, (c) one canonical-hash pin update.
|
||||
All three are mechanical downstream consequences of Task 2.
|
||||
- `Emitter.strings` field in `crates/ailang-codegen/src/lib.rs`
|
||||
is now functionally dead (no caller after `intern_string`
|
||||
removal) but the field + the per-module collection loop +
|
||||
the emit-time globals loop are all still in place. They
|
||||
cycle over an empty map and are harmless. Cleaning them
|
||||
up cascades through `emit_workspace_with_alloc` and
|
||||
parallel `str_literals` infrastructure; out of scope for
|
||||
this iter.
|
||||
|
||||
## Known debt
|
||||
|
||||
- The `int_arg_to_effect_op_does_not_rc_track` test name now
|
||||
describes a pre-iter property; the body pins the post-iter
|
||||
shape (one Show-Int slab cycle). Renaming to e.g.
|
||||
`print_int_one_slab_cycle` was floated and explicitly
|
||||
declined (plan Task 8 step 2 said "lean toward keep-name-
|
||||
update-doc"). If a future iter wants the rename, the test
|
||||
body is already a clean reference.
|
||||
- The `intern_string` helper is gone but `Emitter.strings`
|
||||
field remains — see Concerns above.
|
||||
|
||||
## Files touched
|
||||
|
||||
121 files in the working tree (1 new + 120 modified).
|
||||
|
||||
Code (compiler):
|
||||
- `crates/ailang-check/src/builtins.rs` (3 effect_ops + 3 list() rows + 1 test + 2 doc-comments deleted/swapped)
|
||||
- `crates/ailang-check/src/lib.rs` (2 test-body sites swapped)
|
||||
- `crates/ailang-codegen/src/lib.rs` (3 lower_app arms + 1 test + `intern_string` helper removed)
|
||||
- `crates/ailang-codegen/src/synth.rs` (builtin_effect_op_ret match-arm narrowed)
|
||||
- `crates/ailang-surface/src/lex.rs` (1 test-body + 1 module doc swap)
|
||||
- `crates/ailang-surface/src/parse.rs` (1 diagnostic example swap)
|
||||
- `crates/ailang-prose/src/lib.rs` (1 round-trip test-body swap)
|
||||
- `crates/ail/src/main.rs` (2 doc-comments swapped)
|
||||
- `runtime/str.c` (1 comment re-anchored on %g)
|
||||
|
||||
Tests:
|
||||
- `crates/ailang-check/tests/no_per_type_print_ops.rs` (NEW)
|
||||
- `crates/ailang-core/tests/spec_drift.rs` (1 swap)
|
||||
- `crates/ailang-core/tests/design_schema_drift.rs` (1 swap)
|
||||
- `crates/ailang-core/tests/hash_pin.rs` (1 hash pin updated)
|
||||
- `crates/ail/tests/e2e.rs` (6 Cat B harness patches + 1 assertion update + 1 doc-comment)
|
||||
- `crates/ail/tests/eq_ord_e2e.rs` (2 doc-comments)
|
||||
- `crates/ail/tests/floats_e2e.rs` (1 doc-comment)
|
||||
- `crates/ail/tests/snapshots/{sum,list,max3,ws_main}.ll` (regenerated; format-string globals removed)
|
||||
|
||||
Docs:
|
||||
- `docs/DESIGN.md` (7 sites)
|
||||
- `docs/roadmap.md` (P2 entry struck [x])
|
||||
- `crates/ailang-core/specs/form_a.md` (1 example swap)
|
||||
|
||||
Examples: 92 `.ail` migrations + 6 `.prose.txt` regenerations + 5 ancillary comment touch-ups.
|
||||
|
||||
## Verification
|
||||
|
||||
- `cargo test --workspace`: 564 passed, 0 failed (was 563 at
|
||||
start; +1 = new RED test, no test counts shifted otherwise).
|
||||
- `cargo build --workspace`: clean.
|
||||
- `python3 bench/check.py` exit 0 (drift within tolerance,
|
||||
no ratification).
|
||||
- `python3 bench/compile_check.py` exit 0.
|
||||
- `python3 bench/cross_lang.py` exit 0 (25/25 stable).
|
||||
- `grep -rl "io/print_int\|io/print_bool\|io/print_float"
|
||||
examples/` returns 0 results (acceptance criterion met).
|
||||
- `grep -n "io/print_int\|io/print_bool\|io/print_float"
|
||||
crates/ailang-check/src/builtins.rs` returns 0 lines.
|
||||
- `crates/ailang-codegen/src/lib.rs::lower_app` has no arm
|
||||
for any of the three retired op names.
|
||||
|
||||
## Stats
|
||||
|
||||
`bench/orchestrator-stats/2026-05-14-iter-rpe.1.json`
|
||||
+3
-10
@@ -77,17 +77,10 @@ context. Pick the next milestone from P1.)_
|
||||
|
||||
## P2 — Medium-term
|
||||
|
||||
- [ ] **\[milestone\]** Retire `io/print_int` / `io/print_bool` /
|
||||
- [x] **\[milestone\]** Retire `io/print_int` / `io/print_bool` /
|
||||
`io/print_float` effect-ops + migrate example corpus to `print`.
|
||||
Bulk text substitution `do io/print_<T> x` → `do print x` across
|
||||
`examples/*.ail.json` (~86 fixtures affected). Per-type effect-ops
|
||||
are deleted from `crates/ailang-check/src/builtins.rs`,
|
||||
`crates/ailang-codegen/src/lib.rs::lower_app` arms, and the
|
||||
corresponding runtime C glue.
|
||||
- context: post-milestone-24 mechanical follow-up; the architecture-
|
||||
shipping milestone (24) and corpus-migration milestone (this) are
|
||||
separate so migration runs against a frozen architecture. Same
|
||||
call milestone 23 made for `==` / `eq`.
|
||||
Shipped 2026-05-14 as iter rpe.1.
|
||||
- context: per-iter journal `docs/journals/2026-05-14-iter-rpe.1.md`.
|
||||
|
||||
- [x] **\[todo\]** Author `examples/prelude.ail` alongside
|
||||
`examples/prelude.ail.json`. (Satisfied 2026-05-13 by iter
|
||||
|
||||
Reference in New Issue
Block a user