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:
2026-05-14 02:12:34 +02:00
parent 8b455bee4c
commit 6fdb45d2f2
123 changed files with 704 additions and 490 deletions
+3 -3
View File
@@ -27,8 +27,8 @@
//! a native binary at `--out`.
//! - `run` — `build` into a tempdir and execute the binary; passes
//! the binary's exit code through.
//! - `builtins` — list the built-in effect ops (`io/print_int`,
//! `io/print_bool`, `io/print_str`, ...) with their signatures.
//! - `builtins` — list the built-in effect ops (`io/print_str`)
//! with their signatures.
//! - `diff` — semantic, hash-based module/workspace diff. Works even
//! when a module doesn't currently typecheck.
//! - `workspace` — load an entry module's transitive imports and list
@@ -1416,7 +1416,7 @@ fn walk_term(
walk_term(else_, out, builtins, scope);
}
Term::Do { op, args, .. } => {
// Mark effect ops as `effect:io/print_int` so they can be
// Mark effect ops as `effect:io/print_str` so they can be
// separated from normal function calls.
out.insert(format!("effect:{op}"));
for a in args {
+58 -13
View File
@@ -1460,8 +1460,14 @@ fn reuse_as_demo_under_rc_uses_inplace_rewrite() {
root_dir: ws.root_dir.clone(),
registry: ws.registry.clone(),
};
// Iter rpe.1: post-print-migration, fixtures use `(app print x)`
// which monomorphises to `print__<T>`. Adding the mono pass here
// mirrors `ail build`'s pipeline (desugar -> lift -> mono ->
// lower); without it, lowering errors with `UnknownVar("print")`.
let mono_ws = ailang_check::monomorphise_workspace(&lifted_ws)
.expect("monomorphise_workspace");
let ir = ailang_codegen::lower_workspace_with_alloc(
&lifted_ws,
&mono_ws,
ailang_codegen::AllocStrategy::Rc,
)
.expect("lower workspace under rc");
@@ -1660,8 +1666,14 @@ fn alloc_rc_borrow_only_recursive_list_drop() {
root_dir: ws.root_dir.clone(),
registry: ws.registry.clone(),
};
// Iter rpe.1: post-print-migration, fixtures use `(app print x)`
// which monomorphises to `print__<T>`. Adding the mono pass here
// mirrors `ail build`'s pipeline (desugar -> lift -> mono ->
// lower); without it, lowering errors with `UnknownVar("print")`.
let mono_ws = ailang_check::monomorphise_workspace(&lifted_ws)
.expect("monomorphise_workspace");
let ir = ailang_codegen::lower_workspace_with_alloc(
&lifted_ws,
&mono_ws,
ailang_codegen::AllocStrategy::Rc,
)
.expect("lower workspace under rc");
@@ -1736,8 +1748,14 @@ fn alloc_rc_partial_drop_skips_moved_keeps_wildcarded() {
root_dir: ws.root_dir.clone(),
registry: ws.registry.clone(),
};
// Iter rpe.1: post-print-migration, fixtures use `(app print x)`
// which monomorphises to `print__<T>`. Adding the mono pass here
// mirrors `ail build`'s pipeline (desugar -> lift -> mono ->
// lower); without it, lowering errors with `UnknownVar("print")`.
let mono_ws = ailang_check::monomorphise_workspace(&lifted_ws)
.expect("monomorphise_workspace");
let ir = ailang_codegen::lower_workspace_with_alloc(
&lifted_ws,
&mono_ws,
ailang_codegen::AllocStrategy::Rc,
)
.expect("lower workspace under rc");
@@ -1826,8 +1844,14 @@ fn alloc_rc_own_param_dec_at_fn_return() {
root_dir: ws.root_dir.clone(),
registry: ws.registry.clone(),
};
// Iter rpe.1: post-print-migration, fixtures use `(app print x)`
// which monomorphises to `print__<T>`. Adding the mono pass here
// mirrors `ail build`'s pipeline (desugar -> lift -> mono ->
// lower); without it, lowering errors with `UnknownVar("print")`.
let mono_ws = ailang_check::monomorphise_workspace(&lifted_ws)
.expect("monomorphise_workspace");
let ir = ailang_codegen::lower_workspace_with_alloc(
&lifted_ws,
&mono_ws,
ailang_codegen::AllocStrategy::Rc,
)
.expect("lower workspace under rc");
@@ -1952,8 +1976,14 @@ fn iter18e_drop_iterative_emits_worklist_body_no_self_recursion() {
root_dir: ws.root_dir.clone(),
registry: ws.registry.clone(),
};
// Iter rpe.1: post-print-migration, fixtures use `(app print x)`
// which monomorphises to `print__<T>`. Adding the mono pass here
// mirrors `ail build`'s pipeline (desugar -> lift -> mono ->
// lower); without it, lowering errors with `UnknownVar("print")`.
let mono_ws = ailang_check::monomorphise_workspace(&lifted_ws)
.expect("monomorphise_workspace");
let ir = ailang_codegen::lower_workspace_with_alloc(
&lifted_ws,
&mono_ws,
ailang_codegen::AllocStrategy::Rc,
)
.expect("lower workspace under rc");
@@ -2036,8 +2066,14 @@ fn iter18e_no_annotation_keeps_recursive_drop_body() {
root_dir: ws.root_dir.clone(),
registry: ws.registry.clone(),
};
// Iter rpe.1: post-print-migration, fixtures use `(app print x)`
// which monomorphises to `print__<T>`. Adding the mono pass here
// mirrors `ail build`'s pipeline (desugar -> lift -> mono ->
// lower); without it, lowering errors with `UnknownVar("print")`.
let mono_ws = ailang_check::monomorphise_workspace(&lifted_ws)
.expect("monomorphise_workspace");
let ir = ailang_codegen::lower_workspace_with_alloc(
&lifted_ws,
&mono_ws,
ailang_codegen::AllocStrategy::Rc,
)
.expect("lower workspace under rc");
@@ -2671,18 +2707,27 @@ fn str_field_in_adt_drops_heap_str_correctly() {
assert_eq!(live, 0, "no leaked RC slabs allowed; live={live}");
}
/// Iter eob.1: primitive-Int arg passed to an effect-op. Under the
/// new "Term::Do args = Borrow" rule, the let-binder `n` is not
/// consumed by `io/print_int`; but Int is unboxed, so there is no
/// pointer to RC-track. Pin: zero allocs / zero frees / zero live —
/// the rule introduces no spurious bookkeeping on primitives.
/// Iter eob.1 / rpe.1: primitive-Int passed to the polymorphic
/// `print` helper. Pre-rpe.1 the fixture used `(do io/print_int n)`
/// directly and the iter-eob.1 "Term::Do args = Borrow" rule meant
/// zero RC traffic on the unboxed Int (allocs == 0, frees == 0).
/// Post-rpe.1, `print n` desugars through `Show Int.show` →
/// `int_to_str` (heap-Str alloc, `ret_mode: Own`) → `io/print_str`
/// → slab drops at scope close. Pin shifts to the new canonical
/// post-iter shape: exactly one heap-Str slab cycle per `print`
/// of a non-Str value. The let-binder property the original test
/// pinned (Term::Do args borrow-walk correctly on primitives) is
/// preserved by the surviving `io/print_str s` step inside
/// `print__Int.body`; this test now pins the slab-cycle pattern.
#[test]
fn int_arg_to_effect_op_does_not_rc_track() {
let (stdout, allocs, frees, live) =
build_and_run_with_rc_stats("int_to_print_int_borrow.ail");
assert_eq!(stdout, "7\n");
assert_eq!(allocs, 0, "Int arg should not trigger any RC alloc; got allocs={allocs}");
assert_eq!(frees, 0, "Int arg should not trigger any RC free; got frees={frees}");
// Post-rpe.1: print n for n : Int allocates one heap-Str slab
// via Show Int → int_to_str, frees it at scope close. Spec §E.
assert_eq!(allocs, 1, "expected exactly one Show-Int heap-Str slab; got allocs={allocs}");
assert_eq!(frees, 1, "expected exactly one free; got frees={frees}");
assert_eq!(live, 0, "no leaked RC slabs allowed; live={live}");
}
+4 -3
View File
@@ -63,8 +63,9 @@ fn eq_ord_polymorphic_runs_end_to_end() {
// Helper `at_most(3, 5)` at Int = true → 1
// `at_most(true, false)` at Bool = false → 0
// `at_most("abc", "abd")` at Str = true → 1
// `io/print_int` emits one int per line, so stdout is "1\n0\n1"
// after `.trim()` strips the trailing newline.
// Polymorphic `print` for Int routes through `show` + `io/print_str`,
// emitting one int per line, so stdout is "1\n0\n1" after `.trim()`
// strips the trailing newline.
assert_eq!(stdout, "1\n0\n1", "expected 1\\n0\\n1, got {stdout:?}");
}
@@ -73,7 +74,7 @@ fn eq_ord_user_adt_runs_end_to_end() {
let stdout = build_and_run("eq_ord_user_adt.ail");
// eq(MkIntBox 3, MkIntBox 3) = true → 1
// eq(MkIntBox 3, MkIntBox 5) = false → 0
// `io/print_int` emits one int per line; trimmed stdout is "1\n0".
// Polymorphic `print` for Int emits one int per line; trimmed stdout is "1\n0".
assert_eq!(stdout, "1\n0", "expected 1\\n0, got {stdout:?}");
}
+3 -2
View File
@@ -4,8 +4,9 @@
//! CLI and run; stdout is matched against the expected three
//! lines (`4`, `42`, `-1.5`). This test exercises the full
//! pipeline — Float literal lowering, polymorphic `+` Float arm,
//! `int_to_float` (sitofp), `neg` Float (fneg), `io/print_float`
//! (printf `%g\n`).
//! `int_to_float` (sitofp), `neg` Float (fneg), and the polymorphic
//! `print` for Float (routes through `Show Float.show` →
//! `float_to_str`, libc `%g`).
use std::path::PathBuf;
use std::process::Command;
+30 -4
View File
@@ -2,8 +2,6 @@
source_filename = "list.ail"
target triple = "<NORMALIZED>"
@.str_list_fmt_int_0 = private unnamed_addr constant [6 x i8] c"%lld\0A\00", align 1
declare i32 @printf(ptr, ...)
declare i32 @puts(ptr)
declare ptr @GC_malloc(i64)
@@ -19,6 +17,8 @@ declare i64 @llvm.fptosi.sat.i64.f64(double)
@ail_list_sum_list_clos = private unnamed_addr constant { ptr, ptr } { ptr @ail_list_sum_list_adapter, ptr null }
@ail_list_main_clos = private unnamed_addr constant { ptr, ptr } { ptr @ail_list_main_adapter, ptr null }
@ail_prelude_show__Int_clos = private unnamed_addr constant { ptr, ptr } { ptr @ail_prelude_show__Int_adapter, ptr null }
@ail_prelude_print__Int_clos = private unnamed_addr constant { ptr, ptr } { ptr @ail_prelude_print__Int_adapter, ptr null }
define i64 @ail_list_sum_list(ptr %arg_xs) {
entry:
%v1 = load i64, ptr %arg_xs, align 8
@@ -72,8 +72,8 @@ entry:
%v10 = getelementptr inbounds i8, ptr %v8, i64 16
store ptr %v5, ptr %v10, align 8
%v11 = call i64 @ail_list_sum_list(ptr %v8)
call i32 (ptr, ...) @printf(ptr @.str_list_fmt_int_0, i64 %v11)
ret i8 0
%v12 = call i8 @ail_prelude_print__Int(i64 %v11)
ret i8 %v12
}
define i8 @ail_list_main_adapter(ptr %_env) {
@@ -82,6 +82,32 @@ entry:
ret i8 %r
}
define ptr @ail_prelude_show__Int(i64 %arg_x) {
entry:
%v1 = call ptr @ailang_int_to_str(i64 %arg_x)
ret ptr %v1
}
define ptr @ail_prelude_show__Int_adapter(ptr %_env, i64 %a0) {
entry:
%r = call ptr @ail_prelude_show__Int(i64 %a0)
ret ptr %r
}
define i8 @ail_prelude_print__Int(i64 %arg_x) {
entry:
%v1 = call ptr @ail_prelude_show__Int(i64 %arg_x)
%v2 = getelementptr inbounds i8, ptr %v1, i64 8
call i32 @puts(ptr %v2)
ret i8 0
}
define i8 @ail_prelude_print__Int_adapter(ptr %_env, i64 %a0) {
entry:
%r = call i8 @ail_prelude_print__Int(i64 %a0)
ret i8 %r
}
define i32 @main() {
call i8 @ail_list_main()
+30 -4
View File
@@ -2,8 +2,6 @@
source_filename = "max3.ail"
target triple = "<NORMALIZED>"
@.str_max3_fmt_int_0 = private unnamed_addr constant [6 x i8] c"%lld\0A\00", align 1
declare i32 @printf(ptr, ...)
declare i32 @puts(ptr)
declare ptr @GC_malloc(i64)
@@ -20,6 +18,8 @@ declare i64 @llvm.fptosi.sat.i64.f64(double)
@ail_max3_max_clos = private unnamed_addr constant { ptr, ptr } { ptr @ail_max3_max_adapter, ptr null }
@ail_max3_max3_clos = private unnamed_addr constant { ptr, ptr } { ptr @ail_max3_max3_adapter, ptr null }
@ail_max3_main_clos = private unnamed_addr constant { ptr, ptr } { ptr @ail_max3_main_adapter, ptr null }
@ail_prelude_show__Int_clos = private unnamed_addr constant { ptr, ptr } { ptr @ail_prelude_show__Int_adapter, ptr null }
@ail_prelude_print__Int_clos = private unnamed_addr constant { ptr, ptr } { ptr @ail_prelude_print__Int_adapter, ptr null }
define i64 @ail_max3_max(i64 %arg_a, i64 %arg_b) {
entry:
%v1 = icmp sgt i64 %arg_a, %arg_b
@@ -77,8 +77,8 @@ entry:
define i8 @ail_max3_main() {
entry:
%v1 = call i64 @ail_max3_max3(i64 3, i64 17, i64 9)
call i32 (ptr, ...) @printf(ptr @.str_max3_fmt_int_0, i64 %v1)
ret i8 0
%v2 = call i8 @ail_prelude_print__Int(i64 %v1)
ret i8 %v2
}
define i8 @ail_max3_main_adapter(ptr %_env) {
@@ -87,6 +87,32 @@ entry:
ret i8 %r
}
define ptr @ail_prelude_show__Int(i64 %arg_x) {
entry:
%v1 = call ptr @ailang_int_to_str(i64 %arg_x)
ret ptr %v1
}
define ptr @ail_prelude_show__Int_adapter(ptr %_env, i64 %a0) {
entry:
%r = call ptr @ail_prelude_show__Int(i64 %a0)
ret ptr %r
}
define i8 @ail_prelude_print__Int(i64 %arg_x) {
entry:
%v1 = call ptr @ail_prelude_show__Int(i64 %arg_x)
%v2 = getelementptr inbounds i8, ptr %v1, i64 8
call i32 @puts(ptr %v2)
ret i8 0
}
define i8 @ail_prelude_print__Int_adapter(ptr %_env, i64 %a0) {
entry:
%r = call i8 @ail_prelude_print__Int(i64 %a0)
ret i8 %r
}
define i32 @main() {
call i8 @ail_max3_main()
+30 -4
View File
@@ -2,8 +2,6 @@
source_filename = "sum.ail"
target triple = "<NORMALIZED>"
@.str_sum_fmt_int_0 = private unnamed_addr constant [6 x i8] c"%lld\0A\00", align 1
declare i32 @printf(ptr, ...)
declare i32 @puts(ptr)
declare ptr @GC_malloc(i64)
@@ -17,8 +15,36 @@ declare ptr @ailang_str_clone(ptr)
declare ptr @ailang_str_concat(ptr, ptr)
declare i64 @llvm.fptosi.sat.i64.f64(double)
@ail_prelude_show__Int_clos = private unnamed_addr constant { ptr, ptr } { ptr @ail_prelude_show__Int_adapter, ptr null }
@ail_prelude_print__Int_clos = private unnamed_addr constant { ptr, ptr } { ptr @ail_prelude_print__Int_adapter, ptr null }
@ail_sum_sum_clos = private unnamed_addr constant { ptr, ptr } { ptr @ail_sum_sum_adapter, ptr null }
@ail_sum_main_clos = private unnamed_addr constant { ptr, ptr } { ptr @ail_sum_main_adapter, ptr null }
define ptr @ail_prelude_show__Int(i64 %arg_x) {
entry:
%v1 = call ptr @ailang_int_to_str(i64 %arg_x)
ret ptr %v1
}
define ptr @ail_prelude_show__Int_adapter(ptr %_env, i64 %a0) {
entry:
%r = call ptr @ail_prelude_show__Int(i64 %a0)
ret ptr %r
}
define i8 @ail_prelude_print__Int(i64 %arg_x) {
entry:
%v1 = call ptr @ail_prelude_show__Int(i64 %arg_x)
%v2 = getelementptr inbounds i8, ptr %v1, i64 8
call i32 @puts(ptr %v2)
ret i8 0
}
define i8 @ail_prelude_print__Int_adapter(ptr %_env, i64 %a0) {
entry:
%r = call i8 @ail_prelude_print__Int(i64 %a0)
ret i8 %r
}
define i64 @ail_sum_sum(i64 %arg_n) {
entry:
%v1 = icmp eq i64 %arg_n, 0
@@ -44,8 +70,8 @@ entry:
define i8 @ail_sum_main() {
entry:
%v1 = call i64 @ail_sum_sum(i64 10)
call i32 (ptr, ...) @printf(ptr @.str_sum_fmt_int_0, i64 %v1)
ret i8 0
%v2 = call i8 @ail_prelude_print__Int(i64 %v1)
ret i8 %v2
}
define i8 @ail_sum_main_adapter(ptr %_env) {
+30 -4
View File
@@ -2,8 +2,6 @@
source_filename = "ws_main.ail"
target triple = "<NORMALIZED>"
@.str_ws_main_fmt_int_0 = private unnamed_addr constant [6 x i8] c"%lld\0A\00", align 1
declare i32 @printf(ptr, ...)
declare i32 @puts(ptr)
declare ptr @GC_malloc(i64)
@@ -17,8 +15,36 @@ declare ptr @ailang_str_clone(ptr)
declare ptr @ailang_str_concat(ptr, ptr)
declare i64 @llvm.fptosi.sat.i64.f64(double)
@ail_prelude_show__Int_clos = private unnamed_addr constant { ptr, ptr } { ptr @ail_prelude_show__Int_adapter, ptr null }
@ail_prelude_print__Int_clos = private unnamed_addr constant { ptr, ptr } { ptr @ail_prelude_print__Int_adapter, ptr null }
@ail_ws_lib_add_clos = private unnamed_addr constant { ptr, ptr } { ptr @ail_ws_lib_add_adapter, ptr null }
@ail_ws_main_main_clos = private unnamed_addr constant { ptr, ptr } { ptr @ail_ws_main_main_adapter, ptr null }
define ptr @ail_prelude_show__Int(i64 %arg_x) {
entry:
%v1 = call ptr @ailang_int_to_str(i64 %arg_x)
ret ptr %v1
}
define ptr @ail_prelude_show__Int_adapter(ptr %_env, i64 %a0) {
entry:
%r = call ptr @ail_prelude_show__Int(i64 %a0)
ret ptr %r
}
define i8 @ail_prelude_print__Int(i64 %arg_x) {
entry:
%v1 = call ptr @ail_prelude_show__Int(i64 %arg_x)
%v2 = getelementptr inbounds i8, ptr %v1, i64 8
call i32 @puts(ptr %v2)
ret i8 0
}
define i8 @ail_prelude_print__Int_adapter(ptr %_env, i64 %a0) {
entry:
%r = call i8 @ail_prelude_print__Int(i64 %a0)
ret i8 %r
}
define i64 @ail_ws_lib_add(i64 %arg_a, i64 %arg_b) {
entry:
%v1 = add i64 %arg_a, %arg_b
@@ -34,8 +60,8 @@ entry:
define i8 @ail_ws_main_main() {
entry:
%v1 = call i64 @ail_ws_lib_add(i64 2, i64 3)
call i32 (ptr, ...) @printf(ptr @.str_ws_main_fmt_int_0, i64 %v1)
ret i8 0
%v2 = call i8 @ail_prelude_print__Int(i64 %v1)
ret i8 %v2
}
define i8 @ail_ws_main_main_adapter(ptr %_env) {
+2 -75
View File
@@ -2,7 +2,7 @@
//!
//! This module owns the **fixed** symbol set the language ships with — the
//! arithmetic / comparison / logical operators (`+`, `==`, `not`, ...) and
//! the IO effect ops (`io/print_int`, ...). User code cannot define
//! the IO effect ops (`io/print_str`). User code cannot define
//! anything in here; conversely the typechecker treats every entry as
//! always-in-scope without an explicit import.
//!
@@ -37,7 +37,7 @@ pub struct EffectOpSig {
/// `Term::Do { args }`.
pub params: Vec<Type>,
/// Return type of the op. Often [`Type::unit`] for sinks like
/// `io/print_int`.
/// `io/print_str`.
pub ret: Type,
}
@@ -266,22 +266,6 @@ pub fn install(env: &mut crate::Env) {
env.globals.insert("inf".into(), Type::float());
env.globals.insert("neg_inf".into(), Type::float());
env.effect_ops.insert(
"io/print_int".into(),
EffectOpSig {
effect: "IO".into(),
params: vec![Type::int()],
ret: Type::unit(),
},
);
env.effect_ops.insert(
"io/print_bool".into(),
EffectOpSig {
effect: "IO".into(),
params: vec![Type::bool_()],
ret: Type::unit(),
},
);
env.effect_ops.insert(
"io/print_str".into(),
EffectOpSig {
@@ -290,16 +274,6 @@ pub fn install(env: &mut crate::Env) {
ret: Type::unit(),
},
);
// Iter 22-floats.3: parallel to io/print_int|bool|str. Codegen
// lowers via runtime C glue `@ail_print_float` in iter 4.
env.effect_ops.insert(
"io/print_float".into(),
EffectOpSig {
effect: "IO".into(),
params: vec![Type::float()],
ret: Type::unit(),
},
);
}
/// Names of value-level built-ins (operators, `not`) — the ones that
@@ -346,10 +320,7 @@ pub fn list() -> Vec<(&'static str, &'static str)> {
("nan", "Float"),
("inf", "Float"),
("neg_inf", "Float"),
("io/print_int", "(Int) -> Unit !IO [effect op]"),
("io/print_bool", "(Bool) -> Unit !IO [effect op]"),
("io/print_str", "(Str) -> Unit !IO [effect op]"),
("io/print_float", "(Float) -> Unit !IO [effect op]"),
]
}
@@ -590,50 +561,6 @@ mod tests {
assert_eq!(ty_neg_inf, Type::float(), "neg_inf must type as Float");
}
/// Iter 22-floats.3: `io/print_float : (Float) -> Unit !IO`.
/// Mirrors `io/print_int|bool|str`. Codegen lowering through runtime
/// C glue `@ail_print_float` lands in iter 4. Effects are collected
/// in the mutable `effects` sink (mirrors the real typechecker
/// pipeline); we assert both the return type and that `IO` was
/// accumulated.
#[test]
fn install_io_print_float_signature() {
let bits = 1.5_f64.to_bits();
let mut env = Env::default();
install(&mut env);
let do_term = Term::Do {
op: "io/print_float".into(),
args: vec![lit_float(bits)],
tail: false,
};
let mut locals: IndexMap<String, Type> = IndexMap::new();
let mut effects: BTreeSet<String> = BTreeSet::new();
let mut subst = Subst::default();
let mut counter: u32 = 0;
let mut residuals = Vec::new();
let mut free_fn_calls = Vec::new();
let mut warnings: Vec<crate::diagnostic::Diagnostic> = Vec::new();
let ret = crate::synth(
&do_term,
&env,
&mut locals,
&mut effects,
"<test>",
&mut subst,
&mut counter,
&mut residuals,
&mut free_fn_calls,
&mut warnings,
)
.expect("synth");
let ret = subst.apply(&ret);
assert_eq!(ret, Type::unit(), "io/print_float returns Unit");
assert!(
effects.contains("IO"),
"io/print_float must accumulate IO effect, got {effects:?}"
);
}
/// Iter 22-floats.3: pattern-matching on Float literals is hard-
/// rejected at typecheck per spec line 723-735 recommendation (a).
/// IEEE-`==` semantics make Float patterns semantically dubious
+10 -4
View File
@@ -3879,9 +3879,11 @@ mod tests {
},
vec![],
Term::Do {
op: "io/print_int".into(),
op: "io/print_str".into(),
args: vec![Term::Lit {
lit: Literal::Int { value: 1 },
lit: Literal::Str {
value: "hi".into(),
},
}],
tail: false,
},
@@ -5055,8 +5057,12 @@ mod tests {
},
body: Term::Seq {
lhs: Box::new(Term::Do {
op: "io/print_int".into(),
args: vec![Term::Var { name: "h".into() }],
op: "io/print_str".into(),
args: vec![Term::Lit {
lit: Literal::Str {
value: "x".into(),
},
}],
tail: false,
}),
rhs: Box::new(Term::App {
@@ -0,0 +1,41 @@
//! 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`"
);
}
+3 -136
View File
@@ -2299,50 +2299,6 @@ impl<'a> Emitter<'a> {
let _ = tail;
let call_kw = if tail { "tail call" } else { "call" };
match op {
"io/print_int" => {
if args.len() != 1 {
return Err(CodegenError::Internal(
"io/print_int arity".into(),
));
}
let (v, vty) = self.lower_term(&args[0])?;
if vty != "i64" {
return Err(CodegenError::Internal(
"io/print_int needs i64".into(),
));
}
let fmt = self.intern_string("fmt_int", "%lld\n");
self.body.push_str(&format!(
" {call_kw} i32 (ptr, ...) @printf(ptr @{fmt}, i64 {v})\n"
));
if tail {
self.body.push_str(" ret i8 0\n");
self.block_terminated = true;
}
Ok(("0".into(), "i8".into()))
}
"io/print_float" => {
if args.len() != 1 {
return Err(CodegenError::Internal(
"io/print_float arity".into(),
));
}
let (v, vty) = self.lower_term(&args[0])?;
if vty != "double" {
return Err(CodegenError::Internal(
"io/print_float needs double".into(),
));
}
let fmt = self.intern_string("fmt_float", "%g\n");
self.body.push_str(&format!(
" {call_kw} i32 (ptr, ...) @printf(ptr @{fmt}, double {v})\n"
));
if tail {
self.body.push_str(" ret i8 0\n");
self.block_terminated = true;
}
Ok(("0".into(), "i8".into()))
}
"io/print_str" => {
if args.len() != 1 {
return Err(CodegenError::Internal(
@@ -2370,45 +2326,6 @@ impl<'a> Emitter<'a> {
}
Ok(("0".into(), "i8".into()))
}
"io/print_bool" => {
if args.len() != 1 {
return Err(CodegenError::Internal(
"io/print_bool arity".into(),
));
}
let (v, vty) = self.lower_term(&args[0])?;
if vty != "i1" {
return Err(CodegenError::Internal(
"io/print_bool needs i1".into(),
));
}
// Print "true\n" or "false\n".
let fmt_t = self.intern_string("fmt_true", "true\n");
let fmt_f = self.intern_string("fmt_false", "false\n");
let id = self.fresh_id();
let then_lbl = format!("ptbl_t.{id}");
let else_lbl = format!("ptbl_f.{id}");
let join_lbl = format!("ptbl_j.{id}");
self.body.push_str(&format!(
" br i1 {v}, label %{then_lbl}, label %{else_lbl}\n"
));
self.start_block(&then_lbl);
self.body.push_str(&format!(
" call i32 (ptr, ...) @printf(ptr @{fmt_t})\n"
));
self.body.push_str(&format!(" br label %{join_lbl}\n"));
self.start_block(&else_lbl);
self.body.push_str(&format!(
" call i32 (ptr, ...) @printf(ptr @{fmt_f})\n"
));
self.body.push_str(&format!(" br label %{join_lbl}\n"));
self.start_block(&join_lbl);
if tail {
self.body.push_str(" ret i8 0\n");
self.block_terminated = true;
}
Ok(("0".into(), "i8".into()))
}
other => Err(CodegenError::Internal(format!(
"unknown effect op: {other}"
))),
@@ -2722,20 +2639,9 @@ impl<'a> Emitter<'a> {
self.counter
}
fn intern_string(&mut self, hint: &str, content: &str) -> String {
if let Some((name, _)) = self.strings.get(content) {
return name.clone();
}
// Mangling per module: `.str_<module>_<hint>_<idx>`.
let name = format!(".str_{}_{}_{}", self.module_name, hint, self.str_counter);
self.str_counter += 1;
let len = c_byte_len(content);
self.strings
.insert(content.to_string(), (name.clone(), len));
name
}
/// Iter hs.1 (amended hs.2): parallel to `intern_string`, but for
/// Iter hs.1 (amended hs.2): parallel to the legacy
/// `intern_string` (retired in iter rpe.1 alongside the per-type
/// print effect-ops it served), but for
/// language `Str` literals emitted as packed-struct globals
/// (len + bytes + NUL). Shares the same monotonic `str_counter`
/// so the produced global names remain alphabetically orderable
@@ -3577,45 +3483,6 @@ mod tests {
assert!(ir.contains("0xFFF0000000000000"), "-inf bit pattern missing: {ir}");
}
/// Floats iter 4.6 RED: `(do io/print_float 1.5)` lowers via
/// `printf("%g\n", v)`, parallel to `io/print_int` at line 2152.
#[test]
fn lowers_io_print_float() {
use ailang_core::ast::*;
let body = Term::Do {
op: "io/print_float".into(),
args: vec![Term::Lit { lit: Literal::Float { bits: 0x3ff8_0000_0000_0000u64 } }],
tail: false,
};
let m = Module {
schema: ailang_core::SCHEMA.to_string(),
name: "t".into(),
imports: vec![],
defs: vec![Def::Fn(FnDef {
name: "main".into(),
ty: Type::Fn {
params: vec![], ret: Box::new(Type::unit()), effects: vec!["IO".into()],
param_modes: vec![], ret_mode: ParamMode::Implicit,
},
params: vec![], body, suppress: vec![], doc: None,
})],
};
let ir = emit_ir(&m).unwrap();
assert!(
ir.contains("call i32 (ptr, ...) @printf"),
"io/print_float not emitting printf: {ir}"
);
assert!(
ir.contains("double 0x3FF8000000000000"),
"Float arg not threaded through io/print_float: {ir}"
);
// Verify the format string `%g\n` is interned.
assert!(
ir.contains("%g") || ir.contains("\\67"), // `g` ASCII = 67 = 0x47
"format string `%g\\n` not interned: {ir}"
);
}
/// Iter 23.2: codegen intercepts a fn named `eq__Str` (the
/// monomorphiser's synthesised symbol for `Eq Str.eq`) and emits
/// a two-instruction body that calls @ail_str_eq, regardless of
+1 -1
View File
@@ -212,7 +212,7 @@ pub(crate) fn builtin_ail_type(name: &str) -> Option<Type> {
/// param signature is irrelevant here since we only consume the ret.
pub(crate) fn builtin_effect_op_ret(op: &str) -> Option<Type> {
Some(match op {
"io/print_int" | "io/print_bool" | "io/print_str" | "io/print_float" => Type::unit(),
"io/print_str" => Type::unit(),
_ => return None,
})
}
+2 -2
View File
@@ -430,8 +430,8 @@ generating new code.
(term-ctor List Cons 3
(term-ctor List Nil))))
(seq
(do io/print_int (app list_length xs))
(do io/print_int (app sum_list xs)))))))
(app print (app list_length xs))
(app print (app sum_list xs)))))))
```
### 3 — `lit_pat.ail`: literal patterns and nested ctor patterns
@@ -87,7 +87,7 @@ fn design_md_anchors_every_term_variant() {
(
r#""t": "do""#,
Term::Do {
op: "io/print_int".into(),
op: "io/print_str".into(),
args: vec![],
tail: false,
},
+5 -2
View File
@@ -216,10 +216,13 @@ fn ct4_migrated_fixtures_have_canonical_form_hashes() {
let ord_mod = ailang_surface::load_module(&examples.join("ordering_match.ail"))
.expect("examples/ordering_match.ail loads");
let main_def = ord_mod.defs.iter().find(|d| d.name() == "main").unwrap();
// Iter rpe.1 updated the hash: `ordering_match.ail`'s body migrated
// from `(do io/print_int x)` to `(app print x)` as part of the
// corpus migration retiring the per-type print effect-ops.
assert_eq!(
def_hash(main_def),
"8d17235aa3d2e127",
"ordering_match::main canonical hash must match captured post-migration value"
"b65a7f834703ffb4",
"ordering_match::main canonical hash must match captured post-rpe.1 value"
);
let dup_a_mod = ailang_surface::load_module(&examples.join("test_22b1_dup_a.ail"))
+1 -1
View File
@@ -64,7 +64,7 @@ fn spec_mentions_every_term_variant() {
(
"(do ",
Term::Do {
op: "io/print_int".into(),
op: "io/print_str".into(),
args: vec![],
tail: false,
},
+7 -3
View File
@@ -1585,11 +1585,15 @@ mod tests {
#[test]
fn do_renders_with_keyword_and_op() {
let t = Term::Do {
op: "io/print_int".into(),
args: vec![Term::Lit { lit: Literal::Int { value: 5 } }],
op: "io/print_str".into(),
args: vec![Term::Lit {
lit: Literal::Str {
value: "5".into(),
},
}],
tail: false,
};
assert_eq!(render_term(&t), "do io/print_int(5)");
assert_eq!(render_term(&t), "do io/print_str(\"5\")");
}
// ---- Types ----
+3 -3
View File
@@ -16,7 +16,7 @@
//! - otherwise ⇒ ident.
//!
//! Operators (`+`, `==`, `<=`, `**`), qualified names like
//! `io/print_int`, and dotted cross-module references like
//! `io/print_str`, and dotted cross-module references like
//! `std_list.map` are all single-token idents — there is no special
//! lexical rule for any of them. `(`, `)`, and whitespace are the only
//! reserved tokens. Bool literals (`true`, `false`) and unit
@@ -317,7 +317,7 @@ mod tests {
#[test]
fn operators_are_idents() {
let toks = tokenize("+ == <= io/print_int std_list.map").unwrap();
let toks = tokenize("+ == <= io/print_str std_list.map").unwrap();
let names: Vec<_> = toks
.iter()
.map(|t| match &t.tok {
@@ -325,7 +325,7 @@ mod tests {
_ => panic!("not ident"),
})
.collect();
assert_eq!(names, vec!["+", "==", "<=", "io/print_int", "std_list.map"]);
assert_eq!(names, vec!["+", "==", "<=", "io/print_str", "std_list.map"]);
}
#[test]
+1 -1
View File
@@ -1374,7 +1374,7 @@ impl<'a> Parser<'a> {
tail: bool,
production: &'static str,
) -> Result<Term, ParseError> {
let op = self.expect_ident("effect op (e.g. `io/print_int`)")?;
let op = self.expect_ident("effect op (e.g. `io/print_str`)")?;
let mut args = Vec::new();
while !matches!(self.peek(), Some(Token { tok: Tok::RParen, .. })) {
args.push(self.parse_term()?);