From 849eca4fcfbd61d747cd7d3afdc83b6f5d26bfa9 Mon Sep 17 00:00:00 2001 From: Brummel Date: Thu, 7 May 2026 13:09:18 +0200 Subject: [PATCH] Iter 9a/9b: dogfood (list_map) + ail run MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Iter 9a — dogfood. examples/list_map.ail.json exercises everything Iter 1-8 shipped in one program: ADTs (IntList = Nil | Cons Int IntList), recursive fns over the ADT (map_int, print_list), pattern matching with nested Var fields, a closure (`\\x. x * 2`, no captures), fn-typed parameters, IO effects propagating through recursion, and `let`- sequencing of an effectful sub-expression inside a match arm. Result: nothing broke. The full pipeline (typecheck → IR emit → clang → run) produces "2\\n4\\n6\\n". The language is now sufficient for "small but real" programs. Iter 9b — `ail run`. Convenience subcommand that builds into a tempdir and executes, propagating the binary's exit code. Equivalent to `ail build && ./bin` in one step. Build logic factored out of `Cmd::Build` into a shared `build_to` helper used by both Build and Run. Tests: 50 green (was 49). New e2e `list_map_doubles_then_prints`. No test for `ail run` itself — the existing build_and_run helper in e2e.rs already exercises the same build+exec sequence path. Co-Authored-By: Claude Opus 4.7 (1M context) --- crates/ail/src/main.rs | 121 ++++++++++++++-------- crates/ail/tests/e2e.rs | 11 ++ examples/list_map.ail.json | 207 +++++++++++++++++++++++++++++++++++++ 3 files changed, 297 insertions(+), 42 deletions(-) create mode 100644 examples/list_map.ail.json diff --git a/crates/ail/src/main.rs b/crates/ail/src/main.rs index 556bb35..16216a1 100644 --- a/crates/ail/src/main.rs +++ b/crates/ail/src/main.rs @@ -79,6 +79,19 @@ enum Cmd { #[arg(long, default_value = "-O0")] opt: String, }, + /// Build into a tempdir and execute. Exits with the binary's exit code. + /// Convenience wrapper around `build` + invocation of the resulting + /// binary; useful in iteration loops where we don't care about the + /// output artefact's location. + Run { + path: PathBuf, + /// Optimization (e.g. `-O2`); default `-O0` for debuggability. + #[arg(long, default_value = "-O0")] + opt: String, + /// Args passed through to the compiled program. + #[arg(last = true)] + args: Vec, + }, /// Lists built-in operations with their signatures. Builtins { #[arg(long)] @@ -357,50 +370,25 @@ fn main() -> Result<()> { } } Cmd::Build { path, out, opt } => { - // Iter 5c: same pipeline as `emit-ir`, but clang runs at the end. - let ws = ailang_core::load_workspace(&path)?; - let diags = ailang_check::check_workspace(&ws); - if !diags.is_empty() { - for d in &diags { - eprintln!( - "{}: [{}] {}{}", - match d.severity { - ailang_check::Severity::Error => "error", - ailang_check::Severity::Warning => "warning", - }, - d.code, - d.def - .as_ref() - .map(|n| format!("{n}: ")) - .unwrap_or_default(), - d.message, - ); - } - std::process::exit(1); - } - let ir = ailang_codegen::lower_workspace(&ws)?; - let tmpdir = std::env::temp_dir().join(format!("ailang-{}", std::process::id())); + let bin = build_to(&path, out, &opt)?; + eprintln!("built {}", bin.display()); + } + Cmd::Run { path, opt, args } => { + // Iter 9b: build into a fresh tempdir per run, exec, propagate + // exit code. The artefact dir is left around (no cleanup) so + // it can be inspected in case of a crash; OS temp policy + // collects them. + let tmpdir = std::env::temp_dir().join(format!( + "ailang-run-{}", + std::process::id() + )); std::fs::create_dir_all(&tmpdir)?; - let ll_path = tmpdir.join(format!("{}.ll", ws.entry)); - std::fs::write(&ll_path, &ir)?; - let out_bin = out.unwrap_or_else(|| { - Path::new(".").join(&ws.entry).with_extension("") - }); - let status = std::process::Command::new("clang") - .arg(&opt) - .arg("-o") - .arg(&out_bin) - .arg(&ll_path) + let bin = build_to(&path, Some(tmpdir.join("bin")), &opt)?; + let status = std::process::Command::new(&bin) + .args(&args) .status() - .context("running clang")?; - if !status.success() { - anyhow::bail!( - "clang failed (status {}); ll at {}", - status, - ll_path.display() - ); - } - eprintln!("built {}", out_bin.display()); + .with_context(|| format!("executing {}", bin.display()))?; + std::process::exit(status.code().unwrap_or(127)); } Cmd::Builtins { json } => { let list = ailang_check::builtins::list(); @@ -1409,3 +1397,52 @@ fn render_workspace_diff_text(r: &WorkspaceDiffReport) -> String { out } + +/// Iter 9b: shared build helper for `Cmd::Build` and `Cmd::Run`. +/// Loads the workspace, runs the typechecker, emits IR, and links via +/// clang. On typecheck failure, prints diagnostics to stderr and exits +/// the process with code 1. On clang failure, returns a Result error +/// (the .ll path is preserved for post-mortem inspection). +fn build_to(path: &Path, out: Option, opt: &str) -> Result { + let ws = ailang_core::load_workspace(path)?; + let diags = ailang_check::check_workspace(&ws); + if !diags.is_empty() { + for d in &diags { + eprintln!( + "{}: [{}] {}{}", + match d.severity { + ailang_check::Severity::Error => "error", + ailang_check::Severity::Warning => "warning", + }, + d.code, + d.def + .as_ref() + .map(|n| format!("{n}: ")) + .unwrap_or_default(), + d.message, + ); + } + std::process::exit(1); + } + let ir = ailang_codegen::lower_workspace(&ws)?; + let tmpdir = std::env::temp_dir().join(format!("ailang-{}", std::process::id())); + std::fs::create_dir_all(&tmpdir)?; + let ll_path = tmpdir.join(format!("{}.ll", ws.entry)); + std::fs::write(&ll_path, &ir)?; + let out_bin = out.unwrap_or_else(|| Path::new(".").join(&ws.entry).with_extension("")); + let status = std::process::Command::new("clang") + .arg(opt) + .arg("-o") + .arg(&out_bin) + .arg(&ll_path) + .status() + .context("running clang")?; + if !status.success() { + anyhow::bail!( + "clang failed (status {}); ll at {}", + status, + ll_path.display() + ); + } + Ok(out_bin) +} diff --git a/crates/ail/tests/e2e.rs b/crates/ail/tests/e2e.rs index b48060d..053d478 100644 --- a/crates/ail/tests/e2e.rs +++ b/crates/ail/tests/e2e.rs @@ -82,6 +82,17 @@ fn closure_captures_let_n() { assert_eq!(stdout.trim(), "42"); } +/// Iter 9 dogfood: a non-trivial program that combines ADTs, recursion, +/// closure-without-capture, fn-typed parameters, IO effects, and `let`- +/// sequencing of effectful sub-expressions in a pattern-match arm. +/// `map (\x. x * 2)` over `[1, 2, 3]` then print each element. +#[test] +fn list_map_doubles_then_prints() { + let stdout = build_and_run("list_map.ail.json"); + let lines: Vec<&str> = stdout.lines().collect(); + assert_eq!(lines, vec!["2", "4", "6"]); +} + /// Guards `ail diff`: a modified body changes the hash of `sum`, while /// `main` stays unchanged. Expects exit code 1, `changed` contains exactly /// `sum`, `unchanged` contains `main`, `added`/`removed` empty. diff --git a/examples/list_map.ail.json b/examples/list_map.ail.json new file mode 100644 index 0000000..003c5bf --- /dev/null +++ b/examples/list_map.ail.json @@ -0,0 +1,207 @@ +{ + "schema": "ailang/v0", + "name": "list_map", + "imports": [], + "defs": [ + { + "kind": "type", + "name": "IntList", + "doc": "Singly-linked list of Int, boxed.", + "ctors": [ + { "name": "Nil", "fields": [] }, + { + "name": "Cons", + "fields": [ + { "k": "con", "name": "Int" }, + { "k": "con", "name": "IntList" } + ] + } + ] + }, + { + "kind": "fn", + "name": "map_int", + "type": { + "k": "fn", + "params": [ + { + "k": "fn", + "params": [{ "k": "con", "name": "Int" }], + "ret": { "k": "con", "name": "Int" }, + "effects": [] + }, + { "k": "con", "name": "IntList" } + ], + "ret": { "k": "con", "name": "IntList" }, + "effects": [] + }, + "params": ["f", "xs"], + "doc": "Apply f to every element. Recursive on the tail.", + "body": { + "t": "match", + "scrutinee": { "t": "var", "name": "xs" }, + "arms": [ + { + "pat": { "p": "ctor", "ctor": "Nil", "fields": [] }, + "body": { + "t": "ctor", + "type": "IntList", + "ctor": "Nil", + "args": [] + } + }, + { + "pat": { + "p": "ctor", + "ctor": "Cons", + "fields": [ + { "p": "var", "name": "h" }, + { "p": "var", "name": "t" } + ] + }, + "body": { + "t": "ctor", + "type": "IntList", + "ctor": "Cons", + "args": [ + { + "t": "app", + "fn": { "t": "var", "name": "f" }, + "args": [{ "t": "var", "name": "h" }] + }, + { + "t": "app", + "fn": { "t": "var", "name": "map_int" }, + "args": [ + { "t": "var", "name": "f" }, + { "t": "var", "name": "t" } + ] + } + ] + } + } + ] + } + }, + { + "kind": "fn", + "name": "print_list", + "type": { + "k": "fn", + "params": [{ "k": "con", "name": "IntList" }], + "ret": { "k": "con", "name": "Unit" }, + "effects": ["IO"] + }, + "params": ["xs"], + "doc": "Print each Int on its own line.", + "body": { + "t": "match", + "scrutinee": { "t": "var", "name": "xs" }, + "arms": [ + { + "pat": { "p": "ctor", "ctor": "Nil", "fields": [] }, + "body": { "t": "lit", "lit": { "kind": "unit" } } + }, + { + "pat": { + "p": "ctor", + "ctor": "Cons", + "fields": [ + { "p": "var", "name": "h" }, + { "p": "var", "name": "t" } + ] + }, + "body": { + "t": "let", + "name": "_print_h", + "value": { + "t": "do", + "op": "io/print_int", + "args": [{ "t": "var", "name": "h" }] + }, + "body": { + "t": "app", + "fn": { "t": "var", "name": "print_list" }, + "args": [{ "t": "var", "name": "t" }] + } + } + } + ] + } + }, + { + "kind": "fn", + "name": "main", + "type": { + "k": "fn", + "params": [], + "ret": { "k": "con", "name": "Unit" }, + "effects": ["IO"] + }, + "params": [], + "doc": "Build [1,2,3], double each, print result.", + "body": { + "t": "let", + "name": "xs", + "value": { + "t": "ctor", + "type": "IntList", + "ctor": "Cons", + "args": [ + { "t": "lit", "lit": { "kind": "int", "value": 1 } }, + { + "t": "ctor", + "type": "IntList", + "ctor": "Cons", + "args": [ + { "t": "lit", "lit": { "kind": "int", "value": 2 } }, + { + "t": "ctor", + "type": "IntList", + "ctor": "Cons", + "args": [ + { "t": "lit", "lit": { "kind": "int", "value": 3 } }, + { + "t": "ctor", + "type": "IntList", + "ctor": "Nil", + "args": [] + } + ] + } + ] + } + ] + }, + "body": { + "t": "app", + "fn": { "t": "var", "name": "print_list" }, + "args": [ + { + "t": "app", + "fn": { "t": "var", "name": "map_int" }, + "args": [ + { + "t": "lam", + "params": ["x"], + "paramTypes": [{ "k": "con", "name": "Int" }], + "retType": { "k": "con", "name": "Int" }, + "effects": [], + "body": { + "t": "app", + "fn": { "t": "var", "name": "*" }, + "args": [ + { "t": "var", "name": "x" }, + { "t": "lit", "lit": { "kind": "int", "value": 2 } } + ] + } + }, + { "t": "var", "name": "xs" } + ] + } + ] + } + } + } + ] +}