iter str-concat: heap-Str concatenation primitive in four-site lockstep
Closes fieldtest-form-a friction finding #4. `str_concat : (borrow Str, borrow Str) -> Str` ships in the four-site-lockstep pattern established by `str_clone` / `int_to_str` / `bool_to_str` (iter 24.1). The LLM-natural Show-MyType body `(app str_concat "label=" (app int_to_str x))` now parses, checks, builds, and runs end-to-end. Sites touched (lockstep): - runtime/str.c — `ailang_str_concat(a, b)` slab-allocates and memcpys both source payloads into a new heap-Str. - ailang-check/src/builtins.rs — `env.globals.insert("str_concat", Fn { 2x Str borrow, ret Str own, effects [] })` + `list()` row + `install_str_concat_signature` unit test. - ailang-codegen/src/lib.rs — `declare ptr @ailang_str_concat(ptr, ptr)` extern + `lower_app` arm after str_clone + `is_builtin_callable` extension + IR-pin unit test `str_concat_emits_call_to_ailang_str_concat`. - examples/show_user_adt_with_label.ail (new) + crates/ail/tests/ str_concat_e2e.rs (new) — corpus fixture exercising the LLM-natural Show body shape + E2E pin asserting check + build + run produce `Item 42\n`. Lockstep collision repaired: examples/bug_unbound_in_instance_method.ail used `str_concat` as its UNBOUND name (because that was the literal fieldtester repro). Renamed to `format_label` (LLM-author-realistic helper name that will never become a builtin) and updated the pin test `crates/ail/tests/unbound_in_instance_method_pin.rs` accordingly, preserving the regression guard's intent (instance-method-body walked through unbound-var check). DESIGN.md amended: new §"Heap-Str primitives" subsection between the milestone-24 Show-backer enumeration and the existing `Primitive output goes through ...` paragraph, cataloguing all five heap-Str primitives (`int_to_str`, `bool_to_str`, `float_to_str`, `str_clone`, `str_concat`) with signatures, iter origins, and the user-visible-vs-prelude-internal distinction. Show-backer block unchanged. IR snapshots regenerated (hello, list, max3, sum, ws_main) to absorb the new `declare ptr @ailang_str_concat(ptr, ptr)` line in the unconditional extern header — same upkeep pattern as hs.4 which regenerated the same 5 snapshots for the same reason (unconditional declares dead-stripped by clang -O2 when unused). Tests: 559 + 3 = 562 green (E2E pin + builtin-signature test + IR-pin test). Zero re-loops across all 7 tasks.
This commit is contained in:
@@ -546,6 +546,7 @@ fn lower_workspace_inner(ws: &Workspace, alloc: AllocStrategy) -> Result<String>
|
||||
// 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");
|
||||
out.push_str("declare ptr @ailang_str_concat(ptr, 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
|
||||
@@ -1975,6 +1976,26 @@ impl<'a> Emitter<'a> {
|
||||
));
|
||||
return Ok((dst, "ptr".to_string()));
|
||||
}
|
||||
if name == "str_concat" {
|
||||
// Iter str-concat: lowers to the runtime C glue
|
||||
// `ailang_str_concat(ptr, ptr) -> ptr` defined in
|
||||
// `runtime/str.c`. Reads `len` from offset 0 of each
|
||||
// source Str payload and allocates a fresh heap-Str
|
||||
// slab sized for the combined bytes; works uniformly
|
||||
// on static-Str and heap-Str inputs because the
|
||||
// consumer ABI is identical. Common shape in Show
|
||||
// bodies: `(app str_concat "label=" (app int_to_str x))`.
|
||||
if args.len() != 2 {
|
||||
return Err(CodegenError::Internal("str_concat arity".into()));
|
||||
}
|
||||
let (a, _) = self.lower_term(&args[0])?;
|
||||
let (b, _) = self.lower_term(&args[1])?;
|
||||
let dst = self.fresh_ssa();
|
||||
self.body.push_str(&format!(
|
||||
" {dst} = call ptr @ailang_str_concat(ptr {a}, ptr {b})\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`).
|
||||
@@ -2190,6 +2211,7 @@ impl<'a> Emitter<'a> {
|
||||
| "int_to_str"
|
||||
| "bool_to_str"
|
||||
| "str_clone"
|
||||
| "str_concat"
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
@@ -4291,4 +4313,51 @@ mod tests {
|
||||
"expected lowering of str_clone to call @ailang_str_clone; ir was:\n{ir}"
|
||||
);
|
||||
}
|
||||
|
||||
/// Iter str-concat: a `Term::App` calling `str_concat` lowers to
|
||||
/// `call ptr @ailang_str_concat(ptr %a, ptr %b)`. Pins the new
|
||||
/// builtin's extern declaration AND the lower_app arm together
|
||||
/// (mirror of the str_clone IR pin above).
|
||||
#[test]
|
||||
fn str_concat_emits_call_to_ailang_str_concat() {
|
||||
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_concat".into() }),
|
||||
args: vec![
|
||||
Term::Lit { lit: Literal::Str { value: "x".into() } },
|
||||
Term::Lit { lit: Literal::Str { value: "y".into() } },
|
||||
],
|
||||
tail: false,
|
||||
}],
|
||||
tail: false,
|
||||
},
|
||||
suppress: vec![],
|
||||
doc: None,
|
||||
})],
|
||||
};
|
||||
let ir = emit_ir(&m).unwrap();
|
||||
assert!(
|
||||
ir.contains("declare ptr @ailang_str_concat(ptr, ptr)"),
|
||||
"expected extern declaration; ir was:\n{ir}"
|
||||
);
|
||||
assert!(
|
||||
ir.contains("call ptr @ailang_str_concat(ptr "),
|
||||
"expected lowering of str_concat to call @ailang_str_concat; ir was:\n{ir}"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user