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(["examples/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(["examples/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-tree"),
"expected --run-tree, 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_tree() {
let out = ilo()
.args(["f x:n>n;*x 2", "--run-tree", "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-tree", "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");
if path.exists() {
let _ = std::fs::set_permissions(&path, std::fs::Permissions::from_mode(0o644));
}
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_early_return_vs_braced_conditional() {
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(&braceless.stdout).trim(),
"gold",
"braceless guard should early-return"
);
let braced = 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).trim(),
"bronze",
"braced guard should be conditional execution (no early return)"
);
}
#[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();
}
#[test]
fn builtin_alias_length() {
let out = ilo()
.args(["f xs:L n>n;length 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 builtin_alias_filter() {
let out = ilo()
.args([
"pos x:n>b;>x 0 main xs:L n>L n;filter pos xs",
"main",
"-3,0,2,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(), "[2, 4]");
}
#[test]
fn builtin_alias_sort() {
let out = ilo()
.args(["f xs:L n>L n;sort xs", "3,1,2"])
.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(), "[1, 2, 3]");
}
#[test]
fn builtin_alias_reverse() {
let out = ilo()
.args(["f xs:L n>L n;reverse 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, 2, 1]");
}
#[test]
fn builtin_alias_trim() {
let out = ilo()
.args(["f s:t>t;trim 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 builtin_alias_average() {
let out = ilo()
.args(["f xs:L n>n;average xs", "2,4,6"])
.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(), "4");
}
#[test]
fn builtin_alias_hint_emitted() {
let out = ilo()
.args(["f xs:L n>n;length xs", "1,2,3"])
.output()
.expect("failed to run ilo");
let stderr = String::from_utf8_lossy(&out.stderr);
assert!(
stderr.contains("hint") && stderr.contains("length") && stderr.contains("len"),
"expected alias hint, got: {stderr}"
);
}
#[test]
fn builtin_alias_floor_and_ceil() {
let out = ilo()
.args(["f x:n>n;floor x", "3.7"])
.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");
let out = ilo()
.args(["f x:n>n;ceil x", "3.2"])
.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(), "4");
}
#[test]
fn builtin_alias_format() {
let out = ilo()
.args(["f>t;format \"{} + {} = {}\" 1 2 3", "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(), "1 + 2 = 3");
}
#[test]
fn builtin_alias_no_hint_suppressed() {
let out = ilo()
.args(["f xs:L n>n;length xs", "--no-hints", "1,2,3"])
.output()
.expect("failed to run ilo");
let stderr = String::from_utf8_lossy(&out.stderr);
assert!(
!stderr.contains("hint"),
"hints should be suppressed: {stderr}"
);
}
#[test]
fn alias_in_guard() {
let out = ilo()
.args(["f x:n>n;>=x 5{floor x}{ceil x}", "7.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(), "7");
}
#[test]
fn alias_in_guard_else() {
let out = ilo()
.args(["f x:n>n;>=x 5{floor x}{ceil x}", "3.2"])
.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(), "4");
}
#[test]
fn alias_in_let() {
let out = ilo()
.args(["f xs:L n>n;n=length xs;n", "5,6,7,8"])
.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(), "4");
}
#[test]
fn alias_in_foreach() {
let out = ilo()
.args(["f xs:L n>n;r=0;@x xs{r=+r (floor 1.9)};r", "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 alias_in_match_stmt() {
let out = ilo()
.args(["f x:n>n;?x{1:(floor 3.7);_: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(), "3");
}
#[test]
fn alias_in_list_literal() {
let out = ilo()
.args(["f x:n>L n;[floor x, ceil x]", "3.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(), "[3, 4]");
}
#[test]
fn alias_in_binop() {
let out = ilo()
.args(["f x:n>n;+(floor x) 10", "3.7"])
.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(), "13");
}
#[test]
fn alias_in_for_range() {
let out = ilo()
.args(["f n:n>n;r=0;@i 0..n{r=+r (floor 1.5)};r", "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");
}
fn run_repl(input: &str) -> std::process::Output {
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();
stdin.write_all(input.as_bytes()).unwrap();
}
child.wait_with_output().unwrap()
}
#[test]
fn repl_quit_q() {
let out = run_repl(":q\n");
assert!(
out.status.success(),
"stderr: {}",
String::from_utf8_lossy(&out.stderr)
);
}
#[test]
fn repl_quit_exit_command() {
let out = run_repl(":exit\n");
assert!(
out.status.success(),
"stderr: {}",
String::from_utf8_lossy(&out.stderr)
);
}
#[test]
fn repl_quit_x_command() {
let out = run_repl(":x\n");
assert!(
out.status.success(),
"stderr: {}",
String::from_utf8_lossy(&out.stderr)
);
}
#[test]
fn repl_quit_word_exit() {
let out = run_repl("exit\n");
assert!(
out.status.success(),
"stderr: {}",
String::from_utf8_lossy(&out.stderr)
);
}
#[test]
fn repl_quit_word_quit() {
let out = run_repl("quit\n");
assert!(
out.status.success(),
"stderr: {}",
String::from_utf8_lossy(&out.stderr)
);
}
#[test]
fn repl_eval_expression() {
let out = run_repl("+1 2\n:q\n");
assert!(
out.status.success(),
"stderr: {}",
String::from_utf8_lossy(&out.stderr)
);
let stdout = String::from_utf8_lossy(&out.stdout);
assert!(stdout.contains("3"), "expected 3 in output, got: {stdout}");
}
#[test]
fn repl_defs_empty() {
let out = run_repl(":defs\n:q\n");
assert!(
out.status.success(),
"stderr: {}",
String::from_utf8_lossy(&out.stderr)
);
let stdout = String::from_utf8_lossy(&out.stdout);
assert!(
stdout.contains("(no definitions)"),
"expected '(no definitions)', got: {stdout}"
);
}
#[test]
fn repl_defs_lists_functions() {
let out = run_repl("f x:n>n;x\n:defs\n:q\n");
assert!(
out.status.success(),
"stderr: {}",
String::from_utf8_lossy(&out.stderr)
);
let stdout = String::from_utf8_lossy(&out.stdout);
assert!(
stdout.contains("f x:n>n;x"),
"expected definition in :defs output, got: {stdout}"
);
}
#[test]
fn repl_clear_defs() {
let out = run_repl("f x:n>n;x\n:clear\n:defs\n:q\n");
assert!(
out.status.success(),
"stderr: {}",
String::from_utf8_lossy(&out.stderr)
);
let stdout = String::from_utf8_lossy(&out.stdout);
assert!(
stdout.contains("cleared all definitions"),
"expected 'cleared all definitions', got: {stdout}"
);
assert!(
stdout.contains("(no definitions)"),
"expected empty defs after clear, got: {stdout}"
);
}
#[test]
fn repl_help_command() {
let out = run_repl(":help\n:q\n");
assert!(
out.status.success(),
"stderr: {}",
String::from_utf8_lossy(&out.stderr)
);
let stdout = String::from_utf8_lossy(&out.stdout);
assert!(
stdout.contains(":w <file>"),
"expected help text, got: {stdout}"
);
assert!(
stdout.contains(":defs"),
"expected :defs in help, got: {stdout}"
);
}
#[test]
fn repl_unknown_command() {
let out = run_repl(":foobar\n:q\n");
assert!(
out.status.success(),
"stderr: {}",
String::from_utf8_lossy(&out.stderr)
);
let stderr = String::from_utf8_lossy(&out.stderr);
assert!(
stderr.contains("unknown command"),
"expected 'unknown command', got: {stderr}"
);
}
#[test]
fn repl_wq_no_defs() {
let out = run_repl(":wq\n:q\n");
assert!(
out.status.success(),
"stderr: {}",
String::from_utf8_lossy(&out.stderr)
);
let stderr = String::from_utf8_lossy(&out.stderr);
assert!(
stderr.contains("no definitions to save"),
"expected 'no definitions to save', got: {stderr}"
);
}
#[test]
fn repl_wq_with_defs_no_path() {
let out = run_repl("f x:n>n;x\n:wq\n:q\n");
assert!(
out.status.success(),
"stderr: {}",
String::from_utf8_lossy(&out.stderr)
);
let stderr = String::from_utf8_lossy(&out.stderr);
assert!(
stderr.contains("usage: :w <file.ilo>"),
"expected usage hint, got: {stderr}"
);
}
#[test]
fn repl_w_save_file() {
let path = "/tmp/ilo_repl_test_save_cov.ilo";
let _ = std::fs::remove_file(path);
let out = run_repl(&format!("f x:n>n;*x 2\n:w {path}\n:q\n"));
assert!(
out.status.success(),
"stderr: {}",
String::from_utf8_lossy(&out.stderr)
);
let stdout = String::from_utf8_lossy(&out.stdout);
assert!(
stdout.contains("saved 1 definition(s)"),
"expected save message, got: {stdout}"
);
let contents = std::fs::read_to_string(path).expect("saved file should exist");
assert!(
contents.contains("f x:n>n;*x 2"),
"expected definition in file, got: {contents}"
);
let _ = std::fs::remove_file(path);
}
#[test]
fn repl_wq_save_and_quit() {
let path = "/tmp/ilo_repl_test_wq_cov.ilo";
let _ = std::fs::remove_file(path);
let out = run_repl(&format!("f x:n>n;+x 1\n:wq {path}\n"));
assert!(
out.status.success(),
"stderr: {}",
String::from_utf8_lossy(&out.stderr)
);
let stdout = String::from_utf8_lossy(&out.stdout);
assert!(
stdout.contains("saved 1 definition(s)"),
"expected save message, got: {stdout}"
);
let _ = std::fs::remove_file(path);
}
#[test]
fn repl_w_no_defs_to_save() {
let out = run_repl(":w /tmp/ilo_repl_nodefs_cov.ilo\n:q\n");
assert!(
out.status.success(),
"stderr: {}",
String::from_utf8_lossy(&out.stderr)
);
let stderr = String::from_utf8_lossy(&out.stderr);
assert!(
stderr.contains("no definitions to save"),
"expected 'no definitions to save', got: {stderr}"
);
}
#[test]
fn repl_multiline_braces() {
let out = run_repl("f x:n>n;<=x 0{\n0\n};x\nf 5\n:q\n");
assert!(
out.status.success(),
"stderr: {}",
String::from_utf8_lossy(&out.stderr)
);
let stdout = String::from_utf8_lossy(&out.stdout);
assert!(stdout.contains("5"), "expected 5 in output, got: {stdout}");
}
#[test]
fn repl_multiline_semicolon() {
let out = run_repl("f x:n>n;\n+x 1\nf 5\n:q\n");
assert!(
out.status.success(),
"stderr: {}",
String::from_utf8_lossy(&out.stderr)
);
let stdout = String::from_utf8_lossy(&out.stdout);
assert!(
stdout.contains("defined:"),
"expected definition, got: {stdout}"
);
}
#[test]
fn repl_empty_lines_ignored() {
let out = run_repl("\n\n+1 1\n:q\n");
assert!(
out.status.success(),
"stderr: {}",
String::from_utf8_lossy(&out.stderr)
);
let stdout = String::from_utf8_lossy(&out.stdout);
assert!(stdout.contains("2"), "expected 2 in output, got: {stdout}");
}
#[test]
fn repl_eof_exits() {
use std::process::Stdio;
let out = ilo()
.args(["repl"])
.stdin(Stdio::null())
.output()
.expect("failed to run ilo");
assert!(
out.status.success(),
"stderr: {}",
String::from_utf8_lossy(&out.stderr)
);
}
#[test]
fn repl_define_typedef() {
let out = run_repl("type point{x:n;y:n}\n:defs\n:q\n");
assert!(
out.status.success(),
"stderr: {}",
String::from_utf8_lossy(&out.stderr)
);
let stdout = String::from_utf8_lossy(&out.stdout);
assert!(
stdout.contains("defined type: point"),
"expected typedef output, got: {stdout}"
);
}
#[test]
fn repl_define_alias() {
let out = run_repl("alias num n\n:defs\n:q\n");
assert!(
out.status.success(),
"stderr: {}",
String::from_utf8_lossy(&out.stderr)
);
let stdout = String::from_utf8_lossy(&out.stdout);
assert!(
stdout.contains("defined alias: num"),
"expected alias output, got: {stdout}"
);
}
#[test]
fn repl_parse_error() {
let out = run_repl("@@@\n:q\n");
assert!(
out.status.success(),
"stderr: {}",
String::from_utf8_lossy(&out.stderr)
);
let stderr = String::from_utf8_lossy(&out.stderr);
assert!(!stderr.is_empty(), "expected error on stderr, got nothing");
}
#[test]
fn emit_dense_format() {
let out = ilo()
.args(["f x:n>n;y=*x 2;+y 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.is_empty(), "expected dense output");
}
#[test]
fn emit_dense_short_flag() {
let out = ilo()
.args(["f x:n>n;*x 2", "-d"])
.output()
.expect("failed to run ilo");
assert!(
out.status.success(),
"stderr: {}",
String::from_utf8_lossy(&out.stderr)
);
}
#[test]
fn emit_fmt_alias() {
let out = ilo()
.args(["f x:n>n;*x 2", "--fmt"])
.output()
.expect("failed to run ilo");
assert!(
out.status.success(),
"stderr: {}",
String::from_utf8_lossy(&out.stderr)
);
}
#[test]
fn emit_expanded_format() {
let out = ilo()
.args(["f x:n>n;y=*x 2;+y 1", "--expanded"])
.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.is_empty(), "expected expanded output");
}
#[test]
fn emit_expanded_short_flag() {
let out = ilo()
.args(["f x:n>n;*x 2", "-e"])
.output()
.expect("failed to run ilo");
assert!(
out.status.success(),
"stderr: {}",
String::from_utf8_lossy(&out.stderr)
);
}
#[test]
fn emit_fmt_expanded_alias() {
let out = ilo()
.args(["f x:n>n;*x 2", "--fmt-expanded"])
.output()
.expect("failed to run ilo");
assert!(
out.status.success(),
"stderr: {}",
String::from_utf8_lossy(&out.stderr)
);
}
#[test]
fn no_hints_flag() {
let out = ilo()
.args(["--no-hints", "f x:n y:n>b;=x y", "1", "1"])
.output()
.expect("failed to run ilo");
assert!(
out.status.success(),
"stderr: {}",
String::from_utf8_lossy(&out.stderr)
);
let stderr = String::from_utf8_lossy(&out.stderr);
assert!(
!stderr.contains("hint:"),
"expected no hints with --no-hints, got: {stderr}"
);
}
#[test]
fn no_hints_short_flag() {
let out = ilo()
.args(["-nh", "f x:n y:n>b;=x y", "1", "1"])
.output()
.expect("failed to run ilo");
assert!(
out.status.success(),
"stderr: {}",
String::from_utf8_lossy(&out.stderr)
);
let stderr = String::from_utf8_lossy(&out.stderr);
assert!(
!stderr.contains("hint:"),
"expected no hints with -nh, got: {stderr}"
);
}
#[test]
fn serv_run_program_with_response() {
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");
{
let stdin = child.stdin.as_mut().unwrap();
writeln!(
stdin,
r#"{{"program":"f x:n>n;*x 2","args":["5"],"func":"f"}}"#
)
.unwrap();
}
let out = child.wait_with_output().unwrap();
assert!(
out.status.success(),
"stderr: {}",
String::from_utf8_lossy(&out.stderr)
);
let stdout = String::from_utf8_lossy(&out.stdout);
let lines: Vec<&str> = stdout.lines().collect();
assert!(lines.len() >= 2, "expected at least 2 lines, got: {stdout}");
let resp: serde_json::Value = serde_json::from_str(lines[1])
.unwrap_or_else(|_| panic!("expected JSON response, got: {}", lines[1]));
assert_eq!(
resp["ok"],
serde_json::json!(10),
"expected ok=10, got: {resp}"
);
}
#[test]
fn serv_invalid_json_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");
{
let stdin = child.stdin.as_mut().unwrap();
writeln!(stdin, "not json").unwrap();
}
let out = child.wait_with_output().unwrap();
assert!(
out.status.success(),
"stderr: {}",
String::from_utf8_lossy(&out.stderr)
);
let stdout = String::from_utf8_lossy(&out.stdout);
let lines: Vec<&str> = stdout.lines().collect();
assert!(lines.len() >= 2, "expected at least 2 lines, got: {stdout}");
let resp: serde_json::Value = serde_json::from_str(lines[1])
.unwrap_or_else(|_| panic!("expected JSON response, got: {}", lines[1]));
assert!(
resp["error"].is_object(),
"expected error in response, got: {resp}"
);
}
#[test]
fn cli_nil_arg_to_optional_param() {
let out = ilo()
.args(["f x:O n>n;x??0", "f", "nil"])
.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 cli_nil_arg_equality() {
let out = ilo()
.args(["f x:O n>b;=x nil", "f", "nil"])
.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(), "true");
}
#[test]
fn cli_single_number_coerced_to_list() {
let out = ilo()
.args(["f xs:L n>n;sum xs", "f", "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(), "10");
}
#[test]
fn cli_single_text_coerced_to_list() {
let out = ilo()
.args(["f xs:L t>n;len xs", "f", "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(), "1");
}
#[test]
fn cli_comma_list_still_works() {
let out = ilo()
.args(["f xs:L n>n;sum xs", "f", "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(), "6");
}
#[test]
fn sum_type_match_all_variants_runs() {
let out = ilo()
.args([
r#"f x:S red green blue>t;?x{"red":"r";"green":"g";"blue":"b"}"#,
"f",
"red",
])
.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(), "r");
}
#[test]
fn sum_type_match_missing_variant_errors() {
let out = ilo()
.args([
r#"f x:S red green blue>t;?x{"red":"r";"green":"g"}"#,
"f",
"red",
])
.output()
.expect("failed to run ilo");
assert!(!out.status.success());
let stderr = String::from_utf8_lossy(&out.stderr);
assert!(stderr.contains("ILO-T024") || stderr.contains("non-exhaustive"));
}