From 6b7ee45e49e7c4531155bad106f4fabb7417f45a Mon Sep 17 00:00:00 2001 From: Brummel Date: Tue, 2 Jun 2026 12:59:34 +0200 Subject: [PATCH] test(series): RED pin for the SMA worked example (#61 headline) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The headline acceptance of #61: the simple-moving-average worked example from design/models/0007-kernel-extensions.md must build via `ail build --alloc=rc`, run, and print the correct moving averages. Green ⇒ the kernel-tier `Series` library extension works end to end (ring-buffer push, financial-style indexing at index 0 = newest, count/total bookkeeping). The fixture is the 0007 worked example with two corrections applied, both verified against the live tool this session: - `>=` is not a surface symbol; the polymorphic Ord helper is `ge` (here on Int operands: total_count vs window size). - `print` on Float emits no trailing newline, so the four averages would concatenate into an unreadable blob. The emit branch now prints the average followed by `io/print_str "\n"`, so each average lands on its own line and the output is deterministic and assertable. (This legibility fix is also queued to be carried back into model 0007.) RED reason (right-for-the-right-reason): `ail check` rejects with [bare-cross-module-type-ref] on `Series` — the kernel-tier module does not exist yet. The fixture itself parses and is otherwise valid. Expected stdout was derived by probing the live %g float formatter independently (the SMA averages are 9/3, 16/3, 17/3, 16/3 over the stream 1,5,3,8,6,2 with window 3): `3.0\n5.33333\n5.66667\n5.33333`. GREEN side (the `series` kernel module + the five ops) is the next mini-mode iteration. --- crates/ail/tests/series_sma_pin.rs | 114 +++++++++++++++++++++++++++++ examples/series_sma.ail | 58 +++++++++++++++ 2 files changed, 172 insertions(+) create mode 100644 crates/ail/tests/series_sma_pin.rs create mode 100644 examples/series_sma.ail diff --git a/crates/ail/tests/series_sma_pin.rs b/crates/ail/tests/series_sma_pin.rs new file mode 100644 index 0000000..3c487a3 --- /dev/null +++ b/crates/ail/tests/series_sma_pin.rs @@ -0,0 +1,114 @@ +//! Headline RED-pin for issue #61 ("[series] Step 1 — primitive +//! Series over {Int, Float}"): the simple-moving-average (SMA) worked +//! example from `design/models/0007-kernel-extensions.md` §"Worked +//! example — `Series` for streaming analytics" must `ail build +//! --alloc=rc` to a native binary, run, and print the correct moving +//! averages. +//! +//! Property protected: the kernel-tier `Series` library extension +//! works end-to-end as an ordinary AILang consumer sees it — the bare +//! kernel-tier type `(con Series (con Float))`, construction via +//! `(new Series (con Float) 3)`, type-scoped ops `Series.push`, +//! `Series.at`, `Series.total_count`, the bounded ring-buffer +//! semantics (window size 3), financial-style indexing (index 0 = +//! newest), and count/total bookkeeping. This single fixture +//! transitively forces the whole Series surface to be correct: a +//! wrong ring-buffer, a wrong index orientation, or wrong total_count +//! bookkeeping would print different averages. +//! +//! The SMA fixture (`examples/series_sma.ail`) is the worked example +//! from model 0007 with two VERIFIED corrections applied: +//! - `>=` (line 77 of 0007) replaced with the polymorphic Ord +//! prelude helper `ge` — `>=` does not exist as a symbol; both +//! operands here are `Int`. +//! - a newline separator (`(do io/print_str "\n")`) appended to each +//! emitted average — `print` emits no trailing newline, so without +//! it the four averages concatenate into an unreadable, unassertable +//! blob. +//! Everything else in the worked example is valid against today's tool. +//! +//! Why this is RED right now: the `series` kernel module does not exist +//! yet. `ail check examples/series_sma.ail` rejects with +//! `[bare-cross-module-type-ref]` — bare type `Series` not in scope, +//! candidate list empty — i.e. the feature is ABSENT, not a parse / +//! operator / scaffolding error. The GREEN side (authoring the kernel +//! `series` module + its 5 ops) is a later `implement` mini-mode +//! dispatch; this pin authors only the SMA *consumer*. +//! +//! This is a CLI-boundary E2E test because the acceptance spans the +//! check -> mono -> codegen -> link -> run pipeline and the symptom is +//! the printed moving averages of the produced binary. +//! +//! Expected stdout was derived by probing the live `%g`-style float +//! formatter INDEPENDENTLY of the (nonexistent) Series, so the +//! assertion encodes the intended averages formatted exactly as the +//! tool will format them once Series exists. Push stream +//! `1.0, 5.0, 3.0, 8.0, 6.0, 2.0`, window size 3, financial index +//! (0 = newest), `emit_if_full` emits once `total_count >= n`: +//! - after push 3.0: (3+5+1)/3 = 9/3 -> "3.0" +//! - after push 8.0: (8+3+5)/3 = 16/3 -> "5.33333" +//! - after push 6.0: (6+8+3)/3 = 17/3 -> "5.66667" +//! - after push 2.0: (2+6+8)/3 = 16/3 -> "5.33333" +//! Probe command (run this session): +//! ./target/release/ail run /tmp/sma_probe.ail --alloc=rc +//! where /tmp/sma_probe.ail printed each of 9.0/3, 16.0/3, 17.0/3, +//! 16.0/3 via `(app print (app / X (app int_to_float 3)))` separated +//! by `(do io/print_str "\n")`. Captured bytes (od -c): +//! `3.0\n5.33333\n5.66667\n5.33333\n`. After `.trim()`: +//! `3.0\n5.33333\n5.66667\n5.33333`. + +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() +} + +/// The SMA worked example from model 0007 (a pure consumer of the +/// kernel-tier `Series` type) must build under `--alloc=rc`, run, and +/// print the four correct moving averages, one per line. Green ⇒ the +/// `Series` ring buffer, financial indexing, and count bookkeeping all +/// work end-to-end. +#[test] +fn series_sma_worked_example_builds_runs_correct_output() { + let fixture = repo_root().join("examples/series_sma.ail"); + let tmp = std::env::temp_dir().join(format!( + "ailang_series_sma_{}", + 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 the SMA worked example (the \ + kernel-tier `Series` library extension): 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_eq!( + stdout.trim(), + "3.0\n5.33333\n5.66667\n5.33333", + "expected the four simple-moving-averages of the stream \ + 1.0,5.0,3.0,8.0,6.0,2.0 (window 3, financial index, emit once \ + total_count>=3), formatted by the live float formatter; got: {stdout:?}" + ); +} diff --git a/examples/series_sma.ail b/examples/series_sma.ail new file mode 100644 index 0000000..a28052c --- /dev/null +++ b/examples/series_sma.ail @@ -0,0 +1,58 @@ +(module series_sma + + (data FloatList + (ctor FNil) + (ctor FCons (con Float) (con FloatList))) + + (fn sum_window_step + (type (fn-type + (params (borrow (con Series (con Float))) (own (con Int)) (own (con Int)) (own (con Float))) + (ret (own (con Float))))) + (params s n i acc) + (body + (if (app eq i n) + acc + (tail-app sum_window_step s n + (app + i 1) + (app + acc (app Series.at s i)))))) + + (fn emit_if_full + (type (fn-type + (params (borrow (con Series (con Float))) (own (con Int))) + (ret (own (con Unit))) (effects IO))) + (params s n) + (body + (if (app ge (app Series.total_count s) n) + (seq + (app print (app / + (app sum_window_step s n 0 0.0) + (app int_to_float n))) + (do io/print_str "\n")) + (do io/print_str "")))) + + (fn run_stream + (type (fn-type + (params (own (con Series (con Float))) (own (con Int)) (own (con FloatList))) + (ret (own (con Unit))) (effects IO))) + (params s n input) + (body + (match input + (case (pat-ctor FNil) (do io/print_str "")) + (case (pat-ctor FCons v rest) + (let s_new (app Series.push s v) + (seq (app emit_if_full s_new n) + (tail-app run_stream s_new n rest))))))) + + (fn main + (type (fn-type (params) (ret (own (con Unit))) (effects IO))) + (params) + (body + (let s (new Series (con Float) 3) + (app run_stream s 3 + (term-ctor FloatList FCons 1.0 + (term-ctor FloatList FCons 5.0 + (term-ctor FloatList FCons 3.0 + (term-ctor FloatList FCons 8.0 + (term-ctor FloatList FCons 6.0 + (term-ctor FloatList FCons 2.0 + (term-ctor FloatList FNil))))))))))))