ilo 0.12.0

ilo - the token-minimal programming language AI agents write
Documentation
// Regression tests for inline lambdas (Phase 1 + Phase 2).
//
// Inline lambda = `(params>return;body)` literal passed where a fn-ref is
// expected (HOF arg position). The parser lifts each lambda to a synthetic
// top-level `Decl::Function { name: "__lit_N", ... }` and replaces the call
// site with `Expr::Ref("__lit_N")`, so the rest of the toolchain (verifier,
// tree interpreter, fmt, python codegen, ...) treats it identically to a
// named helper.
//
// Phase 2 adds closure capture: free variables in the body get lifted as
// trailing params on the synthetic decl, and the call site emits
// `Expr::MakeClosure { fn_name, captures }` which evaluates to a
// `Value::Closure { fn_name, captures }` runtime value. Closure-aware HOFs
// append the captures after the per-item args at each call, matching the
// existing single-ctx form (#186) generalised to N captures.
//
// With #384 (VM closure support) + #385 (Cranelift closure parity) +
// #387 (HOF native closure dispatch) all merged, closure capture works
// across every engine. Phase 2 PR4 (this file) parameterises every test
// over tree, VM, and Cranelift via `run_all`.
//
// Tests that exercise HOFs which are still routed through the tree-bridge
// on VM / Cranelift (`srt`, `grp`, `uniqby`) stay tree-only with a TODO
// pointing at PR 3c (#391) — once that lands the helper switch is trivial.

use std::process::Command;
use std::sync::atomic::{AtomicU64, Ordering};

fn ilo() -> Command {
    Command::new(env!("CARGO_BIN_EXE_ilo"))
}

fn write_src(name: &str, src: &str) -> std::path::PathBuf {
    static COUNTER: AtomicU64 = AtomicU64::new(0);
    let n = COUNTER.fetch_add(1, Ordering::Relaxed);
    let mut path = std::env::temp_dir();
    path.push(format!("ilo_lam_{name}_{}_{n}.ilo", std::process::id()));
    std::fs::write(&path, src).expect("write src");
    path
}

fn run_engine(engine: &str, src: &str, entry: &str, args: &[&str]) -> String {
    let path = write_src(entry, src);
    let mut cmd = ilo();
    cmd.arg(&path).arg(engine).arg(entry);
    for a in args {
        cmd.arg(a);
    }
    let out = cmd.output().expect("failed to run ilo");
    let _ = std::fs::remove_file(&path);
    assert!(
        out.status.success(),
        "ilo {engine} failed for `{src}`: stderr={}",
        String::from_utf8_lossy(&out.stderr)
    );
    String::from_utf8_lossy(&out.stdout).trim().to_string()
}

/// Run `src` across every engine and assert each produces `expected`.
/// Use this by default — closure capture works natively on tree, VM,
/// and Cranelift after #384 + #385 + #387.
fn run_all(src: &str, entry: &str, args: &[&str], expected: &str) {
    for engine in ["--run-tree", "--run-vm", "--jit"] {
        let actual = run_engine(engine, src, entry, args);
        assert_eq!(
            actual, expected,
            "engine {engine} produced {actual:?}, expected {expected:?} for src `{src}`"
        );
    }
}

/// Tree-only runner. Use ONLY when:
///   - the test exercises srt / grp / uniqby with a closure (PR 3c #391
///     is the unblock — TODO comment required at call site), or
///   - the test intentionally probes tree-walker-specific semantics
///     (e.g. a verifier error whose wording differs per engine).
fn run_tree_only(src: &str, entry: &str, args: &[&str], expected: &str) {
    let actual = run_engine("--run-tree", src, entry, args);
    assert_eq!(
        actual, expected,
        "tree produced {actual:?}, expected {expected:?} for src `{src}`"
    );
}

// ── srt: 1-arg key fn ──────────────────────────────────────────────────────

#[test]
fn srt_inline_key_by_length() {
    // TODO PR3c follow-up: srt closure dispatch is tree-bridged on VM /
    // Cranelift until #391 lands. Flip to run_all once merged.
    let src = "f ws:L t>L t;srt (s:t>n;len s) ws";
    run_tree_only(
        src,
        "f",
        &["[\"banana\",\"fig\",\"apple\"]"],
        "[fig, apple, banana]",
    );
}

