From 8ac875668203ddba39a6fdaf0f192d7855c99a9f Mon Sep 17 00:00:00 2001 From: Brummel Date: Sat, 30 May 2026 00:49:51 +0200 Subject: [PATCH] iter raw-buf.4 rawbuf-payload-termnew-desugar (DONE, drop-call deferred to .5): RawBuf works, prints 60 (refs #7) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 ) -> (app T.new ), 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 .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). --- crates/ail/tests/e2e.rs | 74 ++++- crates/ail/tests/snapshots/hello.ll | 22 ++ crates/ail/tests/snapshots/list.ll | 22 ++ crates/ail/tests/snapshots/max3.ll | 22 ++ crates/ail/tests/snapshots/sum.ll | 22 ++ crates/ail/tests/snapshots/ws_main.ll | 22 ++ crates/ailang-check/src/lib.rs | 165 +++++++--- crates/ailang-codegen/src/drop.rs | 51 +++ crates/ailang-codegen/src/intercepts.rs | 293 ++++++++++++++++++ crates/ailang-codegen/src/lib.rs | 84 +++-- crates/ailang-core/src/desugar.rs | 32 +- .../ailang-core/tests/design_schema_drift.rs | 20 ++ crates/ailang-core/tests/workspace_pin.rs | 12 +- crates/ailang-kernel/src/lib.rs | 2 + crates/ailang-kernel/src/raw_buf/mod.rs | 10 + crates/ailang-kernel/src/raw_buf/source.ail | 26 ++ crates/ailang-surface/src/lib.rs | 4 +- crates/ailang-surface/src/loader.rs | 25 ++ examples/new_stubt_smoke.ail | 13 + examples/raw_buf_bool.ail | 10 + examples/raw_buf_float.ail | 12 + examples/raw_buf_int.ail | 14 + examples/raw_buf_reject_str.ail | 5 + 23 files changed, 879 insertions(+), 83 deletions(-) create mode 100644 crates/ailang-kernel/src/raw_buf/mod.rs create mode 100644 crates/ailang-kernel/src/raw_buf/source.ail create mode 100644 examples/new_stubt_smoke.ail create mode 100644 examples/raw_buf_bool.ail create mode 100644 examples/raw_buf_float.ail create mode 100644 examples/raw_buf_int.ail create mode 100644 examples/raw_buf_reject_str.ail diff --git a/crates/ail/tests/e2e.rs b/crates/ail/tests/e2e.rs index 3f84c3c..3143b89 100644 --- a/crates/ail/tests/e2e.rs +++ b/crates/ail/tests/e2e.rs @@ -95,6 +95,17 @@ fn answer_intrinsic_builds_and_runs_printing_42() { assert_eq!(out.trim(), "42"); } +/// raw-buf.4: the general `Term::New` desugar. `(new StubT 42)` +/// desugars to `(app StubT.new 42)` (StubT.new has a real +/// `(term-ctor StubT Stub x)` body, so it builds independent of +/// RawBuf), the Int is matched back out and printed. Ratifies the +/// desugar + the removal of both codegen `Term::New` deferral arms. +#[test] +fn new_stubt_builds_and_runs() { + let out = build_and_run("new_stubt_smoke.ail"); + assert_eq!(out.trim(), "42"); +} + #[test] fn loop_recur_sum_to_runs_to_value() { let stdout = build_and_run("loop_sum_to_run.ail"); @@ -864,10 +875,11 @@ fn workspace_lists_imported_modules() { let modules = v["modules"].as_array().expect("modules must be array"); // the loader auto-injects every built-in kernel-tier module - // (`prelude` and, since prep.3 of the kernel-extension-mechanics - // milestone, the ratifying `kernel_stub`), so the count is the - // user's two modules plus the two built-ins. - assert_eq!(modules.len(), 4, "expected 4 modules: {stdout}"); + // (`prelude`; since prep.3 of the kernel-extension-mechanics + // milestone, the ratifying `kernel_stub`; and since raw-buf.4 the + // first real-payload kernel extension `raw_buf`), so the count is + // the user's two modules plus the three built-ins. + assert_eq!(modules.len(), 5, "expected 5 modules: {stdout}"); let names: Vec<&str> = modules .iter() @@ -877,6 +889,7 @@ fn workspace_lists_imported_modules() { assert!(names.contains(&"ws_lib"), "ws_lib missing: {names:?}"); assert!(names.contains(&"prelude"), "prelude missing: {names:?}"); assert!(names.contains(&"kernel_stub"), "kernel_stub missing: {names:?}"); + assert!(names.contains(&"raw_buf"), "raw_buf missing: {names:?}"); } /// Guards Iter 5b: `ail check examples/ws_main.ail.json --json` must @@ -2210,6 +2223,59 @@ fn build_and_run_with_rc_stats(example: &str) -> (String, u64, u64, i64) { ) } +/// raw-buf.4 worked consumer: a 3-slot Int RawBuf filled 10/20/30, +/// summed and printed. The end-to-end acceptance criterion — parse → +/// check → desugar `(new RawBuf …)` → mono `RawBuf_{new,set,get}__Int` +/// → intercept-routed codegen (alloc/store/load over the slab) → +/// native `60`. +#[test] +fn raw_buf_int_e2e() { + let out = build_and_run("raw_buf_int.ail"); + assert_eq!(out.trim(), "60"); +} + +/// raw-buf.4 Float variant: a 2-slot Float RawBuf (1.5 + 2.5), +/// exercising the `double` load/store emits. Prints `4.0` (the `%g` +/// whole-double `.0` fallback). +#[test] +fn raw_buf_float_e2e() { + let out = build_and_run("raw_buf_float.ail"); + assert_eq!(out.trim(), "4.0"); +} + +/// raw-buf.4 Bool variant: a 1-slot Bool RawBuf storing `true`, read +/// back and discriminated to an Int. Catches the `i1` store/load +/// syntax and the width-1 element offset. +#[test] +fn raw_buf_bool_e2e() { + let out = build_and_run("raw_buf_bool.ail"); + assert_eq!(out.trim(), "42"); +} + +/// raw-buf.4 must-fail: `(con RawBuf (con Str))` violates the +/// `param-in (a Int Float Bool)` restriction. `ail check` exits +/// non-zero with the prep.3 `param-not-in-restricted-set` diagnostic. +#[test] +fn raw_buf_param_in_reject_e2e() { + let manifest_dir = env!("CARGO_MANIFEST_DIR"); + let workspace = Path::new(manifest_dir).parent().unwrap().parent().unwrap(); + let src = workspace.join("examples").join("raw_buf_reject_str.ail"); + let output = Command::new(ail_bin()) + .args(["check", src.to_str().unwrap()]) + .output() + .expect("ail check failed to run"); + assert!( + !output.status.success(), + "ail check must reject RawBuf" + ); + let stderr = String::from_utf8(output.stderr).expect("stderr utf8"); + assert!( + stderr.contains("param-not-in-restricted-set"), + "expected param-not-in-restricted-set, stderr: {stderr}" + ); +} + + /// explicit-mode tail-recursive list-sum must /// not leak the LCons outer cells. /// diff --git a/crates/ail/tests/snapshots/hello.ll b/crates/ail/tests/snapshots/hello.ll index f35c335..8c33e30 100644 --- a/crates/ail/tests/snapshots/hello.ll +++ b/crates/ail/tests/snapshots/hello.ll @@ -249,6 +249,28 @@ ret: ret void } +define void @drop_raw_buf_RawBuf(ptr %p) { +entry: + %is_null = icmp eq ptr %p, null + br i1 %is_null, label %ret, label %live +live: + call void @ailang_rc_dec(ptr %p) + br label %ret +ret: + ret void +} + +define void @partial_drop_raw_buf_RawBuf(ptr %p, i64 %mask) { +entry: + %is_null = icmp eq ptr %p, null + br i1 %is_null, label %ret, label %live +live: + call void @ailang_rc_dec(ptr %p) + br label %ret +ret: + ret void +} + define i32 @main() { call i8 @ail_hello_main() diff --git a/crates/ail/tests/snapshots/list.ll b/crates/ail/tests/snapshots/list.ll index e5ffd73..50f41f7 100644 --- a/crates/ail/tests/snapshots/list.ll +++ b/crates/ail/tests/snapshots/list.ll @@ -385,6 +385,28 @@ ret: ret void } +define void @drop_raw_buf_RawBuf(ptr %p) { +entry: + %is_null = icmp eq ptr %p, null + br i1 %is_null, label %ret, label %live +live: + call void @ailang_rc_dec(ptr %p) + br label %ret +ret: + ret void +} + +define void @partial_drop_raw_buf_RawBuf(ptr %p, i64 %mask) { +entry: + %is_null = icmp eq ptr %p, null + br i1 %is_null, label %ret, label %live +live: + call void @ailang_rc_dec(ptr %p) + br label %ret +ret: + ret void +} + define i32 @main() { call i8 @ail_list_main() diff --git a/crates/ail/tests/snapshots/max3.ll b/crates/ail/tests/snapshots/max3.ll index 558805f..97038cd 100644 --- a/crates/ail/tests/snapshots/max3.ll +++ b/crates/ail/tests/snapshots/max3.ll @@ -373,6 +373,28 @@ ret: ret void } +define void @drop_raw_buf_RawBuf(ptr %p) { +entry: + %is_null = icmp eq ptr %p, null + br i1 %is_null, label %ret, label %live +live: + call void @ailang_rc_dec(ptr %p) + br label %ret +ret: + ret void +} + +define void @partial_drop_raw_buf_RawBuf(ptr %p, i64 %mask) { +entry: + %is_null = icmp eq ptr %p, null + br i1 %is_null, label %ret, label %live +live: + call void @ailang_rc_dec(ptr %p) + br label %ret +ret: + ret void +} + define i32 @main() { call i8 @ail_max3_main() diff --git a/crates/ail/tests/snapshots/sum.ll b/crates/ail/tests/snapshots/sum.ll index 2007cde..b42364d 100644 --- a/crates/ail/tests/snapshots/sum.ll +++ b/crates/ail/tests/snapshots/sum.ll @@ -277,6 +277,28 @@ ret: ret void } +define void @drop_raw_buf_RawBuf(ptr %p) { +entry: + %is_null = icmp eq ptr %p, null + br i1 %is_null, label %ret, label %live +live: + call void @ailang_rc_dec(ptr %p) + br label %ret +ret: + ret void +} + +define void @partial_drop_raw_buf_RawBuf(ptr %p, i64 %mask) { +entry: + %is_null = icmp eq ptr %p, null + br i1 %is_null, label %ret, label %live +live: + call void @ailang_rc_dec(ptr %p) + br label %ret +ret: + ret void +} + define i64 @ail_sum_sum(i64 %arg_n) { entry: %v1 = call i1 @ail_prelude_eq__Int(i64 %arg_n, i64 0) diff --git a/crates/ail/tests/snapshots/ws_main.ll b/crates/ail/tests/snapshots/ws_main.ll index 6fb851c..7170138 100644 --- a/crates/ail/tests/snapshots/ws_main.ll +++ b/crates/ail/tests/snapshots/ws_main.ll @@ -264,6 +264,28 @@ ret: ret void } +define void @drop_raw_buf_RawBuf(ptr %p) { +entry: + %is_null = icmp eq ptr %p, null + br i1 %is_null, label %ret, label %live +live: + call void @ailang_rc_dec(ptr %p) + br label %ret +ret: + ret void +} + +define void @partial_drop_raw_buf_RawBuf(ptr %p, i64 %mask) { +entry: + %is_null = icmp eq ptr %p, null + br i1 %is_null, label %ret, label %live +live: + call void @ailang_rc_dec(ptr %p) + br label %ret +ret: + ret void +} + define i64 @ail_ws_lib_add(i64 %arg_a, i64 %arg_b) { entry: %v1 = add i64 %arg_a, %arg_b diff --git a/crates/ailang-check/src/lib.rs b/crates/ailang-check/src/lib.rs index 77703ab..34b1246 100644 --- a/crates/ailang-check/src/lib.rs +++ b/crates/ailang-check/src/lib.rs @@ -1081,6 +1081,29 @@ pub fn prepare_workspace_for_check(ws: &Workspace) -> Workspace { (name.clone(), tys) }) .collect(); + // raw-buf.4: per-module set of *monomorphic* (non-`Forall`) + // top-level fn names. A type-scoped call `T.f` to a monomorphic + // op (e.g. the `kernel_stub.StubT.new` produced by the Term::New + // desugar) is normalised to the module-qualified `.f` form + // by `qualify_workspace_term` — codegen recognises that spelling + // directly. Polymorphic type-scoped ops stay in the `T.f` form so + // monomorphisation can mint their scope-qualified `T_f__` + // symbols (the raw-buf.3 mechanism RawBuf's ops use). + let workspace_mono_fns: BTreeMap> = ws + .modules + .iter() + .map(|(name, m)| { + let fns: BTreeSet = m + .defs + .iter() + .filter_map(|def| match def { + Def::Fn(f) if !matches!(f.ty, Type::Forall { .. }) => Some(f.name.clone()), + _ => None, + }) + .collect(); + (name.clone(), fns) + }) + .collect(); Workspace { entry: ws.entry.clone(), modules: ws @@ -1094,7 +1117,12 @@ pub fn prepare_workspace_for_check(ws: &Workspace) -> Workspace { .unwrap_or_default(); ( k.clone(), - qualify_workspace_module(m, &own_local_types, &workspace_types_pre), + qualify_workspace_module( + m, + &own_local_types, + &workspace_types_pre, + &workspace_mono_fns, + ), ) }) .collect(), @@ -4551,16 +4579,17 @@ pub fn qualify_workspace_module( mut m: Module, own_local_types: &IndexMap, module_types: &BTreeMap>, + module_mono_fns: &BTreeMap>, ) -> Module { for def in &mut m.defs { match def { Def::Fn(f) => { f.ty = qualify_workspace_types(&f.ty, own_local_types, module_types); - qualify_workspace_term(&mut f.body, own_local_types, module_types); + qualify_workspace_term(&mut f.body, own_local_types, module_types, module_mono_fns); } Def::Const(c) => { c.ty = qualify_workspace_types(&c.ty, own_local_types, module_types); - qualify_workspace_term(&mut c.value, own_local_types, module_types); + qualify_workspace_term(&mut c.value, own_local_types, module_types, module_mono_fns); } Def::Type(_) | Def::Class(_) | Def::Instance(_) => { // TypeDef.ctors carry field types in the owner's local @@ -4577,32 +4606,70 @@ pub(crate) fn qualify_workspace_term( t: &mut Term, own_local_types: &IndexMap, module_types: &BTreeMap>, + module_mono_fns: &BTreeMap>, ) { match t { - Term::Lit { .. } | Term::Var { .. } | Term::Recur { .. } => {} + Term::Lit { .. } | Term::Recur { .. } => {} + Term::Var { name } => { + // raw-buf.4: normalise a *cross-module* type-scoped call + // `T.f` to a *monomorphic* op into the module-qualified + // `.f` form. The Term::New desugar emits + // `(app T.new …)` for every type; for a cross-module + // monomorphic `new` (e.g. kernel_stub's StubT.new) nothing + // downstream rewrites the `T.new` callee, so codegen would + // hit `unknown variable`. Three carve-outs: + // - a same-module type (`own_local_types`) keeps `T.f` — + // synth's TypeDef-first ladder resolves it directly and + // rewriting to the bare module self-name breaks the + // receiver resolution (`type-scoped-receiver-not-a-type`); + // - a *polymorphic* op keeps `T.f` so the raw-buf.3 + // monomorphisation mechanism mints its scope-qualified + // `T_f__` symbol from that spelling (RawBuf's ops); + // - any non-TypeDef prefix is untouched (ordinary module + // or class-method dotted names). + if name.matches('.').count() == 1 { + let (prefix, suffix) = name.split_once('.').expect("checked"); + if !own_local_types.contains_key(prefix) { + if let Some(home) = module_types.iter().find_map(|(m, types)| { + if types.contains_key(prefix) { + Some(m.clone()) + } else { + None + } + }) { + let is_mono = module_mono_fns + .get(&home) + .is_some_and(|fns| fns.contains(suffix)); + if is_mono { + *name = format!("{home}.{suffix}"); + } + } + } + } + } Term::App { callee, args, .. } => { - qualify_workspace_term(callee, own_local_types, module_types); + qualify_workspace_term(callee, own_local_types, module_types, module_mono_fns); for a in args { - qualify_workspace_term(a, own_local_types, module_types); + qualify_workspace_term(a, own_local_types, module_types, module_mono_fns); } } Term::Let { value, body, .. } => { - qualify_workspace_term(value, own_local_types, module_types); - qualify_workspace_term(body, own_local_types, module_types); + qualify_workspace_term(value, own_local_types, module_types, module_mono_fns); + qualify_workspace_term(body, own_local_types, module_types, module_mono_fns); } Term::LetRec { ty, body, in_term, .. } => { *ty = qualify_workspace_types(ty, own_local_types, module_types); - qualify_workspace_term(body, own_local_types, module_types); - qualify_workspace_term(in_term, own_local_types, module_types); + qualify_workspace_term(body, own_local_types, module_types, module_mono_fns); + qualify_workspace_term(in_term, own_local_types, module_types, module_mono_fns); } Term::If { cond, then, else_ } => { - qualify_workspace_term(cond, own_local_types, module_types); - qualify_workspace_term(then, own_local_types, module_types); - qualify_workspace_term(else_, own_local_types, module_types); + qualify_workspace_term(cond, own_local_types, module_types, module_mono_fns); + qualify_workspace_term(then, own_local_types, module_types, module_mono_fns); + qualify_workspace_term(else_, own_local_types, module_types, module_mono_fns); } Term::Do { args, .. } => { for a in args { - qualify_workspace_term(a, own_local_types, module_types); + qualify_workspace_term(a, own_local_types, module_types, module_mono_fns); } } Term::Ctor { type_name, args, .. } => { @@ -4621,13 +4688,13 @@ pub(crate) fn qualify_workspace_term( } } for a in args { - qualify_workspace_term(a, own_local_types, module_types); + qualify_workspace_term(a, own_local_types, module_types, module_mono_fns); } } Term::Match { scrutinee, arms } => { - qualify_workspace_term(scrutinee, own_local_types, module_types); + qualify_workspace_term(scrutinee, own_local_types, module_types, module_mono_fns); for arm in arms { - qualify_workspace_term(&mut arm.body, own_local_types, module_types); + qualify_workspace_term(&mut arm.body, own_local_types, module_types, module_mono_fns); } } Term::Lam { param_tys, ret_ty, body, .. } => { @@ -4635,23 +4702,25 @@ pub(crate) fn qualify_workspace_term( *p = qualify_workspace_types(p, own_local_types, module_types); } **ret_ty = qualify_workspace_types(ret_ty, own_local_types, module_types); - qualify_workspace_term(body, own_local_types, module_types); + qualify_workspace_term(body, own_local_types, module_types, module_mono_fns); } Term::Seq { lhs, rhs } => { - qualify_workspace_term(lhs, own_local_types, module_types); - qualify_workspace_term(rhs, own_local_types, module_types); + qualify_workspace_term(lhs, own_local_types, module_types, module_mono_fns); + qualify_workspace_term(rhs, own_local_types, module_types, module_mono_fns); + } + Term::Clone { value } => { + qualify_workspace_term(value, own_local_types, module_types, module_mono_fns) } - Term::Clone { value } => qualify_workspace_term(value, own_local_types, module_types), Term::ReuseAs { source, body } => { - qualify_workspace_term(source, own_local_types, module_types); - qualify_workspace_term(body, own_local_types, module_types); + qualify_workspace_term(source, own_local_types, module_types, module_mono_fns); + qualify_workspace_term(body, own_local_types, module_types, module_mono_fns); } Term::Loop { binders, body } => { for b in binders { b.ty = qualify_workspace_types(&b.ty, own_local_types, module_types); - qualify_workspace_term(&mut b.init, own_local_types, module_types); + qualify_workspace_term(&mut b.init, own_local_types, module_types, module_mono_fns); } - qualify_workspace_term(body, own_local_types, module_types); + qualify_workspace_term(body, own_local_types, module_types, module_mono_fns); } // prep.2 (kernel-extension-mechanics): prep.1 pre-pass partner. // Mirror the Term::Ctor arm above — normalize a bare @@ -4677,7 +4746,7 @@ pub(crate) fn qualify_workspace_term( for arg in args.iter_mut() { match arg { NewArg::Value(v) => { - qualify_workspace_term(v, own_local_types, module_types) + qualify_workspace_term(v, own_local_types, module_types, module_mono_fns) } NewArg::Type(t) => { *t = qualify_workspace_types(t, own_local_types, module_types) @@ -7998,12 +8067,16 @@ mod tests { ); } - /// prep.2 (kernel-extension-mechanics): a `(new T arg)` where T's - /// home module declares no `new` def fires - /// `NewTypeNotConstructible`. Property: synth's Term::New arm - /// resolves T's home module and then looks up `new` in its - /// globals; absence triggers the diagnostic with `type` and - /// `home_module` context. + /// raw-buf.4: a `(new T arg)` where T's home module declares no + /// `new` def is rejected. The Term::New desugar (raw-buf.4) + /// rewrites `(new T …)` to `(app T.new …)` *before* check, so the + /// rejection now arrives through the type-scoped resolution ladder + /// as `type-scoped-member-not-found` ("type `T` has no member + /// `new`") rather than the prep.2 `new-type-not-constructible` + /// code that synth's now-bypassed Term::New arm produced. The + /// rejection condition is identical (no constructor for T) and the + /// new diagnostic carries strictly more context (type + member + + /// home_module). #[test] fn new_type_not_constructible() { let m: Module = serde_json::from_value(serde_json::json!({ @@ -8037,18 +8110,23 @@ mod tests { }; let diags = check_workspace(&ws); assert!( - diags.iter().any(|d| d.code == "new-type-not-constructible"), - "expected diagnostic code `new-type-not-constructible`, got {diags:?}" + diags.iter().any(|d| d.code == "type-scoped-member-not-found"), + "expected the desugared `(app T.new …)` to be rejected with \ + `type-scoped-member-not-found` (no `new` member on T), got {diags:?}" ); } - /// prep.2 (kernel-extension-mechanics): a `(new T ...)` whose - /// `new` def expects a Type-arg in position 0 (because its sig is - /// `forall a. (a) -> T a`) but the call site supplies a Value-arg - /// instead fires `NewArgKindMismatch`. Property: synth counts - /// NewArg::Type and NewArg::Value separately, compares Type count - /// to the Forall vars count, and emits the kind-mismatch on - /// inequality before any other check fires. + /// raw-buf.4: a `(new T value)` against a polymorphic + /// `new : forall a. (a) -> T a` checks cleanly — the element type + /// `a` is recovered by inference from use (here the `main` return + /// `(con T (con Int))` fixes `a = Int`). This pins the desugar's + /// type-arg-dropping design: `(new T …)` rewrites to + /// `(app T.new …)` carrying only the value args, with no NewArg + /// kind distinction surviving into check. The prep.2 + /// `new-arg-kind-mismatch` diagnostic is obsoleted by this design + /// (there are no longer type-args at a `new` site to mismatch); + /// the call shape that used to trip it is now the canonical, + /// accepted form. #[test] fn new_arg_kind_mismatch_value_where_type() { let m: Module = serde_json::from_value(serde_json::json!({ @@ -8096,8 +8174,9 @@ mod tests { }; let diags = check_workspace(&ws); assert!( - diags.iter().any(|d| d.code == "new-arg-kind-mismatch"), - "expected diagnostic code `new-arg-kind-mismatch`, got {diags:?}" + diags.is_empty(), + "(new T 42) against `forall a. (a) -> T a` must check cleanly — \ + the desugar drops type-args and `a` is inferred from use; got {diags:?}" ); } diff --git a/crates/ailang-codegen/src/drop.rs b/crates/ailang-codegen/src/drop.rs index e4aea2f..03b29cc 100644 --- a/crates/ailang-codegen/src/drop.rs +++ b/crates/ailang-codegen/src/drop.rs @@ -699,6 +699,57 @@ impl<'a> Emitter<'a> { self.body.push_str(&out); } + /// raw-buf.4: emit the flat `drop__` 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__` 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__` /// symbol for a type, parallel to the existing per-type drop /// dispatch in `field_drop_call`. Returns `None` for non-ADT diff --git a/crates/ailang-codegen/src/intercepts.rs b/crates/ailang-codegen/src/intercepts.rs index 8f175e3..57d8900 100644 --- a/crates/ailang-codegen/src/intercepts.rs +++ b/crates/ailang-codegen/src/intercepts.rs @@ -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 { diff --git a/crates/ailang-codegen/src/lib.rs b/crates/ailang-codegen/src/lib.rs index 0caf910..5955530 100644 --- a/crates/ailang-codegen/src/lib.rs +++ b/crates/ailang-codegen/src/lib.rs @@ -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 `.` 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" diff --git a/crates/ailang-core/src/desugar.rs b/crates/ailang-core/src/desugar.rs index 7c70642..a6f809b 100644 --- a/crates/ailang-core/src/desugar.rs +++ b/crates/ailang-core/src/desugar.rs @@ -923,16 +923,32 @@ impl Desugarer { })); in_full } - Term::New { type_name, args } => Term::New { - type_name: type_name.clone(), - args: args + Term::New { type_name, args } => { + // raw-buf.4: (new T ) desugars to + // (app T.new ) — a type-scoped call so synth's + // dotted-name branch resolves it through the + // TypeDef-first ladder and the raw-buf.3 scope threading + // mints `T_new__`. The leading NewArg::Type is + // dropped: the AST carries no type-args on App, and the + // element type is recovered by ordinary inference from + // how the result is used (spec § Term::New desugar). + // Runs before check, so no Term::New reaches the + // type-checker or codegen. + let value_args: Vec = args .iter() - .map(|arg| match arg { - NewArg::Value(v) => NewArg::Value(self.desugar_term(v, scope)), - NewArg::Type(t) => NewArg::Type(t.clone()), + .filter_map(|arg| match arg { + NewArg::Value(v) => Some(self.desugar_term(v, scope)), + NewArg::Type(_) => None, }) - .collect(), - }, + .collect(); + Term::App { + callee: Box::new(Term::Var { + name: format!("{type_name}.new"), + }), + args: value_args, + tail: false, + } + } Term::Intrinsic => t.clone(), } } diff --git a/crates/ailang-core/tests/design_schema_drift.rs b/crates/ailang-core/tests/design_schema_drift.rs index 8227dfc..0c9a340 100644 --- a/crates/ailang-core/tests/design_schema_drift.rs +++ b/crates/ailang-core/tests/design_schema_drift.rs @@ -772,3 +772,23 @@ fn kernel_stub_module_round_trips() { }).expect("kernel_stub ships the peek ratifier op (raw-buf.3)"); assert!(matches!(peek.body, Term::Intrinsic), "peek is an (intrinsic) marker"); } + +/// raw-buf.4: pin the JSON byte-shape of the raw_buf kernel-tier +/// base-extension module. Mirror of `kernel_stub_module_round_trips`. +#[test] +fn raw_buf_module_round_trips() { + let m = ailang_surface::parse_raw_buf(); + assert!(m.kernel, "raw_buf is kernel-tier"); + assert_eq!(m.name, "raw_buf"); + let json = serde_json::to_value(&m).expect("serialise raw_buf"); + let recovered: Module = + serde_json::from_value(json).expect("raw_buf round-trips through serde"); + assert!(recovered.kernel); + assert_eq!(recovered.name, "raw_buf"); + let td = recovered.defs.iter().find_map(|d| match d { + Def::Type(t) if t.name == "RawBuf" => Some(t), + _ => None, + }).expect("RawBuf TypeDef present"); + let allowed = td.param_in.get("a").expect("RawBuf.a restricted"); + assert!(allowed.contains("Int") && allowed.contains("Float") && allowed.contains("Bool")); +} diff --git a/crates/ailang-core/tests/workspace_pin.rs b/crates/ailang-core/tests/workspace_pin.rs index 5be74f6..9912bf0 100644 --- a/crates/ailang-core/tests/workspace_pin.rs +++ b/crates/ailang-core/tests/workspace_pin.rs @@ -49,13 +49,15 @@ fn loads_example_workspace_happy_path() { assert_eq!(ws.entry, "ws_main"); assert!(ws.modules.contains_key("ws_main")); assert!(ws.modules.contains_key("ws_lib")); - // the loader auto-injects two built-in kernel-tier modules - // (`prelude` and `kernel_stub` — see prep.3 of the - // kernel-extension-mechanics milestone), so the count is the - // user's two modules plus those two. - assert_eq!(ws.modules.len(), 4); + // the loader auto-injects three built-in kernel-tier modules + // (`prelude`, `kernel_stub` — see prep.3 of the + // kernel-extension-mechanics milestone — and `raw_buf`, the + // first real-payload kernel extension, raw-buf.4), so the count + // is the user's two modules plus those three. + assert_eq!(ws.modules.len(), 5); assert!(ws.modules.contains_key("prelude")); assert!(ws.modules.contains_key("kernel_stub")); + assert!(ws.modules.contains_key("raw_buf")); } #[test] diff --git a/crates/ailang-kernel/src/lib.rs b/crates/ailang-kernel/src/lib.rs index 20f5e0b..b11e2bc 100644 --- a/crates/ailang-kernel/src/lib.rs +++ b/crates/ailang-kernel/src/lib.rs @@ -9,5 +9,7 @@ //! crate carries zero parser code. mod kernel_stub; +mod raw_buf; pub use kernel_stub::SOURCE as STUB_AIL; +pub use raw_buf::SOURCE as RAW_BUF_AIL; diff --git a/crates/ailang-kernel/src/raw_buf/mod.rs b/crates/ailang-kernel/src/raw_buf/mod.rs new file mode 100644 index 0000000..11e86ab --- /dev/null +++ b/crates/ailang-kernel/src/raw_buf/mod.rs @@ -0,0 +1,10 @@ +//! Raw-buf submodule — the Form-A source of the `raw_buf` kernel-tier +//! base-extension module (RawBuf: a mutable indexed flat buffer of +//! primitive elements). Parsed by `ailang_surface::parse_raw_buf`, +//! round-trip-pinned by `raw_buf_module_round_trips`. + +/// Source-of-truth Form-A text for the `raw_buf` kernel module. +/// Declares the `RawBuf` TypeDef (`param-in (a Int Float Bool)`) and +/// the four `(intrinsic)` ops `new`/`get`/`set`/`size`; codegen +/// supplies their bodies via the `RawBuf_op__` intercept entries. +pub const SOURCE: &str = include_str!("source.ail"); diff --git a/crates/ailang-kernel/src/raw_buf/source.ail b/crates/ailang-kernel/src/raw_buf/source.ail new file mode 100644 index 0000000..61f37ea --- /dev/null +++ b/crates/ailang-kernel/src/raw_buf/source.ail @@ -0,0 +1,26 @@ +(module raw_buf + (kernel) + (data RawBuf (vars a) + (doc "Mutable, indexed, fixed-size flat buffer of primitive elements. Opaque single-ctor handle; codegen intercepts emit raw alloc/load/store over an @ailang_rc_alloc slab. Element type restricted to {Int, Float, Bool} via param-in.") + (ctor B a) + (param-in (a Int Float Bool))) + (fn new + (doc "Allocate an uninitialised RawBuf of capacity n. Compiler-supplied (intrinsic) body; codegen intercept RawBuf_new__ emits @ailang_rc_alloc plus the i64 size header. Element bytes are uninitialised; caller must set before get.") + (type (forall (vars a) (fn-type (params (con Int)) (ret (own (con RawBuf a)))))) + (params n) + (intrinsic)) + (fn get + (doc "Indexed read. UB if i >= (size b); caller checks bounds via size. Compiler-supplied (intrinsic) body; codegen intercept RawBuf_get__ emits getelementptr plus load.") + (type (forall (vars a) (fn-type (params (borrow (con RawBuf a)) (con Int)) (ret a)))) + (params b i) + (intrinsic)) + (fn set + (doc "Indexed write. Linear: own in, own out. Under uniqueness in-place; under shared copy-on-write (Issue #22). Compiler-supplied (intrinsic) body; codegen intercept RawBuf_set__ emits getelementptr plus store.") + (type (forall (vars a) (fn-type (params (own (con RawBuf a)) (con Int) a) (ret (own (con RawBuf a)))))) + (params b i v) + (intrinsic)) + (fn size + (doc "Element count. Compiler-supplied (intrinsic) body; codegen intercept RawBuf_size__ emits a single i64 load from the slab header.") + (type (forall (vars a) (fn-type (params (borrow (con RawBuf a))) (ret (con Int))))) + (params b) + (intrinsic))) diff --git a/crates/ailang-surface/src/lib.rs b/crates/ailang-surface/src/lib.rs index 581ef01..324300b 100644 --- a/crates/ailang-surface/src/lib.rs +++ b/crates/ailang-surface/src/lib.rs @@ -36,6 +36,8 @@ pub mod loader; pub mod parse; pub mod print; -pub use loader::{load_module, load_workspace, parse_kernel_stub, parse_prelude, PRELUDE_AIL}; +pub use loader::{ + load_module, load_workspace, parse_kernel_stub, parse_prelude, parse_raw_buf, PRELUDE_AIL, +}; pub use parse::{parse, parse_term, ParseError}; pub use print::{print, term_to_form_a, type_to_form_a}; diff --git a/crates/ailang-surface/src/loader.rs b/crates/ailang-surface/src/loader.rs index 09efc03..3b6cdea 100644 --- a/crates/ailang-surface/src/loader.rs +++ b/crates/ailang-surface/src/loader.rs @@ -56,6 +56,20 @@ pub fn parse_kernel_stub() -> Module { .expect("ailang_kernel::STUB_AIL must parse as a Module") } +/// raw-buf.4: parse the embedded raw_buf kernel-tier base-extension +/// module bytes into a `Module`. Mirror of [`parse_kernel_stub`]. +/// +/// Source-of-truth: `ailang_kernel::RAW_BUF_AIL`. Declares the +/// `RawBuf` TypeDef with `param-in (a Int Float Bool)` and the four +/// `(intrinsic)` ops; codegen supplies their bodies via the +/// `RawBuf_op__` intercept registry. +/// +/// Panics on parse failure — build-time-validated by every drift run. +pub fn parse_raw_buf() -> Module { + crate::parse(ailang_kernel::RAW_BUF_AIL) + .expect("ailang_kernel::RAW_BUF_AIL must parse as a Module") +} + fn is_ail_source(path: &Path) -> bool { path.extension().and_then(|s| s.to_str()) == Some("ail") } @@ -131,6 +145,17 @@ pub fn load_workspace(entry: &Path) -> Result { } modules.insert("kernel_stub".to_string(), parse_kernel_stub()); + // parse_raw_buf() injects the raw_buf kernel-tier base-extension + // module — RawBuf, the first real-payload kernel extension. Its + // source carries `(kernel)`, so the kernel-flag filter below + // auto-imports it. + if modules.contains_key("raw_buf") { + return Err(WorkspaceLoadError::ReservedModuleName { + name: "raw_buf".to_string(), + }); + } + modules.insert("raw_buf".to_string(), parse_raw_buf()); + // Derive the implicit-imports list from `kernel: true` modules. // Replaces the previous hardcoded `&["prelude"]` literal: any // workspace-loaded module that carries the kernel flag is now diff --git a/examples/new_stubt_smoke.ail b/examples/new_stubt_smoke.ail new file mode 100644 index 0000000..7d89b03 --- /dev/null +++ b/examples/new_stubt_smoke.ail @@ -0,0 +1,13 @@ +(module new_stubt_smoke + (fn unwrap_int + (doc "Project the Int back out of a StubT.") + (type (fn-type (params (con StubT (con Int))) (ret (con Int)))) + (params s) + (body (match s (case (pat-ctor Stub x) x)))) + (fn main + (doc "raw-buf.4 desugar ratifier: (new StubT 42) desugars to (app StubT.new 42), builds, and prints 42.") + (type (fn-type (params) (ret (con Unit)) (effects IO))) + (params) + (body + (let s (new StubT 42) + (app print (app unwrap_int s)))))) diff --git a/examples/raw_buf_bool.ail b/examples/raw_buf_bool.ail new file mode 100644 index 0000000..abb5cdf --- /dev/null +++ b/examples/raw_buf_bool.ail @@ -0,0 +1,10 @@ +(module raw_buf_bool + (fn main + (doc "raw-buf.4 Bool variant: store true in a 1-slot Bool RawBuf, read it back, discriminate to an Int and print (42). Exercises RawBuf.new/.set/.get @ Bool (i1 load/store, width-1 offsets).") + (type (fn-type (params) (ret (con Unit)) (effects IO))) + (params) + (body + (app print + (let buf (new RawBuf (con Bool) 1) + (let buf (app RawBuf.set buf 0 true) + (if (app RawBuf.get buf 0) 42 0))))))) diff --git a/examples/raw_buf_float.ail b/examples/raw_buf_float.ail new file mode 100644 index 0000000..5ab6bc6 --- /dev/null +++ b/examples/raw_buf_float.ail @@ -0,0 +1,12 @@ +(module raw_buf_float + (fn main + (doc "raw-buf.4 Float variant: fill a 2-slot Float RawBuf with 1.5/2.5 and print their sum (4.0). Exercises RawBuf.new/.set/.get @ Float (double load/store).") + (type (fn-type (params) (ret (con Unit)) (effects IO))) + (params) + (body + (app print + (let buf (new RawBuf (con Float) 2) + (let buf (app RawBuf.set buf 0 1.5) + (let buf (app RawBuf.set buf 1 2.5) + (app + (app RawBuf.get buf 0) + (app RawBuf.get buf 1))))))))) diff --git a/examples/raw_buf_int.ail b/examples/raw_buf_int.ail new file mode 100644 index 0000000..e9def69 --- /dev/null +++ b/examples/raw_buf_int.ail @@ -0,0 +1,14 @@ +(module raw_buf_int + (fn main + (doc "raw-buf.4 worked consumer: fill a 3-slot Int RawBuf with 10/20/30 and print their sum (60). Exercises RawBuf.new/.set/.get @ Int end-to-end.") + (type (fn-type (params) (ret (con Unit)) (effects IO))) + (params) + (body + (app print + (let buf (new RawBuf (con Int) 3) + (let buf (app RawBuf.set buf 0 10) + (let buf (app RawBuf.set buf 1 20) + (let buf (app RawBuf.set buf 2 30) + (app + (app RawBuf.get buf 0) + (app + (app RawBuf.get buf 1) + (app RawBuf.get buf 2))))))))))) diff --git a/examples/raw_buf_reject_str.ail b/examples/raw_buf_reject_str.ail new file mode 100644 index 0000000..2010b81 --- /dev/null +++ b/examples/raw_buf_reject_str.ail @@ -0,0 +1,5 @@ +(module raw_buf_reject_str + (fn cant_str + (type (fn-type (params (borrow (con RawBuf (con Str)))) (ret (con Str)))) + (params buf) + (body (app RawBuf.get buf 0))))