iter raw-buf.4 rawbuf-payload-termnew-desugar (DONE, drop-call deferred to .5): RawBuf works, prints 60 (refs #7)

Ships RawBuf end-to-end as a consumer of the raw-buf.3 scope-qualified
intrinsic mechanism, plus the general Term::New construction sugar.
Working subset committed; the drop-CALL ratification (no-leak) is
re-carved to raw-buf.5 (see b49f57d) — the flat drop FUNCTION ships here,
its call-insertion needs a codegen resolution mechanism.

What ships (all green):
- raw_buf kernel-tier submodule (crates/ailang-kernel/src/raw_buf/):
  RawBuf TypeDef (param-in {Int,Float,Bool}, ctor B a) + four
  (intrinsic) ops new/get/set/size. parse_raw_buf + workspace injection
  (kernel-tier auto-imported); workspace count 4 -> 5.
- 12 scope-qualified INTERCEPTS entries RawBuf_{new,get,set,size}__{Int,
  Float,Bool} + emit fns: new allocs an @ailang_rc_alloc slab
  (8-byte i64 size header + n*width element bytes), get/set
  getelementptr+load/store at offset 8 + i*width, size loads the header.
  Mechanical on the raw-buf.3 naming + bijection machinery; bijection
  green (4 markers -> 12 entries).
- Term::New desugar (crates/ailang-core/src/desugar.rs): (new T <types>
  <values>) -> (app T.new <values>), runs before check so the
  type-scoped callee flows through the raw-buf.3 scope threading to
  RawBuf_new__T; drops the NewArg::Type (element type inferred from
  use). Both codegen Term::New deferral arms removed (replaced with
  unreachable!). Ratified by new_stubt_builds_and_runs.
- Flat intrinsic-storage drop FUNCTION @drop_raw_buf_RawBuf (single
  @ailang_rc_dec on the slab), emitted for any TypeDef whose new op is
  (intrinsic)-bodied — distinguishes RawBuf (intrinsic new) from StubT
  (real-body new -> generic ADT drop).
- E2E: raw_buf_int (-> 60), raw_buf_float (-> 4.0), raw_buf_bool
  (-> 42), raw_buf_param_in_reject (param-not-in-restricted-set).

In-scope additions beyond the literal plan (both sound, ratified):
- qualify_workspace_term now normalises a monomorphic cross-module
  type-scoped callee (StubT.new) to <home>.f; without it
  new_stubt_builds_and_runs cannot build (StubT.new is monomorphic, so
  the raw-buf.3 poly-mono path mints no symbol). Same-module + polymorphic
  callees carved out.
- diagnostic-behaviour: (new T ..) missing-new-op now surfaces
  type-scoped-member-not-found (desugar runs before check, bypassing
  synth's Term::New arm); new-arg-kind-mismatch obsoleted. Two unit
  tests updated to the new behaviour. param-in reject unaffected.

The 5 .ll snapshots gained @drop_raw_buf_RawBuf (+ partial) — injecting
raw_buf emits its drop fn into every program; benign, regenerated to the
final IR. (The plan's "snapshots stay green" assumption was wrong.)

Verification (orchestrator, this session): cargo test --workspace 676
passed / 0 failed / 2 ignored. raw_buf_int_e2e prints 60; bijection,
round-trip, new_stubt, float/bool/reject all green. The raw_buf_no_leak
test is NOT in this commit — it moves to raw-buf.5 with the drop-call
resolution that makes it pass (the slab currently leaks at scope close;
tracked, fixed next iter; milestone not released until close).
This commit is contained in:
2026-05-30 00:49:51 +02:00
parent b49f57d9c6
commit 8ac8756682
23 changed files with 879 additions and 83 deletions
+51
View File
@@ -699,6 +699,57 @@ impl<'a> Emitter<'a> {
self.body.push_str(&out);
}
/// raw-buf.4: emit the flat `drop_<m>_<T>` for an
/// intrinsic-storage TypeDef (e.g. RawBuf). The slab is
/// `[size:i64][primitive elements]` with no tag at offset 0, so
/// the generic tag-switch drop is wrong; the elements are
/// primitives carrying no recursive drops. The flat drop is just
/// the null-guarded outer rc-dec — same `entry/live/ret` shape
/// and `@ailang_rc_dec` call form as the generic drop's `join`
/// tail, minus the tag-switch.
pub(crate) fn emit_flat_intrinsic_drop_fn(&mut self, td: &TypeDef) {
let m = self.module_name;
let tname = &td.name;
let mut out = String::new();
out.push_str(&format!("define void @drop_{m}_{tname}(ptr %p) {{\n"));
out.push_str("entry:\n");
out.push_str(" %is_null = icmp eq ptr %p, null\n");
out.push_str(" br i1 %is_null, label %ret, label %live\n");
out.push_str("live:\n");
out.push_str(" call void @ailang_rc_dec(ptr %p)\n");
out.push_str(" br label %ret\n");
out.push_str("ret:\n");
out.push_str(" ret void\n");
out.push_str("}\n\n");
self.body.push_str(&out);
}
/// raw-buf.4: emit the flat `partial_drop_<m>_<T>` for an
/// intrinsic-storage TypeDef. Emitted for symbol-resolution
/// parity with the generic path (every TypeDef gets both a drop
/// and a partial-drop). No carve-out site can actually call it —
/// the sole ctor field is a primitive, never a moved-out ptr
/// field — so the `%mask` arg is ignored and the body is the same
/// flat outer rc-dec as the drop fn.
pub(crate) fn emit_flat_intrinsic_partial_drop_fn(&mut self, td: &TypeDef) {
let m = self.module_name;
let tname = &td.name;
let mut out = String::new();
out.push_str(&format!(
"define void @partial_drop_{m}_{tname}(ptr %p, i64 %mask) {{\n"
));
out.push_str("entry:\n");
out.push_str(" %is_null = icmp eq ptr %p, null\n");
out.push_str(" br i1 %is_null, label %ret, label %live\n");
out.push_str("live:\n");
out.push_str(" call void @ailang_rc_dec(ptr %p)\n");
out.push_str(" br label %ret\n");
out.push_str("ret:\n");
out.push_str(" ret void\n");
out.push_str("}\n\n");
self.body.push_str(&out);
}
/// resolve the `partial_drop_<owner>_<T>`
/// symbol for a type, parallel to the existing per-type drop
/// dispatch in `field_drop_call`. Returns `None` for non-ADT
+293
View File
@@ -185,6 +185,97 @@ pub(crate) static INTERCEPTS: &[Intercept] = &[
wants_alwaysinline: false,
emit: emit_stubt_peek_float,
},
// raw-buf.4: the 12 scope-qualified RawBuf ops. Symbols are
// `RawBuf_{new,get,set,size}__{Int,Float,Bool}` (raw-buf.3
// scope-qualified mono mangling). Slab layout:
// `[ size:i64 @0 ][ elem_0 @8 ][ elem_1 @8+w ]…`; element widths
// Int/Float = 8, Bool = 1. `own`/`borrow (con RawBuf T)` both
// lower to `ptr`. The header is always i64, so `new`/`size` are
// element-type-independent; `get`/`set` carry the element type.
Intercept {
name: "RawBuf_new__Int",
expected_params: &["i64"],
expected_ret: "ptr",
wants_alwaysinline: false,
emit: emit_rawbuf_new_int,
},
Intercept {
name: "RawBuf_get__Int",
expected_params: &["ptr", "i64"],
expected_ret: "i64",
wants_alwaysinline: false,
emit: emit_rawbuf_get_int,
},
Intercept {
name: "RawBuf_set__Int",
expected_params: &["ptr", "i64", "i64"],
expected_ret: "ptr",
wants_alwaysinline: false,
emit: emit_rawbuf_set_int,
},
Intercept {
name: "RawBuf_size__Int",
expected_params: &["ptr"],
expected_ret: "i64",
wants_alwaysinline: false,
emit: emit_rawbuf_size_int,
},
Intercept {
name: "RawBuf_new__Float",
expected_params: &["i64"],
expected_ret: "ptr",
wants_alwaysinline: false,
emit: emit_rawbuf_new_float,
},
Intercept {
name: "RawBuf_get__Float",
expected_params: &["ptr", "i64"],
expected_ret: "double",
wants_alwaysinline: false,
emit: emit_rawbuf_get_float,
},
Intercept {
name: "RawBuf_set__Float",
expected_params: &["ptr", "i64", "double"],
expected_ret: "ptr",
wants_alwaysinline: false,
emit: emit_rawbuf_set_float,
},
Intercept {
name: "RawBuf_size__Float",
expected_params: &["ptr"],
expected_ret: "i64",
wants_alwaysinline: false,
emit: emit_rawbuf_size_float,
},
Intercept {
name: "RawBuf_new__Bool",
expected_params: &["i64"],
expected_ret: "ptr",
wants_alwaysinline: false,
emit: emit_rawbuf_new_bool,
},
Intercept {
name: "RawBuf_get__Bool",
expected_params: &["ptr", "i64"],
expected_ret: "i1",
wants_alwaysinline: false,
emit: emit_rawbuf_get_bool,
},
Intercept {
name: "RawBuf_set__Bool",
expected_params: &["ptr", "i64", "i1"],
expected_ret: "ptr",
wants_alwaysinline: false,
emit: emit_rawbuf_set_bool,
},
Intercept {
name: "RawBuf_size__Bool",
expected_params: &["ptr"],
expected_ret: "i64",
wants_alwaysinline: false,
emit: emit_rawbuf_size_bool,
},
];
pub(crate) fn lookup(name: &str) -> Option<&'static Intercept> {
@@ -499,6 +590,207 @@ pub(crate) fn emit_stubt_peek_float(emitter: &mut Emitter<'_>) -> Result<()> {
Ok(())
}
// ---------------------------------------------------------------
// raw-buf.4: RawBuf op emits over an `@ailang_rc_alloc` slab.
// Slab layout `[ size:i64 @0 ][ elem_0 @8 ][ elem_1 @8+w ]…`. The
// rc-header is auto-prepended by `@ailang_rc_alloc` (it returns the
// payload ptr); the i64 size header lives at payload offset 0, the
// elements follow at offset 8. Element widths: Int/Float = 8, Bool
// = 1. `new`/`size` are byte-identical across element types (the
// header is always i64); `get`/`set` carry the element load/store
// type and offset width. Intercepts run only under `--alloc=rc`, so
// `@ailang_rc_alloc` is hardcoded (not `alloc.fn_name()`).
// ---------------------------------------------------------------
// --- Int variants (element type i64, width 8) ---
pub(crate) fn emit_rawbuf_new_int(emitter: &mut Emitter<'_>) -> Result<()> {
let n = emitter.locals.len();
let cap = emitter.locals[n - 1].1.clone(); // i64 capacity
let elems_bytes = emitter.fresh_ssa();
let total = emitter.fresh_ssa();
let slab = emitter.fresh_ssa();
emitter.body.push_str(&format!(" {elems_bytes} = mul i64 {cap}, 8\n"));
emitter.body.push_str(&format!(" {total} = add i64 {elems_bytes}, 8\n"));
emitter
.body
.push_str(&format!(" {slab} = call ptr @ailang_rc_alloc(i64 {total})\n"));
emitter.body.push_str(&format!(" store i64 {cap}, ptr {slab}\n"));
emitter.body.push_str(&format!(" ret ptr {slab}\n"));
emitter.body.push_str("}\n\n");
emitter.block_terminated = true;
Ok(())
}
pub(crate) fn emit_rawbuf_get_int(emitter: &mut Emitter<'_>) -> Result<()> {
let n = emitter.locals.len();
let b = emitter.locals[n - 2].1.clone();
let i = emitter.locals[n - 1].1.clone();
let off = emitter.fresh_ssa();
let byteoff = emitter.fresh_ssa();
let ptr = emitter.fresh_ssa();
let v = emitter.fresh_ssa();
emitter.body.push_str(&format!(" {off} = mul i64 {i}, 8\n"));
emitter.body.push_str(&format!(" {byteoff} = add i64 {off}, 8\n"));
emitter.body.push_str(&format!(
" {ptr} = getelementptr inbounds i8, ptr {b}, i64 {byteoff}\n"
));
emitter.body.push_str(&format!(" {v} = load i64, ptr {ptr}\n"));
emitter.body.push_str(&format!(" ret i64 {v}\n"));
emitter.body.push_str("}\n\n");
emitter.block_terminated = true;
Ok(())
}
pub(crate) fn emit_rawbuf_set_int(emitter: &mut Emitter<'_>) -> Result<()> {
let n = emitter.locals.len();
let b = emitter.locals[n - 3].1.clone();
let i = emitter.locals[n - 2].1.clone();
let v = emitter.locals[n - 1].1.clone();
let off = emitter.fresh_ssa();
let byteoff = emitter.fresh_ssa();
let ptr = emitter.fresh_ssa();
emitter.body.push_str(&format!(" {off} = mul i64 {i}, 8\n"));
emitter.body.push_str(&format!(" {byteoff} = add i64 {off}, 8\n"));
emitter.body.push_str(&format!(
" {ptr} = getelementptr inbounds i8, ptr {b}, i64 {byteoff}\n"
));
emitter.body.push_str(&format!(" store i64 {v}, ptr {ptr}\n"));
emitter.body.push_str(&format!(" ret ptr {b}\n"));
emitter.body.push_str("}\n\n");
emitter.block_terminated = true;
Ok(())
}
pub(crate) fn emit_rawbuf_size_int(emitter: &mut Emitter<'_>) -> Result<()> {
let n = emitter.locals.len();
let b = emitter.locals[n - 1].1.clone();
let sz = emitter.fresh_ssa();
emitter.body.push_str(&format!(" {sz} = load i64, ptr {b}\n"));
emitter.body.push_str(&format!(" ret i64 {sz}\n"));
emitter.body.push_str("}\n\n");
emitter.block_terminated = true;
Ok(())
}
// --- Float variants (element type double, width 8) ---
pub(crate) fn emit_rawbuf_new_float(emitter: &mut Emitter<'_>) -> Result<()> {
// Header always i64; element width 8 — byte-identical to Int's new.
emit_rawbuf_new_int(emitter)
}
pub(crate) fn emit_rawbuf_get_float(emitter: &mut Emitter<'_>) -> Result<()> {
let n = emitter.locals.len();
let b = emitter.locals[n - 2].1.clone();
let i = emitter.locals[n - 1].1.clone();
let off = emitter.fresh_ssa();
let byteoff = emitter.fresh_ssa();
let ptr = emitter.fresh_ssa();
let v = emitter.fresh_ssa();
emitter.body.push_str(&format!(" {off} = mul i64 {i}, 8\n"));
emitter.body.push_str(&format!(" {byteoff} = add i64 {off}, 8\n"));
emitter.body.push_str(&format!(
" {ptr} = getelementptr inbounds i8, ptr {b}, i64 {byteoff}\n"
));
emitter.body.push_str(&format!(" {v} = load double, ptr {ptr}\n"));
emitter.body.push_str(&format!(" ret double {v}\n"));
emitter.body.push_str("}\n\n");
emitter.block_terminated = true;
Ok(())
}
pub(crate) fn emit_rawbuf_set_float(emitter: &mut Emitter<'_>) -> Result<()> {
let n = emitter.locals.len();
let b = emitter.locals[n - 3].1.clone();
let i = emitter.locals[n - 2].1.clone();
let v = emitter.locals[n - 1].1.clone();
let off = emitter.fresh_ssa();
let byteoff = emitter.fresh_ssa();
let ptr = emitter.fresh_ssa();
emitter.body.push_str(&format!(" {off} = mul i64 {i}, 8\n"));
emitter.body.push_str(&format!(" {byteoff} = add i64 {off}, 8\n"));
emitter.body.push_str(&format!(
" {ptr} = getelementptr inbounds i8, ptr {b}, i64 {byteoff}\n"
));
emitter.body.push_str(&format!(" store double {v}, ptr {ptr}\n"));
emitter.body.push_str(&format!(" ret ptr {b}\n"));
emitter.body.push_str("}\n\n");
emitter.block_terminated = true;
Ok(())
}
pub(crate) fn emit_rawbuf_size_float(emitter: &mut Emitter<'_>) -> Result<()> {
// Header always i64 — byte-identical to Int's size.
emit_rawbuf_size_int(emitter)
}
// --- Bool variants (element type i1, width 1) ---
pub(crate) fn emit_rawbuf_new_bool(emitter: &mut Emitter<'_>) -> Result<()> {
let n = emitter.locals.len();
let cap = emitter.locals[n - 1].1.clone(); // i64 capacity
let elems_bytes = emitter.fresh_ssa();
let total = emitter.fresh_ssa();
let slab = emitter.fresh_ssa();
// element width 1 byte for Bool.
emitter.body.push_str(&format!(" {elems_bytes} = mul i64 {cap}, 1\n"));
emitter.body.push_str(&format!(" {total} = add i64 {elems_bytes}, 8\n"));
emitter
.body
.push_str(&format!(" {slab} = call ptr @ailang_rc_alloc(i64 {total})\n"));
emitter.body.push_str(&format!(" store i64 {cap}, ptr {slab}\n"));
emitter.body.push_str(&format!(" ret ptr {slab}\n"));
emitter.body.push_str("}\n\n");
emitter.block_terminated = true;
Ok(())
}
pub(crate) fn emit_rawbuf_get_bool(emitter: &mut Emitter<'_>) -> Result<()> {
let n = emitter.locals.len();
let b = emitter.locals[n - 2].1.clone();
let i = emitter.locals[n - 1].1.clone();
let off = emitter.fresh_ssa();
let byteoff = emitter.fresh_ssa();
let ptr = emitter.fresh_ssa();
let v = emitter.fresh_ssa();
emitter.body.push_str(&format!(" {off} = mul i64 {i}, 1\n"));
emitter.body.push_str(&format!(" {byteoff} = add i64 {off}, 8\n"));
emitter.body.push_str(&format!(
" {ptr} = getelementptr inbounds i8, ptr {b}, i64 {byteoff}\n"
));
emitter.body.push_str(&format!(" {v} = load i1, ptr {ptr}\n"));
emitter.body.push_str(&format!(" ret i1 {v}\n"));
emitter.body.push_str("}\n\n");
emitter.block_terminated = true;
Ok(())
}
pub(crate) fn emit_rawbuf_set_bool(emitter: &mut Emitter<'_>) -> Result<()> {
let n = emitter.locals.len();
let b = emitter.locals[n - 3].1.clone();
let i = emitter.locals[n - 2].1.clone();
let v = emitter.locals[n - 1].1.clone();
let off = emitter.fresh_ssa();
let byteoff = emitter.fresh_ssa();
let ptr = emitter.fresh_ssa();
emitter.body.push_str(&format!(" {off} = mul i64 {i}, 1\n"));
emitter.body.push_str(&format!(" {byteoff} = add i64 {off}, 8\n"));
emitter.body.push_str(&format!(
" {ptr} = getelementptr inbounds i8, ptr {b}, i64 {byteoff}\n"
));
emitter.body.push_str(&format!(" store i1 {v}, ptr {ptr}\n"));
emitter.body.push_str(&format!(" ret ptr {b}\n"));
emitter.body.push_str("}\n\n");
emitter.block_terminated = true;
Ok(())
}
pub(crate) fn emit_rawbuf_size_bool(emitter: &mut Emitter<'_>) -> Result<()> {
// Header always i64 — byte-identical to Int's size.
emit_rawbuf_size_int(emitter)
}
#[cfg(test)]
mod tests {
use super::{lookup, INTERCEPTS};
@@ -523,6 +815,7 @@ mod tests {
for module in [
ailang_surface::parse_prelude(),
ailang_surface::parse_kernel_stub(),
ailang_surface::parse_raw_buf(),
] {
for def in &module.defs {
match def {
+62 -22
View File
@@ -694,6 +694,29 @@ fn llvm_scalar(t: &Type) -> &'static str {
}
}
/// raw-buf.4: does `f`'s declared return type construct the TypeDef
/// named `td_name`? Used by the drop-dispatch loop to recognise an
/// intrinsic-storage `new` op (its return is `(own (con T …))`).
/// Peels `Forall` → `Fn.ret`, then matches the result `Type::Con`
/// name against `td_name` (the `own`/`borrow` mode lives in
/// `ret_mode`, not in the type, so it needs no peeling). Accepts both
/// the bare same-module name and a `<module>.<T>` qualified spelling.
fn fn_returns_type(f: &FnDef, td_name: &str) -> bool {
let mut ty = &f.ty;
if let Type::Forall { body, .. } = ty {
ty = body;
}
let Type::Fn { ret, .. } = ty else {
return false;
};
match ret.as_ref() {
Type::Con { name, .. } => {
name == td_name || name.rsplit('.').next() == Some(td_name)
}
_ => false,
}
}
fn main_is_void(t: &Type) -> bool {
match t {
Type::Fn { params, ret, .. } => {
@@ -1070,6 +1093,32 @@ impl<'a> Emitter<'a> {
if matches!(self.alloc, AllocStrategy::Rc) {
for def in &self.module.defs {
if let Def::Type(td) = def {
// raw-buf.4: a TypeDef whose construction is
// compiler-supplied (its `new` op is an (intrinsic),
// not a real term-ctor body) has a flat slab layout
// [size:i64][elements], NOT a tagged-ADT layout. The
// generic drop loads a tag at offset 0 and switches
// on it — but offset 0 here is the size, so the
// generic tag-switch drop is wrong. Emit a flat
// drop instead: a single rc-dec on the slab pointer
// (primitive elements carry no recursive drops).
// This distinguishes RawBuf (intrinsic `new`) from
// StubT (real-body `new` → generic ADT drop), where
// a param_in heuristic would misclassify StubT
// (also param_in). A matching flat partial-drop is
// emitted for symbol-resolution parity with the
// generic path (no carve-out site can actually
// target it — the sole field is a primitive).
if self.module.defs.iter().any(|d| {
matches!(d, Def::Fn(f)
if f.name == "new"
&& matches!(f.body, Term::Intrinsic)
&& fn_returns_type(f, &td.name))
}) {
self.emit_flat_intrinsic_drop_fn(td);
self.emit_flat_intrinsic_partial_drop_fn(td);
continue;
}
if td.drop_iterative {
// opt-in iterative-drop body. The
// recursive cascade overflows the C stack on
@@ -2083,18 +2132,13 @@ impl<'a> Emitter<'a> {
self.block_terminated = true;
Ok(("0".into(), "i8".into()))
}
// prep.2 (kernel-extension-mechanics): Term::New has no
// direct codegen path in this milestone — typecheck
// accepts it via synth's elaboration, but lowering to LLVM
// IR requires a desugar pass that resolves the `new` def
// and rewrites the site as a Term::App. That desugar lands
// in the raw-buf milestone. Until then, codegen of
// Term::New is an internal error: a workspace that types-
// checks should not reach codegen with a surviving Term::New.
Term::New { .. } => Err(CodegenError::Internal(
"Term::New requires the type's `new` def to be desugared first; \
codegen does not yet support Term::New directly — milestone raw-buf".into(),
)),
// raw-buf.4: the Term::New desugar (desugar.rs) rewrites
// every `(new T …)` to `(app T.new …)` before check and
// codegen, so no Term::New survives to lowering. The arm is
// kept only for match exhaustiveness.
Term::New { .. } => {
unreachable!("Term::New is desugared to (app T.new …) before codegen — raw-buf.4")
}
Term::Intrinsic => Err(CodegenError::Internal(
"Term::Intrinsic must be consumed by the intercept route in fn-body \
emission, not lowered as an expression; reaching lower_term means an \
@@ -3287,16 +3331,12 @@ impl<'a> Emitter<'a> {
// value at its own position).
Term::Loop { body, .. } => self.synth_with_extras(body, extras),
Term::Recur { .. } => Ok(Type::unit()),
// prep.2 (kernel-extension-mechanics): Term::New has no
// codegen path in this milestone (see lower_term's arm).
// synth_with_extras would only fire if codegen reaches a
// surviving Term::New as a callee or sub-expression — same
// BUG class as a surviving LetRec — so an internal error
// here mirrors lower_term's stance.
Term::New { .. } => Err(CodegenError::Internal(
"Term::New cannot be synthed at codegen — must be desugared first; \
codegen support lands in the raw-buf milestone".into(),
)),
// raw-buf.4: Term::New is desugared to (app T.new …) before
// codegen (see lower_term's arm), so it never reaches the
// synth path. The arm is kept only for match exhaustiveness.
Term::New { .. } => {
unreachable!("Term::New is desugared to (app T.new …) before codegen — raw-buf.4")
}
Term::Intrinsic => Err(CodegenError::Internal(
"Term::Intrinsic cannot be synthed at codegen — an intrinsic body is \
signature-only and routed through the intercept registry, never an expression"