#[test]
fn srt_inline_key_absolute_value() {
    // Body uses `abs` builtin — no captures, no helper.
    // TODO PR3c follow-up: srt closure dispatch is tree-bridged on VM /
    // Cranelift until #391 lands. Flip to run_all once merged.
    let src = "f xs:L n>L n;srt (x:n>n;abs x) xs";
    run_tree_only(src, "f", &["[-3,1,-5,2]"], "[1, 2, -3, -5]");
}

// ── flt: 1-arg predicate ───────────────────────────────────────────────────

#[test]
fn flt_inline_predicate() {
    let src = "f xs:L n>L n;flt (x:n>b;>x 0) xs";
    run_all(src, "f", &["[-2,3,-1,4,0]"], "[3, 4]");
}

// ── map: 1-arg transform ───────────────────────────────────────────────────

#[test]
fn map_inline_double() {
    let src = "f xs:L n>L n;map (x:n>n;*x 2) xs";
    run_all(src, "f", &["[1,2,3]"], "[2, 4, 6]");
}

// ── fld: 2-arg accumulator ─────────────────────────────────────────────────

#[test]
fn fld_inline_sum_of_squares() {
    let src = "f xs:L n>n;fld (a:n x:n>n;+a *x x) xs 0";
    run_all(src, "f", &["[1,2,3,4]"], "30");
}

// ── multi-statement body (let + final expression) ──────────────────────────

#[test]
fn lambda_multi_statement_body() {
    // TODO PR3c follow-up: srt closure dispatch is tree-bridged on VM /
    // Cranelift until #391 lands. Flip to run_all once merged.
    let src = "f xs:L n>L n;srt (x:n>n;sq=*x x;sq) xs";
    run_tree_only(src, "f", &["[-3,1,-5,2]"], "[1, 2, -3, -5]");
}

// ── lambda calling a top-level helper (HOF inside HOF) ─────────────────────

#[test]
fn lambda_can_call_top_level_helper() {
    let src = "dbl x:n>n;*x 2\nf xs:L n>L n;map (x:n>n;dbl x) xs";
    run_all(src, "f", &["[1,2,3]"], "[2, 4, 6]");
}

// ── lambda calling a builtin ───────────────────────────────────────────────

#[test]
fn lambda_can_call_builtin() {
    // TODO PR3c follow-up: srt closure dispatch is tree-bridged on VM /
    // Cranelift until #391 lands. Flip to run_all once merged.
    let src = "f xs:L n>L n;srt (x:n>n;abs x) xs";
    run_tree_only(src, "f", &["[-3,1,-5,2]"], "[1, 2, -3, -5]");
}

// ── Multiple lambdas in one function (counter increments) ──────────────────

#[test]
fn multiple_lambdas_in_one_function() {
    let src = "f xs:L n>L n;ys=map (x:n>n;*x 2) xs;flt (x:n>b;>x 4) ys";
    run_all(src, "f", &["[1,2,3,4]"], "[6, 8]");
}

// ── Lambda inside a top-level helper, used twice via different entries ─────

#[test]
fn lambda_inside_helper() {
    // TODO PR3c follow-up: srt closure dispatch is tree-bridged on VM /
    // Cranelift until #391 lands. Flip to run_all once merged.
    let src = "
sorted xs:L n>L n;srt (x:n>n;abs x) xs
f xs:L n>L n;sorted xs
";
    run_tree_only(src, "f", &["[-3,1,-5,2]"], "[1, 2, -3, -5]");
}

// ── Phase 2: closure capture works ─────────────────────────────────────────

#[test]
fn closure_capture_single_var_filter() {
    // Single capture: `thr` is in the enclosing fn's scope and the lambda
    // references it. The parser lifts `__lit_0(x, thr)` and emits a
    // MakeClosure at the call site; flt appends the capture to each call.
    let src = "f xs:L n thr:n>L n;flt (x:n>b;>x thr) xs";
    run_all(src, "f", &["[1,5,3,8,2]", "4"], "[5, 8]");
}

#[test]
fn closure_capture_in_sort_key() {
    // `srt` with an inline key that closes over `target`.
    // TODO PR3c follow-up: srt closure dispatch is tree-bridged on VM /
    // Cranelift until #391 lands. Flip to run_all once merged.
    let src = "f xs:L n target:n>L n;srt (x:n>n;abs -x target) xs";
    run_tree_only(src, "f", &["[1,5,10,20]", "8"], "[10, 5, 1, 20]");
}

