iter 24.1: bool_to_str + str_clone runtime + codegen wiring
First iter of milestone 24 (Show + print rewire). Wires two new heap-Str-producing primitives parallel to hs.4's int_to_str / float_to_str: - runtime/str.c gains ailang_bool_to_str(bool) → heap-Str "true" / "false" and ailang_str_clone(const char *) → memcpy'd heap-Str copy. Both use the existing str_alloc slab helper. - builtins.rs + synth.rs install the two signatures lockstep with ret_mode: Own; str_clone carries param_modes: [Borrow]. - IR-header preamble gains two unconditional `declare ptr @...` lines; Emitter::lower_app gets two new arms; is_static_callee whitelist extends with the two names. - Five IR snapshots regenerate for the two new declares. - Pre-existing-drift fix: int_to_str row added to builtins.rs::list() (hs.4 installed env.globals entry but missed the list() row). Substantive deviation flagged by orchestrator (DONE_WITH_CONCERNS): builtin signatures registered in uniqueness.rs::infer_module and linearity.rs::check_module_with_visible (8 LOC × 2 files), symmetric to iter 23.4-prep's class-method registration in the same globals maps. Without this fix str_clone's param_modes: [Borrow] is invisible to the App-arg walker, src_heap walks as Position::Consume, the scope-close ailang_rc_dec is gated off, and the str_clone_cross_realisation_uniform_abi test's plan-literal `frees == 3` assertion does not hold. The fix is the substantively correct repair, not a design departure. 9 new tests: 2 builtins-install unit, 2 IR-shape unit pins, 5 E2E (2 RC-stats, 2 stdout-smoke for both Bool branches, 1 cross- realisation). 4 new .ail.json fixtures. Full cargo test --workspace: 513 passed, 0 failed. bench/compile_check.py: 24/24 stable. bench/cross_lang.py: 25/25 stable.
This commit is contained in:
@@ -0,0 +1,19 @@
|
||||
{
|
||||
"iter_id": "24.1",
|
||||
"date": "2026-05-12",
|
||||
"mode": "standard",
|
||||
"outcome": "DONE",
|
||||
"tasks_total": 6,
|
||||
"tasks_completed": 6,
|
||||
"reloops_per_task": {
|
||||
"1": 0,
|
||||
"2": 0,
|
||||
"3": 0,
|
||||
"4": 0,
|
||||
"5": 0,
|
||||
"6": 1
|
||||
},
|
||||
"review_loops_spec": 0,
|
||||
"review_loops_quality": 0,
|
||||
"blocked_reason": null
|
||||
}
|
||||
@@ -2655,3 +2655,70 @@ fn heap_str_repeated_print_balances_rc_stats() {
|
||||
assert_eq!(frees, 1, "expected exactly one free; got frees={frees}");
|
||||
assert_eq!(live, 0, "no leaked heap-Str slabs allowed; live={live}");
|
||||
}
|
||||
|
||||
/// Iter 24.1: `bool_to_str(true)` bound to a let, consumed by
|
||||
/// `io/print_str`. The heap-Str slab from `ailang_bool_to_str`
|
||||
/// rides the same rc_header + ailang_rc_dec path as int_to_str's
|
||||
/// output — `allocs == 1, frees == 1, live == 0` and stdout is
|
||||
/// `true\n`. Companion of `bool_to_str_emits_false_branch` which
|
||||
/// covers the false-branch stdout.
|
||||
#[test]
|
||||
fn bool_to_str_drop_balances_rc_stats() {
|
||||
let (stdout, allocs, frees, live) =
|
||||
build_and_run_with_rc_stats("bool_to_str_drop_rc.ail.json");
|
||||
assert_eq!(stdout, "true\n");
|
||||
assert_eq!(allocs, 1, "expected exactly one heap-Str slab; got allocs={allocs}");
|
||||
assert_eq!(frees, 1, "expected exactly one free; got frees={frees}");
|
||||
assert_eq!(live, 0, "no leaked heap-Str slabs allowed; live={live}");
|
||||
}
|
||||
|
||||
/// Iter 24.1: stdout-smoke for the true branch — same fixture as
|
||||
/// `bool_to_str_drop_balances_rc_stats` but checked here without
|
||||
/// the RC-stats overhead. Pins the byte content of `ailang_bool_to_str`'s
|
||||
/// "true" slab.
|
||||
#[test]
|
||||
fn bool_to_str_emits_true_branch() {
|
||||
let out = build_and_run("bool_to_str_drop_rc.ail.json");
|
||||
assert_eq!(out, "true\n");
|
||||
}
|
||||
|
||||
/// Iter 24.1: stdout-smoke for the false branch. Pins the byte
|
||||
/// content of `ailang_bool_to_str`'s "false" slab.
|
||||
#[test]
|
||||
fn bool_to_str_emits_false_branch() {
|
||||
let out = build_and_run("bool_to_str_smoke_false.ail.json");
|
||||
assert_eq!(out, "false\n");
|
||||
}
|
||||
|
||||
/// Iter 24.1: `str_clone("hello")` bound to a let, consumed by
|
||||
/// `io/print_str`. The heap-Str clone allocates a fresh slab (via
|
||||
/// str_alloc) and the let-binder drops it at scope close.
|
||||
/// `allocs == 1, frees == 1, live == 0`; stdout is `hello\n` (puts
|
||||
/// adds the newline) — confirming the memcpy preserved every byte.
|
||||
#[test]
|
||||
fn str_clone_drop_balances_rc_stats() {
|
||||
let (stdout, allocs, frees, live) =
|
||||
build_and_run_with_rc_stats("str_clone_drop_rc.ail.json");
|
||||
assert_eq!(stdout, "hello\n");
|
||||
assert_eq!(allocs, 1, "expected exactly one heap-Str clone slab; got allocs={allocs}");
|
||||
assert_eq!(frees, 1, "expected exactly one free; got frees={frees}");
|
||||
assert_eq!(live, 0, "no leaked heap-Str slabs allowed; live={live}");
|
||||
}
|
||||
|
||||
/// Iter 24.1: cross-realisation invariant — `str_clone` works
|
||||
/// uniformly on heap-Str input (`int_to_str 42`'s output) AND on
|
||||
/// static-Str input (literal `"abc"`). Both clones print their
|
||||
/// input bytes; RC stats account for three heap-Str slabs (one
|
||||
/// from int_to_str, two from str_clone) and three matching frees.
|
||||
/// Pins the "uniform consumer ABI" claim from DESIGN.md §"Str
|
||||
/// ABI" — str_clone only reads len + bytes + NUL, never the
|
||||
/// rc_header.
|
||||
#[test]
|
||||
fn str_clone_cross_realisation_uniform_abi() {
|
||||
let (stdout, allocs, frees, live) =
|
||||
build_and_run_with_rc_stats("str_clone_cross_realisation.ail.json");
|
||||
assert_eq!(stdout, "42\nabc\n");
|
||||
assert_eq!(allocs, 3, "expected one int_to_str slab + two str_clone slabs; got allocs={allocs}");
|
||||
assert_eq!(frees, 3, "expected three matching frees; got frees={frees}");
|
||||
assert_eq!(live, 0, "no leaked heap-Str slabs allowed; live={live}");
|
||||
}
|
||||
|
||||
@@ -12,6 +12,8 @@ declare zeroext i1 @ail_str_eq(ptr, ptr)
|
||||
declare i32 @ail_str_compare(ptr, ptr)
|
||||
declare ptr @ailang_int_to_str(i64)
|
||||
declare ptr @ailang_float_to_str(double)
|
||||
declare ptr @ailang_bool_to_str(i1)
|
||||
declare ptr @ailang_str_clone(ptr)
|
||||
declare i64 @llvm.fptosi.sat.i64.f64(double)
|
||||
|
||||
@ail_hello_main_clos = private unnamed_addr constant { ptr, ptr } { ptr @ail_hello_main_adapter, ptr null }
|
||||
|
||||
@@ -12,6 +12,8 @@ declare zeroext i1 @ail_str_eq(ptr, ptr)
|
||||
declare i32 @ail_str_compare(ptr, ptr)
|
||||
declare ptr @ailang_int_to_str(i64)
|
||||
declare ptr @ailang_float_to_str(double)
|
||||
declare ptr @ailang_bool_to_str(i1)
|
||||
declare ptr @ailang_str_clone(ptr)
|
||||
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 }
|
||||
|
||||
@@ -12,6 +12,8 @@ declare zeroext i1 @ail_str_eq(ptr, ptr)
|
||||
declare i32 @ail_str_compare(ptr, ptr)
|
||||
declare ptr @ailang_int_to_str(i64)
|
||||
declare ptr @ailang_float_to_str(double)
|
||||
declare ptr @ailang_bool_to_str(i1)
|
||||
declare ptr @ailang_str_clone(ptr)
|
||||
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 }
|
||||
|
||||
@@ -12,6 +12,8 @@ declare zeroext i1 @ail_str_eq(ptr, ptr)
|
||||
declare i32 @ail_str_compare(ptr, ptr)
|
||||
declare ptr @ailang_int_to_str(i64)
|
||||
declare ptr @ailang_float_to_str(double)
|
||||
declare ptr @ailang_bool_to_str(i1)
|
||||
declare ptr @ailang_str_clone(ptr)
|
||||
declare i64 @llvm.fptosi.sat.i64.f64(double)
|
||||
|
||||
@ail_sum_sum_clos = private unnamed_addr constant { ptr, ptr } { ptr @ail_sum_sum_adapter, ptr null }
|
||||
|
||||
@@ -12,6 +12,8 @@ declare zeroext i1 @ail_str_eq(ptr, ptr)
|
||||
declare i32 @ail_str_compare(ptr, ptr)
|
||||
declare ptr @ailang_int_to_str(i64)
|
||||
declare ptr @ailang_float_to_str(double)
|
||||
declare ptr @ailang_bool_to_str(i1)
|
||||
declare ptr @ailang_str_clone(ptr)
|
||||
declare i64 @llvm.fptosi.sat.i64.f64(double)
|
||||
|
||||
@ail_ws_lib_add_clos = private unnamed_addr constant { ptr, ptr } { ptr @ail_ws_lib_add_adapter, ptr null }
|
||||
|
||||
@@ -210,6 +210,26 @@ pub fn install(env: &mut crate::Env) {
|
||||
ret_mode: ailang_core::ast::ParamMode::Own,
|
||||
},
|
||||
);
|
||||
env.globals.insert(
|
||||
"bool_to_str".into(),
|
||||
Type::Fn {
|
||||
params: vec![Type::bool_()],
|
||||
ret: Box::new(Type::str_()),
|
||||
effects: vec![],
|
||||
param_modes: vec![],
|
||||
ret_mode: ailang_core::ast::ParamMode::Own,
|
||||
},
|
||||
);
|
||||
env.globals.insert(
|
||||
"str_clone".into(),
|
||||
Type::Fn {
|
||||
params: vec![Type::str_()],
|
||||
ret: Box::new(Type::str_()),
|
||||
effects: vec![],
|
||||
param_modes: vec![ailang_core::ast::ParamMode::Borrow],
|
||||
ret_mode: ailang_core::ast::ParamMode::Own,
|
||||
},
|
||||
);
|
||||
env.globals.insert(
|
||||
"is_nan".into(),
|
||||
Type::Fn {
|
||||
@@ -305,6 +325,9 @@ pub fn list() -> Vec<(&'static str, &'static str)> {
|
||||
("int_to_float", "(Int) -> Float"),
|
||||
("float_to_int_truncate", "(Float) -> Int"),
|
||||
("float_to_str", "(Float) -> Str"),
|
||||
("int_to_str", "(Int) -> Str"),
|
||||
("bool_to_str", "(Bool) -> Str"),
|
||||
("str_clone", "(Str) -> Str"),
|
||||
("is_nan", "(Float) -> Bool"),
|
||||
("nan", "Float"),
|
||||
("inf", "Float"),
|
||||
@@ -466,6 +489,28 @@ mod tests {
|
||||
assert_eq!(ty, Type::str_(), "(int_to_str 42) must type as Str");
|
||||
}
|
||||
|
||||
/// Iter 24.1: `bool_to_str : (Bool) -> Str`. Codegen lowers via the
|
||||
/// runtime C glue `ailang_bool_to_str` from `runtime/str.c`.
|
||||
#[test]
|
||||
fn install_bool_to_str_signature() {
|
||||
let ty = synth_in_builtins_env(&app(
|
||||
"bool_to_str",
|
||||
vec![Term::Lit { lit: Literal::Bool { value: true } }],
|
||||
));
|
||||
assert_eq!(ty, Type::str_(), "(bool_to_str true) must type as Str");
|
||||
}
|
||||
|
||||
/// Iter 24.1: `str_clone : (Str borrow) -> Str`. Codegen lowers via the
|
||||
/// runtime C glue `ailang_str_clone` from `runtime/str.c`.
|
||||
#[test]
|
||||
fn install_str_clone_signature() {
|
||||
let ty = synth_in_builtins_env(&app(
|
||||
"str_clone",
|
||||
vec![Term::Lit { lit: Literal::Str { value: "hi".into() } }],
|
||||
));
|
||||
assert_eq!(ty, Type::str_(), "(str_clone \"hi\") must type as Str");
|
||||
}
|
||||
|
||||
/// Iter 22-floats.3: `is_nan : (Float) -> Bool`. Codegen lowers to
|
||||
/// `fcmp uno double %x, %x` in iter 4 — typecheck only validates
|
||||
/// the signature here.
|
||||
|
||||
@@ -213,6 +213,19 @@ pub(crate) fn check_module_with_visible(m: &Module, visible_extra: &[&Module]) -
|
||||
// does not force ownership of the scrutinee.
|
||||
let mut ctors: HashMap<String, Vec<Type>> = HashMap::new();
|
||||
|
||||
// Iter 24.1: register builtin signatures so `callee_arg_modes`
|
||||
// resolves them when a user fn forwards a Borrow-mode param
|
||||
// into a builtin like `str_clone : (Str borrow) -> Str`. The
|
||||
// previously-registered class-method types (below, in the
|
||||
// `Def::Class` arm) cover one half of the same problem; this
|
||||
// registration covers the builtin half. Symmetric to the
|
||||
// uniqueness pass's iter 24.1 builtin registration.
|
||||
let mut builtins_env = crate::Env::default();
|
||||
crate::builtins::install(&mut builtins_env);
|
||||
for (name, ty) in &builtins_env.globals {
|
||||
globals.insert(name.clone(), strip_forall(ty).clone());
|
||||
}
|
||||
|
||||
// Iter 23.5: register fns + class methods from visible-extra
|
||||
// modules first, so a name-clash inside `m` overwrites the
|
||||
// imported entry (matching the synth-side scope rule that
|
||||
|
||||
@@ -108,6 +108,19 @@ pub type UniquenessTable = BTreeMap<(String, String), UniquenessInfo>;
|
||||
/// typecheck first.
|
||||
pub fn infer_module(m: &Module) -> UniquenessTable {
|
||||
let mut globals: BTreeMap<String, Type> = BTreeMap::new();
|
||||
// Iter 24.1: register builtin signatures so the App-arg walker
|
||||
// sees their `param_modes` and walks `Borrow`-mode args as
|
||||
// `Position::Borrow` (not `Consume`). Without this, a builtin
|
||||
// like `str_clone : (Str borrow) -> Str` invoked on a heap-Str
|
||||
// let-binder would falsely classify the binder as Consumed,
|
||||
// gating off the scope-close `ailang_rc_dec` and leaking the
|
||||
// slab. Symmetric to the class-method registration the
|
||||
// linearity pass added in iter 23.4-prep.
|
||||
let mut builtins_env = crate::Env::default();
|
||||
crate::builtins::install(&mut builtins_env);
|
||||
for (name, ty) in &builtins_env.globals {
|
||||
globals.insert(name.clone(), strip_forall(ty).clone());
|
||||
}
|
||||
for def in &m.defs {
|
||||
match def {
|
||||
Def::Fn(f) => {
|
||||
|
||||
@@ -536,6 +536,16 @@ fn lower_workspace_inner(ws: &Workspace, alloc: AllocStrategy) -> Result<String>
|
||||
// clang -O2 dead-strips the declarations when no caller exists.
|
||||
out.push_str("declare ptr @ailang_int_to_str(i64)\n");
|
||||
out.push_str("declare ptr @ailang_float_to_str(double)\n");
|
||||
// Iter 24.1: heap-Str primitive externs for the `show` instances
|
||||
// — `ailang_bool_to_str` (Show Bool) and `ailang_str_clone`
|
||||
// (Show Str). Same dual-realisation ABI as int_to_str /
|
||||
// float_to_str: rc_header at offset -8 on the heap-Str output;
|
||||
// consumer ABI shared with static-Str (len at offset 0, bytes
|
||||
// at offset 8). Declared unconditionally on the same rationale
|
||||
// — runtime/str.c is unconditionally linked and clang -O2 dead-
|
||||
// strips when no caller exists.
|
||||
out.push_str("declare ptr @ailang_bool_to_str(i1)\n");
|
||||
out.push_str("declare ptr @ailang_str_clone(ptr)\n");
|
||||
// Floats iter 4.4: saturating fp-to-int intrinsic for
|
||||
// float_to_int_truncate. NaN → 0, +Inf → i64::MAX, -Inf →
|
||||
// i64::MIN, finite-out-of-range saturates, finite-in-range
|
||||
@@ -1931,6 +1941,40 @@ impl<'a> Emitter<'a> {
|
||||
));
|
||||
return Ok((dst, "ptr".to_string()));
|
||||
}
|
||||
if name == "bool_to_str" {
|
||||
// Iter 24.1: lowers to the runtime C glue
|
||||
// `ailang_bool_to_str(i1) -> ptr` defined in
|
||||
// `runtime/str.c`. Returned pointer is a heap-Str
|
||||
// (rc_header at offset -8; consumer ABI shared with
|
||||
// static-Str). Used by `show__Bool` in milestone 24.
|
||||
if args.len() != 1 {
|
||||
return Err(CodegenError::Internal("bool_to_str arity".into()));
|
||||
}
|
||||
let (a, _) = self.lower_term(&args[0])?;
|
||||
let dst = self.fresh_ssa();
|
||||
self.body.push_str(&format!(
|
||||
" {dst} = call ptr @ailang_bool_to_str(i1 {a})\n"
|
||||
));
|
||||
return Ok((dst, "ptr".to_string()));
|
||||
}
|
||||
if name == "str_clone" {
|
||||
// Iter 24.1: lowers to the runtime C glue
|
||||
// `ailang_str_clone(ptr) -> ptr` defined in
|
||||
// `runtime/str.c`. Reads `len` from offset 0 of the
|
||||
// source Str payload and allocates a fresh heap-Str
|
||||
// slab; works uniformly on static-Str and heap-Str
|
||||
// inputs because the consumer ABI is identical. Used by
|
||||
// `show__Str` in milestone 24.
|
||||
if args.len() != 1 {
|
||||
return Err(CodegenError::Internal("str_clone arity".into()));
|
||||
}
|
||||
let (a, _) = self.lower_term(&args[0])?;
|
||||
let dst = self.fresh_ssa();
|
||||
self.body.push_str(&format!(
|
||||
" {dst} = call ptr @ailang_str_clone(ptr {a})\n"
|
||||
));
|
||||
return Ok((dst, "ptr".to_string()));
|
||||
}
|
||||
|
||||
// Cross-module call: exactly one dot in the name → resolve via import map.
|
||||
// Logic identical to the typechecker (see `synth` for `Term::Var`).
|
||||
@@ -2131,6 +2175,8 @@ impl<'a> Emitter<'a> {
|
||||
| "is_nan"
|
||||
| "float_to_str"
|
||||
| "int_to_str"
|
||||
| "bool_to_str"
|
||||
| "str_clone"
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
@@ -4202,4 +4248,84 @@ mod tests {
|
||||
"expected lowering of float_to_str to call @ailang_float_to_str; ir was:\n{ir}"
|
||||
);
|
||||
}
|
||||
|
||||
/// Iter 24.1: a `Term::App` calling `bool_to_str` lowers to
|
||||
/// `call ptr @ailang_bool_to_str(i1 %a)`. Pins the new builtin's
|
||||
/// lowering shape against the runtime C glue introduced in this
|
||||
/// iter (mirror of hs.4's `int_to_str_lowers_to_ailang_int_to_str_call`).
|
||||
#[test]
|
||||
fn bool_to_str_emits_call_to_ailang_bool_to_str() {
|
||||
let m = Module {
|
||||
schema: SCHEMA.into(),
|
||||
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: Term::Do {
|
||||
op: "io/print_str".into(),
|
||||
args: vec![Term::App {
|
||||
callee: Box::new(Term::Var { name: "bool_to_str".into() }),
|
||||
args: vec![Term::Lit { lit: Literal::Bool { value: true } }],
|
||||
tail: false,
|
||||
}],
|
||||
tail: false,
|
||||
},
|
||||
suppress: vec![],
|
||||
doc: None,
|
||||
})],
|
||||
};
|
||||
let ir = emit_ir(&m).unwrap();
|
||||
assert!(
|
||||
ir.contains("call ptr @ailang_bool_to_str(i1 "),
|
||||
"expected lowering of bool_to_str to call @ailang_bool_to_str; ir was:\n{ir}"
|
||||
);
|
||||
}
|
||||
|
||||
/// Iter 24.1: a `Term::App` calling `str_clone` lowers to
|
||||
/// `call ptr @ailang_str_clone(ptr %a)`. Pins the new builtin's
|
||||
/// lowering shape against the runtime C glue introduced in this
|
||||
/// iter (mirror of hs.4's `int_to_str_lowers_to_ailang_int_to_str_call`).
|
||||
#[test]
|
||||
fn str_clone_emits_call_to_ailang_str_clone() {
|
||||
let m = Module {
|
||||
schema: SCHEMA.into(),
|
||||
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: Term::Do {
|
||||
op: "io/print_str".into(),
|
||||
args: vec![Term::App {
|
||||
callee: Box::new(Term::Var { name: "str_clone".into() }),
|
||||
args: vec![Term::Lit { lit: Literal::Str { value: "hi".into() } }],
|
||||
tail: false,
|
||||
}],
|
||||
tail: false,
|
||||
},
|
||||
suppress: vec![],
|
||||
doc: None,
|
||||
})],
|
||||
};
|
||||
let ir = emit_ir(&m).unwrap();
|
||||
assert!(
|
||||
ir.contains("call ptr @ailang_str_clone(ptr "),
|
||||
"expected lowering of str_clone to call @ailang_str_clone; ir was:\n{ir}"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -178,6 +178,20 @@ pub(crate) fn builtin_ail_type(name: &str) -> Option<Type> {
|
||||
param_modes: vec![],
|
||||
ret_mode: ParamMode::Own,
|
||||
},
|
||||
"bool_to_str" => Type::Fn {
|
||||
params: vec![Type::bool_()],
|
||||
ret: Box::new(Type::str_()),
|
||||
effects: vec![],
|
||||
param_modes: vec![],
|
||||
ret_mode: ParamMode::Own,
|
||||
},
|
||||
"str_clone" => Type::Fn {
|
||||
params: vec![Type::str_()],
|
||||
ret: Box::new(Type::str_()),
|
||||
effects: vec![],
|
||||
param_modes: vec![ParamMode::Borrow],
|
||||
ret_mode: ParamMode::Own,
|
||||
},
|
||||
"is_nan" => Type::Fn {
|
||||
params: vec![Type::float()],
|
||||
ret: Box::new(Type::bool_()),
|
||||
|
||||
@@ -0,0 +1,216 @@
|
||||
# iter 24.1 — `bool_to_str` + `str_clone` runtime + codegen wiring
|
||||
|
||||
**Date:** 2026-05-12
|
||||
**Started from:** 8bfa09adc7774a1c94af609fc4c78a70ba77be48
|
||||
**Status:** DONE_WITH_CONCERNS
|
||||
**Tasks completed:** 6 of 6
|
||||
|
||||
## Summary
|
||||
|
||||
First iter of milestone 24 ("Show + print rewire"). Wires two new
|
||||
heap-Str-producing primitives — `bool_to_str : (Bool) -> Str` and
|
||||
`str_clone : (Str borrow) -> Str` — through the runtime C code, type
|
||||
checker, codegen IR lowering, and IR-header preamble, mechanically
|
||||
mirror-imaging the hs.4 wiring for `int_to_str` / `float_to_str`. The
|
||||
two new symbols install lockstep in `builtins.rs::install` and
|
||||
`synth.rs::builtin_value_type` with `ret_mode: Own`; codegen gains
|
||||
two new IR-header `declare` lines, two `Emitter::lower_app` arms, and
|
||||
two `is_static_callee` whitelist entries. Plan's pre-existing-drift
|
||||
fix landed alongside: `int_to_str` row added to `builtins.rs::list()`
|
||||
(hs.4 added the `env.globals` entry but missed the `list()` row).
|
||||
|
||||
Nine new tests landed per plan: 2 builtins-install unit tests
|
||||
(`install_bool_to_str_signature`, `install_str_clone_signature`),
|
||||
2 IR-shape unit pins (`bool_to_str_emits_call_to_ailang_bool_to_str`,
|
||||
`str_clone_emits_call_to_ailang_str_clone`), 5 E2E tests
|
||||
(`bool_to_str_drop_balances_rc_stats`, `bool_to_str_emits_true_branch`,
|
||||
`bool_to_str_emits_false_branch`, `str_clone_drop_balances_rc_stats`,
|
||||
`str_clone_cross_realisation_uniform_abi`). Four `.ail.json` fixtures
|
||||
shipped. Five IR snapshots regenerated for the two new declare lines.
|
||||
|
||||
One substantive deviation from the plan's "purely-additive wiring"
|
||||
framing surfaced during Task 6 fixture validation: the
|
||||
`str_clone_cross_realisation_uniform_abi` test's literal assertion
|
||||
`allocs == 3, frees == 3, live == 0` could not hold under the
|
||||
pre-24.1 uniqueness analyser because the analyser's `globals` map
|
||||
does not contain builtin signatures — `str_clone`'s `param_modes:
|
||||
[Borrow]` is invisible to `callee_arg_modes`, so a `src_heap` let-
|
||||
binder consumed by `str_clone(src_heap)` walks as `Position::Consume`,
|
||||
incrementing `consume_count` and gating off the scope-close
|
||||
`ailang_rc_dec`. Fixed inline by registering builtin signatures in
|
||||
both `uniqueness.rs::infer_module` and
|
||||
`linearity.rs::check_module_with_visible` (8 lines × 2 files,
|
||||
symmetric to the existing class-method registration the linearity
|
||||
pass added in iter 23.4-prep). The fix is substantively correct (it
|
||||
applies a pre-existing pattern to a new class of callees) but is
|
||||
NOT in the plan's literal Edit text — see Concerns for the
|
||||
discussion.
|
||||
|
||||
Acceptance verified: full `cargo test --workspace` green (513 tests
|
||||
passing, 0 failures); the five new E2E tests pass with the plan's
|
||||
literal numeric assertions; the two new IR-shape pins pass; the five
|
||||
IR snapshot tests pass after `UPDATE_SNAPSHOTS=1` regen (diff is
|
||||
exactly the two new `declare ptr @ailang_bool_to_str(i1)` and
|
||||
`declare ptr @ailang_str_clone(ptr)` lines). `bench/compile_check.py`
|
||||
24/24 stable, 0 regressions. `bench/cross_lang.py` 25/25 stable, 0
|
||||
regressions. Grep verification: 44 hits across runtime/str.c +
|
||||
builtins.rs + synth.rs + lib.rs (well above the plan's ≥ 10
|
||||
threshold), 10 hits in snapshots (2 declares × 5 files), 0 build
|
||||
warnings.
|
||||
|
||||
## Per-task notes
|
||||
|
||||
- iter 24.1.1: Appended `ailang_bool_to_str(bool b) -> char *` and
|
||||
`ailang_str_clone(const char *src) -> char *` to `runtime/str.c`
|
||||
after `ailang_float_to_str` (str.c:128-143). Both use the existing
|
||||
static `str_alloc(uint64_t)` helper for heap-Str slab allocation;
|
||||
`bool_to_str` writes either "true" (4 bytes) or "false" (5 bytes);
|
||||
`str_clone` reads `len` from offset 0 of source and memcpy's bytes
|
||||
+ trailing NUL to a fresh slab. Standalone clang -c compile green.
|
||||
- iter 24.1.2: Installed `bool_to_str : (Bool) -> Str` and
|
||||
`str_clone : (Str borrow) -> Str` in `builtins.rs::install`
|
||||
(builtins.rs:213-232, between the existing `int_to_str` block and
|
||||
the `is_nan` block), both with `ret_mode: ParamMode::Own`. Mirror-
|
||||
installed the same two signatures in `synth.rs::builtin_value_type`
|
||||
(synth.rs:181-194, lockstep). Added three rows to
|
||||
`builtins.rs::list()` (`int_to_str` drift fix from hs.4 +
|
||||
`bool_to_str` + `str_clone`). Two new unit tests
|
||||
(`install_bool_to_str_signature`, `install_str_clone_signature`)
|
||||
appended to `builtins.rs::tests` using the fallback explicit
|
||||
`Term::Lit` shape (plan-noted: `lit_bool` / `lit_str` helpers do
|
||||
not exist; only `lit_int` / `lit_float` are defined). Both new
|
||||
unit tests PASS.
|
||||
- iter 24.1.3: Codegen IR wiring. (a) Extended the IR-header preamble
|
||||
at `lib.rs:539-548` with two unconditional declares
|
||||
`declare ptr @ailang_bool_to_str(i1)` and
|
||||
`declare ptr @ailang_str_clone(ptr)`. (b) Inserted two new arms in
|
||||
`Emitter::lower_app` at `lib.rs:1934-1969` (after the existing
|
||||
`float_to_str` arm), each emitting a direct call to the runtime
|
||||
C glue with arity check + `lower_term`+`fresh_ssa`+call triple
|
||||
matching `int_to_str`'s structural shape. (c) Extended the
|
||||
`is_static_callee` builtin-callee whitelist at `lib.rs:2133-2135`
|
||||
with `bool_to_str` and `str_clone` (strictly required so
|
||||
`Term::Var { name: "bool_to_str" }` reaches `lower_app`'s chain
|
||||
instead of the UnknownVar fallback). Full workspace build clean.
|
||||
- iter 24.1.4: Two IR-shape unit pin tests
|
||||
(`bool_to_str_emits_call_to_ailang_bool_to_str`,
|
||||
`str_clone_emits_call_to_ailang_str_clone`) appended at the bottom
|
||||
of `lib.rs::mod tests`, mirroring the
|
||||
`int_to_str_lowers_to_ailang_int_to_str_call` template. Both PASS.
|
||||
- iter 24.1.5: Five IR snapshots (`hello.ll`, `list.ll`, `max3.ll`,
|
||||
`sum.ll`, `ws_main.ll`) regenerated via `UPDATE_SNAPSHOTS=1`. RED
|
||||
observed first as expected (5 ir_snapshot_* tests failed with
|
||||
"run UPDATE_SNAPSHOTS=1 cargo test ir_snapshot_ to refresh");
|
||||
GREEN after regen. Diff is exactly the two new declare lines.
|
||||
Each .ll file shows 2 matches for the new symbols.
|
||||
- iter 24.1.6: Four `.ail.json` fixtures created
|
||||
(`bool_to_str_drop_rc.ail.json`, `bool_to_str_smoke_false.ail.json`,
|
||||
`str_clone_drop_rc.ail.json`, `str_clone_cross_realisation.ail.json`)
|
||||
per the plan's verbatim JSON. Five new E2E tests appended to
|
||||
`crates/ail/tests/e2e.rs` after `heap_str_repeated_print_balances_rc_stats`.
|
||||
The `str_clone_cross_realisation_uniform_abi` test initially
|
||||
FAILED with `frees=2, expected=3`; root-caused to the
|
||||
uniqueness-analyser-globals scope gap (see Concerns); fixed by
|
||||
registering builtin signatures in `uniqueness.rs::infer_module`
|
||||
and `linearity.rs::check_module_with_visible`. After the fix all
|
||||
five new E2E tests PASS with the plan's literal numeric
|
||||
assertions. Full `cargo test --workspace`: 513 passed, 0 failed.
|
||||
`bench/compile_check.py` 24/24 stable. `bench/cross_lang.py` 25/25
|
||||
stable. Grep verification per plan Step 9 met: 44 code-side hits,
|
||||
10 snapshot hits, 0 build warnings.
|
||||
|
||||
## Concerns
|
||||
|
||||
- **Analyser-globals registration extends the plan's "purely-additive"
|
||||
scope.** The plan framed 24.1 as "Pure-additive wiring across four
|
||||
crates / files." The actual landed change extends to two more
|
||||
files (`uniqueness.rs`, `linearity.rs`) with 8 lines each — a fix
|
||||
to the uniqueness / linearity analyser-globals scope so the
|
||||
`str_clone` builtin's `param_modes: [Borrow]` is visible to the
|
||||
`callee_arg_modes` walker. Without the fix, the
|
||||
`str_clone_cross_realisation_uniform_abi` test's plan-literal
|
||||
assertion `frees == 3` does not hold (one heap-Str slab leaks
|
||||
because the App-arg walker can't see the Borrow mode → walks
|
||||
`src_heap` as Consume → `consume_count == 1` → drop emission
|
||||
gated off).
|
||||
|
||||
The fix is **symmetric** to the iter 23.4-prep extension that
|
||||
registered class-method signatures in the same `globals` maps for
|
||||
the same reason (a builtin/class-method callee carrying
|
||||
`param_modes` would not be visible to the analyser, false-firing
|
||||
`consume-while-borrowed` lints AND, in this iter, leaking heap-Str
|
||||
slabs). The plan's verbatim Edit text did not include the fix; the
|
||||
plan's verbatim test assertion required it. Per the orchestrator-
|
||||
agent's "make the reasonable call and continue" mandate I applied
|
||||
the fix in-place; it is the substantively-correct repair, not a
|
||||
design departure.
|
||||
|
||||
The hs.4 journal's "Known debt" item on this is partially closed
|
||||
by this iter (the App-side of the Borrow-walker visibility is now
|
||||
fixed for builtin callees). The eob.1 fix (effect-op args walk as
|
||||
Borrow regardless of callee `param_modes`) covered the `Term::Do`
|
||||
side; the corresponding `Term::App` side is now covered for
|
||||
builtins. User-fn `Term::App` callees still benefit from their
|
||||
own def being registered in the analyser's `globals` from the
|
||||
module's own def list (lines 113-115 in uniqueness.rs); imported
|
||||
user-fn callees benefit from the iter 23.5 visible-extra
|
||||
registration in linearity.rs (lines 220-238). So the only
|
||||
remaining unknown-callee case is one this iter does not encounter.
|
||||
|
||||
## Known debt
|
||||
|
||||
- **`str_clone` could rc_inc on heap-Str input** instead of allocating
|
||||
a fresh copy, but this requires distinguishing heap-Str from
|
||||
static-Str at runtime — a tagging scheme the codebase deliberately
|
||||
dropped post-hs.2 (UINT64_MAX sentinel removal). Out of scope per
|
||||
the parent spec's "`str_clone` as rc_header bump" entry.
|
||||
|
||||
- **`bool_to_str` as schema-`if`** with two static-Str literals would
|
||||
avoid the per-call heap allocation but requires phi-of-static-Str
|
||||
through the drop-elision pipeline that is not load-bearing-ratified
|
||||
today. Open commitment per the parent spec.
|
||||
|
||||
## Files touched
|
||||
|
||||
- Modified (12 files):
|
||||
- `runtime/str.c` — appended `ailang_bool_to_str` (16 LOC incl.
|
||||
comment) and `ailang_str_clone` (18 LOC incl. comment)
|
||||
- `crates/ailang-check/src/builtins.rs` — two new
|
||||
`env.globals.insert` blocks (`bool_to_str` + `str_clone`), three
|
||||
new rows in `list()` (drift fix + two new), two new unit tests
|
||||
- `crates/ailang-codegen/src/synth.rs` — two new arms in
|
||||
`builtin_value_type` (lockstep partner of the checker insert)
|
||||
- `crates/ailang-codegen/src/lib.rs` — IR-header preamble extends
|
||||
with 2 new `declare` lines; 2 new `lower_app` arms; 2-entry
|
||||
`is_static_callee` whitelist extension; 2 new IR-shape pin tests
|
||||
- `crates/ailang-check/src/uniqueness.rs` — register builtin
|
||||
signatures in `infer_module`'s `globals` map so `callee_arg_modes`
|
||||
resolves builtin `param_modes` correctly
|
||||
- `crates/ailang-check/src/linearity.rs` — register builtin
|
||||
signatures in `check_module_with_visible`'s `globals` map for the
|
||||
same reason
|
||||
- `crates/ail/tests/e2e.rs` — 5 new E2E tests appended
|
||||
- `crates/ail/tests/snapshots/hello.ll`,
|
||||
`crates/ail/tests/snapshots/list.ll`,
|
||||
`crates/ail/tests/snapshots/max3.ll`,
|
||||
`crates/ail/tests/snapshots/sum.ll`,
|
||||
`crates/ail/tests/snapshots/ws_main.ll` — golden-IR regen via
|
||||
`UPDATE_SNAPSHOTS=1`; diff is exactly the two new
|
||||
`declare ptr @ailang_bool_to_str(i1)` and
|
||||
`declare ptr @ailang_str_clone(ptr)` lines
|
||||
|
||||
- Created (4 files):
|
||||
- `examples/bool_to_str_drop_rc.ail.json` — RC-stats fixture for
|
||||
`bool_to_str true` (4-byte slab, single drop)
|
||||
- `examples/bool_to_str_smoke_false.ail.json` — stdout-smoke
|
||||
fixture for `bool_to_str false` (5-byte slab)
|
||||
- `examples/str_clone_drop_rc.ail.json` — RC-stats fixture for
|
||||
`str_clone "hello"` (static-Str input → fresh heap-Str clone)
|
||||
- `examples/str_clone_cross_realisation.ail.json` — cross-
|
||||
realisation invariant: clones one heap-Str (output of
|
||||
`int_to_str 42`) AND one static-Str (literal `"abc"`), printing
|
||||
both; pins the "uniform consumer ABI" claim
|
||||
|
||||
## Stats
|
||||
|
||||
bench/orchestrator-stats/2026-05-12-iter-24.1.json
|
||||
@@ -41,3 +41,4 @@
|
||||
- 2026-05-12 — iter ctt.2: `Registry.type_def_module` re-key from `BTreeMap<String, String>` to `BTreeMap<(String, String), String>` keyed by `(owning_module, bare_name)`; new `caller_module: &str` parameter threads through `normalize_type_for_registry` + `Registry::normalize_type_for_lookup`; four `ailang-check` consumer sites (lib.rs:1640, mono.rs:121/624/1175) pass the correct module-context (env.current_module / defining_module / module_name) per the plan's Design Notes; new regression test plus three-module fixture (`ctt2_collision_{cls,lib,main}.ail.json`) exercises the cross-module bare-name collision shape that pre-ctt.2 silently overwrote; RED-failure observed as `OrphanInstance` rather than the plan's predicted `DuplicateInstance` because the pass-2 coherence check (workspace.rs:656-659) is also a bare-name consumer and fires earlier; both consumers fixed by the same re-key; plan's verbatim two-module fixture had two structural defects (two-way imports → Cycle, qualified `InstanceDef.class` → QualifiedClassName) that the implementer-phase repair handled by introducing the third cls-module; spec-intent (RED-first against bare-name collision) preserved; `cargo test --workspace` green (16 binary-test suites, ~600 tests, zero failures) → 2026-05-12-iter-ctt.2.md
|
||||
- 2026-05-12 — iter ctt.3: `KindMismatch` retire — pure deletion across 3 files (workspace.rs: variant + dispatch + helper + "dead-but-defensive" doc; main.rs: Display arm; lib.rs: 22b.1 archaeology comment). Existing test `class_param_in_applied_position_fires_canonical_form_rejection` stays green unchanged (asserts `BareCrossModuleTypeRef` on the malformed fixture — the canonical-form-validator successor path). Two adjacent textual-consistency edits ride along (validate_classdefs doc-comment "Three → Two", lib.rs archaeology comment gains a ctt.3 retirement marker). One mid-deletion mechanical fix: dispatch-loop deletion orphaned `mod_name` binding → rewrote `for (mod_name, m) in modules` to `for m in modules.values()` (same body type, mechanical). 2/2 tasks, ~600 tests green across 16 binary-test suites → 2026-05-12-iter-ctt.3.md
|
||||
- 2026-05-12 — audit-ct-tidy: milestone close (ct-tidy) — architect drift fixed inline as `ctt.tidy` (3 doc-side edits: DESIGN.md §"Class-schema diagnostics" drops the retired `KindMismatch` bullet + adds successor paragraph naming `BareCrossModuleTypeRef`; DESIGN.md §"Higher-kinded class params" rewritten to name `BareCrossModuleTypeRef` from canonical-form validation instead; roadmap.md two P2 todos struck `[x]` with forward-reference to ctt.3 and ctt.2). Bench mixed (check.py exit 1: bump_s persistence on `bench_list_sum` second consecutive sighting at +12.23% → +12.60%; two new first-sighting rc-cohort max_us latency regressions classified as noise pending next audit; the recurring 3-audit `latency.explicit_at_rc` improvement cluster narrowed to within tolerance this audit, withdrawing the ratify-pending plan from audit-eob — the meaningful shrinkage is itself attribution evidence that the cluster is noise-class not signal-class), baseline pristine for the 4th consecutive audit → 2026-05-12-audit-ct-tidy.md
|
||||
- 2026-05-12 — iter 24.1: `bool_to_str` + `str_clone` runtime + codegen wiring — 2 new C functions in `runtime/str.c` (slab-allocating heap-Str primitives via existing `str_alloc`), lockstep checker + synth.rs installs with `ret_mode: Own`, 2 IR-header declares + 2 `lower_app` arms + `is_static_callee` whitelist extension, 5 IR snapshots regen (2 declares × 5 files), 9 new tests (2 builtins-install unit + 2 IR-shape pin + 5 E2E all green), pre-existing-drift fix included (int_to_str row added to `builtins.rs::list()`). One substantive deviation: builtin-signatures registered in `uniqueness.rs::infer_module` + `linearity.rs::check_module_with_visible` (8 LOC × 2 files) so `str_clone`'s `param_modes: [Borrow]` is visible to the App-arg walker; symmetric to 23.4-prep's class-method registration; necessary for the plan's literal `frees == 3` cross-realisation assertion. Full `cargo test --workspace` 513 passed, 0 failed. `bench/compile_check.py` + `bench/cross_lang.py` green → 2026-05-12-iter-24.1.md
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
{
|
||||
"schema": "ailang/v0",
|
||||
"name": "bool_to_str_drop_rc",
|
||||
"imports": [],
|
||||
"defs": [
|
||||
{
|
||||
"kind": "fn",
|
||||
"name": "main",
|
||||
"type": {
|
||||
"k": "fn",
|
||||
"params": [],
|
||||
"ret": { "k": "con", "name": "Unit" },
|
||||
"effects": ["IO"]
|
||||
},
|
||||
"params": [],
|
||||
"doc": "Iter 24.1: pin that `bool_to_str` participates in RC discipline. Bind the heap-Str result to `s`, consume `s` once in `io/print_str`, let the binder drop at scope close. Under --alloc=rc with AILANG_RC_STATS=1 the atexit summary must report allocs == 1 && frees == 1 && live == 0 — the heap-Str slab allocated by str_alloc inside ailang_bool_to_str rides the same rc_header + ailang_rc_dec path as int_to_str.",
|
||||
"body": {
|
||||
"t": "let",
|
||||
"name": "s",
|
||||
"value": {
|
||||
"t": "app",
|
||||
"fn": { "t": "var", "name": "bool_to_str" },
|
||||
"args": [{ "t": "lit", "lit": { "kind": "bool", "value": true } }]
|
||||
},
|
||||
"body": {
|
||||
"t": "do",
|
||||
"op": "io/print_str",
|
||||
"args": [{ "t": "var", "name": "s" }]
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
{
|
||||
"schema": "ailang/v0",
|
||||
"name": "bool_to_str_smoke_false",
|
||||
"imports": [],
|
||||
"defs": [
|
||||
{
|
||||
"kind": "fn",
|
||||
"name": "main",
|
||||
"type": {
|
||||
"k": "fn",
|
||||
"params": [],
|
||||
"ret": { "k": "con", "name": "Unit" },
|
||||
"effects": ["IO"]
|
||||
},
|
||||
"params": [],
|
||||
"doc": "Iter 24.1: stdout-smoke for the false branch of bool_to_str. Stdout must be `false\\n` (puts adds the newline). Companion of bool_to_str_drop_rc.ail.json (which covers the true branch and the RC-stats invariant).",
|
||||
"body": {
|
||||
"t": "let",
|
||||
"name": "s",
|
||||
"value": {
|
||||
"t": "app",
|
||||
"fn": { "t": "var", "name": "bool_to_str" },
|
||||
"args": [{ "t": "lit", "lit": { "kind": "bool", "value": false } }]
|
||||
},
|
||||
"body": {
|
||||
"t": "do",
|
||||
"op": "io/print_str",
|
||||
"args": [{ "t": "var", "name": "s" }]
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
{
|
||||
"schema": "ailang/v0",
|
||||
"name": "str_clone_cross_realisation",
|
||||
"imports": [],
|
||||
"defs": [
|
||||
{
|
||||
"kind": "fn",
|
||||
"name": "main",
|
||||
"type": {
|
||||
"k": "fn",
|
||||
"params": [],
|
||||
"ret": { "k": "con", "name": "Unit" },
|
||||
"effects": ["IO"]
|
||||
},
|
||||
"params": [],
|
||||
"doc": "Iter 24.1: cross-realisation invariant for str_clone. Clones a heap-Str (produced by int_to_str 42) AND a static-Str (literal `abc`); both clones produce fresh heap-Str slabs that print their input bytes. Pins the uniform-consumer-ABI claim — str_clone only reads the len-field at offset 0 plus the bytes plus the NUL; it never consults the (absent for static-Str) rc_header. RC stats under --alloc=rc: allocs == 3 (int_to_str output + two str_clone outputs), frees == 3, live == 0.",
|
||||
"body": {
|
||||
"t": "let",
|
||||
"name": "src_heap",
|
||||
"value": {
|
||||
"t": "app",
|
||||
"fn": { "t": "var", "name": "int_to_str" },
|
||||
"args": [{ "t": "lit", "lit": { "kind": "int", "value": 42 } }]
|
||||
},
|
||||
"body": {
|
||||
"t": "let",
|
||||
"name": "c1",
|
||||
"value": {
|
||||
"t": "app",
|
||||
"fn": { "t": "var", "name": "str_clone" },
|
||||
"args": [{ "t": "var", "name": "src_heap" }]
|
||||
},
|
||||
"body": {
|
||||
"t": "let",
|
||||
"name": "c2",
|
||||
"value": {
|
||||
"t": "app",
|
||||
"fn": { "t": "var", "name": "str_clone" },
|
||||
"args": [{ "t": "lit", "lit": { "kind": "str", "value": "abc" } }]
|
||||
},
|
||||
"body": {
|
||||
"t": "let",
|
||||
"name": "_a",
|
||||
"value": {
|
||||
"t": "do",
|
||||
"op": "io/print_str",
|
||||
"args": [{ "t": "var", "name": "c1" }]
|
||||
},
|
||||
"body": {
|
||||
"t": "do",
|
||||
"op": "io/print_str",
|
||||
"args": [{ "t": "var", "name": "c2" }]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
{
|
||||
"schema": "ailang/v0",
|
||||
"name": "str_clone_drop_rc",
|
||||
"imports": [],
|
||||
"defs": [
|
||||
{
|
||||
"kind": "fn",
|
||||
"name": "main",
|
||||
"type": {
|
||||
"k": "fn",
|
||||
"params": [],
|
||||
"ret": { "k": "con", "name": "Unit" },
|
||||
"effects": ["IO"]
|
||||
},
|
||||
"params": [],
|
||||
"doc": "Iter 24.1: pin that `str_clone` participates in RC discipline AND emits correct bytes. Input is a static-Str literal; the clone produces a fresh heap-Str slab whose payload is byte-equal to the input. RC stats under --alloc=rc with AILANG_RC_STATS=1: allocs == 1 (the clone), frees == 1 (let-binder drop at scope close), live == 0.",
|
||||
"body": {
|
||||
"t": "let",
|
||||
"name": "s",
|
||||
"value": {
|
||||
"t": "app",
|
||||
"fn": { "t": "var", "name": "str_clone" },
|
||||
"args": [{ "t": "lit", "lit": { "kind": "str", "value": "hello" } }]
|
||||
},
|
||||
"body": {
|
||||
"t": "do",
|
||||
"op": "io/print_str",
|
||||
"args": [{ "t": "var", "name": "s" }]
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -141,3 +141,46 @@ char *ailang_float_to_str(double x) {
|
||||
payload[8 + len] = '\0';
|
||||
return payload;
|
||||
}
|
||||
|
||||
/* Convert a bool into a heap-allocated Str — "true" (4 bytes) or
|
||||
* "false" (5 bytes). Heap-allocates every call; the static-Str
|
||||
* literal alternative (used by a hypothetical `\x -> if x then "true"
|
||||
* else "false"` schema body) would require phi-of-static-Str
|
||||
* through the drop-elision pipeline which is not load-bearing-
|
||||
* ratified today (see parent spec §"Out of scope" / `bool_to_str` as
|
||||
* schema-if).
|
||||
*
|
||||
* Returns the payload pointer of a fresh heap-Str slab.
|
||||
*/
|
||||
char *ailang_bool_to_str(bool b) {
|
||||
const char *lit = b ? "true" : "false";
|
||||
uint64_t len = b ? 4 : 5;
|
||||
char *payload = str_alloc(len);
|
||||
memcpy(payload + 8, lit, len);
|
||||
payload[8 + len] = '\0';
|
||||
return payload;
|
||||
}
|
||||
|
||||
/* Clone a Str into a fresh heap-Str slab. Reads `len` from the
|
||||
* first 8 bytes of the source payload, allocates a new slab of the
|
||||
* same length via `str_alloc`, and memcpy's the payload bytes plus
|
||||
* the trailing NUL. Works uniformly on static-Str and heap-Str
|
||||
* inputs because the consumer ABI (i64 len at offset 0, bytes
|
||||
* starting at offset 8, NUL at offset 8 + len) is identical
|
||||
* between realisations (see DESIGN.md §"Str ABI"); the rc_header at
|
||||
* offset -8 — present only on heap-Str — is never consulted.
|
||||
*
|
||||
* Used by `show__Str` (parent spec milestone 24): converts a
|
||||
* borrowed Str input into an Owned heap-Str output, preserving
|
||||
* the uniform `show : forall a. Show a => (a borrow) -> Str Own`
|
||||
* signature.
|
||||
*
|
||||
* Returns the payload pointer of a fresh heap-Str slab.
|
||||
*/
|
||||
char *ailang_str_clone(const char *src) {
|
||||
uint64_t len = *(const uint64_t *)src;
|
||||
char *payload = str_alloc(len);
|
||||
memcpy(payload + 8, src + 8, len);
|
||||
payload[8 + len] = '\0';
|
||||
return payload;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user