Iter 9a/9b: dogfood (list_map) + ail run

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) <noreply@anthropic.com>
This commit is contained in:
2026-05-07 13:09:18 +02:00
parent 91b9bf0581
commit 849eca4fcf
3 changed files with 297 additions and 42 deletions
+79 -42
View File
@@ -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<String>,
},
/// 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<PathBuf>, opt: &str) -> Result<PathBuf> {
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)
}
+11
View File
@@ -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.
+207
View File
@@ -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" }
]
}
]
}
}
}
]
}