diff --git a/crates/ail/tests/raw_buf_new_type_arg_pin.rs b/crates/ail/tests/raw_buf_new_type_arg_pin.rs new file mode 100644 index 0000000..b191183 --- /dev/null +++ b/crates/ail/tests/raw_buf_new_type_arg_pin.rs @@ -0,0 +1,114 @@ +//! RED-pin for #51 (raw-buf fieldtest F7): the explicit element +//! type-arg of `(new RawBuf )` must be honoured. +//! +//! Property protected: the author's `(con Elem)` annotation on a +//! `(new RawBuf ...)` is the binding declaration of the buffer's +//! element type — it must NOT depend on a downstream `get`/`set` to +//! be recovered, and it must be subject to RawBuf's `param-in` +//! restricted set {Int, Float, Bool} at check time. +//! +//! Two legs, both stemming from the same defect: `(new T +//! )` desugars to `(app T.new )` and DROPS the +//! leading `NewArg::Type` (crates/ailang-core/src/desugar.rs:1053). +//! +//! - Leg 1 (legitimate, must build+run): `(new RawBuf (con Int) 3)` +//! used only via `RawBuf.size`. With the type-arg dropped, the +//! result's forall var `a` is never pinned by use, monomorph +//! defaults it to Unit (mono.rs free-fn-call arm / subst.rs), and +//! codegen mints `RawBuf_new__Unit`, which has no registered +//! intercept (intercepts.rs registers only Int/Float/Bool). Today +//! `ail build --alloc=rc` aborts with +//! `intrinsic fn RawBuf_new__Unit has no registered intercept`. +//! Expected post-fix: build succeeds, program prints `3`. +//! +//! - Leg 2 (forbidden, must reject at check): `(new RawBuf (con +//! Unit) 3)`. Unit is not in RawBuf's param-in set, but because +//! the `(con Unit)` type-arg is dropped before check, the +//! `param-in` enforcement in `check_type_well_formed` +//! (crates/ailang-check/src/lib.rs:2084) never sees a `RawBuf` +//! type, so `ail check` wrongly passes (exit 0) and the program +//! crashes at `ail build` with the same `RawBuf_new__Unit` error. +//! Expected post-fix: `ail check` fails with +//! `param-not-in-restricted-set` naming `Unit`; codegen unreached. +//! +//! These are CLI-boundary E2E tests because the defect spans the +//! desugar -> check/codegen pipeline and the symptoms are the exit +//! behaviour of `ail check` / `ail build` themselves. + +use std::path::{Path, PathBuf}; +use std::process::Command; + +fn repo_root() -> PathBuf { + let manifest_dir = env!("CARGO_MANIFEST_DIR"); + Path::new(manifest_dir) + .parent() + .unwrap() + .parent() + .unwrap() + .to_path_buf() +} + +/// Leg 1: a `(new RawBuf (con Int) 3)` buffer used only via +/// `RawBuf.size` must build under `--alloc=rc` and print its size. +#[test] +fn rawbuf_new_int_size_only_builds_and_runs() { + let fixture = repo_root().join("examples/raw_buf_new_type_arg_intsize.ail"); + let tmp = std::env::temp_dir().join(format!( + "ailang_rawbuf_new_type_arg_intsize_{}", + std::process::id() + )); + std::fs::create_dir_all(&tmp).unwrap(); + let out = tmp.join("bin"); + + let build = Command::new(env!("CARGO_BIN_EXE_ail")) + .args(["build", fixture.to_str().unwrap(), "--alloc=rc", "-o"]) + .arg(&out) + .output() + .expect("ail build failed to spawn"); + assert!( + build.status.success(), + "ail build --alloc=rc failed for size-only RawBuf: stderr={}", + String::from_utf8_lossy(&build.stderr) + ); + + let run = Command::new(&out).output().expect("execute binary"); + assert!( + run.status.success(), + "binary exited non-zero: {:?}", + run.status + ); + let stdout = String::from_utf8_lossy(&run.stdout); + assert!( + stdout.trim() == "3", + "expected the program to print the buffer size `3`, got: {stdout:?}" + ); +} + +/// Leg 2: a `(new RawBuf (con Unit) 3)` names a forbidden element +/// type and must be rejected at `ail check` with +/// `param-not-in-restricted-set` naming `Unit`. +#[test] +fn rawbuf_new_unit_element_rejected_at_check() { + let fixture = repo_root().join("examples/raw_buf_new_type_arg_unit_reject.ail"); + + let out = Command::new(env!("CARGO_BIN_EXE_ail")) + .arg("check") + .arg(&fixture) + .output() + .expect("ail check failed to spawn"); + + assert!( + !out.status.success(), + "expected `ail check` to REJECT RawBuf, but it passed: stdout={}", + String::from_utf8_lossy(&out.stdout) + ); + let stderr = String::from_utf8_lossy(&out.stderr); + assert!( + stderr.contains("param-not-in-restricted-set"), + "expected diagnostic code `param-not-in-restricted-set`, got: {stderr}" + ); + assert!( + stderr.contains("Unit"), + "expected the diagnostic to name the forbidden element `Unit`, got: {stderr}" + ); +} diff --git a/examples/raw_buf_new_type_arg_intsize.ail b/examples/raw_buf_new_type_arg_intsize.ail new file mode 100644 index 0000000..3b6177d --- /dev/null +++ b/examples/raw_buf_new_type_arg_intsize.ail @@ -0,0 +1,8 @@ +(module raw_buf_new_type_arg_intsize + (fn main + (doc "RED-pin fixture for #51 leg 1 (raw-buf F7). A `(new RawBuf (con Int) 3)` buffer used ONLY via `RawBuf.size` — its element type is never observed by a later get/set, so the only carrier of the element type is the author's explicit `(con Int)` type-arg on `new`. Today `(new T )` desugars to `(app T.new )` and DROPS the leading NewArg::Type (crates/ailang-core/src/desugar.rs:1053), so the result's forall var `a` is unpinned and monomorphisation defaults it to Unit (crates/ailang-check/src/mono.rs free-fn-call arm; mirror crates/ailang-codegen/src/subst.rs). Codegen then mints `RawBuf_new__Unit`, which has no registered intercept (only Int/Float/Bool are in crates/ailang-codegen/src/intercepts.rs) and `ail build --alloc=rc` aborts: `intrinsic fn RawBuf_new__Unit has no registered intercept`. Expected post-fix: the explicit `(con Int)` is carried through monomorphisation so the buffer specialises to `RawBuf_new__Int`; build succeeds and the program prints 3 (the size).") + (type (fn-type (params) (ret (con Unit)) (effects IO))) + (params) + (body + (let b (new RawBuf (con Int) 3) + (app print (app RawBuf.size b)))))) diff --git a/examples/raw_buf_new_type_arg_unit_reject.ail b/examples/raw_buf_new_type_arg_unit_reject.ail new file mode 100644 index 0000000..53bb792 --- /dev/null +++ b/examples/raw_buf_new_type_arg_unit_reject.ail @@ -0,0 +1,8 @@ +(module raw_buf_new_type_arg_unit_reject + (fn main + (doc "RED-pin fixture for #51 leg 2 (raw-buf F7). A `(new RawBuf (con Unit) 3)` names Unit as the element type — but Unit is NOT in RawBuf's param-in restricted set {Int, Float, Bool}, so this must be REJECTED at `ail check` with `param-not-in-restricted-set` naming Unit. Today the `(new T )` desugar (crates/ailang-core/src/desugar.rs:1053) drops the `NewArg::Type` BEFORE check, so the `(con Unit)` element annotation never reaches the param-in enforcement in check_type_well_formed (crates/ailang-check/src/lib.rs:2084). `ail check` therefore passes (exit 0) and the program only crashes later at `ail build` with the same unregistered `RawBuf_new__Unit` intercept as leg 1. Expected post-fix: a pre-desugar reject on the Term::New shape (per the CLAUDE.md Term::New/desugar lockstep, crates/ailang-check/src/pre_desugar_validation.rs) fails `ail check` with `param-not-in-restricted-set` naming Unit; codegen is never reached.") + (type (fn-type (params) (ret (con Unit)) (effects IO))) + (params) + (body + (let b (new RawBuf (con Unit) 3) + (app print (app RawBuf.size b))))))