#[test]
fn closure_capture_in_map() {
    // `map` with an inline transform that closes over `bump`.
    let src = "f xs:L n bump:n>L n;map (x:n>n;+x bump) xs";
    run_all(src, "f", &["[1,2,3]", "10"], "[11, 12, 13]");
}

#[test]
fn closure_capture_in_fld() {
    // `fld` with an inline reducer that closes over `weight`.
    let src = "f xs:L n weight:n>n;fld (a:n x:n>n;+a *x weight) xs 0";
    run_all(src, "f", &["[1,2,3,4]", "5"], "50");
}

#[test]
fn closure_capture_multiple_vars() {
    // Two captures: `lo` and `hi` both appear in the body. Both lift as
    // trailing params on the synthetic decl, and both flow as captures.
    let src = "f xs:L n lo:n hi:n>L n;flt (x:n>b;&(>=x lo) <=x hi) xs";
    run_all(src, "f", &["[1,3,5,7,9,11]", "3", "7"], "[3, 5, 7]");
}

#[test]
fn closure_capture_text_value() {
    // Capture a Text value, not just numbers. By-value snapshot semantics.
    let src = "f ws:L t prefix:t>L t;flt (w:t>b;has w prefix) ws";
    run_all(
        src,
        "f",
        &["[\"apple\",\"banana\",\"apricot\"]", "ap"],
        "[apple, apricot]",
    );
}

#[test]
fn closure_capture_by_value_snapshot() {
    // The capture is snapshot when the closure is constructed, not read
    // live at each call. We mutate the source local after the `srt` runs
    // (well — srt has already completed by then). This just exercises that
    // mutating the capture's source name post-construction is irrelevant
    // because srt already consumed it. The real check is value-equality.
    // TODO PR3c follow-up: srt closure dispatch is tree-bridged on VM /
    // Cranelift until #391 lands. Flip to run_all once merged.
    let src = "f xs:L n bias:n>L n;ys=srt (x:n>n;+x bias) xs;ys";
    run_tree_only(src, "f", &["[3,1,2]", "0"], "[1, 2, 3]");
}

// ── Phase 1 ctx-arg form is still supported alongside captures ─────────────

#[test]
fn ctx_arg_form_works_with_inline_lambda() {
    // Phase 1 capture rejection nudges users to ctx-arg form, which already
    // works for inline lambdas too — the lambda just takes an extra param.
    let src = "f xs:L n thr:n>L n;flt (x:n c:n>b;>x c) thr xs";
    run_all(src, "f", &["[1,5,3,8,2]", "4"], "[5, 8]");
}

// ── No regression: grouped parenthesised expressions still parse ───────────

#[test]
fn grouped_expression_still_parses() {
    // `(+a b)` is a grouped expression, not a lambda — no `ident:` and no
    // leading `>`. Must not trip the inline-lambda lookahead.
    let src = "f a:n b:n>n;*(+a b) 2";
    run_all(src, "f", &["3", "4"], "14");
}

// ── No regression: existing named-helper HOF call still works ──────────────

#[test]
fn named_helper_hof_unaffected() {
    // TODO PR3c follow-up: srt named-helper dispatch is tree-bridged on
    // VM / Cranelift until #391 lands. Flip to run_all once merged.
    let src = "k x:n>n;abs x\nf xs:L n>L n;srt k xs";
    run_tree_only(src, "f", &["[-3,1,-5,2]"], "[1, 2, -3, -5]");
}

// ── Lambda inside foreach body shadowing is honored ────────────────────────

#[test]
fn lambda_local_binding_shadows_nothing_outside() {
    // The `s` inside the lambda is a param, not a capture of any outer name.
    // Even though there's no outer `s`, this exercises the param/local
    // resolution path explicitly.
    // TODO PR3c follow-up: srt closure dispatch is tree-bridged on VM /
    // Cranelift until #391 lands. Flip to run_all once merged.
    let src = "f ws:L t>L t;srt (s:t>n;n=len s;n) ws";
    run_tree_only(
        src,
        "f",
        &["[\"banana\",\"fig\",\"apple\"]"],
        "[fig, apple, banana]",
    );
}