use std::process::Command;
fn ilo() -> Command {
Command::new(env!("CARGO_BIN_EXE_ilo"))
}
#[test]
fn inline_single_func_bare_args() {
let out = ilo()
.args(["tot p:n q:n r:n>n;s=*p q;t=*s r;+s t", "10", "20", "30"])
.output()
.expect("failed to run ilo");
assert!(out.status.success(), "stderr: {}", String::from_utf8_lossy(&out.stderr));
assert_eq!(String::from_utf8_lossy(&out.stdout).trim(), "6200");
}
#[test]
fn inline_no_args_outputs_ast() {
let out = ilo()
.args(["tot p:n q:n r:n>n;s=*p q;t=*s r;+s t"])
.output()
.expect("failed to run ilo");
assert!(out.status.success());
let stdout = String::from_utf8_lossy(&out.stdout);
assert!(stdout.contains("\"name\""), "expected AST JSON, got: {}", stdout);
}
#[test]
fn inline_multi_func_select_by_name() {
let out = ilo()
.args(["dbl x:n>n;s=*x 2;+s 0 tot p:n q:n r:n>n;s=*p q;t=*s r;+s t", "tot", "10", "20", "30"])
.output()
.expect("failed to run ilo");
assert!(out.status.success(), "stderr: {}", String::from_utf8_lossy(&out.stderr));
assert_eq!(String::from_utf8_lossy(&out.stdout).trim(), "6200");
}
#[test]
fn inline_multi_func_first_by_default() {
let out = ilo()
.args(["dbl x:n>n;s=*x 2;+s 0 tot p:n q:n r:n>n;s=*p q;t=*s r;+s t", "5"])
.output()
.expect("failed to run ilo");
assert!(out.status.success(), "stderr: {}", String::from_utf8_lossy(&out.stderr));
assert_eq!(String::from_utf8_lossy(&out.stdout).trim(), "10");
}
#[test]
fn inline_emit_python() {
let out = ilo()
.args(["tot p:n q:n r:n>n;s=*p q;t=*s r;+s t", "--emit", "python"])
.output()
.expect("failed to run ilo");
assert!(out.status.success());
let stdout = String::from_utf8_lossy(&out.stdout);
assert!(stdout.contains("def tot"), "expected 'def tot', got: {}", stdout);
}
#[test]
fn inline_explicit_run() {
let out = ilo()
.args(["tot p:n q:n r:n>n;s=*p q;t=*s r;+s t", "--run", "tot", "10", "20", "30"])
.output()
.expect("failed to run ilo");
assert!(out.status.success(), "stderr: {}", String::from_utf8_lossy(&out.stderr));
assert_eq!(String::from_utf8_lossy(&out.stdout).trim(), "6200");
}
#[test]
fn no_args_shows_usage() {
let out = ilo()
.output()
.expect("failed to run ilo");
assert!(!out.status.success());
let stderr = String::from_utf8_lossy(&out.stderr);
assert!(stderr.contains("Usage"), "expected usage message, got: {}", stderr);
}
#[test]
fn inline_empty_string_errors() {
let out = ilo()
.args([""])
.output()
.expect("failed to run ilo");
assert!(!out.status.success());
}
#[test]
fn inline_invalid_code_errors() {
let out = ilo()
.args(["this is not valid ilo code @@##$$"])
.output()
.expect("failed to run ilo");
assert!(!out.status.success());
let stderr = String::from_utf8_lossy(&out.stderr);
assert!(!stderr.is_empty(), "expected error on stderr");
}
#[test]
fn file_bare_args_runs_first_func() {
let out = ilo()
.args(["research/explorations/idea9-ultra-dense-short/01-simple-function.ilo", "10", "20", "0.1"])
.output()
.expect("failed to run ilo");
assert!(out.status.success(), "stderr: {}", String::from_utf8_lossy(&out.stderr));
assert_eq!(String::from_utf8_lossy(&out.stdout).trim(), "220");
}
#[test]
fn file_no_args_outputs_ast() {
let out = ilo()
.args(["research/explorations/idea9-ultra-dense-short/01-simple-function.ilo"])
.output()
.expect("failed to run ilo");
assert!(out.status.success());
let stdout = String::from_utf8_lossy(&out.stdout);
assert!(stdout.contains("\"name\""), "expected AST JSON, got: {}", stdout);
}
#[test]
fn inline_nested_prefix() {
let out = ilo()
.args(["f a:n b:n c:n>n;+*a b c", "2", "3", "4"])
.output()
.expect("failed to run ilo");
assert!(out.status.success(), "stderr: {}", String::from_utf8_lossy(&out.stderr));
assert_eq!(String::from_utf8_lossy(&out.stdout).trim(), "10");
}
#[test]
fn inline_run_vm_mode() {
let out = ilo()
.args(["f x:n>n;*x 2", "--run-vm", "f", "5"])
.output()
.expect("failed to run ilo");
assert!(out.status.success(), "stderr: {}", String::from_utf8_lossy(&out.stderr));
assert_eq!(String::from_utf8_lossy(&out.stdout).trim(), "10");
}
#[test]
fn inline_run_with_func_name() {
let out = ilo()
.args(["f x:n>n;*x 2", "--run", "f", "5"])
.output()
.expect("failed to run ilo");
assert!(out.status.success(), "stderr: {}", String::from_utf8_lossy(&out.stderr));
assert_eq!(String::from_utf8_lossy(&out.stdout).trim(), "10");
}
#[test]
fn inline_emit_unknown_target() {
let out = ilo()
.args(["f x:n>n;*x 2", "--emit", "javascript"])
.output()
.expect("failed to run ilo");
assert!(!out.status.success());
let stderr = String::from_utf8_lossy(&out.stderr);
assert!(stderr.contains("Unknown emit target"), "expected emit error, got: {}", stderr);
}
#[test]
fn inline_parse_bool_arg() {
let out = ilo()
.args(["f x:b>b;!x", "true"])
.output()
.expect("failed to run ilo");
assert!(out.status.success(), "stderr: {}", String::from_utf8_lossy(&out.stderr));
assert_eq!(String::from_utf8_lossy(&out.stdout).trim(), "false");
}
#[test]
fn inline_parse_false_arg() {
let out = ilo()
.args(["f x:b>b;x", "false"])
.output()
.expect("failed to run ilo");
assert!(out.status.success(), "stderr: {}", String::from_utf8_lossy(&out.stderr));
assert_eq!(String::from_utf8_lossy(&out.stdout).trim(), "false");
}
#[test]
fn inline_parse_text_arg() {
let out = ilo()
.args(["f x:t>t;x", "hello"])
.output()
.expect("failed to run ilo");
assert!(out.status.success(), "stderr: {}", String::from_utf8_lossy(&out.stderr));
assert_eq!(String::from_utf8_lossy(&out.stdout).trim(), "hello");
}
#[test]
fn inline_parse_error() {
let out = ilo()
.args(["f x:>n;x", "5"])
.output()
.expect("failed to run ilo");
assert!(!out.status.success());
let stderr = String::from_utf8_lossy(&out.stderr);
assert!(stderr.contains("Parse error") || stderr.contains("error"), "expected parse error, got: {}", stderr);
}
#[test]
fn inline_bench_mode() {
let out = ilo()
.args(["f x:n>n;*x 2", "--bench", "f", "5"])
.output()
.expect("failed to run ilo");
assert!(out.status.success(), "stderr: {}", String::from_utf8_lossy(&out.stderr));
let stdout = String::from_utf8_lossy(&out.stdout);
assert!(stdout.contains("interpreter") || stdout.contains("vm"), "expected benchmark output, got: {}", stdout);
}
#[test]
fn help_flag_shows_usage() {
let out = ilo()
.args(["--help"])
.output()
.expect("failed to run ilo");
assert!(out.status.success());
let stdout = String::from_utf8_lossy(&out.stdout);
assert!(stdout.contains("Backends:"), "expected backends section, got: {}", stdout);
}
#[test]
fn help_short_flag_shows_usage() {
let out = ilo()
.args(["-h"])
.output()
.expect("failed to run ilo");
assert!(out.status.success());
let stdout = String::from_utf8_lossy(&out.stdout);
assert!(stdout.contains("Backends:"), "expected backends section, got: {}", stdout);
}
#[test]
fn inline_list_arg_bracketed() {
let out = ilo()
.args(["f xs:L n>n;len xs", "[1,2,3]"])
.output()
.expect("failed to run ilo");
assert!(out.status.success(), "stderr: {}", String::from_utf8_lossy(&out.stderr));
assert_eq!(String::from_utf8_lossy(&out.stdout).trim(), "3");
}
#[test]
fn inline_list_arg_bracketed_index() {
let out = ilo()
.args(["f xs:L n>n;xs.0", "[10,20,30]"])
.output()
.expect("failed to run ilo");
assert!(out.status.success(), "stderr: {}", String::from_utf8_lossy(&out.stderr));
assert_eq!(String::from_utf8_lossy(&out.stdout).trim(), "10");
}
#[test]
fn inline_list_arg_bare_comma() {
let out = ilo()
.args(["f xs:L n>n;len xs", "1,2,3"])
.output()
.expect("failed to run ilo");
assert!(out.status.success(), "stderr: {}", String::from_utf8_lossy(&out.stderr));
assert_eq!(String::from_utf8_lossy(&out.stdout).trim(), "3");
}
#[test]
fn inline_list_arg_bare_comma_index() {
let out = ilo()
.args(["f xs:L n>n;xs.0", "10,20,30"])
.output()
.expect("failed to run ilo");
assert!(out.status.success(), "stderr: {}", String::from_utf8_lossy(&out.stderr));
assert_eq!(String::from_utf8_lossy(&out.stdout).trim(), "10");
}
#[test]
fn help_shows_usage() {
let out = ilo()
.args(["help"])
.output()
.expect("failed to run ilo");
assert!(out.status.success());
let stdout = String::from_utf8_lossy(&out.stdout);
assert!(stdout.contains("Backends:"), "expected backends section, got: {}", stdout);
assert!(stdout.contains("--run-interp"), "expected --run-interp, got: {}", stdout);
}
#[test]
fn help_lang_shows_spec() {
let out = ilo()
.args(["help", "lang"])
.output()
.expect("failed to run ilo");
assert!(out.status.success());
let stdout = String::from_utf8_lossy(&out.stdout);
assert!(stdout.contains("ilo Language Spec"), "expected spec header, got: {}", stdout);
}
#[test]
fn inline_run_interp() {
let out = ilo()
.args(["f x:n>n;*x 2", "--run-interp", "f", "5"])
.output()
.expect("failed to run ilo");
assert!(out.status.success(), "stderr: {}", String::from_utf8_lossy(&out.stderr));
assert_eq!(String::from_utf8_lossy(&out.stdout).trim(), "10");
}
#[test]
fn inline_run_cranelift() {
let out = ilo()
.args(["f x:n>n;*x 2", "--run-cranelift", "f", "5"])
.output()
.expect("failed to run ilo");
assert!(out.status.success(), "stderr: {}", String::from_utf8_lossy(&out.stderr));
assert_eq!(String::from_utf8_lossy(&out.stdout).trim(), "10");
}
#[test]
fn default_falls_back_for_non_numeric() {
let out = ilo()
.args(["f x:b>b;!x", "true"])
.output()
.expect("failed to run ilo");
assert!(out.status.success(), "stderr: {}", String::from_utf8_lossy(&out.stderr));
assert_eq!(String::from_utf8_lossy(&out.stdout).trim(), "false");
}
#[test]
fn legacy_e_flag_still_works() {
let out = ilo()
.args(["-e", "tot p:n q:n r:n>n;s=*p q;t=*s r;+s t", "--run", "tot", "10", "20", "30"])
.output()
.expect("failed to run ilo");
assert!(out.status.success(), "stderr: {}", String::from_utf8_lossy(&out.stderr));
assert_eq!(String::from_utf8_lossy(&out.stdout).trim(), "6200");
}
#[test]
fn legacy_e_flag_missing_code() {
let out = ilo()
.args(["-e"])
.output()
.expect("failed to run ilo");
assert!(!out.status.success());
let stderr = String::from_utf8_lossy(&out.stderr);
assert!(stderr.contains("Usage"), "expected usage message, got: {}", stderr);
}
#[test]
fn verify_undefined_variable() {
let out = ilo()
.args(["--text", "f x:n>n;*y 2", "5"])
.output()
.expect("failed to run ilo");
assert!(!out.status.success());
let stderr = String::from_utf8_lossy(&out.stderr);
assert!(stderr.contains("error[ILO-T004]"), "expected error in stderr, got: {}", stderr);
assert!(stderr.contains("undefined variable 'y'"), "expected undefined var error, got: {}", stderr);
}
#[test]
fn verify_undefined_function() {
let out = ilo()
.args(["--text", "f x:n>n;foo x", "5"])
.output()
.expect("failed to run ilo");
assert!(!out.status.success());
let stderr = String::from_utf8_lossy(&out.stderr);
assert!(stderr.contains("error[ILO-T005]"), "expected error in stderr, got: {}", stderr);
assert!(stderr.contains("undefined function 'foo'"), "expected undefined func error, got: {}", stderr);
}
#[test]
fn verify_arity_mismatch() {
let out = ilo()
.args(["--text", "g a:n b:n>n;+a b f x:n>n;g x", "5"])
.output()
.expect("failed to run ilo");
assert!(!out.status.success());
let stderr = String::from_utf8_lossy(&out.stderr);
assert!(stderr.contains("arity mismatch"), "expected arity error, got: {}", stderr);
}
#[test]
fn verify_type_mismatch() {
let out = ilo()
.args(["--text", "f x:t>n;*x 2", "hello"])
.output()
.expect("failed to run ilo");
assert!(!out.status.success());
let stderr = String::from_utf8_lossy(&out.stderr);
assert!(stderr.contains("error[ILO-T009]"), "expected error in stderr, got: {}", stderr);
}
#[test]
fn verify_valid_program_runs() {
let out = ilo()
.args(["f x:n>n;*x 2", "5"])
.output()
.expect("failed to run ilo");
assert!(out.status.success(), "stderr: {}", String::from_utf8_lossy(&out.stderr));
assert_eq!(String::from_utf8_lossy(&out.stdout).trim(), "10");
}
#[test]
fn inline_factorial_with_prefix_call_arg() {
let out = ilo()
.args(["fac n:n>n;<=n 1{1};r=fac -n 1;*n r", "5"])
.output()
.expect("failed to run ilo");
assert!(out.status.success(), "stderr: {}", String::from_utf8_lossy(&out.stderr));
assert_eq!(String::from_utf8_lossy(&out.stdout).trim(), "120");
}
#[test]
fn inline_fibonacci_with_prefix_call_args() {
let out = ilo()
.args(["fib n:n>n;<=n 1{n};a=fib -n 1;b=fib -n 2;+a b", "10"])
.output()
.expect("failed to run ilo");
assert!(out.status.success(), "stderr: {}", String::from_utf8_lossy(&out.stderr));
assert_eq!(String::from_utf8_lossy(&out.stdout).trim(), "55");
}
#[test]
fn inline_call_with_nested_prefix_unchanged() {
let out = ilo()
.args(["f a:n b:n c:n>n;+*a b c", "2", "3", "4"])
.output()
.expect("failed to run ilo");
assert!(out.status.success(), "stderr: {}", String::from_utf8_lossy(&out.stderr));
assert_eq!(String::from_utf8_lossy(&out.stdout).trim(), "10");
}
#[test]
fn json_flag_produces_json_error() {
let out = ilo()
.args(["--json", "not-valid-ilo!!!"])
.output()
.expect("failed to run ilo");
assert!(!out.status.success());
let stderr = String::from_utf8_lossy(&out.stderr);
let v: serde_json::Value = serde_json::from_str(stderr.trim())
.unwrap_or_else(|_| panic!("expected JSON on stderr, got: {}", stderr));
assert_eq!(v["severity"], "error");
}
#[test]
fn text_flag_produces_plain_error() {
let out = ilo()
.args(["--text", "f x:n>n;+x \"hi\""])
.output()
.expect("failed to run ilo");
assert!(!out.status.success());
let stderr = String::from_utf8_lossy(&out.stderr);
assert!(stderr.contains("error["), "expected 'error[' in stderr: {}", stderr);
assert!(!stderr.contains("\x1b["), "unexpected ANSI codes in text mode: {}", stderr);
}
#[test]
fn ansi_flag_produces_colored_error() {
let out = ilo()
.args(["--ansi", "f x:n>n;+x \"hi\""])
.output()
.expect("failed to run ilo");
assert!(!out.status.success());
let stderr = String::from_utf8_lossy(&out.stderr);
assert!(stderr.contains("error"), "expected error in stderr: {}", stderr);
assert!(stderr.contains("\x1b["), "expected ANSI codes in ansi mode: {}", stderr);
}
#[test]
fn json_flag_parse_error_has_span() {
let out = ilo()
.args(["--json", "42 x:n>n;x"])
.output()
.expect("failed to run ilo");
assert!(!out.status.success());
let stderr = String::from_utf8_lossy(&out.stderr);
let first_line = stderr.lines().next()
.unwrap_or_else(|| panic!("expected output on stderr, got empty"));
let v: serde_json::Value = serde_json::from_str(first_line)
.unwrap_or_else(|_| panic!("expected JSON on first line of stderr, got: {}", stderr));
assert_eq!(v["severity"], "error");
assert!(v["labels"].as_array().is_some_and(|l| !l.is_empty()),
"expected labels in: {}", stderr);
}
#[test]
fn text_flag_verify_error_has_function_note() {
let out = ilo()
.args(["--text", "f x:n>n;+x \"hi\""])
.output()
.expect("failed to run ilo");
assert!(!out.status.success());
let stderr = String::from_utf8_lossy(&out.stderr);
assert!(stderr.contains("note:"), "expected note in stderr: {}", stderr);
assert!(stderr.contains("'f'"), "expected function name in stderr: {}", stderr);
}
#[test]
fn mutual_exclusion_json_text() {
let out = ilo()
.args(["--json", "--text", "f x:n>n;x"])
.output()
.expect("failed to run ilo");
assert!(!out.status.success());
let stderr = String::from_utf8_lossy(&out.stderr);
assert!(stderr.contains("mutually exclusive"), "expected mutual exclusion error: {}", stderr);
}
#[test]
fn no_color_env_produces_no_ansi() {
let out = ilo()
.args(["f x:n>n;+x \"hi\""])
.env("NO_COLOR", "1")
.output()
.expect("failed to run ilo");
assert!(!out.status.success());
let stderr = String::from_utf8_lossy(&out.stderr);
assert!(!stderr.contains("\x1b["), "unexpected ANSI codes with NO_COLOR: {}", stderr);
}
#[test]
fn help_ai_subcommand_exits_success() {
let out = ilo()
.args(["help", "ai"])
.output()
.expect("failed to run ilo");
assert!(out.status.success(), "stderr: {}", String::from_utf8_lossy(&out.stderr));
}
#[test]
fn ai_flag_exits_success() {
let out = ilo()
.args(["-ai"])
.output()
.expect("failed to run ilo");
assert!(out.status.success(), "stderr: {}", String::from_utf8_lossy(&out.stderr));
}
#[test]
fn help_ai_and_ai_flag_produce_same_output() {
let out1 = ilo().args(["help", "ai"]).output().expect("failed to run ilo");
let out2 = ilo().args(["-ai"]).output().expect("failed to run ilo");
assert_eq!(out1.stdout, out2.stdout, "help ai and -ai should produce identical output");
}
#[test]
fn help_ai_contains_no_blank_lines() {
let out = ilo().args(["help", "ai"]).output().expect("failed to run ilo");
let stdout = String::from_utf8_lossy(&out.stdout);
for line in stdout.lines() {
assert!(!line.trim().is_empty(), "unexpected blank line in compact spec");
}
}
#[test]
fn help_ai_strips_code_fences() {
let out = ilo().args(["help", "ai"]).output().expect("failed to run ilo");
let stdout = String::from_utf8_lossy(&out.stdout);
for line in stdout.lines() {
assert!(!line.trim_start().starts_with("```"), "code fence found in compact spec: {}", line);
}
}
#[test]
fn help_ai_strips_horizontal_rules() {
let out = ilo().args(["help", "ai"]).output().expect("failed to run ilo");
let stdout = String::from_utf8_lossy(&out.stdout);
for line in stdout.lines() {
assert!(line.trim() != "---", "horizontal rule found in compact spec");
}
}
#[test]
fn help_ai_preserves_key_content() {
let out = ilo().args(["help", "ai"]).output().expect("failed to run ilo");
let stdout = String::from_utf8_lossy(&out.stdout);
assert!(stdout.contains("fac n:n>n"), "missing factorial pattern");
assert!(stdout.contains("FUNCTIONS:"), "missing FUNCTIONS section");
assert!(stdout.contains("TYPES:"), "missing TYPES section");
assert!(stdout.contains("OPERATORS:"), "missing OPERATORS section");
}
#[test]
fn help_ai_is_smaller_than_full_spec() {
let full = ilo().args(["help", "lang"]).output().expect("failed to run ilo");
let compact = ilo().args(["help", "ai"]).output().expect("failed to run ilo");
assert!(
compact.stdout.len() < full.stdout.len(),
"compact spec ({} bytes) should be smaller than full spec ({} bytes)",
compact.stdout.len(), full.stdout.len()
);
}
#[test]
fn version_flag() {
let out = ilo().args(["--version"]).output().expect("failed to run ilo");
assert!(out.status.success(), "stderr: {}", String::from_utf8_lossy(&out.stderr));
let stdout = String::from_utf8_lossy(&out.stdout);
assert!(stdout.contains("ilo "), "expected version string, got: {stdout}");
}
#[test]
fn version_flag_short() {
let out = ilo().args(["-V"]).output().expect("failed to run ilo");
assert!(out.status.success(), "stderr: {}", String::from_utf8_lossy(&out.stderr));
let stdout = String::from_utf8_lossy(&out.stdout);
assert!(stdout.contains("ilo "), "expected version string, got: {stdout}");
}
#[test]
fn explain_known_code() {
let out = ilo().args(["--explain", "ILO-T005"]).output().expect("failed to run ilo");
assert!(out.status.success(), "stderr: {}", String::from_utf8_lossy(&out.stderr));
let stdout = String::from_utf8_lossy(&out.stdout);
assert!(stdout.contains("ILO-T005"), "expected explanation, got: {stdout}");
}
#[test]
fn explain_unknown_code() {
let out = ilo().args(["--explain", "ILO-XXXX"]).output().expect("failed to run ilo");
assert!(!out.status.success(), "should exit with error for unknown code");
let stderr = String::from_utf8_lossy(&out.stderr);
assert!(stderr.contains("unknown error code"), "expected 'unknown error code' in stderr: {stderr}");
}
#[test]
fn explain_no_code_arg() {
let out = ilo().args(["--explain"]).output().expect("failed to run ilo");
assert!(!out.status.success(), "should exit with error when no code given");
let stderr = String::from_utf8_lossy(&out.stderr);
assert!(stderr.contains("Usage"), "expected 'Usage' in stderr: {stderr}");
}
#[test]
fn source_explain_fn_start() {
let out = ilo()
.args(["f x:n>n;+x 1", "--explain"])
.output()
.expect("failed to run ilo");
assert!(out.status.success(), "stderr: {}", String::from_utf8_lossy(&out.stderr));
let stdout = String::from_utf8_lossy(&out.stdout);
assert!(stdout.contains("fn start"), "expected 'fn start' annotation: {stdout}");
}
#[test]
fn source_explain_short_flag() {
let out = ilo()
.args(["f x:n>n;+x 1", "-x"])
.output()
.expect("failed to run ilo");
assert!(out.status.success(), "stderr: {}", String::from_utf8_lossy(&out.stderr));
let stdout = String::from_utf8_lossy(&out.stdout);
assert!(stdout.contains("fn start"), "expected 'fn start' annotation: {stdout}");
}
#[test]
fn source_explain_bind_annotation() {
let out = ilo()
.args(["f x:n>n;y=+x 1;y", "--explain"])
.output()
.expect("failed to run ilo");
assert!(out.status.success(), "stderr: {}", String::from_utf8_lossy(&out.stderr));
let stdout = String::from_utf8_lossy(&out.stdout);
assert!(stdout.contains("bind → y"), "expected 'bind → y': {stdout}");
}
#[test]
fn source_explain_guard_annotation() {
let out = ilo()
.args(["f x:n>n;<=x 0{x};+x 1", "--explain"])
.output()
.expect("failed to run ilo");
assert!(out.status.success(), "stderr: {}", String::from_utf8_lossy(&out.stderr));
let stdout = String::from_utf8_lossy(&out.stdout);
assert!(stdout.contains("guard"), "expected 'guard' annotation: {stdout}");
}
#[test]
fn source_explain_return_annotation() {
let out = ilo()
.args(["f x:n>n;+x 1", "--explain"])
.output()
.expect("failed to run ilo");
assert!(out.status.success(), "stderr: {}", String::from_utf8_lossy(&out.stderr));
let stdout = String::from_utf8_lossy(&out.stdout);
assert!(stdout.contains("return"), "expected 'return' annotation: {stdout}");
}
#[test]
fn inline_trm_basic() {
let out = ilo()
.args(["f s:t>t;trm s", " hello "])
.output()
.expect("failed to run ilo");
assert!(out.status.success(), "stderr: {}", String::from_utf8_lossy(&out.stderr));
assert_eq!(String::from_utf8_lossy(&out.stdout).trim(), "hello");
}
#[test]
fn inline_unq_text() {
let out = ilo()
.args(["f s:t>t;unq s", "aabbc"])
.output()
.expect("failed to run ilo");
assert!(out.status.success(), "stderr: {}", String::from_utf8_lossy(&out.stderr));
assert_eq!(String::from_utf8_lossy(&out.stdout).trim(), "abc");
}
#[test]
fn inline_fmt_basic() {
let out = ilo()
.args([r#"f a:t b:t>t;fmt "{} and {}" a b"#, "foo", "bar"])
.output()
.expect("failed to run ilo");
assert!(out.status.success(), "stderr: {}", String::from_utf8_lossy(&out.stderr));
assert_eq!(String::from_utf8_lossy(&out.stdout).trim(), "foo and bar");
}
#[cfg(all(target_arch = "aarch64", target_os = "macos"))]
#[test]
fn run_jit_arm64_numeric() {
let out = ilo()
.args(["f x:n>n;*x 2", "--run-jit", "f", "5"])
.output()
.expect("failed to run ilo");
if out.status.success() {
let stdout = String::from_utf8_lossy(&out.stdout);
assert_eq!(stdout.trim(), "10", "expected 10, got: {stdout}");
}
}
#[cfg(all(target_arch = "aarch64", target_os = "macos"))]
#[test]
fn run_jit_arm64_no_arg() {
let out = ilo()
.args(["f>n;42", "--run-jit", "f"])
.output()
.expect("failed to run ilo");
if out.status.success() {
assert_eq!(String::from_utf8_lossy(&out.stdout).trim(), "42");
}
}
#[cfg(all(target_arch = "aarch64", target_os = "macos"))]
#[test]
fn run_jit_arm64_addition() {
let out = ilo()
.args(["f x:n y:n>n;+x y", "--run-jit", "f", "3", "4"])
.output()
.expect("failed to run ilo");
if out.status.success() {
assert_eq!(String::from_utf8_lossy(&out.stdout).trim(), "7");
}
}
#[cfg(all(target_arch = "aarch64", target_os = "macos"))]
#[test]
fn run_jit_arm64_subtraction() {
let out = ilo()
.args(["f x:n y:n>n;-x y", "--run-jit", "f", "10", "3"])
.output()
.expect("failed to run ilo");
if out.status.success() {
assert_eq!(String::from_utf8_lossy(&out.stdout).trim(), "7");
}
}
#[cfg(all(target_arch = "aarch64", target_os = "macos"))]
#[test]
fn run_jit_arm64_division_nn() {
let out = ilo()
.args(["f x:n y:n>n;/x y", "--run-jit", "f", "1", "3"])
.output()
.expect("failed to run ilo");
if out.status.success() {
let stdout = String::from_utf8_lossy(&out.stdout);
assert!(stdout.trim().starts_with("0.333"), "expected 0.333…, got: {stdout}");
}
}
#[cfg(all(target_arch = "aarch64", target_os = "macos"))]
#[test]
fn run_jit_arm64_addk() {
let out = ilo()
.args(["f x:n>n;+x 1", "--run-jit", "f", "5"])
.output()
.expect("failed to run ilo");
if out.status.success() {
assert_eq!(String::from_utf8_lossy(&out.stdout).trim(), "6");
}
}
#[cfg(all(target_arch = "aarch64", target_os = "macos"))]
#[test]
fn run_jit_arm64_subk() {
let out = ilo()
.args(["f x:n>n;-x 1", "--run-jit", "f", "5"])
.output()
.expect("failed to run ilo");
if out.status.success() {
assert_eq!(String::from_utf8_lossy(&out.stdout).trim(), "4");
}
}
#[cfg(all(target_arch = "aarch64", target_os = "macos"))]
#[test]
fn run_jit_arm64_divk() {
let out = ilo()
.args(["f x:n>n;/x 2", "--run-jit", "f", "10"])
.output()
.expect("failed to run ilo");
if out.status.success() {
assert_eq!(String::from_utf8_lossy(&out.stdout).trim(), "5");
}
}
#[cfg(all(target_arch = "aarch64", target_os = "macos"))]
#[test]
fn run_jit_arm64_negate() {
let out = ilo()
.args(["f x:n>n;-x", "--run-jit", "f", "5"])
.output()
.expect("failed to run ilo");
if out.status.success() {
assert_eq!(String::from_utf8_lossy(&out.stdout).trim(), "-5");
}
}
#[cfg(all(target_arch = "aarch64", target_os = "macos"))]
#[test]
fn run_jit_arm64_not_eligible() {
let out = ilo()
.args(["f x:n y:n>b;=x y", "--run-jit", "f", "1", "1"])
.output()
.expect("failed to run ilo");
assert!(!out.status.success(), "should fail for non-eligible function");
let stderr = String::from_utf8_lossy(&out.stderr);
assert!(
stderr.contains("eligible") || stderr.contains("JIT"),
"expected eligibility error, got: {stderr}"
);
}
#[cfg(all(target_arch = "aarch64", target_os = "macos"))]
#[test]
fn run_jit_arm64_const_dedup() {
let out = ilo()
.args(["f x:n>n;a=+x 1;+a 1", "--run-jit", "f", "5"])
.output()
.expect("failed to run ilo");
if out.status.success() {
assert_eq!(String::from_utf8_lossy(&out.stdout).trim(), "7");
}
}
#[cfg(all(target_arch = "aarch64", target_os = "macos"))]
#[test]
fn run_jit_arm64_3_args() {
let out = ilo()
.args(["f x:n y:n z:n>n;+x y", "--run-jit", "f", "3", "4", "0"])
.output()
.expect("failed to run ilo");
if out.status.success() {
assert_eq!(String::from_utf8_lossy(&out.stdout).trim(), "7");
}
}
#[cfg(all(target_arch = "aarch64", target_os = "macos"))]
#[test]
fn run_jit_arm64_4_args() {
let out = ilo()
.args(["f x:n y:n z:n w:n>n;+x y", "--run-jit", "f", "3", "4", "0", "0"])
.output()
.expect("failed to run ilo");
if out.status.success() {
assert_eq!(String::from_utf8_lossy(&out.stdout).trim(), "7");
}
}
#[cfg(all(target_arch = "aarch64", target_os = "macos"))]
#[test]
fn run_jit_arm64_5_args() {
let out = ilo()
.args(["f x:n y:n z:n w:n p:n>n;+x y", "--run-jit", "f", "3", "4", "0", "0", "0"])
.output()
.expect("failed to run ilo");
if out.status.success() {
assert_eq!(String::from_utf8_lossy(&out.stdout).trim(), "7");
}
}
#[cfg(all(target_arch = "aarch64", target_os = "macos"))]
#[test]
fn run_jit_arm64_6_args() {
let out = ilo()
.args(["f x:n y:n z:n w:n p:n q:n>n;+x y", "--run-jit", "f", "3", "4", "0", "0", "0", "0"])
.output()
.expect("failed to run ilo");
if out.status.success() {
assert_eq!(String::from_utf8_lossy(&out.stdout).trim(), "7");
}
}
#[cfg(all(target_arch = "aarch64", target_os = "macos"))]
#[test]
fn run_jit_arm64_7_args() {
let out = ilo()
.args(["f x:n y:n z:n w:n p:n q:n r:n>n;+x y", "--run-jit", "f", "3", "4", "0", "0", "0", "0", "0"])
.output()
.expect("failed to run ilo");
if out.status.success() {
assert_eq!(String::from_utf8_lossy(&out.stdout).trim(), "7");
}
}
#[cfg(all(target_arch = "aarch64", target_os = "macos"))]
#[test]
fn run_jit_arm64_8_args() {
let out = ilo()
.args(["f x:n y:n z:n w:n p:n q:n r:n s:n>n;+x y", "--run-jit", "f", "3", "4", "0", "0", "0", "0", "0", "0"])
.output()
.expect("failed to run ilo");
if out.status.success() {
assert_eq!(String::from_utf8_lossy(&out.stdout).trim(), "7");
}
}
#[cfg(all(target_arch = "aarch64", target_os = "macos"))]
#[test]
fn run_jit_arm64_multiply_nn() {
let out = ilo()
.args(["f x:n y:n>n;*x y", "--run-jit", "f", "3", "4"])
.output()
.expect("failed to run ilo");
if out.status.success() {
assert_eq!(String::from_utf8_lossy(&out.stdout).trim(), "12");
}
}
#[cfg(not(all(target_arch = "aarch64", target_os = "macos")))]
#[test]
fn run_jit_unavailable_on_non_arm64() {
let out = ilo()
.args(["f x:n>n;*x 2", "--run-jit", "f", "5"])
.output()
.expect("failed to run ilo");
assert!(!out.status.success(), "should fail on non-arm64 platform");
let stderr = String::from_utf8_lossy(&out.stderr);
assert!(
stderr.contains("arm64") || stderr.contains("aarch64") || stderr.contains("JIT"),
"expected JIT unavailability message, got: {stderr}"
);
}
#[test]
fn run_vm_runtime_error() {
let out = ilo()
.args(["f>n;/1 0", "--run-vm", "f"])
.output()
.expect("failed to run ilo");
assert!(!out.status.success(), "should exit with error");
let stderr = String::from_utf8_lossy(&out.stderr);
assert!(
stderr.contains("division") || stderr.contains("zero") || stderr.contains("ILO"),
"expected runtime error in stderr: {stderr}"
);
}
#[test]
fn run_interp_runtime_error() {
let out = ilo()
.args(["f>n;/1 0", "--run-interp", "f"])
.output()
.expect("failed to run ilo");
assert!(!out.status.success(), "should exit with error");
let stderr = String::from_utf8_lossy(&out.stderr);
assert!(
stderr.contains("division") || stderr.contains("zero") || stderr.contains("ILO"),
"expected runtime error in stderr: {stderr}"
);
}
#[test]
fn typedef_in_func_names_filter() {
let out = ilo()
.args(["f x:n>n;+x 1\ntype point{x:n}", "5"])
.output()
.expect("failed to run ilo");
assert!(out.status.success(), "stderr: {}", String::from_utf8_lossy(&out.stderr));
let stdout = String::from_utf8_lossy(&out.stdout);
assert_eq!(stdout.trim(), "6", "expected 6, got: {stdout}");
}
#[test]
fn run_default_float_result() {
let out = ilo()
.args(["f x:n>n;/x 3", "2"])
.output()
.expect("failed to run ilo");
assert!(out.status.success(), "stderr: {}", String::from_utf8_lossy(&out.stderr));
let stdout = String::from_utf8_lossy(&out.stdout);
let val: f64 = stdout.trim().parse().expect("expected float output");
assert!((val - 2.0/3.0).abs() < 1e-6, "expected ~0.666, got: {val}");
}
#[cfg(not(feature = "llvm"))]
#[test]
fn run_llvm_not_enabled() {
let out = ilo()
.args(["f x:n>n;*x 2", "--run-llvm", "f", "5"])
.output()
.expect("failed to run ilo");
assert!(!out.status.success(), "should fail when LLVM not enabled");
let stderr = String::from_utf8_lossy(&out.stderr);
assert!(
stderr.contains("LLVM") || stderr.contains("llvm") || stderr.contains("not enabled"),
"expected LLVM not enabled message, got: {stderr}"
);
}
#[cfg(unix)]
#[test]
fn file_read_error() {
use std::os::unix::fs::PermissionsExt;
let dir = std::env::temp_dir();
let path = dir.join("ilo_test_unreadable.ilo");
std::fs::write(&path, "f>n;42").unwrap();
std::fs::set_permissions(&path, std::fs::Permissions::from_mode(0o000)).unwrap();
let out = ilo()
.arg(path.to_str().unwrap())
.output()
.expect("failed to run ilo");
std::fs::set_permissions(&path, std::fs::Permissions::from_mode(0o644)).unwrap();
std::fs::remove_file(&path).ok();
assert!(!out.status.success(), "should fail on unreadable file");
let stderr = String::from_utf8_lossy(&out.stderr);
assert!(
stderr.contains("Error reading") || stderr.contains("Permission"),
"expected read error, got: {stderr}"
);
}
#[cfg(feature = "cranelift")]
#[test]
fn run_cranelift_no_extra_args() {
let out = ilo()
.args(["f>n;42", "--run-cranelift", "f"])
.output()
.expect("failed to run ilo");
assert!(out.status.success(), "stderr: {}", String::from_utf8_lossy(&out.stderr));
assert_eq!(String::from_utf8_lossy(&out.stdout).trim(), "42");
}
#[cfg(feature = "cranelift")]
#[test]
fn run_cranelift_float_result() {
let out = ilo()
.args(["f x:n>n;/x 3", "--run-cranelift", "f", "2"])
.output()
.expect("failed to run ilo");
assert!(out.status.success(), "stderr: {}", String::from_utf8_lossy(&out.stderr));
let val: f64 = String::from_utf8_lossy(&out.stdout).trim().parse().expect("expected float");
assert!((val - 2.0/3.0).abs() < 1e-6, "expected ~0.666, got: {val}");
}
#[cfg(feature = "cranelift")]
#[test]
fn run_cranelift_not_eligible() {
let out = ilo()
.args(["f x:n>n;?x{1:2;_:3}", "--run-cranelift", "f", "5"])
.output()
.expect("failed to run ilo");
assert!(out.status.success(), "match should be JIT-eligible now, stderr: {}", String::from_utf8_lossy(&out.stderr));
let stdout = String::from_utf8_lossy(&out.stdout);
assert!(stdout.trim() == "3", "expected wildcard arm result 3, got: {stdout}");
}
#[test]
fn run_default_interpreter_error() {
let out = ilo()
.args(["f xs:L n>n;xs.0", "f", "[]"])
.output()
.expect("failed to run ilo");
assert!(out.status.success(), "JIT handles empty list index, stderr: {}", String::from_utf8_lossy(&out.stderr));
let stdout = String::from_utf8_lossy(&out.stdout);
assert!(stdout.trim() == "nil", "expected nil for empty list index, got: {stdout}");
}
#[test]
fn bench_simple_function() {
let out = ilo()
.args(["f>n;42", "--bench", "f"])
.output()
.expect("failed to run ilo");
assert!(out.status.success(), "stderr: {}", String::from_utf8_lossy(&out.stderr));
let stdout = String::from_utf8_lossy(&out.stdout);
assert!(stdout.contains("Rust interpreter"), "expected bench output, got: {stdout}");
assert!(stdout.contains("Register VM"), "expected VM bench output");
}
#[test]
fn bench_with_text_arg() {
let out = ilo()
.args(["f x:t>t;x", "--bench", "f", "hello"])
.output()
.expect("failed to run ilo");
assert!(out.status.success(), "stderr: {}", String::from_utf8_lossy(&out.stderr));
let stdout = String::from_utf8_lossy(&out.stdout);
assert!(stdout.contains("Rust interpreter"), "expected bench output, got: {stdout}");
}
#[test]
fn bench_with_bool_arg() {
let out = ilo()
.args(["f x:b>b;x", "--bench", "f", "true"])
.output()
.expect("failed to run ilo");
assert!(out.status.success(), "stderr: {}", String::from_utf8_lossy(&out.stderr));
let stdout = String::from_utf8_lossy(&out.stdout);
assert!(stdout.contains("Rust interpreter"), "expected bench output, got: {stdout}");
}
#[test]
fn bench_with_list_arg() {
let out = ilo()
.args(["f xs:L n>n;+xs.0 1", "--bench", "f", "[1,2,3]"])
.output()
.expect("failed to run ilo");
assert!(out.status.success(), "stderr: {}", String::from_utf8_lossy(&out.stderr));
let stdout = String::from_utf8_lossy(&out.stdout);
assert!(stdout.contains("Rust interpreter"), "expected bench output, got: {stdout}");
}
#[test]
fn bench_jit_float_result() {
let out = ilo()
.args(["f x:n>n;/x 2", "--bench", "f", "1"])
.output()
.expect("failed to run ilo");
assert!(out.status.success(), "stderr: {}", String::from_utf8_lossy(&out.stderr));
let stdout = String::from_utf8_lossy(&out.stdout);
assert!(stdout.contains("Rust interpreter"), "expected bench output, got: {stdout}");
if stdout.contains("Custom JIT") || stdout.contains("Cranelift JIT") {
assert!(stdout.contains("0.5"), "expected float result in JIT output, got: {stdout}");
}
}
#[test]
fn bench_jit_non_numeric_const() {
let out = ilo()
.args(["f x:n>n;y=\"hi\";x", "--bench", "f", "5"])
.output()
.expect("failed to run ilo");
assert!(out.status.success(), "stderr: {}", String::from_utf8_lossy(&out.stderr));
let stdout = String::from_utf8_lossy(&out.stdout);
assert!(stdout.contains("Rust interpreter"), "expected bench output, got: {stdout}");
#[cfg(feature = "cranelift")]
assert!(stdout.contains("Cranelift JIT"), "cranelift JIT should compile text-const fn with NanVal");
}
#[test]
fn bench_jit_move_different_regs() {
let out = ilo()
.args(["f x:n>n;?x{_:+x 1}", "--bench", "f", "7"])
.output()
.expect("failed to run ilo");
assert!(out.status.success(), "stderr: {}", String::from_utf8_lossy(&out.stderr));
let stdout = String::from_utf8_lossy(&out.stdout);
assert!(stdout.contains("Rust interpreter"), "expected bench output, got: {stdout}");
if stdout.contains("Custom JIT") || stdout.contains("Cranelift JIT") {
assert!(stdout.contains(" result: 8"), "expected result 8 in JIT output, got: {stdout}");
}
}
#[test]
fn run_default_no_functions_in_compiled() {
let out = ilo()
.args(["type pt{x:n}", "5"])
.output()
.expect("failed to run ilo");
let _ = out;
}
fn write_temp_ilo(content: &str) -> std::path::PathBuf {
use std::sync::atomic::{AtomicU64, Ordering};
static COUNTER: AtomicU64 = AtomicU64::new(0);
let dir = std::env::temp_dir();
let n = COUNTER.fetch_add(1, Ordering::Relaxed);
let path = dir.join(format!("ilo_test_{}_{}.ilo", std::process::id(), n));
std::fs::write(&path, content).expect("failed to write temp file");
path
}
#[test]
fn unwrap_ok_path_inline() {
let f = write_temp_ilo("outer x:n>R n t;~(inner! x)\ninner x:n>R n t;~x");
let out = ilo()
.args([f.to_str().unwrap(), "42"])
.output()
.expect("failed to run ilo");
std::fs::remove_file(&f).ok();
assert!(out.status.success(), "stderr: {}", String::from_utf8_lossy(&out.stderr));
assert_eq!(String::from_utf8_lossy(&out.stdout).trim(), "~42");
}
#[test]
fn unwrap_err_path_inline() {
let f = write_temp_ilo("outer x:n>R n t;~(inner! x)\ninner x:n>R n t;^\"fail\"");
let out = ilo()
.args([f.to_str().unwrap(), "42"])
.output()
.expect("failed to run ilo");
std::fs::remove_file(&f).ok();
assert!(out.status.success(), "stderr: {}", String::from_utf8_lossy(&out.stderr));
assert_eq!(String::from_utf8_lossy(&out.stdout).trim(), "^fail");
}
#[test]
fn unwrap_nested_propagation_inline() {
let f = write_temp_ilo("a x:n>R n t;~(b! x)\nb x:n>R n t;~(c! x)\nc x:n>R n t;^\"deep\"");
let out = ilo()
.args([f.to_str().unwrap(), "1"])
.output()
.expect("failed to run ilo");
std::fs::remove_file(&f).ok();
assert!(out.status.success(), "stderr: {}", String::from_utf8_lossy(&out.stderr));
assert_eq!(String::from_utf8_lossy(&out.stdout).trim(), "^deep");
}
#[test]
fn unwrap_formatter_roundtrip() {
let f = write_temp_ilo("outer x:n>R n t;~(inner! x)\ninner x:n>R n t;~x");
let out = ilo()
.args([f.to_str().unwrap(), "--fmt"])
.output()
.expect("failed to run ilo");
std::fs::remove_file(&f).ok();
assert!(out.status.success(), "stderr: {}", String::from_utf8_lossy(&out.stderr));
let stdout = String::from_utf8_lossy(&out.stdout);
assert!(stdout.contains("inner!"), "expected inner! in formatted output, got: {}", stdout);
}
#[test]
fn unwrap_verifier_t025() {
let f = write_temp_ilo("outer x:n>R n t;~(inner! x)\ninner x:n>n;x");
let out = ilo()
.args([f.to_str().unwrap()])
.output()
.expect("failed to run ilo");
std::fs::remove_file(&f).ok();
let stderr = String::from_utf8_lossy(&out.stderr);
assert!(stderr.contains("T025") || stderr.contains("not a Result"),
"expected T025 error, got: {}", stderr);
}
#[test]
fn unwrap_verifier_t026() {
let f = write_temp_ilo("outer x:n>n;(inner! x)\ninner x:n>R n t;~x");
let out = ilo()
.args([f.to_str().unwrap()])
.output()
.expect("failed to run ilo");
std::fs::remove_file(&f).ok();
let stderr = String::from_utf8_lossy(&out.stderr);
assert!(stderr.contains("T026") || stderr.contains("not a Result"),
"expected T026 error, got: {}", stderr);
}
#[test]
fn get_verifier_wrong_type() {
let out = ilo()
.args(["f x:n>R t t;get x"])
.output()
.expect("failed to run ilo");
let stderr = String::from_utf8_lossy(&out.stderr);
assert!(stderr.contains("T013") || stderr.contains("expects t"),
"expected type error for get with number, got: {}", stderr);
}
#[test]
fn dollar_parses_inline() {
let out = ilo()
.args([r#"f url:t>R t t;$url"#])
.output()
.expect("failed to run ilo");
assert!(out.status.success(), "stderr: {}", String::from_utf8_lossy(&out.stderr));
let stdout = String::from_utf8_lossy(&out.stdout);
assert!(stdout.contains("get"), "expected 'get' in AST output, got: {}", stdout);
}
#[test]
fn dollar_bang_parses_inline() {
let out = ilo()
.args([r#"f url:t>R t t;~($!url)"#])
.output()
.expect("failed to run ilo");
assert!(out.status.success(), "stderr: {}", String::from_utf8_lossy(&out.stderr));
let stdout = String::from_utf8_lossy(&out.stdout);
assert!(stdout.contains("get"), "expected 'get' in AST output, got: {}", stdout);
}
#[test]
fn post_verifier_wrong_type_url() {
let out = ilo()
.args(["f x:n body:t>R t t;post x body"])
.output()
.expect("failed to run ilo");
let stderr = String::from_utf8_lossy(&out.stderr);
assert!(
stderr.contains("T013") || stderr.contains("expects t"),
"expected type error for post with number url, got: {stderr}"
);
}
#[test]
fn post_verifier_wrong_type_body() {
let out = ilo()
.args(["f url:t x:n>R t t;post url x"])
.output()
.expect("failed to run ilo");
let stderr = String::from_utf8_lossy(&out.stderr);
assert!(
stderr.contains("T013") || stderr.contains("expects t"),
"expected type error for post with number body, got: {stderr}"
);
}
#[test]
fn post_returns_result_type() {
let out = ilo()
.args([r#"f url:t body:t>R t t;post url body"#])
.output()
.expect("failed to run ilo");
assert!(out.status.success(), "stderr: {}", String::from_utf8_lossy(&out.stderr));
}
#[test]
fn post_appears_in_ast() {
let out = ilo()
.args([r#"f url:t body:t>R t t;post url body"#])
.output()
.expect("failed to run ilo");
assert!(out.status.success(), "stderr: {}", String::from_utf8_lossy(&out.stderr));
let stdout = String::from_utf8_lossy(&out.stdout);
assert!(stdout.contains("post"), "expected 'post' in AST output, got: {stdout}");
}
#[test]
fn braceless_guard_classify_cases() {
let program = r#"cls sp:n>t;>=sp 1000 "gold";>=sp 500 "silver";"bronze""#;
for (input, expected) in [("1500", "gold"), ("750", "silver"), ("100", "bronze")] {
let out = ilo().args([program, input]).output().expect("failed to run ilo");
assert!(out.status.success(), "stderr: {}", String::from_utf8_lossy(&out.stderr));
assert_eq!(String::from_utf8_lossy(&out.stdout).trim(), expected);
}
}
#[test]
fn braceless_guard_factorial() {
let out = ilo()
.args(["fac n:n>n;<=n 1 1;r=fac -n 1;*n r", "5"])
.output()
.expect("failed to run ilo");
assert!(out.status.success(), "stderr: {}", String::from_utf8_lossy(&out.stderr));
assert_eq!(String::from_utf8_lossy(&out.stdout).trim(), "120");
}
#[test]
fn braceless_guard_fibonacci() {
let out = ilo()
.args(["fib n:n>n;<=n 1 n;a=fib -n 1;b=fib -n 2;+a b", "10"])
.output()
.expect("failed to run ilo");
assert!(out.status.success(), "stderr: {}", String::from_utf8_lossy(&out.stderr));
assert_eq!(String::from_utf8_lossy(&out.stdout).trim(), "55");
}
#[test]
fn braceless_guard_equivalent_to_braced() {
let braced = ilo()
.args([r#"cls sp:n>t;>=sp 1000{"gold"};>=sp 500{"silver"};"bronze""#, "1500"])
.output()
.expect("failed to run ilo");
let braceless = ilo()
.args([r#"cls sp:n>t;>=sp 1000 "gold";>=sp 500 "silver";"bronze""#, "1500"])
.output()
.expect("failed to run ilo");
assert_eq!(
String::from_utf8_lossy(&braced.stdout),
String::from_utf8_lossy(&braceless.stdout),
"braced and braceless should produce identical output"
);
}
#[test]
fn range_basic() {
let out = ilo()
.args(["f>n;@i 0..3{i}", "--run", "f"])
.output()
.expect("failed to run ilo");
assert!(out.status.success(), "stderr: {}", String::from_utf8_lossy(&out.stderr));
assert_eq!(String::from_utf8_lossy(&out.stdout).trim(), "2");
}
#[test]
fn range_with_arg() {
let out = ilo()
.args(["f n:n>n;@i 0..n{*i i}", "4"])
.output()
.expect("failed to run ilo");
assert!(out.status.success(), "stderr: {}", String::from_utf8_lossy(&out.stderr));
assert_eq!(String::from_utf8_lossy(&out.stdout).trim(), "9");
}
#[test]
fn range_empty() {
let out = ilo()
.args(["f>n;@i 5..2{99};0", "--run", "f"])
.output()
.expect("failed to run ilo");
assert!(out.status.success(), "stderr: {}", String::from_utf8_lossy(&out.stderr));
assert_eq!(String::from_utf8_lossy(&out.stdout).trim(), "0");
}
#[test]
fn alias_basic_run() {
let out = ilo()
.args(["-e", "alias res R n t\nf>res;~42", "--run", "f"])
.output()
.expect("failed to run ilo");
assert!(out.status.success(), "stderr: {}", String::from_utf8_lossy(&out.stderr));
assert_eq!(String::from_utf8_lossy(&out.stdout).trim(), "~42");
}
#[test]
fn alias_in_param_run() {
let out = ilo()
.args(["-e", "alias num n\nf x:num>num;+x 1", "--run", "f", "5"])
.output()
.expect("failed to run ilo");
assert!(out.status.success(), "stderr: {}", String::from_utf8_lossy(&out.stderr));
assert_eq!(String::from_utf8_lossy(&out.stdout).trim(), "6");
}
#[test]
fn use_imports_function_from_file() {
let lib = "/tmp/ilo_test_math.ilo";
let main_file = "/tmp/ilo_test_main.ilo";
std::fs::write(lib, "dbl n:n>n;*n 2\n").unwrap();
std::fs::write(main_file, "use \"ilo_test_math.ilo\"\nrun x:n>n;dbl x\n").unwrap();
let out = ilo()
.args([main_file, "--run", "run", "5"])
.output()
.expect("failed to run ilo");
let _ = std::fs::remove_file(lib);
let _ = std::fs::remove_file(main_file);
assert!(out.status.success(), "stderr: {}", String::from_utf8_lossy(&out.stderr));
assert_eq!(String::from_utf8_lossy(&out.stdout).trim(), "10");
}
#[test]
fn use_file_not_found_error() {
let main_file = "/tmp/ilo_test_missing_import.ilo";
std::fs::write(main_file, "use \"nonexistent_xyz.ilo\"\nf>n;1\n").unwrap();
let out = ilo()
.args([main_file])
.output()
.expect("failed to run ilo");
let _ = std::fs::remove_file(main_file);
assert!(!out.status.success());
let err = String::from_utf8_lossy(&out.stderr);
let combined = format!("{err}{}", String::from_utf8_lossy(&out.stdout));
assert!(combined.contains("ILO-P017") || combined.contains("not found") || combined.contains("nonexistent"), "got: {combined}");
}
#[test]
fn use_circular_import_error() {
let a = "/tmp/ilo_test_circ_a.ilo";
let b = "/tmp/ilo_test_circ_b.ilo";
std::fs::write(a, "use \"ilo_test_circ_b.ilo\"\nfa>n;1\n").unwrap();
std::fs::write(b, "use \"ilo_test_circ_a.ilo\"\nfb>n;2\n").unwrap();
let out = ilo()
.args([a])
.output()
.expect("failed to run ilo");
let _ = std::fs::remove_file(a);
let _ = std::fs::remove_file(b);
assert!(!out.status.success());
let err = String::from_utf8_lossy(&out.stderr);
let combined = format!("{err}{}", String::from_utf8_lossy(&out.stdout));
assert!(combined.contains("ILO-P018") || combined.contains("circular"), "got: {combined}");
}
#[test]
fn use_in_inline_code_error() {
let out = ilo()
.args(["-e", "use \"foo.ilo\"\nf>n;1", "--run", "f"])
.output()
.expect("failed to run ilo");
assert!(!out.status.success());
let err = String::from_utf8_lossy(&out.stderr);
let combined = format!("{err}{}", String::from_utf8_lossy(&out.stdout));
assert!(combined.contains("ILO-P017") || combined.contains("inline") || combined.contains("context"), "got: {combined}");
}
#[test]
fn use_parse_error_in_imported_file() {
let bad = "/tmp/ilo_test_parse_err_import.ilo";
let main_file = "/tmp/ilo_test_parse_err_main.ilo";
std::fs::write(bad, "f x:>n;x\n").unwrap(); std::fs::write(main_file, "use \"ilo_test_parse_err_import.ilo\"\ng x:n>n;+x 1\n").unwrap();
let out = ilo()
.args([main_file])
.output()
.expect("failed to run ilo");
let _ = std::fs::remove_file(bad);
let _ = std::fs::remove_file(main_file);
assert!(!out.status.success(), "should fail when imported file has parse error");
let stderr = String::from_utf8_lossy(&out.stderr);
assert!(stderr.contains("error") || stderr.contains("expected"),
"expected parse error diagnostic, got: {stderr}");
}
#[test]
fn use_transitive_imports() {
let file_b = "/tmp/ilo_test_trans_b.ilo";
let file_a = "/tmp/ilo_test_trans_a.ilo";
let file_main = "/tmp/ilo_test_trans_main.ilo";
std::fs::write(file_b, "triple x:n>n;*x 3\n").unwrap();
std::fs::write(file_a, "use \"ilo_test_trans_b.ilo\"\nsextuple x:n>n;t=triple x;*t 2\n").unwrap();
std::fs::write(file_main, "use \"ilo_test_trans_a.ilo\"\nmain x:n>n;sextuple x\n").unwrap();
let out = ilo()
.args([file_main, "--run", "main", "2"])
.output()
.expect("failed to run ilo");
let _ = std::fs::remove_file(file_b);
let _ = std::fs::remove_file(file_a);
let _ = std::fs::remove_file(file_main);
assert!(out.status.success(), "stderr: {}", String::from_utf8_lossy(&out.stderr));
assert_eq!(String::from_utf8_lossy(&out.stdout).trim(), "12");
}
#[test]
fn dense_flag_formats_code() {
let out = ilo()
.args(["f x:n>n;+x 1", "--dense"])
.output()
.expect("failed to run ilo");
assert!(out.status.success(), "stderr: {}", String::from_utf8_lossy(&out.stderr));
let stdout = String::from_utf8_lossy(&out.stdout);
assert!(stdout.contains("f"), "expected function name in dense output: {stdout}");
}
#[test]
fn dense_short_flag() {
let out = ilo()
.args(["f x:n>n;+x 1", "-d"])
.output()
.expect("failed to run ilo");
assert!(out.status.success(), "stderr: {}", String::from_utf8_lossy(&out.stderr));
assert!(String::from_utf8_lossy(&out.stdout).contains("f"));
}
#[test]
fn expanded_flag_formats_code() {
let out = ilo()
.args(["f x:n>n;+x 1", "--expanded"])
.output()
.expect("failed to run ilo");
assert!(out.status.success(), "stderr: {}", String::from_utf8_lossy(&out.stderr));
assert!(String::from_utf8_lossy(&out.stdout).contains("f"));
}
#[test]
fn json_flag_wraps_ok_result() {
let out = ilo()
.args(["--json", "f x:n>n;*x 2", "--run", "f", "5"])
.output()
.expect("failed to run ilo");
assert!(out.status.success(), "stderr: {}", String::from_utf8_lossy(&out.stderr));
let stdout = String::from_utf8_lossy(&out.stdout);
assert!(stdout.contains("\"ok\""), "expected JSON ok wrapper, got: {stdout}");
assert!(stdout.contains("10"), "expected result 10, got: {stdout}");
}
#[test]
fn json_flag_wraps_err_result() {
let out = ilo()
.args(["--json", "-e", "f>R n t;^\"oops\"", "--run", "f"])
.output()
.expect("failed to run ilo");
assert!(out.status.success(), "stderr: {}", String::from_utf8_lossy(&out.stderr));
let stdout = String::from_utf8_lossy(&out.stdout);
assert!(stdout.contains("\"error\""), "expected JSON error wrapper, got: {stdout}");
assert!(stdout.contains("program"), "expected 'program' phase, got: {stdout}");
}
#[test]
fn json_mode_cross_language_warning() {
let out = ilo()
.args(["--json", "f x:n>n;*x 2", "5"])
.env("NO_COLOR", "1")
.output()
.expect("failed to run ilo");
assert!(out.status.success() || !out.stderr.is_empty());
}
#[test]
fn run_cmd_tools_flag_missing_path() {
let out = ilo()
.args(["f>n;1", "--tools"])
.output()
.expect("failed to run ilo");
assert!(!out.status.success(), "expected failure when --tools has no path");
let stderr = String::from_utf8_lossy(&out.stderr);
assert!(stderr.contains("--tools"), "expected --tools error, got: {stderr}");
}
#[test]
fn run_cmd_mcp_flag_missing_path() {
let out = ilo()
.args(["f>n;1", "--mcp"])
.output()
.expect("failed to run ilo");
assert!(!out.status.success(), "expected failure when --mcp has no path");
let stderr = String::from_utf8_lossy(&out.stderr);
assert!(stderr.contains("--mcp"), "expected --mcp error, got: {stderr}");
}
#[test]
fn run_cmd_mcp_with_path_no_tools_feature() {
use std::io::Write;
let mut path = std::env::temp_dir();
path.push("ilo_run_mcp_test.json");
let mut f = std::fs::File::create(&path).expect("create temp file");
writeln!(f, r#"{{"mcpServers": {{}}}}"#).unwrap();
drop(f);
let out = ilo()
.args(["f>n;1", "--mcp", path.to_str().unwrap()])
.output()
.expect("failed to run ilo");
let stderr = String::from_utf8_lossy(&out.stderr);
assert!(!stderr.contains("thread 'main' panicked"), "unexpected panic: {stderr}");
std::fs::remove_file(&path).ok();
}
#[test]
fn run_cmd_verify_warning_unreachable_code() {
let out = ilo()
.env("NO_COLOR", "1")
.args(["f>n;ret 1;2", "f"])
.output()
.expect("failed to run ilo");
let stdout = String::from_utf8_lossy(&out.stdout);
let stderr = String::from_utf8_lossy(&out.stderr);
assert!(
stdout.trim() == "1" || stderr.contains("T029") || stderr.contains("unreachable") || stderr.contains("warn"),
"expected output=1 or ILO-T029 warning; stdout={stdout:?} stderr={stderr:?}"
);
}
#[test]
fn serv_cmd_empty_stdin_exits_cleanly() {
use std::process::Stdio;
let out = ilo()
.args(["serv"])
.stdin(Stdio::null()) .output()
.expect("failed to run ilo serv");
let stdout = String::from_utf8_lossy(&out.stdout);
assert!(stdout.contains("ready"), "expected ready signal, got: {stdout}");
}
#[test]
fn serv_cmd_processes_one_request() {
use std::io::Write;
use std::process::Stdio;
let mut child = ilo()
.args(["serv"])
.stdin(Stdio::piped())
.stdout(Stdio::piped())
.stderr(Stdio::piped())
.spawn()
.expect("failed to spawn ilo serv");
if let Some(mut stdin) = child.stdin.take() {
writeln!(stdin, r#"{{"program":"f>n;1","args":[],"func":"f"}}"#).unwrap();
}
let out = child.wait_with_output().expect("ilo serv failed");
let stdout = String::from_utf8_lossy(&out.stdout);
assert!(stdout.contains("ready"), "expected ready signal");
assert!(stdout.contains("ok") || stdout.contains("1"), "expected ok result, got: {stdout}");
}
#[test]
fn repl_exits_on_eof() {
use std::process::Stdio;
let out = ilo()
.args(["repl"])
.stdin(Stdio::null())
.output()
.expect("failed to run ilo repl");
let stdout = String::from_utf8_lossy(&out.stdout);
assert!(stdout.contains("ilo"), "expected banner from repl");
assert!(out.status.success(), "repl should exit cleanly on EOF");
}
#[test]
fn repl_json_mode_is_serv() {
use std::process::Stdio;
let out = ilo()
.args(["repl", "-j"])
.stdin(Stdio::null())
.output()
.expect("failed to run ilo repl -j");
let stdout = String::from_utf8_lossy(&out.stdout);
assert!(stdout.contains("ready"), "expected ready signal from repl -j");
}
#[test]
fn repl_define_and_run() {
use std::io::Write;
use std::process::Stdio;
let mut child = ilo()
.args(["repl"])
.stdin(Stdio::piped())
.stdout(Stdio::piped())
.stderr(Stdio::piped())
.spawn()
.expect("failed to spawn ilo repl");
{
let stdin = child.stdin.as_mut().unwrap();
writeln!(stdin, "dbl x:n>n;*x 2").unwrap();
writeln!(stdin, "dbl 21").unwrap();
writeln!(stdin, ":q").unwrap();
}
let out = child.wait_with_output().unwrap();
let stdout = String::from_utf8_lossy(&out.stdout);
assert!(stdout.contains("defined: dbl"), "should show definition: {stdout}");
assert!(stdout.contains("42"), "should compute dbl 21 = 42: {stdout}");
}
#[test]
fn serv_cmd_with_tools_config_loads_http() {
use std::io::Write;
use std::process::Stdio;
let mut path = std::env::temp_dir();
path.push("ilo_serv_test_tools.json");
let mut f = std::fs::File::create(&path).expect("create temp file");
writeln!(f, r#"{{"tools": {{"echo": {{"url": "http://127.0.0.1:19999/echo"}}}}}}"#).unwrap();
drop(f);
let out = ilo()
.args(["serv", "--tools", path.to_str().unwrap()])
.stdin(Stdio::null())
.output()
.expect("failed to run ilo serv --tools");
let stdout = String::from_utf8_lossy(&out.stdout);
assert!(stdout.contains("ready"), "expected ready signal with tools config");
std::fs::remove_file(&path).ok();
}
#[test]
fn serv_cmd_mcp_with_empty_config_exits_cleanly() {
use std::io::Write;
use std::process::Stdio;
let mut path = std::env::temp_dir();
path.push("ilo_serv_mcp_empty.json");
let mut f = std::fs::File::create(&path).expect("create temp file");
writeln!(f, r#"{{"mcpServers": {{}}}}"#).unwrap();
drop(f);
let out = ilo()
.args(["serv", "--mcp", path.to_str().unwrap()])
.stdin(Stdio::null())
.output()
.expect("failed to run ilo serv --mcp");
let stdout = String::from_utf8_lossy(&out.stdout);
let stderr = String::from_utf8_lossy(&out.stderr);
assert!(
stdout.contains("ready") || stderr.contains("tools"),
"expected ready or tools error, got stdout={stdout} stderr={stderr}"
);
std::fs::remove_file(&path).ok();
}
#[test]
fn serv_cmd_mcp_missing_path_exits_with_error() {
use std::process::Stdio;
let out = ilo()
.args(["serv", "--mcp"])
.stdin(Stdio::null())
.output()
.expect("failed to run ilo serv --mcp");
assert!(!out.status.success(), "expected non-zero exit");
let stderr = String::from_utf8_lossy(&out.stderr);
assert!(stderr.contains("--mcp"), "expected --mcp error, got: {stderr}");
}
#[test]
fn serv_cmd_tools_missing_path_exits_with_error() {
use std::process::Stdio;
let out = ilo()
.args(["serv", "--tools"])
.stdin(Stdio::null())
.output()
.expect("failed to run ilo serv --tools");
assert!(!out.status.success(), "expected non-zero exit");
let stderr = String::from_utf8_lossy(&out.stderr);
assert!(stderr.contains("--tools"), "expected --tools error, got: {stderr}");
}
#[test]
fn serv_cmd_tools_invalid_config_exits_with_error() {
use std::io::Write;
use std::process::Stdio;
let mut path = std::env::temp_dir();
path.push("ilo_serv_test_invalid_tools.json");
let mut f = std::fs::File::create(&path).expect("create temp file");
writeln!(f, "not valid json at all!!!").unwrap();
drop(f);
let out = ilo()
.args(["serv", "--tools", path.to_str().unwrap()])
.stdin(Stdio::null())
.output()
.expect("failed to run ilo serv --tools invalid");
assert!(!out.status.success(), "expected non-zero exit");
std::fs::remove_file(&path).ok();
}
#[test]
fn serv_cmd_skips_empty_stdin_lines() {
use std::io::Write;
use std::process::Stdio;
let mut child = ilo()
.args(["serv"])
.stdin(Stdio::piped())
.stdout(Stdio::piped())
.stderr(Stdio::piped())
.spawn()
.expect("failed to spawn ilo serv");
if let Some(mut stdin) = child.stdin.take() {
writeln!(stdin, "").unwrap();
writeln!(stdin, " ").unwrap();
writeln!(stdin, r#"{{"program":"f>n;42","args":[],"func":"f"}}"#).unwrap();
}
let out = child.wait_with_output().expect("ilo serv failed");
let stdout = String::from_utf8_lossy(&out.stdout);
assert!(stdout.contains("ready"), "expected ready signal");
assert!(stdout.contains("ok") || stdout.contains("42"), "expected result, got: {stdout}");
}
#[test]
fn tools_cmd_invalid_tools_config_exits_with_error() {
use std::io::Write;
let mut path = std::env::temp_dir();
path.push("ilo_tools_test_invalid.json");
let mut f = std::fs::File::create(&path).expect("create temp file");
writeln!(f, "{{bad json").unwrap();
drop(f);
let out = ilo()
.args(["tools", "--tools", path.to_str().unwrap()])
.output()
.expect("failed to run ilo tools --tools invalid");
assert!(!out.status.success(), "expected non-zero exit");
std::fs::remove_file(&path).ok();
}
#[test]
fn run_vm_with_tools_config() {
use std::io::Write;
let mut path = std::env::temp_dir();
path.push("ilo_vm_tools_test.json");
let mut f = std::fs::File::create(&path).expect("create temp file");
writeln!(f, r#"{{"tools": {{"echo": {{"url": "http://127.0.0.1:19999/echo"}}}}}}"#).unwrap();
drop(f);
let out = ilo()
.args(["f>n;99", "--run-vm", "f", "--tools", path.to_str().unwrap()])
.output()
.expect("failed to run ilo --run-vm --tools");
let stdout = String::from_utf8_lossy(&out.stdout);
assert!(out.status.success(), "stderr: {}", String::from_utf8_lossy(&out.stderr));
assert!(stdout.trim() == "99", "expected 99, got: {stdout}");
std::fs::remove_file(&path).ok();
}