use std::{
fs,
io::{ErrorKind, Write},
process::{Command, Stdio},
};
const INPUT: &str = "module {\n // Initial value.\n %c = arith.constant 7 : i32\n // Double it.\n %sum = arith.addi %c, %c : i32 // selected\n \"example.observe\"(%sum) : (i32) -> ()\n}\n";
#[test]
fn stablehlo_decoder_supports_complete_dimension_queries_and_edits() {
let input = include_str!("../../../examples/cli/stablelm-decode.mlir");
for (query, expected) in [
(
r#"filter(op("stablehlo.dot_general")) | attr("contracting_dims") | unique"#,
"[0] x [1]\n[2] x [2]\n[2] x [0]\n",
),
(r#"filter(op("func.return")) | count"#, "1\n"),
(
r#"filter(op("stablehlo.dot_general")) | fixpoint(closure) | count"#,
"236\n",
),
(
r#"filter(op("stablehlo.dot_general")) | set_attr("review.tag", "matmul") | count"#,
"19\n",
),
] {
let output = run_stdin_with_registry("stablehlo.json", query, input);
assert!(
output.status.success(),
"{}",
String::from_utf8_lossy(&output.stderr)
);
assert_eq!(
String::from_utf8(output.stdout).unwrap(),
expected,
"{query}"
);
assert!(output.stderr.is_empty());
}
let edited = run_stdin_with_registry(
"stablehlo.json",
r#"filter(op("stablehlo.dot_general")) | set_attr("review.tag", "matmul") | input"#,
input,
);
assert!(
edited.status.success(),
"{}",
String::from_utf8_lossy(&edited.stderr)
);
let reparsed = run_stdin_with_registry(
"stablehlo.json",
r#"filter(has_attr("review.tag")) | attr("contracting_dims") | unique"#,
&String::from_utf8(edited.stdout).unwrap(),
);
assert!(
reparsed.status.success(),
"{}",
String::from_utf8_lossy(&reparsed.stderr)
);
assert_eq!(
String::from_utf8(reparsed.stdout).unwrap(),
"[[0], [1]]\n[[2], [2]]\n[[2], [0]]\n"
);
}
#[test]
fn byte_string_projection_reports_failure_without_partial_output() {
let output = run_stdin(
r#"emit | attr("tag") | count"#,
r#"module { "vendor.thing"() {tag = "\FF"} : () -> () }"#,
);
assert!(!output.status.success());
assert!(output.stdout.is_empty());
assert!(String::from_utf8_lossy(&output.stderr).contains("UTF-8"));
}
#[test]
fn recovery_warns_when_semantic_queries_may_be_incomplete() {
let output = run_stdin("count", "module { %x = vendor.thing : i32 }");
assert!(output.status.success());
assert_eq!(output.stdout, b"2\n");
assert!(String::from_utf8_lossy(&output.stderr).contains("incomplete"));
}
#[test]
fn scalar_count_and_source_aware_selection_remain_byte_exact() {
let source = "module {\n // Keep this comment.\n \"vendor.test\"() : () -> ()\n}\n";
let count = run_stdin(r#"filter(op("vendor.test")) | count"#, source);
assert!(count.status.success(), "{:?}", count.stderr);
assert_eq!(count.stdout, b"1\n");
let selection = run_stdin(r#"filter(op("vendor.test"))"#, source);
assert!(selection.status.success(), "{:?}", selection.stderr);
assert_eq!(
selection.stdout,
b"builtin.module {\n // Keep this comment.\n \"vendor.test\"() : () -> ()\n}\n"
);
}
#[test]
fn format_preserves_source_assembly_and_can_request_generic_assembly() {
let source = "module {\nfunc.func @twice(%input : i32) -> i32 {\n%sum=arith.addi %input,%input:i32 // keep\nfunc.return %sum:i32\n}\n}\n";
let formatted = run_stdin("format", source);
assert!(
formatted.status.success(),
"{}",
String::from_utf8_lossy(&formatted.stderr)
);
assert_eq!(
String::from_utf8(formatted.stdout).unwrap(),
"module {\n func.func @twice(%input: i32) -> i32 {\n %sum = arith.addi %input, %input : i32 // keep\n func.return %sum : i32\n }\n}\n"
);
let generic = run_stdin(r#"format(assembly = "generic")"#, source);
assert!(
generic.status.success(),
"{}",
String::from_utf8_lossy(&generic.stderr)
);
let generic = String::from_utf8(generic.stdout).unwrap();
assert!(generic.contains(r#""arith.addi""#), "{generic}");
assert!(!generic.contains("arith.addi %input"), "{generic}");
let edited = run_stdin(
r#"do filter(op("func.func")) | set_attr("review.tag", "kept-custom"); format"#,
source,
);
assert!(
edited.status.success(),
"{}",
String::from_utf8_lossy(&edited.stderr)
);
let edited = String::from_utf8(edited.stdout).unwrap();
assert!(edited.contains("func.func @twice"), "{edited}");
assert!(!edited.contains(r#""func.func""#), "{edited}");
assert!(edited.contains(r#"review.tag = "kept-custom""#), "{edited}");
}
#[test]
fn check_supports_presence_absence_and_exact_cardinality() {
let passing = run_stdin(
r#"do filter(op("arith.addi")) | check("addition is required");
do filter(op("missing")) | check(0);
filter(op("arith.constant")) | check(1, "expected one constant") | count"#,
INPUT,
);
assert!(
passing.status.success(),
"{}",
String::from_utf8_lossy(&passing.stderr)
);
assert_eq!(passing.stdout, b"1\n");
assert!(passing.stderr.is_empty());
for (query, diagnostic) in [
(
r#"filter(op("missing")) | check"#,
"expected at least one item, got 0",
),
(
r#"filter(op("arith.addi")) | check(2)"#,
"expected 2 items, got 1",
),
(
r#"filter(op("arith.addi")) | check(0)"#,
"expected 0 items, got 1",
),
(
r#"filter(op("missing")) | check("the lowering must produce an addition")"#,
"expected at least one item, got 0: the lowering must produce an addition",
),
(
r#"filter(op("arith.addi")) | check(2, "expected fused pair")"#,
"expected 2 items, got 1: expected fused pair",
),
] {
let failed = run_stdin(query, INPUT);
assert!(!failed.status.success(), "{query}");
assert!(failed.stdout.is_empty(), "{query}");
assert!(
String::from_utf8_lossy(&failed.stderr).contains(diagnostic),
"{query}: {}",
String::from_utf8_lossy(&failed.stderr)
);
}
}
#[test]
fn silent_suppresses_results_without_suppressing_failures() {
for query in [
"count",
r#"filter(op("arith.addi"))"#,
r#"print("report")"#,
r#"names | tally | json"#,
] {
let output = run_stdin_with_options(&["--silent"], query, INPUT);
assert!(
output.status.success(),
"{query}: {}",
String::from_utf8_lossy(&output.stderr)
);
assert!(output.stdout.is_empty(), "{query}");
}
let failed = run_stdin_with_options(&["--silent"], r#"filter(op("missing")) | check"#, INPUT);
assert!(!failed.status.success());
assert!(failed.stdout.is_empty());
assert!(String::from_utf8_lossy(&failed.stderr).contains("check failed"));
}
#[test]
fn jsonl_operation_selection_uses_serde_compatible_escaping() {
let source = "module {\n // quote \" and slash \\\n \"vendor.test\"() {text = \"say \\22hi\\22\"} : () -> ()\n}\n";
let output = run_stdin_with_options(&["--jsonl"], r#"filter(op("vendor.test"))"#, source);
assert!(output.status.success(), "{:?}", output.stderr);
let selection = "builtin.module {\n // quote \" and slash \\\n \"vendor.test\"() {text = \"say \\\"hi\\\"\"} : () -> ()\n}\n";
let mut expected =
serde_json::to_vec(&serde_json::json!({"document": "stdin", "result": selection})).unwrap();
expected.push(b'\n');
assert_eq!(output.stdout, expected);
}
fn run_stdin(query: &str, input: &str) -> std::process::Output {
run_stdin_with_options(&[], query, input)
}
#[test]
fn explicit_stdin_path_works() {
let output = run_stdin_with_options(&["count"], "-", INPUT);
assert!(output.status.success(), "{:?}", output.stderr);
assert_eq!(output.stdout, b"4\n");
}
#[test]
fn value_options_accept_equals_form() {
let output = run_stdin_with_options(&["--max-work=100"], "count", INPUT);
assert!(output.status.success(), "{:?}", output.stderr);
assert_eq!(output.stdout, b"4\n");
}
#[test]
fn fragment_scope_equals_form_does_not_consume_input_path() {
let path = temporary_path("fragment-scope", "mlir");
fs::write(&path, INPUT).unwrap();
let output = Command::new(env!("CARGO_BIN_EXE_zirium"))
.args(["--fragment-scope=minimal", "count"])
.arg(&path)
.output()
.unwrap();
fs::remove_file(path).unwrap();
assert!(output.status.success(), "{:?}", output.stderr);
assert_eq!(output.stdout, b"4\n");
}
#[test]
fn equals_form_is_rejected_for_flags_and_short_options() {
for args in [["--strict=unexpected", "count"], ["-f=missing", "count"]] {
let output = Command::new(env!("CARGO_BIN_EXE_zirium"))
.args(args)
.stdin(Stdio::null())
.output()
.unwrap();
assert_eq!(output.status.code(), Some(1));
}
}
#[test]
fn short_program_file_dash_is_a_literal_filename() {
let output = Command::new(env!("CARGO_BIN_EXE_zirium"))
.args(["-f", "-", "count"])
.stdin(Stdio::null())
.output()
.unwrap();
assert_eq!(output.status.code(), Some(1));
assert!(String::from_utf8_lossy(&output.stderr).contains("program file"));
}
#[test]
fn separator_preserves_dash_prefixed_input_filename() {
let path = "-zirium-f22-input.mlir";
fs::write(path, INPUT).unwrap();
let output = Command::new(env!("CARGO_BIN_EXE_zirium"))
.args(["count", "--", path])
.output()
.unwrap();
fs::remove_file(path).unwrap();
assert!(output.status.success(), "{:?}", output.stderr);
assert_eq!(output.stdout, b"4\n");
}
#[cfg(target_os = "linux")]
#[test]
fn non_utf8_input_filename_is_preserved() {
use std::os::unix::ffi::OsStringExt;
let path = std::ffi::OsString::from_vec(b"f22-\xff.mlir".to_vec());
fs::write(&path, INPUT).unwrap();
let output = Command::new(env!("CARGO_BIN_EXE_zirium"))
.arg("count")
.arg(&path)
.output()
.unwrap();
fs::remove_file(&path).unwrap();
assert!(output.status.success(), "{:?}", output.stderr);
assert_eq!(output.stdout, b"4\n");
}
#[test]
fn duplicate_stdin_rejects_without_waiting_for_stdin_eof() {
let mut child = Command::new(env!("CARGO_BIN_EXE_zirium"))
.args(["count", "-", "-"])
.stdin(Stdio::piped())
.stdout(Stdio::piped())
.stderr(Stdio::piped())
.spawn()
.unwrap();
let mut stdin = child.stdin.take().unwrap();
if let Err(error) = stdin.write_all(b"not consumed") {
assert_eq!(error.kind(), ErrorKind::BrokenPipe, "{error}");
}
let deadline = std::time::Instant::now() + std::time::Duration::from_secs(2);
let status = loop {
if let Some(status) = child.try_wait().unwrap() {
break status;
}
assert!(
std::time::Instant::now() < deadline,
"duplicate stdin check blocked"
);
std::thread::sleep(std::time::Duration::from_millis(10));
};
drop(stdin);
let output = child.wait_with_output().unwrap();
assert_eq!(status.code(), Some(1));
assert!(String::from_utf8_lossy(&output.stderr).contains("only once"));
}
#[test]
fn command_failures_keep_exit_code_one() {
let output = Command::new(env!("CARGO_BIN_EXE_zirium"))
.args(["not valid query"])
.stdin(Stdio::null())
.output()
.unwrap();
assert_eq!(output.status.code(), Some(1));
}
#[test]
fn duplicate_stdin_operands_fail_before_reading_stdin() {
let output = Command::new(env!("CARGO_BIN_EXE_zirium"))
.args(["count", "-", "-"])
.stdin(Stdio::null())
.output()
.unwrap();
assert_eq!(output.status.code(), Some(1));
assert!(String::from_utf8_lossy(&output.stderr).contains("only once"));
}
#[test]
fn cli_file_limit_is_byte_accurate_for_stdin() {
let input = "\"op\"() {name = \"räka\"} : () -> ()";
let byte_length = input.len().to_string();
let exact = run_stdin_with_options(&["--max-file-bytes", &byte_length], "count", input);
assert!(exact.status.success(), "{:?}", exact.stderr);
let over_limit = (input.len() - 1).to_string();
let over = run_stdin_with_options(&["--max-file-bytes", &over_limit], "count", input);
assert!(!over.status.success());
assert!(over.stdout.is_empty());
assert!(String::from_utf8_lossy(&over.stderr).contains("file size"));
}
#[test]
fn cli_file_limit_detects_one_byte_over_from_a_path() {
let path = temporary_path("input-limit", "mlir");
fs::write(&path, INPUT).unwrap();
let output = Command::new(env!("CARGO_BIN_EXE_zirium"))
.args(["--max-file-bytes", &(INPUT.len() - 1).to_string(), "count"])
.arg(&path)
.output()
.unwrap();
fs::remove_file(path).unwrap();
assert!(!output.status.success());
assert!(output.stdout.is_empty());
assert!(String::from_utf8_lossy(&output.stderr).contains("file size"));
}
#[test]
fn cli_help_separates_parser_and_evaluator_limits() {
let output = Command::new(env!("CARGO_BIN_EXE_zirium"))
.arg("--help")
.output()
.unwrap();
let help = String::from_utf8(output.stdout).unwrap();
assert!(help.contains("Parser limits:\n --max-file-bytes"));
assert!(help.contains("Evaluator limits:\n --max-work"));
}
#[test]
fn ancestor_traversal_consumes_the_work_budget() {
let input = "module { func.func @f() { return } }";
let output = run_stdin_with_options(&["--max-work", "4"], "root(false) | count", input);
assert!(!output.status.success());
assert!(output.stdout.is_empty());
assert!(String::from_utf8_lossy(&output.stderr).contains("work limit"));
let output = run_stdin_with_options(&["--max-work", "100"], "root(false) | count", input);
assert!(output.status.success());
assert_eq!(output.stdout, b"0\n");
}
#[cfg(target_os = "linux")]
#[test]
fn cli_accepts_non_utf8_paths() {
use std::os::unix::ffi::OsStringExt;
let path = std::env::temp_dir().join(std::ffi::OsString::from_vec(
format!("zirium-path-{}-", std::process::id())
.into_bytes()
.into_iter()
.chain([0xff])
.collect(),
));
fs::write(&path, INPUT).unwrap();
let output = Command::new(env!("CARGO_BIN_EXE_zirium"))
.arg("count")
.arg(&path)
.output()
.unwrap();
fs::remove_file(path).unwrap();
assert!(output.status.success(), "{:?}", output.stderr);
assert_eq!(output.stdout, b"4\n");
}
#[cfg(unix)]
#[test]
fn cli_handles_closed_output_pipes() {
use std::os::{fd::OwnedFd, unix::net::UnixStream};
for args in [
vec!["--help"],
vec!["--version"],
vec!["--list-presets"],
vec!["print(\"hello\")"],
] {
let (reader, writer) = UnixStream::pair().unwrap();
drop(reader);
let output = Command::new(env!("CARGO_BIN_EXE_zirium"))
.args(args)
.stdin(Stdio::null())
.stdout(Stdio::from(OwnedFd::from(writer)))
.stderr(Stdio::piped())
.output()
.unwrap();
assert!(output.status.success(), "{:?}", output.stderr);
assert!(output.stderr.is_empty());
}
let (reader, writer) = UnixStream::pair().unwrap();
drop(reader);
let mut child = Command::new(env!("CARGO_BIN_EXE_zirium"))
.arg("")
.stdin(Stdio::piped())
.stdout(Stdio::from(OwnedFd::from(writer)))
.stderr(Stdio::piped())
.spawn()
.unwrap();
child
.stdin
.take()
.unwrap()
.write_all(large_staged_output_input().as_bytes())
.unwrap();
let output = child.wait_with_output().unwrap();
assert!(output.status.success(), "{:?}", output.stderr);
assert!(output.stderr.is_empty());
}
fn run_stdin_with_options(options: &[&str], query: &str, input: &str) -> std::process::Output {
let mut child = Command::new(env!("CARGO_BIN_EXE_zirium"))
.args(options)
.arg(query)
.stdin(Stdio::piped())
.stdout(Stdio::piped())
.stderr(Stdio::piped())
.spawn()
.unwrap();
if let Err(error) = child.stdin.take().unwrap().write_all(input.as_bytes()) {
assert_eq!(error.kind(), ErrorKind::BrokenPipe, "{error}");
}
child.wait_with_output().unwrap()
}
#[test]
fn fixed_points_bound_growth_and_keep_explicit_iteration_emissions() {
let input = "module { %c = arith.constant 1 : i32 }";
for (options, message) in [
(["--max-work", "1000"], "work limit"),
(["--max-items", "3"], "stream size limit"),
] {
let output = run_stdin_with_options(&options, "emit | fixpoint(subtree) | count", input);
assert!(!output.status.success());
assert!(output.stdout.is_empty());
assert!(String::from_utf8_lossy(&output.stderr).contains(message));
}
let output = run_stdin("fixpoint(subtree | unique) | count", input);
assert!(output.status.success());
assert_eq!(output.stdout, b"2\n");
let input = "module { %c = arith.constant 1 : i32 %a = arith.addi %c, %c : i32 %b = arith.addi %a, %c : i32 }";
let output = run_stdin(
r#"filter(op("arith.addi")) | fixpoint(closure | json) | count"#,
input,
);
assert!(
output.status.success(),
"{}",
String::from_utf8_lossy(&output.stderr)
);
assert_eq!(
String::from_utf8(output.stdout)
.unwrap()
.matches("\"name\":")
.count(),
6
);
for query in [
r#"filter(op("arith.addi")) | fixpoint(closure) | count"#,
r#"filter(op("arith.addi")) | fixpoint(closure | unique) | count"#,
] {
let output = run_stdin(query, input);
assert!(output.status.success());
assert_eq!(output.stdout, b"3\n");
}
}
#[test]
fn worklist_closure_checks_new_dependencies_and_handles_deep_chains() {
let unknown = r#"module { %c = "vendor.constant"() : () -> i32 %a = arith.addi %c, %c : i32 }"#;
let output = run_stdin(
r#"filter(op("arith.addi")) | fixpoint(closure) | count"#,
unknown,
);
assert!(!output.status.success());
assert!(output.stdout.is_empty());
assert!(String::from_utf8_lossy(&output.stderr).contains("vendor.constant"));
let mut input = String::from("module { %v0 = arith.constant 1 : i32\n");
for i in 1..2000 {
input.push_str(&format!("%v{i} = arith.addi %v{}, %v0 : i32\n", i - 1));
}
input.push_str("%v2000 = \"arith.addi\"(%v1999, %v0) {seed = \"yes\"} : (i32, i32) -> i32\n");
input.push('}');
let output = run_stdin_with_options(
&["--max-work", "100000"],
r#"filter(has_attr("seed")) | fixpoint(closure) | count"#,
&input,
);
assert!(
output.status.success(),
"{}",
String::from_utf8_lossy(&output.stderr)
);
assert_eq!(output.stdout, b"2001\n");
}
#[test]
fn cli_presets_strict_mode_and_inspection_work_together() {
let input = "module { %c = stablehlo.constant dense<1.0> : tensor<2xf32> %a = stablehlo.add %c, %c : tensor<2xf32> }";
let output = run_stdin_with_options(&["--strict"], "count", input);
assert!(!output.status.success());
assert!(output.stdout.is_empty());
assert!(String::from_utf8_lossy(&output.stderr).contains("incomplete"));
for (query, expected) in [
(
r#"filter(dialect("stablehlo") and result_type("tensor<2xf32>")) | names"#,
"stablehlo.constant\nstablehlo.add\n",
),
(
r#"filter(op("stablehlo.add")) | operand_types"#,
"tensor<2xf32>\ntensor<2xf32>\n",
),
(
r#"filter(dialect("stablehlo")) | result_types | unique"#,
"tensor<2xf32>\n",
),
] {
let output = run_stdin_with_options(&["--preset", "stablehlo", "--strict"], query, input);
assert!(
output.status.success(),
"{}",
String::from_utf8_lossy(&output.stderr)
);
assert!(output.stderr.is_empty());
assert_eq!(String::from_utf8(output.stdout).unwrap(), expected);
}
let help = Command::new(env!("CARGO_BIN_EXE_zirium"))
.arg("--help")
.output()
.unwrap();
assert!(help.status.success());
let help = String::from_utf8_lossy(&help.stdout);
for term in [
"--preset",
"reachable",
"tally",
"map_by(key, value)",
"markdown",
"print(\"text\")",
"do",
] {
assert!(help.contains(term), "help omits {term}");
}
let output = Command::new(env!("CARGO_BIN_EXE_zirium"))
.args(["count", "--strict", "--preset", "stablehlo"])
.arg(
std::path::Path::new(env!("CARGO_MANIFEST_DIR"))
.join("../../examples/cli/stablelm-decode.mlir"),
)
.output()
.unwrap();
assert!(
output.status.success(),
"{}",
String::from_utf8_lossy(&output.stderr)
);
assert_eq!(output.stdout, b"237\n");
}
#[test]
fn indexed_navigation_and_ssa_slices_isolate_one_return_path() {
let input = r#"module {
func.func @paths(%arg: i32) -> (i32, i32) {
%c = arith.constant 1 : i32
%left = arith.addi %arg, %c : i32
%right = "arith.muli"(%c, %c) : (i32, i32) -> i32
return %left, %right : i32, i32
}
}"#;
for (query, expected) in [
(
r#"filter(op("func.return")) | defs(0) | slice | names"#,
"arith.constant\narith.addi\n",
),
(
r#"filter(op("func.return")) | defs(1) | slice | names"#,
"arith.constant\narith.muli\n",
),
(
r#"filter(op("func.return")) | defs(1) | backward_slice | names"#,
"arith.constant\narith.muli\n",
),
(r#"filter(op("func.return")) | defs(2) | count"#, "0\n"),
(
r#"filter(op("arith.addi")) | defs(0) | names"#,
"func.func\n",
),
] {
let output = run_stdin(query, input);
assert!(
output.status.success(),
"{}",
String::from_utf8_lossy(&output.stderr)
);
assert_eq!(
String::from_utf8(output.stdout).unwrap(),
expected,
"{query}"
);
}
let input = r#"module {
%a, %b = "test.split"() : () -> (i32, i32)
"test.left"(%a) : (i32) -> ()
"test.right"(%b, %b) : (i32, i32) -> ()
}"#;
for (index, expected) in [(0, "test.left\n"), (1, "test.right\ntest.right\n"), (2, "")] {
let output = run_stdin(
&format!("filter(op(\"test.split\")) | users({index}) | names"),
input,
);
assert!(output.status.success());
assert_eq!(String::from_utf8(output.stdout).unwrap(), expected);
}
for query in [
"defs(-1)",
"users(9999999999999999999999999999999)",
"defs()",
] {
assert!(!run_stdin(query, input).status.success(), "{query}");
}
}
#[test]
fn forward_slice_follows_transitive_ssa_users_once_in_source_order() {
let input = r#"module {
%source = "test.source"() : () -> i32
%left = "test.left"(%source) : (i32) -> i32
%right = "test.right"(%source) : (i32) -> i32
"test.sink"(%left, %right) : (i32, i32) -> ()
"test.unrelated"() : () -> ()
}"#;
let output = run_stdin(
r#"filter(op("test.source")) | forward_slice | names"#,
input,
);
assert!(
output.status.success(),
"{}",
String::from_utf8_lossy(&output.stderr)
);
assert_eq!(
String::from_utf8(output.stdout).unwrap(),
"test.source\ntest.left\ntest.right\ntest.sink\n"
);
}
fn run_stdin_with_registry(registry: &str, query: &str, input: &str) -> std::process::Output {
let registry = std::path::Path::new(env!("CARGO_MANIFEST_DIR"))
.join("registries")
.join(registry);
let mut child = Command::new(env!("CARGO_BIN_EXE_zirium"))
.arg("--registry")
.arg(registry)
.arg(query)
.stdin(Stdio::piped())
.stdout(Stdio::piped())
.stderr(Stdio::piped())
.spawn()
.unwrap();
child
.stdin
.take()
.unwrap()
.write_all(input.as_bytes())
.unwrap();
child.wait_with_output().unwrap()
}
fn temporary_path(name: &str, extension: &str) -> std::path::PathBuf {
std::env::temp_dir().join(format!(
"zirium-cli-{}-{name}.{extension}",
std::process::id()
))
}
fn large_staged_output_input() -> String {
format!(
"module {{ \"test.op\"() {{blob = \"{}\"}} : () -> () }}\n",
"x".repeat(1_100_000)
)
}
fn temporary_directory(name: &str) -> std::path::PathBuf {
let path = temporary_path(name, "dir");
fs::create_dir(&path).unwrap();
path
}
fn assert_directory_empty(path: &std::path::Path) {
assert_eq!(fs::read_dir(path).unwrap().count(), 0);
}
#[test]
fn large_normal_and_jsonl_output_preserve_bytes_after_spilling() {
let source = large_staged_output_input();
let blob = "x".repeat(1_100_000);
let expected =
format!("builtin.module {{\n \"test.op\"() {{blob = \"{blob}\"}} : () -> ()\n}}\n");
let staging = temporary_directory("large-output-staging");
let normal = Command::new(env!("CARGO_BIN_EXE_zirium"))
.arg("")
.env("TMPDIR", &staging)
.stdin(Stdio::piped())
.stdout(Stdio::piped())
.stderr(Stdio::piped())
.spawn()
.and_then(|mut child| {
child.stdin.take().unwrap().write_all(source.as_bytes())?;
child.wait_with_output()
})
.unwrap();
assert!(normal.status.success(), "{:?}", normal.stderr);
assert_eq!(normal.stdout, expected.as_bytes());
assert_directory_empty(&staging);
let jsonl = Command::new(env!("CARGO_BIN_EXE_zirium"))
.args(["--jsonl", ""])
.env("TMPDIR", &staging)
.stdin(Stdio::piped())
.stdout(Stdio::piped())
.stderr(Stdio::piped())
.spawn()
.and_then(|mut child| {
child.stdin.take().unwrap().write_all(source.as_bytes())?;
child.wait_with_output()
})
.unwrap();
assert!(jsonl.status.success(), "{:?}", jsonl.stderr);
let record: serde_json::Value = serde_json::from_slice(&jsonl.stdout).unwrap();
assert_eq!(record["document"], "stdin");
assert_eq!(record["result"], expected);
assert_directory_empty(&staging);
fs::remove_dir(staging).unwrap();
}
#[test]
fn later_input_failure_discards_spilled_output_and_temporary_file() {
let first = temporary_path("spilled-first", "mlir");
let second = temporary_path("malformed-second", "mlir");
let staging = temporary_directory("later-failure-staging");
fs::write(&first, large_staged_output_input()).unwrap();
fs::write(&second, "module {").unwrap();
let output = Command::new(env!("CARGO_BIN_EXE_zirium"))
.arg("")
.arg(&first)
.arg(&second)
.env("TMPDIR", &staging)
.output()
.unwrap();
fs::remove_file(first).unwrap();
fs::remove_file(second).unwrap();
assert!(!output.status.success());
assert!(output.stdout.is_empty());
assert_directory_empty(&staging);
fs::remove_dir(staging).unwrap();
}
#[test]
fn spill_creation_failure_leaves_stdout_empty() {
let missing_staging = temporary_path("missing-staging", "dir");
let _ = fs::remove_dir_all(&missing_staging);
let source = large_staged_output_input();
let mut child = Command::new(env!("CARGO_BIN_EXE_zirium"))
.arg("")
.env("TMPDIR", &missing_staging)
.stdin(Stdio::piped())
.stdout(Stdio::piped())
.stderr(Stdio::piped())
.spawn()
.unwrap();
child
.stdin
.take()
.unwrap()
.write_all(source.as_bytes())
.unwrap();
let output = child.wait_with_output().unwrap();
assert!(!output.status.success());
assert!(output.stdout.is_empty());
assert!(!missing_staging.exists());
}
#[test]
fn stdin_selection_retains_shell_and_comments() {
let output = run_stdin("filter(op(\"arith.addi\"))", INPUT);
assert!(
output.status.success(),
"{}",
String::from_utf8_lossy(&output.stderr)
);
let text = String::from_utf8(output.stdout).unwrap();
assert!(text.contains("builtin.module"));
assert!(text.contains("// Double it."));
assert!(text.contains("arith.addi"));
assert!(text.contains("// selected"));
assert!(!text.contains("arith.constant"));
assert!(!text.contains("example.observe"));
}
#[test]
fn minimal_fragment_scope_omits_shell_metadata_only_when_requested() {
let source = r#""vendor.outer"() ({
"vendor.call"() {keep = "yes"} : () -> ()
}) {metadata = "large incidental value"} : () -> ()"#;
let query = r#"filter(op("vendor.call"))"#;
let full = run_stdin(query, source);
assert!(full.status.success());
assert!(String::from_utf8_lossy(&full.stdout).contains("large incidental value"));
let minimal = run_stdin_with_options(&["--fragment-scope", "minimal"], query, source);
assert!(minimal.status.success());
let text = String::from_utf8(minimal.stdout).unwrap();
assert!(!text.contains("large incidental value"), "{text}");
assert!(text.contains("keep = \"yes\""), "{text}");
assert!(text.contains("vendor.outer"), "{text}");
}
#[test]
fn boolean_predicates_select_names_and_decoded_string_attributes() {
let input = "module {\n \"test.a\"() {tag = \"say \\22hi\\22\"} : () -> ()\n \"test.b\"() {tag = 7 : i32} : () -> ()\n \"test.c\"() : () -> ()\n}\n";
let output = run_stdin(
r#"filter((op("test.a") or op("test.b")) and has_attr("tag") and not string_attr_eq("tag", "other"))"#,
input,
);
assert!(
output.status.success(),
"{}",
String::from_utf8_lossy(&output.stderr)
);
let text = String::from_utf8(output.stdout).unwrap();
assert!(text.contains("test.a"), "{text}");
assert!(text.contains("test.b"), "{text}");
assert!(!text.contains("test.c"), "{text}");
let decoded = run_stdin(r#"filter(string_attr_eq("tag", "say \"hi\""))"#, input);
assert!(
decoded.status.success(),
"{}",
String::from_utf8_lossy(&decoded.stderr)
);
let text = String::from_utf8(decoded.stdout).unwrap();
assert!(text.contains("test.a"), "{text}");
assert!(!text.contains("test.b"), "{text}");
}
#[test]
fn builtin_dense_array_attributes_support_queries_and_ownership() {
let input = "module {\n \"stablehlo.reduce\"() ({\n \"stablehlo.add\"() : () -> ()\n }) {dimensions = array<i64: 1>} : () -> ()\n}\n";
for (query, expected) in [
(
r#"filter(op("stablehlo.reduce") and has_attr("dimensions")) | count"#,
"1\n",
),
(r#"filter(op("stablehlo.add")) | parent | count"#, "1\n"),
(
r#"filter(op("stablehlo.reduce")) | children | count"#,
"1\n",
),
] {
let output = run_stdin(query, input);
assert!(
output.status.success(),
"{}",
String::from_utf8_lossy(&output.stderr)
);
assert_eq!(String::from_utf8(output.stdout).unwrap(), expected);
}
}
#[test]
fn stablehlo_preset_exposes_model_structure_to_queries() {
let root = std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("../../examples/cli");
let registry =
std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("registries/stablehlo.json");
let input = root.join("stablelm-decode.mlir");
for (query, expected) in [
(
r#"filter(op("stablehlo.broadcast_in_dim") and has_attr("dims")) | count"#,
"31\n",
),
(
r#"filter(op("stablehlo.dot_general") and has_attr("contracting_dims")) | count"#,
"19\n",
),
(
r#"filter(op("stablehlo.dynamic_slice") and has_attr("sizes")) | count"#,
"3\n",
),
(
r#"filter(op("stablehlo.reduce") and has_attr("dimensions")) | count"#,
"14\n",
),
] {
let output = Command::new(env!("CARGO_BIN_EXE_zirium"))
.arg("--registry")
.arg(®istry)
.arg(query)
.arg(&input)
.output()
.unwrap();
assert!(
output.status.success(),
"{}",
String::from_utf8_lossy(&output.stderr)
);
assert_eq!(String::from_utf8(output.stdout).unwrap(), expected);
}
}
#[test]
fn tosa_scf_and_linalg_presets_expose_queryable_structure() {
let cases = [
(
"tosa.json",
r#"filter(op("tosa.add") and has_attr("shift")) | count"#,
"module {\n %lhs = \"test.source\"() : () -> tensor<2xf32>\n %rhs = \"test.source\"() : () -> tensor<2xf32>\n %sum = tosa.add %lhs, %rhs {shift = 0 : i32} : (tensor<2xf32>, tensor<2xf32>) -> tensor<2xf32>\n}\n",
"1\n",
),
(
"scf.json",
r#"filter(op("scf.for")) | children | count"#,
"module {\n %lower = \"test.index\"() : () -> index\n %upper = \"test.index\"() : () -> index\n %step = \"test.index\"() : () -> index\n scf.for %iv = %lower to %upper step %step {\n \"test.use\"(%iv) : (index) -> ()\n scf.yield\n }\n}\n",
"2\n",
),
(
"linalg.json",
r#"filter(op("linalg.generic") and has_attr("indexing_maps")) | children | count"#,
"module {\n %input = \"test.source\"() : () -> memref<4xf32>\n %output = \"test.source\"() : () -> memref<4xf32>\n linalg.generic indexing_maps = [], iterator_types = [] ins(%input : memref<4xf32>) outs(%output : memref<4xf32>) {\n ^bb0(%element: f32, %accumulator: f32):\n linalg.yield %element : f32\n }\n}\n",
"1\n",
),
];
for (registry, query, input, expected) in cases {
let output = run_stdin_with_registry(registry, query, input);
assert!(
output.status.success(),
"{registry}: {}",
String::from_utf8_lossy(&output.stderr)
);
assert_eq!(String::from_utf8(output.stdout).unwrap(), expected);
}
}
#[test]
fn malformed_predicates_produce_no_output() {
for query in [
r#"filter(string_attr_eq("tag" "value"))"#,
r#"filter(op("arith.addi") and)"#,
r#"filter(has_attr("bad name"))"#,
] {
let output = run_stdin(query, INPUT);
assert!(!output.status.success());
assert!(output.stdout.is_empty());
assert!(String::from_utf8_lossy(&output.stderr).contains("query error"));
}
}
#[test]
fn closure_adds_shared_ssa_definition_once_with_comments() {
let output = run_stdin("filter(op(\"arith.addi\")) | fixpoint(closure)", INPUT);
assert!(
output.status.success(),
"{}",
String::from_utf8_lossy(&output.stderr)
);
let text = String::from_utf8(output.stdout).unwrap();
assert_eq!(text.matches("arith.constant").count(), 1, "{text}");
assert_eq!(text.matches("// Initial value.").count(), 1, "{text}");
assert!(text.find("arith.constant").unwrap() < text.find("arith.addi").unwrap());
assert!(!text.contains("example.observe"));
}
#[test]
fn direct_ssa_navigation_preserves_multiplicity_and_unique_collapses_it() {
let defs = run_stdin("filter(op(\"arith.addi\")) | defs", INPUT);
assert!(
defs.status.success(),
"{}",
String::from_utf8_lossy(&defs.stderr)
);
let text = String::from_utf8(defs.stdout).unwrap();
assert_eq!(text.matches("arith.constant").count(), 1, "{text}");
assert!(!text.contains("arith.addi"), "{text}");
let users = run_stdin("filter(op(\"arith.constant\")) | users | count", INPUT);
assert!(
users.status.success(),
"{}",
String::from_utf8_lossy(&users.stderr)
);
assert_eq!(String::from_utf8(users.stdout).unwrap(), "2\n");
let unique = run_stdin(
"filter(op(\"arith.constant\")) | users | unique | count",
INPUT,
);
assert!(unique.status.success());
assert_eq!(String::from_utf8(unique.stdout).unwrap(), "1\n");
let unused = run_stdin("filter(op(\"example.observe\")) | users | count", INPUT);
assert!(unused.status.success());
assert_eq!(String::from_utf8(unused.stdout).unwrap(), "0\n");
}
#[test]
fn set_queries_are_ordered_and_composable() {
let input = "module {\n \"test.a\"() {group = \"keep\"} : () -> ()\n \"test.b\"() {group = \"keep\"} : () -> ()\n \"test.c\"() : () -> ()\n}\n";
let union = run_stdin(
r#"(filter(op("test.c")) union filter(op("test.a") or has_attr("group"))) | names"#,
input,
);
assert!(
union.status.success(),
"{}",
String::from_utf8_lossy(&union.stderr)
);
let text = String::from_utf8(union.stdout).unwrap();
assert_eq!(text.matches("test.a").count(), 1, "{text}");
assert_eq!(text.matches("test.b").count(), 1, "{text}");
assert_eq!(text.matches("test.c").count(), 1, "{text}");
assert!(
text.find("test.c").unwrap() < text.find("test.a").unwrap(),
"{text}"
);
let intersect = run_stdin(
r#"(filter(has_attr("group")) intersect filter(not op("test.b"))) | count"#,
input,
);
assert!(intersect.status.success());
assert_eq!(String::from_utf8(intersect.stdout).unwrap(), "1\n");
let strings = run_stdin(
r#"(filter(has_attr("group")) | names | reverse union filter(op("test.c")) | names) | json"#,
input,
);
assert!(
strings.status.success(),
"{}",
String::from_utf8_lossy(&strings.stderr)
);
assert_eq!(
serde_json::from_slice::<serde_json::Value>(&strings.stdout).unwrap(),
serde_json::json!(["test.b", "test.a", "test.c"])
);
for query in [
r#"(filter(has_attr("group")) except filter(string_attr_eq("group", "keep"))) | count"#,
r#"(filter(op("missing")) union filter(op("missing"))) | count"#,
r#"(filter(op("missing")) intersect filter(op("test.a"))) | count"#,
r#"(filter(op("test.a")) except filter(op("missing"))) | count"#,
] {
let output = run_stdin(query, input);
assert!(
output.status.success(),
"{}",
String::from_utf8_lossy(&output.stderr)
);
let expected = if query.contains("except filter(op") {
"1\n"
} else {
"0\n"
};
assert_eq!(String::from_utf8(output.stdout).unwrap(), expected);
}
let first = temporary_path("set-first", "mlir");
let second = temporary_path("set-second", "mlir");
fs::write(&first, input).unwrap();
fs::write(&second, "module { \"test.d\"() : () -> () }\n").unwrap();
let output = Command::new(env!("CARGO_BIN_EXE_zirium"))
.arg(r#"(filter(op("test.a")) union filter(op("test.d"))) | count"#)
.arg(&first)
.arg(&second)
.output()
.unwrap();
let _ = fs::remove_file(first);
let _ = fs::remove_file(second);
assert!(output.status.success());
assert_eq!(String::from_utf8(output.stdout).unwrap(), "1\n1\n");
}
#[test]
fn ownership_navigation_is_one_step_and_handles_block_arguments() {
let input = "\"builtin.module\"() ({\n \"func.func\"() ({\n ^entry(%arg0: i32):\n %sum = \"arith.addi\"(%arg0, %arg0) : (i32, i32) -> i32\n \"func.return\"(%sum) : (i32) -> ()\n }) : () -> ()\n}) : () -> ()\n";
let boundary = run_stdin("filter(op(\"arith.addi\")) | defs", input);
assert!(
boundary.status.success(),
"{}",
String::from_utf8_lossy(&boundary.stderr)
);
let text = String::from_utf8(boundary.stdout).unwrap();
assert!(text.contains("func.func"), "{text}");
assert!(text.contains("arith.addi"), "{text}");
let parent = run_stdin("filter(op(\"arith.addi\")) | parent", input);
assert!(parent.status.success());
let text = String::from_utf8(parent.stdout).unwrap();
assert!(text.contains("func.func"), "{text}");
let children = run_stdin("filter(op(\"func.func\")) | children", input);
assert!(children.status.success());
let text = String::from_utf8(children.stdout).unwrap();
assert!(text.contains("arith.addi"), "{text}");
assert!(text.contains("func.return"), "{text}");
}
#[test]
fn count_prints_one_scalar_line_per_input() {
let first = temporary_path("count-first", "mlir");
let second = temporary_path("count-second", "mlir");
fs::write(&first, INPUT).unwrap();
fs::write(&second, "module { func.return }\n").unwrap();
let output = Command::new(env!("CARGO_BIN_EXE_zirium"))
.arg("filter(op(\"arith.addi\")) | count")
.arg(&first)
.arg(&second)
.output()
.unwrap();
let _ = fs::remove_file(first);
let _ = fs::remove_file(second);
assert!(
output.status.success(),
"{}",
String::from_utf8_lossy(&output.stderr)
);
assert_eq!(String::from_utf8(output.stdout).unwrap(), "1\n0\n");
}
#[test]
fn jsonl_attributes_each_emission_to_its_input() {
let first = temporary_path("ndjson-é-first", "mlir");
let second = temporary_path("ndjson-quoted-\"second", "mlir");
fs::write(&first, "module {}").unwrap();
fs::write(&second, "module { func.return }").unwrap();
let labelled = Command::new(env!("CARGO_BIN_EXE_zirium"))
.arg(r#"print("{document}")"#)
.arg(&first)
.arg(&second)
.output()
.unwrap();
assert!(labelled.status.success());
assert_eq!(
String::from_utf8(labelled.stdout).unwrap(),
format!(
"{}\n{}\n",
first.to_string_lossy(),
second.to_string_lossy()
)
);
let query = "print(\"file: {document}\ncontinued\"); count; {\"label\": \"{document}\"} | json";
let output = Command::new(env!("CARGO_BIN_EXE_zirium"))
.arg("--jsonl")
.arg(query)
.arg(&first)
.arg(&second)
.output()
.unwrap();
let _ = fs::remove_file(&first);
let _ = fs::remove_file(&second);
assert!(
output.status.success(),
"{}",
String::from_utf8_lossy(&output.stderr)
);
let records: Vec<serde_json::Value> = String::from_utf8(output.stdout)
.unwrap()
.lines()
.map(|line| serde_json::from_str(line).unwrap())
.collect();
assert_eq!(records.len(), 6);
for (input_index, path) in [first, second].iter().enumerate() {
let name = path.to_string_lossy();
let records = &records[input_index * 3..input_index * 3 + 3];
assert!(
records
.iter()
.all(|record| record["document"] == name.as_ref())
);
assert_eq!(records[0]["result"], format!("file: {name}\ncontinued\n"));
assert!(records[1]["result"].is_number());
assert_eq!(records[2]["result"]["label"], name.as_ref());
}
let output = run_stdin_with_options(&["--jsonl"], r#"print("{document}")"#, "module {}");
assert!(output.status.success());
let record: serde_json::Value = serde_json::from_slice(&output.stdout).unwrap();
assert_eq!(record["document"], "stdin");
assert_eq!(record["result"], "stdin\n");
for ending in ["", " | json"] {
let ranked = run_stdin_with_options(
&["--jsonl"],
&format!(
"filter(not op(\"builtin.module\")) | names | tally | sort_by(value) | reverse{ending}"
),
"module { \"vendor.z\"() : () -> () \"vendor.z\"() : () -> () \"vendor.a\"() : () -> () }",
);
assert!(ranked.status.success());
let line = String::from_utf8(ranked.stdout).unwrap();
assert_eq!(line.lines().count(), 1);
serde_json::from_str::<serde_json::Value>(line.trim_end()).unwrap();
assert!(
line.find("\"vendor.z\"").unwrap() < line.find("\"vendor.a\"").unwrap(),
"{line}"
);
}
let output = run_stdin_with_options(
&["--jsonl"],
r#"{"text": "spaces é \"quoted\" \\ slash\nnext", "nested": [true, null]} | json"#,
"module {}",
);
assert!(output.status.success());
let line = String::from_utf8(output.stdout).unwrap();
assert_eq!(line.lines().count(), 1);
let record: serde_json::Value = serde_json::from_str(&line).unwrap();
assert_eq!(
record["result"]["text"],
"spaces é \"quoted\" \\ slash\nnext"
);
assert_eq!(record["result"]["nested"], serde_json::json!([true, null]));
let alias = run_stdin_with_options(&["--ndjson"], "count", "module {}");
assert!(alias.status.success());
assert_eq!(
serde_json::from_slice::<serde_json::Value>(&alias.stdout).unwrap()["result"],
1
);
}
#[test]
fn set_attr_keeps_the_changed_selection_and_comments() {
let output = run_stdin(
"filter(op(\"arith.addi\")) | set_attr(\"analysis.tag\", \"hot\")",
INPUT,
);
assert!(
output.status.success(),
"{}",
String::from_utf8_lossy(&output.stderr)
);
let text = String::from_utf8(output.stdout).unwrap();
assert_eq!(text.matches("analysis.tag = \"hot\"").count(), 1, "{text}");
assert!(text.contains("// Double it."), "{text}");
assert!(text.contains("// selected"), "{text}");
assert!(!text.contains("arith.constant"), "{text}");
assert!(!text.contains("example.observe"), "{text}");
}
#[test]
fn input_after_mutation_prints_the_whole_document() {
let output = run_stdin(
"filter(op(\"arith.addi\")) | set_attr(\"analysis.tag\", \"hot\") | input | emit",
INPUT,
);
assert!(
output.status.success(),
"{}",
String::from_utf8_lossy(&output.stderr)
);
let text = String::from_utf8(output.stdout).unwrap();
assert!(text.contains("analysis.tag = \"hot\""), "{text}");
assert!(text.contains("arith.constant"), "{text}");
assert!(text.contains("example.observe"), "{text}");
}
#[test]
fn generic_single_operand_edit_prints_a_parseable_function_type() {
let input =
"%item = \"sample.create\"() : () -> i64\n\"sample.forward\"(%item) : (i64) -> ()\n";
let edited = run_stdin(
r#"filter(op("sample.forward")) | set_attr("roundtrip.marker", "present") | input | emit"#,
input,
);
assert!(
edited.status.success(),
"{}",
String::from_utf8_lossy(&edited.stderr)
);
let printed = String::from_utf8(edited.stdout).unwrap();
assert!(
printed.contains("roundtrip.marker = \"present\""),
"{printed}"
);
let reparsed = run_stdin(r#"filter(op("sample.forward")) | count"#, &printed);
assert!(
reparsed.status.success(),
"{printed}\n{}",
String::from_utf8_lossy(&reparsed.stderr)
);
assert_eq!(reparsed.stdout, b"1\n", "{printed}");
}
#[test]
fn set_attr_mutates_the_selection_at_its_pipeline_position() {
let output = run_stdin(
"filter(op(\"arith.addi\")) | set_attr(\"analysis.tag\", \"hot\") | fixpoint(closure) | input | emit",
INPUT,
);
assert!(
output.status.success(),
"{}",
String::from_utf8_lossy(&output.stderr)
);
let text = String::from_utf8(output.stdout).unwrap();
assert_eq!(text.matches("analysis.tag = \"hot\"").count(), 1, "{text}");
let constant = text.find("arith.constant").unwrap();
let add = text.find("arith.addi").unwrap();
let tag = text.find("analysis.tag = \"hot\"").unwrap();
assert!(constant < add && add < tag, "{text}");
}
#[test]
fn remove_attr_keeps_selection_comments_and_other_attributes() {
let input = "module {\n // Keep this comment.\n \"test.a\"() {analysis.tag = \"hot\", other = \"keep\"} : () -> () // trailing a\n \"test.b\"() {analysis.tag = \"cold\"} : () -> () // trailing b\n}\n";
let output = run_stdin(
r#"filter(op("test.a")) | remove_attr("analysis.tag")"#,
input,
);
assert!(
output.status.success(),
"{}",
String::from_utf8_lossy(&output.stderr)
);
let text = String::from_utf8(output.stdout).unwrap();
assert!(!text.contains("analysis.tag"), "{text}");
assert!(text.contains("other = \"keep\""), "{text}");
assert!(text.contains("// Keep this comment."), "{text}");
let selected_line = text.lines().find(|line| line.contains("test.a")).unwrap();
assert!(selected_line.ends_with("// trailing a"), "{text}");
assert!(!text.contains("// trailing b"), "{text}");
assert!(!text.contains("test.b"), "{text}");
}
#[test]
fn remove_attr_absent_is_a_noop_and_composes_in_pipeline_order() {
let absent_input = "module {\n // Keep the first.\n \"test.a\"() : () -> () // trailing a\n // Keep the second.\n \"test.b\"() : () -> () // trailing b\n}\n";
let absent = run_stdin(
r#"filter(op("test.a") or op("test.b")) | remove_attr("missing.tag")"#,
absent_input,
);
assert!(
absent.status.success(),
"{}",
String::from_utf8_lossy(&absent.stderr)
);
let absent_text = String::from_utf8(absent.stdout).unwrap();
let absent_lines = absent_text.lines().collect::<Vec<_>>();
let a_line = absent_lines
.iter()
.find(|line| line.contains("test.a"))
.unwrap();
let b_line = absent_lines
.iter()
.find(|line| line.contains("test.b"))
.unwrap();
assert!(a_line.ends_with("// trailing a"), "{absent_text}");
assert!(b_line.ends_with("// trailing b"), "{absent_text}");
let first_comment = absent_text.find("// Keep the first.").unwrap();
let test_a = absent_text.find("test.a").unwrap();
let second_comment = absent_text.find("// Keep the second.").unwrap();
let test_b = absent_text.find("test.b").unwrap();
assert!(first_comment < test_a && test_a < second_comment && second_comment < test_b);
let mixed_input = "module {\n // Comment for a.\n \"test.a\"() {analysis.tag = \"hot\"} : () -> () // trailing a\n // Comment for b.\n \"test.b\"() : () -> () // trailing b\n}\n";
let mixed = run_stdin(
r#"filter(op("test.a") or op("test.b")) | remove_attr("analysis.tag")"#,
mixed_input,
);
assert!(
mixed.status.success(),
"{}",
String::from_utf8_lossy(&mixed.stderr)
);
let mixed_text = String::from_utf8(mixed.stdout).unwrap();
assert!(!mixed_text.contains("analysis.tag"), "{mixed_text}");
let comment_for_a = mixed_text.find("// Comment for a.").unwrap();
let test_a = mixed_text.find("\"test.a\"").unwrap();
let comment_for_b = mixed_text.find("// Comment for b.").unwrap();
let test_b = mixed_text.find("\"test.b\"").unwrap();
assert!(
comment_for_a < test_a && test_a < comment_for_b && comment_for_b < test_b,
"{mixed_text}"
);
assert_eq!(mixed_text.matches("// Comment for b.").count(), 1);
let mixed_lines = mixed_text.lines().collect::<Vec<_>>();
let a_line = mixed_lines
.iter()
.find(|line| line.contains("test.a"))
.unwrap();
let b_line = mixed_lines
.iter()
.find(|line| line.contains("test.b"))
.unwrap();
assert!(a_line.ends_with("// trailing a"), "{mixed_text}");
assert!(b_line.ends_with("// trailing b"), "{mixed_text}");
let output = run_stdin(
r#"filter(op("arith.addi")) | set_attr("analysis.tag", "hot") | remove_attr("analysis.tag") | fixpoint(closure) | input | emit"#,
INPUT,
);
assert!(
output.status.success(),
"{}",
String::from_utf8_lossy(&output.stderr)
);
let text = String::from_utf8(output.stdout).unwrap();
assert!(!text.contains("analysis.tag"), "{text}");
assert!(text.contains("arith.constant"), "{text}");
assert!(text.contains("example.observe"), "{text}");
}
#[test]
fn remove_attr_rejects_unparsed_operations_before_editing_the_selection() {
for input in [
"module {\n mystery.consume {debug.note = \"sealed\"}\n}\n",
"module {\n \"example.known\"() {debug.note = \"drop\"} : () -> ()\n mystery.consume {debug.note = \"sealed\"}\n}\n",
] {
let output = run_stdin(
r#"filter(op("example.known") or op("mystery.consume")) | remove_attr("debug.note")"#,
input,
);
assert!(!output.status.success());
assert!(output.stdout.is_empty());
assert!(
String::from_utf8_lossy(&output.stderr).contains("cannot edit an incomplete document"),
"{}",
String::from_utf8_lossy(&output.stderr)
);
}
}
#[test]
fn remove_attr_does_not_attach_a_parent_tail_to_a_nested_operation() {
for child_attributes in [" {analysis.tag = \"hot\"}", ""] {
let input = format!(
"module {{\n \"test.parent\"() ({{ \"test.child\"(){child_attributes} : () -> () }}) : () -> () // parent tail\n}}\n"
);
let output = run_stdin(
r#"filter(op("test.parent") or op("test.child")) | remove_attr("analysis.tag")"#,
&input,
);
assert!(
output.status.success(),
"{}",
String::from_utf8_lossy(&output.stderr)
);
let text = String::from_utf8(output.stdout).unwrap();
assert_eq!(text.matches("// parent tail").count(), 1, "{text}");
let child_line = text
.lines()
.find(|line| line.contains("test.child"))
.unwrap();
assert!(!child_line.contains("// parent tail"), "{text}");
let parent_tail = text
.lines()
.find(|line| line.contains("// parent tail"))
.unwrap();
assert!(parent_tail.trim_start().starts_with('}'), "{text}");
}
}
#[test]
fn rejected_composition_produces_no_output() {
let output = run_stdin("filter(op(\"arith.addi\")) | count | input | emit", INPUT);
assert!(!output.status.success());
assert!(output.stdout.is_empty());
assert!(String::from_utf8_lossy(&output.stderr).contains("no stage may follow count"));
}
#[test]
fn invalid_set_attr_text_produces_no_output() {
for query in [
"filter(op(\"arith.addi\")) | set_attr(\"bad name\", \"hot\") | input | emit",
"filter(op(\"arith.addi\")) | set_attr(\"tag\", \"hot\nvalue\") | input | emit",
] {
let output = run_stdin(query, INPUT);
assert!(!output.status.success());
assert!(output.stdout.is_empty());
assert!(String::from_utf8_lossy(&output.stderr).contains("query error"));
}
}
#[test]
fn escaped_set_attr_whole_document_output_is_parseable() {
let input = "module {\n %c = arith.constant 7 : i32\n %sum = arith.addi %c, %c : i32\n}\n";
let output = run_stdin(
r#"filter(op("arith.addi")) | set_attr("analysis.tag", "say \"hi\" \\ path") | input | emit"#,
input,
);
assert!(
output.status.success(),
"{}",
String::from_utf8_lossy(&output.stderr)
);
let text = String::from_utf8(output.stdout).unwrap();
assert!(text.contains(r#"analysis.tag = "say \"hi\" \\ path""#));
let reparsed = run_stdin("filter(op(\"builtin.module\"))", &text);
assert!(
reparsed.status.success(),
"{}",
String::from_utf8_lossy(&reparsed.stderr)
);
}
#[test]
fn closure_at_function_argument_retains_complete_function_scope() {
let input = "\"builtin.module\"() ({\n \"func.func\"() ({\n ^entry(%arg0: i32):\n %sum = \"arith.addi\"(%arg0, %arg0) : (i32, i32) -> i32\n \"func.return\"(%sum) : (i32) -> ()\n }) : () -> ()\n \"example.other\"() : () -> ()\n}) : () -> ()\n";
let output = run_stdin("filter(op(\"arith.addi\")) | fixpoint(closure)", input);
assert!(
output.status.success(),
"{}",
String::from_utf8_lossy(&output.stderr)
);
let text = String::from_utf8(output.stdout).unwrap();
assert!(text.contains("\"func.func\""), "{text}");
assert!(text.contains("func.return %"), "{text}");
assert!(!text.contains("example.other"), "{text}");
}
#[test]
fn closure_follows_recursive_symbol_once_without_sibling_symbols() {
let input = "module {\n func.func @recursive() {\n func.call @recursive() : () -> ()\n func.return\n }\n func.func @unrelated() { func.return }\n func.func @caller() {\n func.call @recursive() : () -> ()\n func.return\n }\n}\n";
let output = run_stdin("filter(op(\"func.call\")) | fixpoint(closure)", input);
assert!(
output.status.success(),
"{}",
String::from_utf8_lossy(&output.stderr)
);
let text = String::from_utf8(output.stdout).unwrap();
assert_eq!(text.matches("func.func @recursive").count(), 1, "{text}");
assert_eq!(text.matches("func.call @recursive").count(), 2, "{text}");
assert!(!text.contains("func.func @unrelated"), "{text}");
}
#[test]
fn closure_resolves_a_quoted_symbol_containing_path_separators() {
let input = r#"module {
func.func @"a::b"() { func.return }
func.func @unrelated() { func.return }
func.func @caller() {
func.call @"a::b"() : () -> ()
func.return
}
}
"#;
let output = run_stdin("filter(op(\"func.call\")) | fixpoint(closure)", input);
assert!(
output.status.success(),
"{}",
String::from_utf8_lossy(&output.stderr)
);
let text = String::from_utf8(output.stdout).unwrap();
assert!(text.contains(r#"func.func @"a::b""#), "{text}");
assert!(text.contains(r#"func.call @"a::b""#), "{text}");
assert!(!text.contains("func.func @unrelated"), "{text}");
}
#[test]
fn closure_retains_cyclic_cfg_once_for_conditional_and_unconditional_branches() {
let input = "module {\n func.func @loop() {\n ^entry:\n cf.br ^loop\n ^loop:\n %condition = arith.constant 1 : i1\n cf.cond_br %condition, ^loop, ^exit\n ^exit:\n func.return\n }\n}\n";
let output = run_stdin("filter(op(\"cf.cond_br\")) | fixpoint(closure)", input);
assert!(
output.status.success(),
"{}",
String::from_utf8_lossy(&output.stderr)
);
let text = String::from_utf8(output.stdout).unwrap();
assert_eq!(text.matches("func.func @loop").count(), 1, "{text}");
assert_eq!(text.matches("cf.br").count(), 1, "{text}");
assert_eq!(text.matches("cf.cond_br").count(), 1, "{text}");
assert_eq!(text.matches("arith.constant").count(), 1, "{text}");
assert_eq!(text.matches("func.return").count(), 1, "{text}");
}
#[test]
fn closure_reports_unresolved_callee_from_strict_lowering() {
let input = "module {\n func.func @caller() {\n func.call @missing() : () -> ()\n func.return\n }\n}\n";
let output = run_stdin("filter(op(\"func.call\")) | fixpoint(closure)", input);
assert!(!output.status.success());
let diagnostic = String::from_utf8_lossy(&output.stderr);
assert!(
diagnostic.contains("could not resolve func.call callee `@missing`"),
"{diagnostic}"
);
}
#[test]
fn closure_reports_invalid_successor_from_strict_lowering() {
let input = "module {\n func.func @caller() {\n cf.br ^missing\n }\n}\n";
let output = run_stdin("filter(op(\"cf.br\")) | fixpoint(closure)", input);
assert!(!output.status.success());
let diagnostic = String::from_utf8_lossy(&output.stderr);
assert!(
diagnostic.contains("unresolved block `^missing`"),
"{diagnostic}"
);
}
#[test]
fn closure_rejects_symbol_reference_on_unregistered_operation() {
let input = "\"example.def\"() {sym_name = \"callee\"} : () -> ()\n\"example.call\"() {callee = @callee} : () -> ()\n";
let output = run_stdin("filter(op(\"example.call\")) | fixpoint(closure)", input);
assert!(!output.status.success());
let diagnostic = String::from_utf8_lossy(&output.stderr);
assert!(
diagnostic.contains(
"cannot determine reference semantics for unregistered operation `example.call`"
),
"{diagnostic}"
);
}
#[test]
fn short_program_file_flag_reads_query_with_final_newline() {
let program = temporary_path("short-program", "zirium");
fs::write(&program, "filter(op(\"arith.addi\"))\n").unwrap();
let mut child = Command::new(env!("CARGO_BIN_EXE_zirium"))
.arg("-f")
.arg(&program)
.stdin(Stdio::piped())
.stdout(Stdio::piped())
.stderr(Stdio::piped())
.spawn()
.unwrap();
child
.stdin
.take()
.unwrap()
.write_all(INPUT.as_bytes())
.unwrap();
let output = child.wait_with_output().unwrap();
let _ = fs::remove_file(program);
assert!(
output.status.success(),
"{}",
String::from_utf8_lossy(&output.stderr)
);
assert!(
String::from_utf8(output.stdout)
.unwrap()
.contains("arith.addi")
);
}
#[test]
fn long_program_file_flag_keeps_mlir_file_arguments() {
let program = temporary_path("long-program", "zirium");
let first = temporary_path("long-first", "mlir");
let second = temporary_path("long-second", "mlir");
fs::write(&program, "filter(op(\"missing\"))").unwrap();
fs::write(&first, INPUT).unwrap();
fs::write(&second, INPUT).unwrap();
let output = Command::new(env!("CARGO_BIN_EXE_zirium"))
.arg("--program-file")
.arg(&program)
.arg(&first)
.arg(&second)
.output()
.unwrap();
let _ = fs::remove_file(program);
let _ = fs::remove_file(first);
let _ = fs::remove_file(second);
assert!(
output.status.success(),
"{}",
String::from_utf8_lossy(&output.stderr)
);
assert!(output.stdout.is_empty());
}
#[test]
fn unreadable_program_file_fails_usefully() {
let missing = temporary_path("missing-program", "zirium");
let output = Command::new(env!("CARGO_BIN_EXE_zirium"))
.arg("-f")
.arg(&missing)
.output()
.unwrap();
let diagnostic = String::from_utf8_lossy(&output.stderr);
assert!(!output.status.success());
assert!(
diagnostic.contains("could not read program file"),
"{diagnostic}"
);
}
#[test]
fn selected_root_retains_owned_contents_and_comments() {
let input = "// Root comment.\nmodule {\n // Function comment.\n func.func @f() {\n // Nested comment.\n func.return\n }\n}\n";
let output = run_stdin("filter(op(\"builtin.module\"))", input);
assert!(
output.status.success(),
"{}",
String::from_utf8_lossy(&output.stderr)
);
let text = String::from_utf8(output.stdout).unwrap();
assert_eq!(
text,
"// Root comment.\nbuiltin.module {\n // Function comment.\n func.func @f() -> () {\n // Nested comment.\n func.return\n }\n}\n"
);
}
#[test]
fn selected_function_with_argument_can_be_queried_again() {
let input = "module { func.func @identity(%arg: i32) -> i32 { func.return %arg : i32 } }\n";
let selected = run_stdin(r#"filter(op("func.func"))"#, input);
assert!(
selected.status.success(),
"{}",
String::from_utf8_lossy(&selected.stderr)
);
let printed = String::from_utf8(selected.stdout).unwrap();
let reparsed = run_stdin(r#"filter(op("func.func")) | count"#, &printed);
assert!(
reparsed.status.success(),
"{printed}\n{}",
String::from_utf8_lossy(&reparsed.stderr)
);
assert_eq!(reparsed.stdout, b"1\n", "{printed}");
}
#[test]
fn selected_unknown_operation_preserves_enclosing_argument_names() {
let input = "module {\n func.func @pipeline(%source: i64) {\n mystery.observe %source : i64\n func.return\n }\n}\n";
let selected = run_stdin(r#"filter(op("mystery.observe"))"#, input);
assert!(
selected.status.success(),
"{}",
String::from_utf8_lossy(&selected.stderr)
);
let printed = String::from_utf8(selected.stdout).unwrap();
assert!(
printed.contains("func.func @pipeline(%source: i64)"),
"{printed}"
);
assert!(
printed.contains("mystery.observe %source : i64"),
"{printed}"
);
for (query, expected) in [
(
r#"filter(op("mystery.observe")) | count"#,
b"1\n".as_slice(),
),
(
r#"filter(op("mystery.observe")) | parent | count"#,
b"1\n".as_slice(),
),
(
r#"filter(op("func.func")) | children | count"#,
b"1\n".as_slice(),
),
] {
let reparsed = run_stdin(query, &printed);
assert!(
reparsed.status.success(),
"{printed}\n{}",
String::from_utf8_lossy(&reparsed.stderr)
);
assert_eq!(reparsed.stdout, expected, "{printed}");
}
}
#[test]
fn selected_unknown_operation_does_not_rewrite_attribute_strings() {
let input = "module {\n func.func @f(%arg: i32) attributes {note = \"%v0\"} {\n vendor.use %arg : i32\n }\n}\n";
let selected = run_stdin(r#"filter(op("vendor.use"))"#, input);
assert!(
selected.status.success(),
"{}",
String::from_utf8_lossy(&selected.stderr)
);
let printed = String::from_utf8(selected.stdout).unwrap();
assert!(printed.contains("func.func @f(%arg: i32)"), "{printed}");
assert!(printed.contains("note = \"%v0\""), "{printed}");
assert!(printed.contains("vendor.use %arg : i32"), "{printed}");
}
#[test]
fn inline_nested_operation_does_not_steal_parent_comment() {
let input = "module {\n // Nested function.\n func.func @inner() { func.return }\n}\n";
let output = run_stdin("filter(op(\"builtin.module\"))", input);
assert!(
output.status.success(),
"{}",
String::from_utf8_lossy(&output.stderr)
);
let text = String::from_utf8(output.stdout).unwrap();
assert_eq!(text.matches("// Nested function.").count(), 1, "{text}");
assert!(text.contains("func.func @inner() -> ()"), "{text}");
assert!(text.contains("func.return"), "{text}");
}
#[test]
fn multiple_empty_file_results_produce_no_output() {
let directory = std::env::temp_dir();
let first = directory.join(format!("zirium-cli-{}-a.mlir", std::process::id()));
let second = directory.join(format!("zirium-cli-{}-b.mlir", std::process::id()));
fs::write(&first, INPUT).unwrap();
fs::write(&second, INPUT).unwrap();
let output = Command::new(env!("CARGO_BIN_EXE_zirium"))
.arg("filter(op(\"missing\"))")
.arg(&first)
.arg(&second)
.output()
.unwrap();
let _ = fs::remove_file(first);
let _ = fs::remove_file(second);
assert!(output.status.success());
assert!(output.stdout.is_empty());
}
#[test]
fn malformed_query_and_input_fail_usefully() {
let bad_query = run_stdin("filter(op(\"arith.addi\")", INPUT);
assert!(!bad_query.status.success());
assert!(bad_query.stdout.is_empty());
let query_diagnostic = String::from_utf8_lossy(&bad_query.stderr);
assert!(
query_diagnostic.contains("<query>:1:"),
"{query_diagnostic}"
);
assert!(
query_diagnostic.contains("query error"),
"{query_diagnostic}"
);
assert!(query_diagnostic.contains('^'), "{query_diagnostic}");
let bad_input = run_stdin(
"filter(op(\"arith.addi\"))",
"module {\n %x = arith.constant nope : i32\n}\n",
);
assert!(!bad_input.status.success());
assert!(bad_input.stdout.is_empty());
let diagnostic = String::from_utf8_lossy(&bad_input.stderr);
assert!(diagnostic.contains("could not parse stdin"), "{diagnostic}");
assert!(diagnostic.contains("stdin:2:"), "{diagnostic}");
assert!(diagnostic.contains("invalid MLIR syntax"), "{diagnostic}");
assert!(diagnostic.contains('^'), "{diagnostic}");
}
#[test]
fn lowering_failure_reports_identity_and_original_range() {
let input = "module {\n \"example.use\"(%missing) : (i32) -> ()\n}\n";
let output = run_stdin("filter(op(\"example.use\"))", input);
assert!(!output.status.success());
assert!(output.stdout.is_empty());
let diagnostic = String::from_utf8_lossy(&output.stderr);
assert!(diagnostic.contains("stdin:2:"), "{diagnostic}");
assert!(
diagnostic.contains("unresolved SSA value `%missing`"),
"{diagnostic}"
);
assert!(
diagnostic.contains("\"example.use\"(%missing)"),
"{diagnostic}"
);
assert!(diagnostic.contains("^~~~~~~~"), "{diagnostic}");
}
#[test]
fn arbitrary_generic_dialect_operations_support_structural_queries_and_edits() {
let input = "\"builtin.module\"() ({\n \"vendor.container\"() ({\n \"vendor.compute\"() {tag = \"hot\", remove = \"yes\"} : () -> ()\n }) : () -> ()\n}) : () -> ()\n";
for (query, expected) in [
(r#"filter(string_attr_eq("tag", "hot")) | count"#, "1\n"),
(r#"filter(op("vendor.compute")) | parent | count"#, "1\n"),
(
r#"filter(op("vendor.container")) | children | count"#,
"1\n",
),
] {
let output = run_stdin(query, input);
assert!(
output.status.success(),
"{}",
String::from_utf8_lossy(&output.stderr)
);
assert_eq!(String::from_utf8(output.stdout).unwrap(), expected);
}
let edited = run_stdin(
r#"filter(op("vendor.compute")) | set_attr("added", "value") | remove_attr("remove")"#,
input,
);
assert!(
edited.status.success(),
"{}",
String::from_utf8_lossy(&edited.stderr)
);
let text = String::from_utf8(edited.stdout).unwrap();
assert!(text.contains("added = \"value\""), "{text}");
assert!(!text.contains("remove ="), "{text}");
}
#[test]
fn bounded_unknown_custom_operation_supports_name_count_ownership_and_exact_selection() {
let input = "module {\n vendor.compute strangely<balanced>(payload)\n}\n";
let count = run_stdin(r#"filter(op("vendor.compute")) | count"#, input);
assert!(
count.status.success(),
"{}",
String::from_utf8_lossy(&count.stderr)
);
assert_eq!(count.stdout, b"1\n");
let parent_count = run_stdin(r#"filter(op("vendor.compute")) | parent | count"#, input);
assert!(parent_count.status.success());
assert_eq!(parent_count.stdout, b"1\n");
let child_count = run_stdin(r#"filter(op("builtin.module")) | children | count"#, input);
assert!(child_count.status.success());
assert_eq!(child_count.stdout, b"1\n");
let selected = run_stdin(r#"filter(op("vendor.compute"))"#, input);
assert!(
selected.status.success(),
"{}",
String::from_utf8_lossy(&selected.stderr)
);
assert_eq!(
String::from_utf8(selected.stdout).unwrap(),
"builtin.module {\n vendor.compute strangely<balanced>(payload)\n\n}\n"
);
}
#[test]
fn unknown_custom_recovery_rejects_malformed_neighbors_closure_and_edits() {
let malformed = run_stdin(
r#"filter(op("vendor.compute"))"#,
"module { vendor.compute strangely<unclosed(payload) }\n",
);
assert!(!malformed.status.success());
assert!(malformed.stdout.is_empty());
let input = "module { vendor.compute strangely<balanced>(payload) }\n";
let closure = run_stdin(r#"filter(op("vendor.compute")) | fixpoint(closure)"#, input);
assert!(!closure.status.success());
assert!(closure.stdout.is_empty());
assert!(String::from_utf8_lossy(&closure.stderr).contains(
"cannot determine reference semantics for unregistered operation `vendor.compute`"
));
let edit = run_stdin(
r#"filter(op("vendor.compute")) | set_attr("tag", "value")"#,
input,
);
assert!(!edit.status.success());
assert!(edit.stdout.is_empty());
assert!(String::from_utf8_lossy(&edit.stderr).contains("edit failed"));
}
#[test]
fn mutation_before_unknown_closure_failure_emits_no_stdout() {
let input = "module {\n \"vendor.known\"() : () -> ()\n vendor.unknown opaque<payload>\n}\n";
let output = run_stdin(
r#"filter(op("vendor.known")) | set_attr("tag", "value") | (filter(true) union (input | filter(op("vendor.unknown")))) | fixpoint(closure)"#,
input,
);
assert!(!output.status.success());
assert!(output.stdout.is_empty());
}
#[test]
fn recovered_unknown_sibling_does_not_block_empty_or_understood_selections() {
let input = "module {\n \"vendor.known\"() : () -> ()\n vendor.unknown opaque<payload>\n}\n";
let empty = run_stdin(r#"filter(op("vendor.missing"))"#, input);
assert!(
empty.status.success(),
"{}",
String::from_utf8_lossy(&empty.stderr)
);
assert!(empty.stdout.is_empty());
let understood = run_stdin(r#"filter(op("vendor.known"))"#, input);
assert!(
understood.status.success(),
"{}",
String::from_utf8_lossy(&understood.stderr)
);
let text = String::from_utf8(understood.stdout).unwrap();
assert!(text.contains("vendor.known"), "{text}");
assert!(!text.contains("vendor.unknown"), "{text}");
}
#[test]
fn named_nested_and_commented_module_shorthand_uses_the_parser() {
let source = "module @outer attributes {tag = \"keep\"} {\n module // nested module\n @inner {\n \"test.op\"() : () -> ()\n }\n}\n";
let output = run_stdin(r#"filter(op("builtin.module")) | count"#, source);
assert!(
output.status.success(),
"{}",
String::from_utf8_lossy(&output.stderr)
);
assert_eq!(output.stdout, b"2\n");
let printed = run_stdin(r#"filter(op("builtin.module")) | input | emit"#, source);
assert!(
printed.status.success(),
"{}",
String::from_utf8_lossy(&printed.stderr)
);
let restored = run_stdin(
r#"filter(op("test.op")) | count"#,
std::str::from_utf8(&printed.stdout).unwrap(),
);
assert!(
restored.status.success(),
"{}",
String::from_utf8_lossy(&restored.stderr)
);
assert_eq!(restored.stdout, b"1\n");
}
#[test]
fn unknown_custom_sibling_does_not_hide_semantic_errors() {
let source =
"module {\n \"test.use\"(%missing) : (i32) -> ()\n vendor.unknown opaque<payload>\n}\n";
let output = run_stdin(r#"filter(op("test.use")) | count"#, source);
assert!(!output.status.success());
assert!(output.stdout.is_empty());
let diagnostic = String::from_utf8_lossy(&output.stderr);
assert!(diagnostic.contains("could not lower stdin"), "{diagnostic}");
assert!(diagnostic.contains("missing"), "{diagnostic}");
}
#[test]
fn registry_file_drives_cli_lowering_and_round_trip_output() {
use zirium::{
dialect::DialectRegistry,
parser::ParsedFile,
semantic::{LoweringMode, lower_with_dialect_registry},
};
let root = std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("../../examples/cli");
let registry_path = root.join("registry.json");
let input = root.join("registered-shapes.mlir");
let registry = DialectRegistry::from_config_file(®istry_path).unwrap();
let source = fs::read(&input).unwrap();
let parsed = ParsedFile::parse_with_registry(source, ®istry).unwrap();
let original = lower_with_dialect_registry(&parsed, LoweringMode::Strict, ®istry)
.document
.unwrap();
for (query, expected) in [
(
r#"filter(op("vendor.function")) | count"#,
Some(b"1\n".as_slice()),
),
(r#"filter(op("vendor.invoke")) | input | emit"#, None),
] {
let output = Command::new(env!("CARGO_BIN_EXE_zirium"))
.arg("--registry")
.arg(®istry_path)
.arg("--registry")
.arg(®istry_path)
.arg(query)
.arg(&input)
.output()
.unwrap();
assert!(
output.status.success(),
"{}",
String::from_utf8_lossy(&output.stderr)
);
if let Some(expected) = expected {
assert_eq!(output.stdout, expected);
} else {
let parsed = ParsedFile::parse_with_registry(output.stdout, ®istry).unwrap();
assert!(parsed.syntax().diagnostics().is_empty());
let restored = lower_with_dialect_registry(&parsed, LoweringMode::Strict, ®istry)
.document
.unwrap();
assert!(original.structurally_eq(&restored));
}
}
let program = temporary_path("registered-query", "zirium");
fs::write(&program, r#"filter(op("vendor.invoke")) | count"#).unwrap();
let output = Command::new(env!("CARGO_BIN_EXE_zirium"))
.arg("-f")
.arg(&program)
.arg("--registry")
.arg(®istry_path)
.arg(&input)
.output()
.unwrap();
fs::remove_file(program).unwrap();
assert!(
output.status.success(),
"{}",
String::from_utf8_lossy(&output.stderr)
);
assert_eq!(output.stdout, b"1\n");
let closure = Command::new(env!("CARGO_BIN_EXE_zirium"))
.arg("--registry")
.arg(®istry_path)
.arg(r#"filter(op("vendor.invoke")) | fixpoint(closure)"#)
.arg(&input)
.output()
.unwrap();
assert!(!closure.status.success());
assert!(closure.stdout.is_empty());
assert!(String::from_utf8_lossy(&closure.stderr).contains("reference semantics"));
}
#[test]
fn invalid_registry_fails_without_waiting_for_mlir_stdin() {
let registry = temporary_path("invalid-registry", "json");
fs::write(
®istry,
r#"{"builtins":[],"operation_shapes":[],"typo":true}"#,
)
.unwrap();
let mut child = Command::new(env!("CARGO_BIN_EXE_zirium"))
.arg("--registry")
.arg(®istry)
.arg(r#"filter(op("a.b")) | count"#)
.stdin(Stdio::piped())
.stdout(Stdio::piped())
.stderr(Stdio::piped())
.spawn()
.unwrap();
let deadline = std::time::Instant::now() + std::time::Duration::from_secs(5);
while child.try_wait().unwrap().is_none() {
if std::time::Instant::now() >= deadline {
child.kill().unwrap();
panic!("read stdin before rejecting registry");
}
std::thread::sleep(std::time::Duration::from_millis(10));
}
let output = child.wait_with_output().unwrap();
fs::remove_file(registry).unwrap();
assert!(!output.status.success());
assert!(output.stdout.is_empty());
assert!(String::from_utf8_lossy(&output.stderr).contains("unknown field"));
}
#[test]
fn filesystem_registry_bundle_and_preset_share_cli_composition() {
let root = std::path::Path::new(env!("CARGO_MANIFEST_DIR"))
.join("../../tests/fixtures/registry-bundles/root.json");
let mut child = Command::new(env!("CARGO_BIN_EXE_zirium"))
.arg("--registry")
.arg(root)
.args([
"--preset",
"arith",
r#"filter(op("vendor.invoke")) | count"#,
])
.stdin(Stdio::piped())
.stdout(Stdio::piped())
.stderr(Stdio::piped())
.spawn()
.unwrap();
child
.stdin
.take()
.unwrap()
.write_all(b"vendor.invoke @callee() : () -> ()")
.unwrap();
let output = child.wait_with_output().unwrap();
assert!(
output.status.success(),
"{}",
String::from_utf8_lossy(&output.stderr)
);
assert_eq!(output.stdout, b"1\n");
}
#[test]
fn registry_graph_limits_fail_before_reading_mlir_stdin() {
let root = std::path::Path::new(env!("CARGO_MANIFEST_DIR"))
.join("../../tests/fixtures/registry-bundles/root.json");
let mut child = Command::new(env!("CARGO_BIN_EXE_zirium"))
.args(["--max-registry-files", "3", "--registry"])
.arg(root)
.arg("count")
.stdin(Stdio::piped())
.stdout(Stdio::piped())
.stderr(Stdio::piped())
.spawn()
.unwrap();
let deadline = std::time::Instant::now() + std::time::Duration::from_secs(5);
while child.try_wait().unwrap().is_none() {
if std::time::Instant::now() >= deadline {
child.kill().unwrap();
panic!("read stdin before rejecting registry graph limit");
}
std::thread::sleep(std::time::Duration::from_millis(10));
}
let output = child.wait_with_output().unwrap();
assert!(!output.status.success());
assert!(output.stdout.is_empty());
assert!(String::from_utf8_lossy(&output.stderr).contains("file count"));
}
#[test]
fn implicit_input_and_emit_cover_empty_programs_and_explicit_taps() {
let expected = run_stdin("input | emit", INPUT);
assert!(expected.status.success());
for query in ["", " \n # unchanged input\n", "input", "emit", "(emit)"] {
let output = run_stdin(query, INPUT);
assert!(
output.status.success(),
"{query}: {}",
String::from_utf8_lossy(&output.stderr)
);
assert_eq!(output.stdout, expected.stdout, "{query}");
}
let twice = run_stdin("emit | emit", INPUT);
assert!(twice.status.success());
assert_eq!(twice.stdout, [expected.stdout.as_slice(); 2].concat());
}
#[test]
fn do_statement_keeps_edits_without_emitting_its_selection() {
let output = run_stdin(
r#"do filter(op("arith.addi")) | set_attr("analysis.tag", "review"); emit"#,
INPUT,
);
assert!(
output.status.success(),
"{}",
String::from_utf8_lossy(&output.stderr)
);
let stdout = String::from_utf8(output.stdout).unwrap();
assert_eq!(stdout.matches("analysis.tag = \"review\"").count(), 1);
assert!(!stdout.contains("// -----"));
assert!(stdout.contains("example.observe"));
let silent = run_stdin(
r#"do filter(op("arith.addi")) | set_attr("analysis.tag", "review");"#,
INPUT,
);
assert!(silent.status.success());
assert!(silent.stdout.is_empty());
let explicit = run_stdin("do emit;", INPUT);
assert!(explicit.status.success());
assert_eq!(explicit.stdout, run_stdin("emit", INPUT).stdout);
}
#[test]
fn ordering_bounds_and_extrema_compose_on_streams() {
let input = r#"module {
"vendor.item"() {id = 1, label = "beta"} : () -> ()
"vendor.item"() {id = 2, label = "alpha"} : () -> ()
"vendor.item"() {id = 3, label = "alpha"} : () -> ()
"vendor.item"() {id = 4, label = "beta"} : () -> ()
}"#;
for (query, expected) in [
(
r#"filter(op("vendor.item")) | attr("label") | sort"#,
"alpha\nalpha\nbeta\nbeta\n",
),
(
r#"filter(op("vendor.item")) | sort_by(attr("label")) | attr("id")"#,
"2\n3\n1\n4\n",
),
(
r#"filter(op("vendor.item")) | sort_by(children | count) | attr("id")"#,
"1\n2\n3\n4\n",
),
(
r#"filter(op("vendor.item")) | sort_by(attr("label")) | reverse | attr("id")"#,
"4\n1\n3\n2\n",
),
(
r#"filter(op("vendor.item")) | head(2) | attr("id")"#,
"1\n2\n",
),
(
r#"filter(op("vendor.item")) | tail(2) | attr("id")"#,
"3\n4\n",
),
(
r#"filter(op("vendor.item")) | attr("label") | min"#,
"alpha\n",
),
(
r#"filter(op("vendor.item")) | attr("label") | max"#,
"beta\n",
),
(
r#"filter(op("vendor.item")) | attr("label") | min_all"#,
"alpha\nalpha\n",
),
(
r#"filter(op("vendor.item")) | attr("label") | max_all"#,
"beta\nbeta\n",
),
(
r#"filter(op("vendor.item")) | min_by(attr("label")) | attr("id")"#,
"2\n",
),
(
r#"filter(op("vendor.item")) | max_by(attr("label")) | attr("id")"#,
"1\n",
),
(
r#"filter(op("vendor.item")) | min_all_by(attr("label")) | attr("id")"#,
"2\n3\n",
),
(
r#"filter(op("vendor.item")) | max_all_by(attr("label")) | attr("id")"#,
"1\n4\n",
),
] {
let output = run_stdin(query, input);
assert!(
output.status.success(),
"{query}: {}",
String::from_utf8_lossy(&output.stderr)
);
assert_eq!(
String::from_utf8(output.stdout).unwrap(),
expected,
"{query}"
);
}
for query in [
"filter(false) | names | min",
"filter(false) | min_by(names)",
"filter(false) | names | max_all",
"filter(false) | max_all_by(names)",
"filter(op(\"vendor.item\")) | sort_by(children | names)",
"names | emit; filter(op(\"vendor.item\")) | min_all_by(attr(\"label\") | check(2))",
"filter(false) | sort_by(names | emit)",
"saved = count; saved | count",
] {
let output = run_stdin(query, input);
assert!(!output.status.success(), "{query}");
assert!(output.stdout.is_empty(), "{query}");
}
}
#[test]
fn subtree_expands_the_selected_fragment_without_selecting_siblings() {
let input = include_str!("../../../examples/cli/calls.mlir");
for (query, expected) in [
(r#"filter(op("func.call")) | parent | count"#, "1\n"),
(
r#"filter(op("func.call")) | parent | subtree | count"#,
"3\n",
),
(
r#"filter(op("func.call")) | parent | subtree | filter(op("func.return")) | count"#,
"1\n",
),
(
r#"filter(op("func.call")) | parent | subtree | input | filter(op("func.return")) | count"#,
"3\n",
),
(r#"filter(op("func.call")) | subtree | count"#, "1\n"),
("filter(false) | subtree | count", "0\n"),
("subtree | count", "22\n"),
("subtree | unique | count", "9\n"),
] {
let output = run_stdin(query, input);
assert!(
output.status.success(),
"{query}: {}",
String::from_utf8_lossy(&output.stderr)
);
assert_eq!(
String::from_utf8(output.stdout).unwrap(),
expected,
"{query}"
);
}
let edited = run_stdin(
r#"filter(op("func.call")) | parent | subtree | filter(op("func.return")) | set_attr("tag", "chosen") | input"#,
input,
);
assert!(
edited.status.success(),
"{}",
String::from_utf8_lossy(&edited.stderr)
);
assert_eq!(
String::from_utf8(edited.stdout)
.unwrap()
.matches("tag = \"chosen\"")
.count(),
1
);
}
#[test]
fn root_unique_attr_and_json_list_functions_containing_matmuls() {
let input = r#"module {
func.func @two_matmuls() {
"linalg.matmul"() : () -> ()
"linalg.matmul"() : () -> ()
return
}
func.func @one_matmul() {
"linalg.matmul"() : () -> ()
return
}
func.func @none() {
return
}
}
"#;
let duplicates = run_stdin(
r#"filter(op("linalg.matmul")) | root(op("func.func")) | count"#,
input,
);
assert!(
duplicates.status.success(),
"{}",
String::from_utf8_lossy(&duplicates.stderr)
);
assert_eq!(duplicates.stdout, b"3\n");
let output = run_stdin(
r#"filter(op("linalg.matmul")) | root(op("func.func")) | unique | attr("sym_name") | json"#,
input,
);
assert!(
output.status.success(),
"{}",
String::from_utf8_lossy(&output.stderr)
);
assert_eq!(
String::from_utf8(output.stdout).unwrap(),
"[\n \"two_matmuls\",\n \"one_matmul\"\n]\n"
);
let lines = run_stdin(
r#"filter(op("linalg.matmul")) | root(op("func.func")) | unique | attr("sym_name")"#,
input,
);
assert!(lines.status.success());
assert_eq!(lines.stdout, b"two_matmuls\none_matmul\n");
let operations = run_stdin(r#"filter(op("func.func")) | json"#, input);
assert!(operations.status.success());
let json: serde_json::Value = serde_json::from_slice(&operations.stdout).unwrap();
assert_eq!(json.as_array().unwrap().len(), 3);
assert_eq!(json[0]["name"], "func.func");
assert_eq!(json[0]["attributes"]["sym_name"], "@two_matmuls");
}
#[test]
fn closure_is_one_step_and_fixpoint_repeats_arbitrary_selection_queries() {
let input =
include_str!("../../../examples/cli/arithmetic.mlir").replace("arith.muli", "arith.addi");
for (query, expected) in [
(
r#"filter(has_attr("analysis.tag")) | closure | count"#,
"3\n",
),
(
r#"filter(has_attr("analysis.tag")) | fixpoint(closure) | count"#,
"4\n",
),
(
r#"filter(has_attr("analysis.tag")) | fixpoint(defs) | count"#,
"0\n",
),
(
r#"filter(op("arith.addi") and not has_attr("analysis.tag")) | fixpoint(filter(true) union users) | count"#,
"2\n",
),
(
r#"filter(op("arith.addi") and not has_attr("analysis.tag")) | (defs union users) | count"#,
"3\n",
),
(
r#"filter(op("arith.addi") and not has_attr("analysis.tag")) | (defs intersect users) | count"#,
"0\n",
),
(
r#"filter(has_attr("analysis.tag")) | (closure except defs) | count"#,
"1\n",
),
("filter(false) | fixpoint(closure) | count", "0\n"),
("fixpoint(fixpoint(filter(true))) | count", "5\n"),
] {
let output = run_stdin(query, &input);
assert!(
output.status.success(),
"{query}: {}",
String::from_utf8_lossy(&output.stderr)
);
assert_eq!(
String::from_utf8(output.stdout).unwrap(),
expected,
"{query}"
);
}
let cycle = run_stdin("fixpoint(input except filter(true))", &input);
assert!(!cycle.status.success());
assert!(cycle.stdout.is_empty());
assert!(String::from_utf8_lossy(&cycle.stderr).contains("cycles"));
let emitted_cycle = run_stdin("fixpoint((input except filter(true)) | emit)", &input);
assert!(!emitted_cycle.status.success());
assert!(emitted_cycle.stdout.is_empty());
assert!(String::from_utf8_lossy(&emitted_cycle.stderr).contains("cycles"));
}
#[test]
fn emit_captures_each_pipeline_position_before_later_edits() {
let output = run_stdin(
r#"filter(op("arith.addi")) | emit | set_attr("tag", "new") | emit | remove_attr("tag")"#,
INPUT,
);
assert!(
output.status.success(),
"{}",
String::from_utf8_lossy(&output.stderr)
);
let before = run_stdin(r#"filter(op("arith.addi"))"#, INPUT).stdout;
let tagged = run_stdin(
r#"filter(op("arith.addi")) | set_attr("tag", "new")"#,
INPUT,
)
.stdout;
assert_eq!(
output.stdout,
[&before[..], &tagged[..], &before[..]].concat()
);
let output = run_stdin(r#"filter(op("arith.addi")) | emit | users"#, INPUT);
assert!(output.status.success());
let add = run_stdin(r#"filter(op("arith.addi"))"#, INPUT).stdout;
let user = run_stdin(r#"filter(op("arith.addi")) | users"#, INPUT).stdout;
assert_eq!(output.stdout, [&add[..], &user[..]].concat());
let iterations = run_stdin(
r#"filter(op("arith.addi")) | fixpoint(closure | emit) | count"#,
INPUT,
);
assert!(iterations.status.success());
let text = String::from_utf8(iterations.stdout).unwrap();
assert_eq!(text.matches("arith.addi").count(), 2);
assert_eq!(text.matches("arith.constant").count(), 2);
assert!(text.ends_with("2\n"));
let branches = run_stdin(
r#"filter(op("arith.addi")) | (defs | emit union users | emit) | count"#,
INPUT,
);
assert!(branches.status.success());
let text = String::from_utf8(branches.stdout).unwrap();
assert!(text.find("arith.constant").unwrap() < text.find("example.observe").unwrap());
assert!(text.ends_with("2\n"));
let failed = run_stdin(r#"emit | filter(op("example.observe")) | closure"#, INPUT);
assert!(!failed.status.success());
assert!(failed.stdout.is_empty());
}
#[test]
fn filters_and_input_observe_prior_edits() {
let output = run_stdin(
r#"filter(op("arith.addi")) | set_attr("tag", "new") | users | input | filter(string_attr_eq("tag", "new")) | count"#,
INPUT,
);
assert!(output.status.success());
assert_eq!(output.stdout, b"1\n");
let scoped = run_stdin(
r#"filter(op("arith.addi")) | filter(op("arith.constant")) | count"#,
INPUT,
);
assert!(scoped.status.success());
assert_eq!(scoped.stdout, b"0\n");
}
#[test]
fn query_diagnostics_keep_program_file_lines_and_unicode_columns() {
let program = temporary_path("query-diagnostics", "zirium");
fs::write(&program, "\n# a comment\nfilter(op(\"é\")) | typo\n").unwrap();
let output = Command::new(env!("CARGO_BIN_EXE_zirium"))
.args(["-f", program.to_str().unwrap()])
.output()
.unwrap();
let _ = fs::remove_file(&program);
assert!(!output.status.success());
let error = String::from_utf8(output.stderr).unwrap();
assert!(
error.contains(&format!("{}:3:19", program.to_string_lossy())),
"{error}"
);
assert!(error.contains("\n ^"), "{error}");
}
#[test]
fn set_operand_diagnostic_points_to_the_stage_after_leading_comments() {
let program = temporary_path("set-operand-diagnostic", "zirium");
let source =
"# first line\n# second line\nfilter(true) | names union filter(true) | names | count\n";
fs::write(&program, source).unwrap();
let output = Command::new(env!("CARGO_BIN_EXE_zirium"))
.args(["-f", program.to_str().unwrap()])
.output()
.unwrap();
let _ = fs::remove_file(&program);
assert!(!output.status.success());
let error = String::from_utf8(output.stderr).unwrap();
let prefix = &source[source[..source.find("count").unwrap()].rfind('\n').unwrap() + 1
..source.find("count").unwrap()];
let column = prefix.chars().count() + 1;
assert!(
error.contains(&format!("{}:3:{column}", program.to_string_lossy())),
"{error}"
);
assert!(
error.contains(&format!("\n{}^", " ".repeat(column - 1))),
"{error}"
);
}
#[test]
fn named_queries_build_function_histograms_and_keep_live_selections() {
let decoder = include_str!("../../../examples/cli/stablelm-decode.mlir");
let output = run_stdin_with_registry(
"stablehlo.json",
r#"matmuls = filter(op("stablehlo.dot_general"));
matmuls | root(op("func.func")) | attr("sym_name") | tally | json"#,
decoder,
);
assert!(
output.status.success(),
"{}",
String::from_utf8_lossy(&output.stderr)
);
assert_eq!(
serde_json::from_slice::<serde_json::Value>(&output.stdout).unwrap(),
serde_json::json!({"main": 19})
);
let source = "module { func.func @a() { func.return } func.func @b() { func.return } }";
for (query, expected) in [
(
r#"F = filter(op("func.func")); F | map_by(attr("sym_name"), children | subtree | names | tally) | json"#,
serde_json::json!({"a":{"func.return":1},"b":{"func.return":1}}),
),
(
r#"F = filter(op("func.func")); F | map_by(attr("sym_name"), children | count)"#,
serde_json::json!({"a":1,"b":1}),
),
(
r#"A = filter(op("func.func")); B = A | children; (A union B) | names | tally"#,
serde_json::json!({"func.func":2,"func.return":2}),
),
(
r#"F = filter(op("func.func")); F | set_attr("tag", "changed") | F | attr("tag") | tally"#,
serde_json::json!({"changed":2}),
),
(
r#"N = names | tally; N | json"#,
serde_json::json!({"builtin.module":1,"func.func":2,"func.return":2}),
),
(
r#"filter(false) | map_by(attr("sym_name"), names | tally)"#,
serde_json::json!({}),
),
] {
let output = run_stdin(query, source);
assert!(
output.status.success(),
"{query}: {}",
String::from_utf8_lossy(&output.stderr)
);
assert_eq!(
serde_json::from_slice::<serde_json::Value>(&output.stdout).unwrap(),
expected,
"{query}"
);
}
for (query, error) in [
("A = B; B = input; A", "unknown query stage"),
("A = input; A = input; A", "already defined"),
("input = names; input", "reserved"),
("A = emit; A", "cannot edit or emit"),
("A = input;", "expected a query stage"),
("A = input A", "expected `;`"),
("map_by(names, emit)", "cannot edit or emit"),
("map_by(names, count)", "duplicate key"),
(
r#"filter(op("func.func")) | map_by(attr("absent"), count)"#,
"exactly one string",
),
("tally", "requires a value stream"),
("N = count; N | names", "requires operations"),
(
"N = names | tally; N union N",
"require operation or value streams",
),
(
"N = names | tally; fixpoint(N)",
"requires an operation or value stream",
),
] {
let output = run_stdin(query, source);
assert!(!output.status.success(), "{query}");
assert!(output.stdout.is_empty(), "{query}");
assert!(
String::from_utf8_lossy(&output.stderr).contains(error),
"{query}: {}",
String::from_utf8_lossy(&output.stderr)
);
}
}
#[test]
fn aggregate_limits_cover_saved_nested_maps() {
let source = "module {}";
let query = "A = names | tally; B = input | map_by(names, A); B";
let output = run_stdin_with_options(&["--max-items", "2"], query, source);
assert!(!output.status.success());
assert!(String::from_utf8_lossy(&output.stderr).contains("stream size limit"));
let mut query = String::from("A0 = names | tally;");
for i in 1..70 {
query.push_str(&format!("A{i} = input | map_by(names, A{});", i - 1));
}
query.push_str("A69");
let output = run_stdin(&query, source);
assert!(!output.status.success());
assert!(output.stdout.is_empty());
assert!(String::from_utf8_lossy(&output.stderr).contains("map nesting limit"));
}
#[test]
fn reachable_counts_shared_and_recursive_callee_bodies_once() {
let source = r#"module {
func.func @main(%arg: i32) {
%dead = arith.addi %arg, %arg : i32
func.call @leaf(%arg) : (i32) -> ()
func.call @leaf(%arg) : (i32) -> ()
func.return
}
func.func @leaf(%arg: i32) {
func.call @leaf(%arg) : (i32) -> ()
func.return
}
}"#;
for (query, expected) in [
(
r#"filter(op("func.func")) | map_by(attr("sym_name"), children | subtree | names | tally)"#,
serde_json::json!({"main":{"arith.addi":1,"func.call":2,"func.return":1},"leaf":{"func.call":1,"func.return":1}}),
),
(
r#"filter(op("func.func")) | map_by(attr("sym_name"), children | subtree | reachable | names | tally)"#,
serde_json::json!({"main":{"arith.addi":1,"func.call":3,"func.return":2},"leaf":{"func.call":1,"func.return":1}}),
),
(
r#"filter(op("func.call")) | reachable | names | tally"#,
serde_json::json!({"func.call":3,"func.return":1}),
),
] {
let output = run_stdin(query, source);
assert!(
output.status.success(),
"{}",
String::from_utf8_lossy(&output.stderr)
);
assert_eq!(
serde_json::from_slice::<serde_json::Value>(&output.stdout).unwrap(),
expected
);
}
let output = run_stdin_with_options(
&["--preset", "async"],
r#"filter(op("async.call")) | reachable | names | tally"#,
"module { func.func @main() { async.call @work() : () -> ()\n func.return } async.func @work() { async.return } }",
);
assert!(
output.status.success(),
"{}",
String::from_utf8_lossy(&output.stderr)
);
assert_eq!(
serde_json::from_slice::<serde_json::Value>(&output.stdout).unwrap(),
serde_json::json!({"async.call":1,"async.return":1})
);
let branch = run_stdin(
r#"filter(op("cf.br")) | reachable | names | tally"#,
"module { func.func @f(%arg: i32) { cf.br ^next(%arg : i32)\n ^dead: func.return\n ^next(%next_arg: i32): %x = arith.addi %next_arg, %next_arg : i32\n func.return } }",
);
assert!(
branch.status.success(),
"{}",
String::from_utf8_lossy(&branch.stderr)
);
assert_eq!(
serde_json::from_slice::<serde_json::Value>(&branch.stdout).unwrap(),
serde_json::json!({"cf.br":1,"arith.addi":1,"func.return":1})
);
for (source, error) in [
(
"module { func.func private @external()\n func.func @main() { func.call @external() : () -> () func.return } }",
"without a body",
),
(
"module { func.func @main() { func.call @missing() : () -> () func.return } }",
"could not resolve",
),
] {
let output = run_stdin("reachable | count", source);
assert!(!output.status.success());
assert!(output.stdout.is_empty());
assert!(
String::from_utf8_lossy(&output.stderr).contains(error),
"{}",
String::from_utf8_lossy(&output.stderr)
);
}
let unknown = "module { \"vendor.lambda\"() : () -> () }";
let output = run_stdin("reachable | names", unknown);
assert!(output.status.success());
assert_eq!(
String::from_utf8(output.stdout).unwrap(),
"builtin.module\nvendor.lambda\n"
);
let output = run_stdin_with_options(&["--strict"], "reachable | count", unknown);
assert!(!output.status.success());
assert!(String::from_utf8_lossy(&output.stderr).contains("`vendor.lambda`"));
}
#[test]
fn markdown_reports_combine_statements_interpolation_and_sparse_tables() {
let source = r#"module {
func.func @a() { func.return }
func.func @b() { %x = arith.constant 1 : i32
func.return }
}"#;
let query = r##"F = filter(op("func.func"));
N = F | count;
print("# Report");
print("{N} functions");
F | map_by(attr("sym_name"), children | names | tally) | markdown;
print("{{done}}");
"##;
let output = run_stdin(query, source);
assert!(
output.status.success(),
"{}",
String::from_utf8_lossy(&output.stderr)
);
assert_eq!(
String::from_utf8(output.stdout).unwrap(),
"# Report\n2 functions\n\n| Key | func.return | arith.constant |\n| --- | --- | --- |\n| a | 1 | |\n| b | 1 | 1 |\n\n{done}\n"
);
let output = run_stdin(
r#"filter(op("func.func")) | set_attr("tag", "marked") | count;
T = filter(op("func.func")) | attr("tag") | unique;
print("Tag: {T}");
filter(op("func.return")) | names | tally | markdown;
N = count; print("Total: {N}");"#,
source,
);
assert!(
output.status.success(),
"{}",
String::from_utf8_lossy(&output.stderr)
);
assert_eq!(
String::from_utf8(output.stdout).unwrap(),
"2\nTag: marked\n\n| Key | Value |\n| --- | --- |\n| func.return | 2 |\n\nTotal: 6\n"
);
}
#[test]
fn markdown_escapes_cells_and_rejects_unsuitable_shapes_atomically() {
let output = run_stdin(
r#"attr("tag") | markdown"#,
r#""test.op"() {tag = "a|b\5C<em>\0A*x*"} : () -> ()"#,
);
assert!(
output.status.success(),
"{}",
String::from_utf8_lossy(&output.stderr)
);
assert_eq!(
String::from_utf8(output.stdout).unwrap(),
"\n| Value |\n| --- |\n| a\\|b\\\\<em><br>\\*x\\* |\n\n"
);
for query in [
"filter(false) | names | markdown",
"filter(false) | names | tally | markdown",
] {
let output = run_stdin(query, "module {}");
assert!(output.status.success());
assert_eq!(output.stdout, b"\n_No entries._\n\n");
}
for (query, error) in [
(r#"print("prefix"); input | markdown"#, "markdown requires"),
(r#"map_by(names, names) | markdown"#, "incompatible shape"),
(
r#"map_by(names, map_by(names, names)) | markdown"#,
"expected a scalar",
),
(
r#"N = names | tally; print("{N}")"#,
"requires a count or exactly one string",
),
(
r#"N = filter(false) | names; print("{N}")"#,
"requires a count or exactly one string",
),
(
r#"print("{later}"); later = count; later"#,
"earlier binding",
),
(r#"print("{broken")"#, "unclosed interpolation"),
(r#"print("broken}")"#, "unmatched closing brace"),
(r#"X = print("bad"); X"#, "cannot edit or emit"),
(r#"X = names | markdown; X"#, "cannot edit or emit"),
(r#"map_by(names, markdown)"#, "cannot edit or emit"),
] {
let output = run_stdin(query, "module {}");
assert!(!output.status.success(), "{query}");
assert!(output.stdout.is_empty(), "{query}");
assert!(
String::from_utf8_lossy(&output.stderr).contains(error),
"{query}: {}",
String::from_utf8_lossy(&output.stderr)
);
}
}
#[test]
fn ranked_histograms_keep_order_through_bounds_bindings_and_markdown() {
let counts = [12, 11, 10, 9, 8, 7, 6, 5, 4, 3, 3, 1];
let mut source = String::from("module {");
for (index, count) in counts.into_iter().enumerate() {
let key = char::from(b'a' + u8::try_from(index).unwrap());
for _ in 0..count {
source.push_str(&format!(r#""vendor.{key}"() : () -> ()"#));
}
}
source.push('}');
let query = "ranked = filter(not op(\"builtin.module\")) | names | tally | sort_by(value) | reverse | head(10); ranked | markdown";
let output = run_stdin(query, &source);
assert!(
output.status.success(),
"{}",
String::from_utf8_lossy(&output.stderr)
);
assert_eq!(
String::from_utf8(output.stdout).unwrap(),
"\n| Key | Value |\n| --- | --- |\n| vendor.a | 12 |\n| vendor.b | 11 |\n| vendor.c | 10 |\n| vendor.d | 9 |\n| vendor.e | 8 |\n| vendor.f | 7 |\n| vendor.g | 6 |\n| vendor.h | 5 |\n| vendor.i | 4 |\n| vendor.k | 3 |\n\n"
);
for ending in ["ranked", "ranked | json"] {
let output = run_stdin(
&format!(
"ranked = filter(not op(\"builtin.module\")) | names | tally | sort_by(value) | reverse | head(3); {ending}"
),
&source,
);
assert!(output.status.success());
assert_eq!(
String::from_utf8(output.stdout).unwrap(),
"{\n \"vendor.a\": 12,\n \"vendor.b\": 11,\n \"vendor.c\": 10\n}\n"
);
}
let output = run_stdin(
"filter(not op(\"builtin.module\")) | names | tally | sort_by(value) | tail(3) | count",
&source,
);
assert!(output.status.success());
assert_eq!(output.stdout, b"3\n");
let output = run_stdin(r#"{"z": 1, "a": 2} | markdown"#, "module {}");
assert!(output.status.success());
assert_eq!(
output.stdout,
b"\n| Key | Value |\n| --- | --- |\n| z | 1 |\n| a | 2 |\n\n"
);
for query in [
"names | tally | sort_by(value) | head(0) | markdown",
"filter(false) | names | tally | sort_by(value) | markdown",
] {
let output = run_stdin(query, "module {}");
assert!(output.status.success());
assert_eq!(output.stdout, b"\n_No entries._\n\n");
}
let output = run_stdin(
"filter(op(\"builtin.module\")) | map_by(names, children | names | tally) | sort_by(value)",
&source,
);
assert!(!output.status.success());
assert!(String::from_utf8_lossy(&output.stderr).contains("scalar map values"));
}
#[test]
fn ranked_maps_compare_large_and_mixed_numbers_exactly() {
for values in [
r#"{"a":9007199254740993,"z":9007199254740992}"#,
r#"{"a":-9007199254740992,"z":-9007199254740993}"#,
r#"{"a":18446744073709551615,"z":9223372036854775807}"#,
r#"{"a":9007199254740993,"z":9007199254740992.0}"#,
r#"{"a":18446744073709551616.0,"z":18446744073709551615}"#,
r#"{"a":0,"z":-0.5}"#,
r#"{"a":0.5,"z":0}"#,
r#"{"a":1e100,"z":18446744073709551615}"#,
r#"{"a":-9223372036854775808,"z":-1e100}"#,
] {
let output = run_stdin(&format!("{values} | sort_by(value) | head(1)"), "module {}");
assert!(output.status.success(), "{values}: {:?}", output.stderr);
let result: serde_json::Value = serde_json::from_slice(&output.stdout).unwrap();
assert!(result.get("z").is_some(), "{values}: {result}");
assert!(result.get("a").is_none(), "{values}: {result}");
}
}
#[test]
fn ranked_scalar_maps_keep_categories_and_rerank_ties() {
for (query, expected) in [
(
r#"{"z":null,"a":null} | sort_by(value) | reverse | sort_by(value) | json"#,
"{\n \"a\": null,\n \"z\": null\n}\n",
),
(
r#"{"a":true,"z":false} | sort_by(value) | json"#,
"{\n \"z\": false,\n \"a\": true\n}\n",
),
(
r#"{"a":"β","z":"a"} | sort_by((value)) | json"#,
"{\n \"z\": \"a\",\n \"a\": \"β\"\n}\n",
),
] {
let output = run_stdin(query, "module {}");
assert!(
output.status.success(),
"{}",
String::from_utf8_lossy(&output.stderr)
);
assert_eq!(String::from_utf8(output.stdout).unwrap(), expected);
}
let query = r#"saved = {"z":{"m":1,"a":2},"a":0}; {"nested":saved} | json"#;
let output = run_stdin(query, "module {}");
assert!(
output.status.success(),
"{}",
String::from_utf8_lossy(&output.stderr)
);
let json: serde_json::Value = serde_json::from_slice(&output.stdout).unwrap();
assert_eq!(
json["nested"]
.as_object()
.unwrap()
.keys()
.map(String::as_str)
.collect::<Vec<_>>(),
["z", "a"]
);
assert_eq!(
json["nested"]["z"]
.as_object()
.unwrap()
.keys()
.map(String::as_str)
.collect::<Vec<_>>(),
["m", "a"]
);
let output = run_stdin_with_options(
&["--jsonl"],
r#"{"z":1,"a":1} | reverse | sort_by(value)"#,
"module {}",
);
assert!(output.status.success());
let line = String::from_utf8(output.stdout).unwrap();
assert!(line.contains(r#""result":{"a":1,"z":1}"#), "{line}");
let output = run_stdin(r#"{"a":true,"z":1} | sort_by(value)"#, "module {}");
assert!(!output.status.success());
assert!(output.stdout.is_empty());
assert!(
String::from_utf8_lossy(&output.stderr)
.contains("sort_by(value) requires comparable scalar map values of one type")
);
}
#[test]
fn markdown_bounds_the_dense_table_from_sparse_histograms() {
let mut source = String::from("module {");
for i in 0..20 {
source.push_str(&format!(
r#""test.op"() {{row = "r{i}", column = "c{i}"}} : () -> ()"#
));
}
source.push('}');
let output = run_stdin_with_options(
&["--max-items", "100"],
r#"filter(op("test.op")) | map_by(attr("row"), attr("column") | tally) | markdown"#,
&source,
);
assert!(!output.status.success());
assert!(output.stdout.is_empty());
assert!(
String::from_utf8_lossy(&output.stderr).contains("stream size limit"),
"{}",
String::from_utf8_lossy(&output.stderr)
);
}
#[test]
fn json_literals_combine_typed_bindings_and_interpolated_strings() {
let source = include_str!("../../../examples/cli/stablelm-decode.mlir");
let output = run_stdin_with_registry(
"stablehlo.json",
r#"M = filter(op("stablehlo.dot_general"));
N = M | count;
Counts = M | root(op("func.func")) | attr("sym_name") | tally;
Title = ["Matmul report"];
{
"title": "{Title}: {N} operations",
"count": N,
"counts": Counts,
"metadata": {"version": 1, "ratio": -2.5e-2, "enabled": true, "missing": null},
"notes": ["{{static}} counts", false, {"text": "quoted: \"hello\"\n\u03BB\uD83D\uDE00"}]
} | json"#,
source,
);
assert!(
output.status.success(),
"{}",
String::from_utf8_lossy(&output.stderr)
);
assert_eq!(
serde_json::from_slice::<serde_json::Value>(&output.stdout).unwrap(),
serde_json::json!({
"title":"Matmul report: 19 operations", "count":19, "counts":{"main":19},
"metadata":{"version":1,"ratio":-0.025,"enabled":true,"missing":null},
"notes":["{static} counts",false,{"text":"quoted: \"hello\"\nλ😀"}]
})
);
let output = run_stdin(
r#"Key = ["summary"]; Names = names;
A = {"{Key}": Names}; [A, {"size": 1}, []]"#,
"module {}",
);
assert!(
output.status.success(),
"{}",
String::from_utf8_lossy(&output.stderr)
);
assert_eq!(
serde_json::from_slice::<serde_json::Value>(&output.stdout).unwrap(),
serde_json::json!([{"summary":["builtin.module"]},{"size":1},[]])
);
let output = run_stdin(r#"{"a": {"x": 1}, "b": {"y": 2}} | markdown"#, "module {}");
assert!(output.status.success());
assert_eq!(
output.stdout,
b"\n| Key | x | y |\n| --- | --- | --- |\n| a | 1 | |\n| b | | 2 |\n\n"
);
let output = run_stdin(
r#"print("one\ntwo\t\u03BB"); [1, "two", true, null] | markdown"#,
"module {}",
);
assert!(output.status.success());
assert_eq!(
output.stdout,
"one\ntwo\tλ\n\n| Value |\n| --- |\n| 1 |\n| two |\n| true |\n| |\n\n".as_bytes()
);
}
#[test]
fn json_literal_errors_are_specific_and_leave_stdout_empty() {
for (query, error) in [
(
r#"print("prefix"); {"a": 1, "a": 2}"#,
"duplicate JSON object key",
),
(
r#"K = ["a"]; {"a": 1, "{K}": 2}"#,
"duplicate JSON object key",
),
(
r#"K = ["a"]; Bad = [1, 2]; {"a": 1, "{K}": "{Bad}"}"#,
"duplicate JSON object key",
),
(r#"{"x": missing}"#, "earlier binding"),
(r#"{"x": "{missing}"}"#, "earlier binding"),
(r#"A = [1, 2]; {"x": "{A}"}"#, "count or exactly one string"),
(r#"{unquoted: 1}"#, "undefined binding"),
(r#"{"x": 1,}"#, "quoted object key"),
("[1,]", "expected a JSON value"),
("[01]", "JSON number"),
("[1e9999]", "JSON number"),
(r#"["\uD800"]"#, "Unicode sequence"),
("[true false]", "expected `]`"),
("fixpoint({})", "selection query"),
("{} union {}", "selection queries"),
("A = []; A | names", "requires operations"),
(
"A = []; fixpoint(A)",
"requires an operation or value stream",
),
] {
let output = run_stdin(query, "module {}");
assert!(!output.status.success(), "{query}");
assert!(output.stdout.is_empty(), "{query}");
assert!(
String::from_utf8_lossy(&output.stderr).contains(error),
"{query}: {}",
String::from_utf8_lossy(&output.stderr)
);
}
}
#[test]
fn json_literals_bound_reused_data_and_nesting() {
let output = run_stdin_with_options(
&["--max-items", "10"],
"A = [[], [], [], []]; [A, A, A]",
"module {}",
);
assert!(!output.status.success());
assert!(String::from_utf8_lossy(&output.stderr).contains("stream size limit"));
let mut query = String::from("A0 = {};");
for i in 1..70 {
query.push_str(&format!("A{i} = {{\"child\": A{}}};", i - 1));
}
query.push_str("A69");
let output = run_stdin(&query, "module {}");
assert!(!output.status.success());
assert!(output.stdout.is_empty());
assert!(
String::from_utf8_lossy(&output.stderr).contains("JSON nesting limit"),
"{}",
String::from_utf8_lossy(&output.stderr)
);
}
#[test]
fn root_keeps_cumulative_work_limits() {
let source = r#""test.op"() : () -> ()"#;
let query = "root(true or false) | count";
let accepted = run_stdin_with_options(&["--max-work", "1000"], query, source);
assert!(accepted.status.success(), "{:?}", accepted.stderr);
assert_eq!(accepted.stdout, b"1\n");
let refused = run_stdin_with_options(&["--max-work", "1"], query, source);
assert!(!refused.status.success());
assert!(refused.stdout.is_empty());
assert!(String::from_utf8_lossy(&refused.stderr).contains("work limit exceeded"));
}
#[test]
fn aggregate_program_failure_is_located_and_keeps_stdout_empty() {
let output = run_stdin_with_options(
&["--max-results", "20"],
"selected = input; selected; selected",
INPUT,
);
assert!(!output.status.success());
assert!(output.stdout.is_empty());
let diagnostic = String::from_utf8_lossy(&output.stderr);
assert!(diagnostic.contains("result limit"), "{diagnostic}");
assert!(diagnostic.contains("query"), "{diagnostic}");
}
#[test]
fn comprehension_reports_render_plain_json_and_document_records() {
use zirium::{
dialect::DialectRegistry,
parser::ParsedFile,
query::{EvaluationOptions, Query},
semantic::{LoweringMode, lower_with_dialect_registry},
};
let input = "\"alpha\"() : () -> ()\n\"beta\"() : () -> ()";
let report = r#"labels = names; [{"name": n, "label": "data:" ++ n} for n in labels]"#;
let expected = serde_json::json!([
{"name": "alpha", "label": "data:alpha"},
{"name": "beta", "label": "data:beta"}
]);
let implicit = run_stdin(report, input);
assert!(implicit.status.success(), "{:?}", implicit.stderr);
assert_eq!(
serde_json::from_slice::<serde_json::Value>(&implicit.stdout).unwrap(),
expected
);
let explicit = run_stdin(&format!("{report} | json"), input);
assert!(explicit.status.success(), "{:?}", explicit.stderr);
assert_eq!(explicit.stdout, implicit.stdout);
let jsonl = run_stdin_with_options(&["--jsonl"], report, input);
assert!(jsonl.status.success(), "{:?}", jsonl.stderr);
assert_eq!(
serde_json::from_slice::<serde_json::Value>(&jsonl.stdout).unwrap(),
serde_json::json!({"document": "stdin", "result": expected})
);
let explicit_jsonl = run_stdin_with_options(&["--jsonl"], &format!("{report} | json"), input);
assert!(
explicit_jsonl.status.success(),
"{:?}",
explicit_jsonl.stderr
);
assert_eq!(explicit_jsonl.stdout, jsonl.stdout);
let registry = DialectRegistry::baseline();
let parsed = ParsedFile::parse_with_registry(input.as_bytes(), registry).unwrap();
let mut document = lower_with_dialect_registry(&parsed, LoweringMode::Strict, registry)
.document
.unwrap();
let limits = zirql::ProgramLimits {
work: 100_000,
results: 100_000,
nesting: 64,
stream_items: 10_000,
};
for (source, expected) in [
("r = [n for n in [1]]; [r]", serde_json::json!([[1]])),
(
r#"r = [n for n in [1]]; {"rows": r}"#,
serde_json::json!({"rows": [1]}),
),
] {
let query = Query::parse_with_document_context_and_program_limits(source, limits).unwrap();
let mut budget = zirql::Budget::for_program(limits);
query
.evaluate_with_budget(
&mut document,
registry,
Some("stdin"),
EvaluationOptions::default(),
&mut budget,
|_, _, _| Ok(()),
)
.unwrap();
let native_work = (limits.work - budget.remaining_work()).to_string();
let limited = run_stdin_with_options(
&["--max-work", &native_work, "--max-results", "100000"],
source,
input,
);
assert!(!limited.status.success());
assert!(limited.stdout.is_empty());
let diagnostic = String::from_utf8_lossy(&limited.stderr);
assert!(diagnostic.contains("work limit exceeded"), "{diagnostic}");
assert!(diagnostic.contains(source), "{diagnostic}");
let accepted = run_stdin(source, input);
assert!(accepted.status.success(), "{:?}", accepted.stderr);
assert_eq!(
serde_json::from_slice::<serde_json::Value>(&accepted.stdout).unwrap(),
expected
);
}
let failure = run_stdin(r#"emit; [1 div 0 for n in [1]]"#, input);
assert!(!failure.status.success());
assert!(failure.stdout.is_empty());
assert!(String::from_utf8_lossy(&failure.stderr).contains("division by zero"));
let limited = run_stdin_with_options(
&["--max-results", "1000"],
"[n for n in 1..100000 if false]",
input,
);
assert!(!limited.status.success());
assert!(limited.stdout.is_empty());
assert!(String::from_utf8_lossy(&limited.stderr).contains("limit exceeded"));
}
#[test]
fn correlated_reports_agree_inline_file_and_explicit_emission() {
let input = include_str!("../../../examples/cli/function-report.mlir");
let program = include_str!("../../../examples/cli/function-report.zirql");
let inline = run_stdin(program, input);
assert!(
inline.status.success(),
"{}",
String::from_utf8_lossy(&inline.stderr)
);
let value: serde_json::Value = serde_json::from_slice(&inline.stdout).unwrap();
assert_eq!(value["total_operations"], 5);
assert_eq!(value["total_matches"], 2);
assert_eq!(value["functions"][0]["name"], "double");
assert_eq!(value["functions"][1]["name"], "mix");
let explicit = run_stdin(
&format!("{} | json;", program.trim_end().strip_suffix(';').unwrap()),
input,
);
assert!(
explicit.status.success(),
"{}",
String::from_utf8_lossy(&explicit.stderr)
);
assert_eq!(inline.stdout, explicit.stdout);
let path = temporary_path("correlated-report", "zirql");
fs::write(&path, program).unwrap();
let mut child = Command::new(env!("CARGO_BIN_EXE_zirium"))
.args(["-f", path.to_str().unwrap()])
.stdin(Stdio::piped())
.stdout(Stdio::piped())
.stderr(Stdio::piped())
.spawn()
.unwrap();
child
.stdin
.take()
.unwrap()
.write_all(input.as_bytes())
.unwrap();
let file = child.wait_with_output().unwrap();
fs::remove_file(&path).unwrap();
assert!(
file.status.success(),
"{}",
String::from_utf8_lossy(&file.stderr)
);
assert_eq!(file.stdout, inline.stdout);
let parameter_program = include_str!("../../../examples/cli/function-report-parameters.zirql");
let arguments = [
"--param",
"operation_name=arith.addi",
"--param",
"minimum_ops=+2",
];
let parameter_inline = run_stdin_with_options(&arguments, parameter_program, input);
assert!(parameter_inline.status.success(), "{parameter_inline:?}");
assert_eq!(parameter_inline.stdout, inline.stdout);
fs::write(&path, parameter_program).unwrap();
let mut child = Command::new(env!("CARGO_BIN_EXE_zirium"))
.args(arguments)
.args(["-f", path.to_str().unwrap()])
.stdin(Stdio::piped())
.stdout(Stdio::piped())
.stderr(Stdio::piped())
.spawn()
.unwrap();
child
.stdin
.take()
.unwrap()
.write_all(input.as_bytes())
.unwrap();
let parameter_file = child.wait_with_output().unwrap();
fs::remove_file(&path).unwrap();
assert!(parameter_file.status.success(), "{parameter_file:?}");
assert_eq!(parameter_file.stdout, inline.stdout);
for bad in [
vec!["operation_name=arith.addi"],
vec![
"operation_name=arith.addi",
"minimum_ops=2",
"minimum_ops=2",
],
vec!["operation_name=arith.addi", "minimum_ops=2", "extra=1"],
vec!["operation_name=arith.addi", "minimum_ops=2.0"],
] {
let arguments: Vec<_> = bad.iter().flat_map(|value| ["--param", *value]).collect();
let refused = run_stdin_with_options(&arguments, parameter_program, input);
assert!(!refused.status.success());
assert!(refused.stdout.is_empty());
}
let exact = run_stdin_with_options(
&["--param", "text= \"x\"=\\ "],
"param text: string; report text;",
input,
);
assert!(exact.status.success(), "{exact:?}");
assert_eq!(
serde_json::from_slice::<serde_json::Value>(&exact.stdout).unwrap(),
" \"x\"=\\ "
);
let refused = run_stdin_with_options(
&["--param", "flag=True"],
"param flag: bool; report flag;",
input,
);
assert!(!refused.status.success() && refused.stdout.is_empty());
let short = run_stdin(
r#"filter(op("func.func")) | project(f, {"name": f | attr("sym_name") | one, "n": f | children | count}) | json"#,
input,
);
assert!(
short.status.success(),
"{}",
String::from_utf8_lossy(&short.stderr)
);
assert_eq!(
serde_json::from_slice::<serde_json::Value>(&short.stdout).unwrap(),
serde_json::json!([{"name":"double","n":2},{"name":"mix","n":3},{"name":"plain","n":1}])
);
let failed = run_stdin(
r#"report {"early": 1}; report require(false, filter(op("func.func")) | attr("absent") | one, "stop");"#,
input,
);
assert!(!failed.status.success());
assert!(failed.stdout.is_empty());
assert!(String::from_utf8_lossy(&failed.stderr).contains("stop"));
let groups = include_str!("../../../examples/cli/function-groups.zirql");
let grouped = run_stdin(groups, input);
assert!(
grouped.status.success() && grouped.stderr.is_empty(),
"{grouped:?}"
);
assert_eq!(
serde_json::from_slice::<serde_json::Value>(&grouped.stdout).unwrap(),
serde_json::json!([
{"key":1,"items":[{"name":"double","matches":1},{"name":"mix","matches":1}]},
{"key":0,"items":[{"name":"plain","matches":0}]},
])
);
fs::write(&path, groups).unwrap();
let mut child = Command::new(env!("CARGO_BIN_EXE_zirium"))
.args(["-f", path.to_str().unwrap()])
.stdin(Stdio::piped())
.stdout(Stdio::piped())
.stderr(Stdio::piped())
.spawn()
.unwrap();
child
.stdin
.take()
.unwrap()
.write_all(input.as_bytes())
.unwrap();
let file = child.wait_with_output().unwrap();
fs::remove_file(&path).unwrap();
assert!(file.status.success() && file.stderr.is_empty(), "{file:?}");
assert_eq!(file.stdout, grouped.stdout);
let source = r#"report 7; group_by([{"matches": 1}], "absent") | json;"#;
let failed = run_stdin(source, input);
assert!(!failed.status.success() && failed.stdout.is_empty());
let diagnostic = String::from_utf8_lossy(&failed.stderr);
assert!(
diagnostic.contains("missing record field: absent") && diagnostic.contains("<query>:1:38:"),
"{diagnostic}"
);
}
#[test]
fn native_capture_reports_agree_inline_file_short_json_and_buffer_failures() {
let input = include_str!("../../../examples/cli/function-report.mlir");
let program = include_str!("../../../examples/cli/native-captures.zirql");
let inline = run_stdin(program, input);
assert!(
inline.status.success(),
"{}",
String::from_utf8_lossy(&inline.stderr)
);
assert_eq!(
serde_json::from_slice::<serde_json::Value>(&inline.stdout).unwrap(),
serde_json::json!({"matches":[{"function":"double","operation":"arith.addi"}],"count":1})
);
let path = temporary_path("native-captures", "zirql");
fs::write(&path, program).unwrap();
let mut child = Command::new(env!("CARGO_BIN_EXE_zirium"))
.args(["--program-file", path.to_str().unwrap()])
.stdin(Stdio::piped())
.stdout(Stdio::piped())
.stderr(Stdio::piped())
.spawn()
.unwrap();
child
.stdin
.take()
.unwrap()
.write_all(input.as_bytes())
.unwrap();
let file = child.wait_with_output().unwrap();
fs::remove_file(&path).unwrap();
assert!(
file.status.success(),
"{}",
String::from_utf8_lossy(&file.stderr)
);
assert_eq!(file.stdout, inline.stdout);
let short = run_stdin(
r#"match { from add in (input | filter(op("arith.addi"))); bind lhs = operand(add,0); bind rhs = operand(add,1); where same_value(lhs,rhs); } | json"#,
input,
);
assert!(
short.status.success(),
"{}",
String::from_utf8_lossy(&short.stderr)
);
let native: serde_json::Value = serde_json::from_slice(&short.stdout).unwrap();
assert_eq!(native.as_array().unwrap().len(), 1);
assert_eq!(native[0]["add"]["name"], "arith.addi");
assert_eq!(
native[0]["lhs"],
serde_json::json!({"kind":"block_argument","index":0,"type":"i32"})
);
assert_eq!(native[0]["lhs"], native[0]["rhs"]);
let missing = run_stdin(
r#"emit; match { from add in (input | filter(op("arith.addi"))); bind bad = operand(add,99); } | json"#,
input,
);
assert!(!missing.status.success());
assert!(missing.stdout.is_empty());
assert!(String::from_utf8_lossy(&missing.stderr).contains("unavailable"));
let filtered = "match { from x in input; from y in input; where false; } | json";
let limited = run_stdin_with_options(&["--max-work", "1000"], filtered, input);
assert!(!limited.status.success());
assert!(limited.stdout.is_empty());
assert!(String::from_utf8_lossy(&limited.stderr).contains("work limit"));
let accepted = run_stdin(filtered, input);
assert!(accepted.status.success());
assert_eq!(
serde_json::from_slice::<serde_json::Value>(&accepted.stdout).unwrap(),
serde_json::json!([])
);
let contextual = run_stdin("let match = input; match | count", input);
assert!(contextual.status.success());
}
#[test]
fn bounded_users_paths_agree_inline_file_and_buffer_later_failure() {
let input = include_str!("../../../examples/cli/function-report.mlir");
let program = include_str!("../../../examples/cli/bounded-paths.zirql");
let inline = run_stdin(program.trim(), input);
assert!(
inline.status.success(),
"{}",
String::from_utf8_lossy(&inline.stderr)
);
let value: serde_json::Value = serde_json::from_slice(&inline.stdout).unwrap();
assert_eq!(
value[0]["steps"]
.as_array()
.unwrap()
.iter()
.map(|s| s["name"].as_str().unwrap())
.collect::<Vec<_>>(),
["arith.muli", "arith.addi", "func.return"]
);
let output = Command::new(env!("CARGO_BIN_EXE_zirium"))
.args([
"-f",
"examples/cli/bounded-paths.zirql",
"examples/cli/function-report.mlir",
])
.current_dir(concat!(env!("CARGO_MANIFEST_DIR"), "/../.."))
.output()
.unwrap();
assert!(output.status.success());
assert_eq!(output.stdout, inline.stdout);
let zero = run_stdin(
r#"filter(op("arith.muli")) | paths(users, to=op("arith.muli"), max_depth=0) | json"#,
input,
);
let value: serde_json::Value = serde_json::from_slice(&zero.stdout).unwrap();
assert_eq!(value[0]["steps"].as_array().unwrap().len(), 1);
let ordinary = run_stdin(
r#"match { from steps in (input | filter(op("arith.muli"))); } | json"#,
input,
);
let value: serde_json::Value = serde_json::from_slice(&ordinary.stdout).unwrap();
assert_eq!(value[0]["steps"]["name"], "arith.muli");
let later = run_stdin(
&format!(
"{}; filter(op(\"func.return\")) | operand(input,99)",
program.trim()
),
input,
);
assert!(!later.status.success());
assert!(later.stdout.is_empty());
}
#[test]
fn optional_reports_agree_inline_file_and_buffer_selected_failures() {
let input = include_str!("../../../examples/cli/function-report.mlir");
let program = include_str!("../../../examples/cli/optional-report.zirql");
let inline = run_stdin(program, input);
assert!(
inline.status.success(),
"{}",
String::from_utf8_lossy(&inline.stderr)
);
let json: serde_json::Value = serde_json::from_slice(&inline.stdout).unwrap();
assert_eq!(json["info"], serde_json::json!({"id":4,"stored":null}));
assert_eq!(json["cells"], serde_json::json!([10, null, 30]));
assert_eq!(json["missing"], false);
assert_eq!(json["native"], 2);
let path = temporary_path("optional-report", "zirql");
fs::write(&path, program).unwrap();
for option in ["-f", "--program-file"] {
let mut child = Command::new(env!("CARGO_BIN_EXE_zirium"))
.args([option, path.to_str().unwrap()])
.stdin(Stdio::piped())
.stdout(Stdio::piped())
.stderr(Stdio::piped())
.spawn()
.unwrap();
child
.stdin
.take()
.unwrap()
.write_all(input.as_bytes())
.unwrap();
let file = child.wait_with_output().unwrap();
assert!(
file.status.success(),
"{}",
String::from_utf8_lossy(&file.stderr)
);
assert_eq!(file.stdout, inline.stdout);
}
for later in ["report null default (1 / 0);", "report [10,null,30][4];"] {
let failed = run_stdin(&format!("{program} {later}"), input);
assert!(!failed.status.success());
assert!(failed.stdout.is_empty());
}
fs::remove_file(path).unwrap();
let legacy = run_stdin(r#"{"stored":null}"#, input);
assert!(legacy.status.success());
assert_eq!(
serde_json::from_slice::<serde_json::Value>(&legacy.stdout).unwrap(),
serde_json::json!({"stored":null})
);
}
#[test]
fn typed_functions_agree_inline_file_and_buffer_later_runtime_failure() {
let input = include_str!("../../../examples/cli/subtree-cost.mlir");
let program = include_str!("../../../examples/cli/subtree-cost.zirql");
let inline = run_stdin(program, input);
assert!(
inline.status.success(),
"{}",
String::from_utf8_lossy(&inline.stderr)
);
assert_eq!(inline.stdout, b"8\n");
let path = temporary_path("typed-functions", "zirql");
for (source, expected, input) in [
(program, Some(inline.stdout.as_slice()), input),
(
r#"filter(op("example.root")) | head(1) | project(row, {"cost":1+(row | children | count)}) | json"#,
Some(b"[\n {\n \"cost\": 3\n }\n]\n".as_slice()),
input,
),
(
r#"fn bump(n:int)->int{n+1} fn label()->string{"example.root"} filter(op(label())) | head(bump(0)) | project(row, {"cost":bump(row | children | count)}) | json"#,
Some(b"[\n {\n \"cost\": 3\n }\n]\n".as_slice()),
input,
),
(
r#"fn broken(n:int)->int{10 div n} report 1 | json; filter(op("example.root")) | project(row, {"cost":broken(row | parent | count)}) | json"#,
None,
input,
),
(
"fn fail(n:int)->int {10 div n} report 1 | json; report fail(0) | json",
None,
input,
),
(
r#"project(row,{"z":one(row | names)})"#,
Some(b"[\n {\n \"z\": \"test.a\"\n }\n]\n".as_slice()),
r#""test.a"() : () -> ()"#,
),
(
r#"fn n()->int{1} project(row,{"z":one(row | names)})"#,
Some(b"[\n {\n \"z\": \"test.a\"\n }\n]\n".as_slice()),
r#""test.a"() : () -> ()"#,
),
(
r#"fn make(root:Operation)->sequence<record {"z":int}> {root | project(row,{"z":1})} make(input | head(1))"#,
Some(b"[\n {\n \"z\": 1\n }\n]\n".as_slice()),
r#""test.a"() : () -> ()"#,
),
(
r#"fn make(root:Operation)->sequence<record {"z":int}> {root | filter(false) | project(row,{"z":1})} let rows=make(input | head(1)); report [r.z for r in rows] | json"#,
Some(b"[]\n".as_slice()),
r#""test.a"() : () -> ()"#,
),
(
r#"fn make(root:Operation)->sequence<record {"z":int}> {root | project(row,{"z":1 div (count-1)})} report 1 | json; make(input | head(1))"#,
None,
r#""test.a"() : () -> ()"#,
),
] {
fs::write(&path, source).unwrap();
let mut child = Command::new(env!("CARGO_BIN_EXE_zirium"))
.args(["--program-file", path.to_str().unwrap()])
.stdin(Stdio::piped())
.stdout(Stdio::piped())
.stderr(Stdio::piped())
.spawn()
.unwrap();
child
.stdin
.take()
.unwrap()
.write_all(input.as_bytes())
.unwrap();
let file = child.wait_with_output().unwrap();
let inline = run_stdin(source, input);
if let Some(expected) = expected {
assert!(
file.status.success(),
"{}",
String::from_utf8_lossy(&file.stderr)
);
assert_eq!(file.stdout, expected);
assert!(
inline.status.success(),
"{}",
String::from_utf8_lossy(&inline.stderr)
);
assert_eq!(inline.stdout, expected);
} else {
assert!(!file.status.success());
assert!(file.stdout.is_empty());
assert!(!inline.status.success());
assert!(inline.stdout.is_empty());
}
}
fs::remove_file(path).unwrap();
let tail = run_stdin(
include_str!("../../../examples/cli/tail-accumulator.zirql"),
input,
);
assert!(
tail.status.success(),
"{}",
String::from_utf8_lossy(&tail.stderr)
);
assert_eq!(tail.stdout, b"500500\n");
let malformed = run_stdin(program, &input.replace("3 : i64", "\"bad\""));
assert!(!malformed.status.success());
assert!(malformed.stdout.is_empty());
assert!(String::from_utf8_lossy(&malformed.stderr).contains("cost annotation"));
}
#[test]
fn string_matching_agrees_inline_file_and_buffers_later_errors() {
let input = include_str!("../../../examples/cli/function-report.mlir");
let source = include_str!("../../../examples/cli/string-matching.zirql");
let inline = run_stdin(source, input);
assert!(inline.status.success(), "{inline:?}");
assert_eq!(
serde_json::from_slice::<serde_json::Value>(&inline.stdout).unwrap(),
serde_json::json!({"names":["mix"], "controls":[true,false,true,false,true]})
);
let path = temporary_path("string-matching", "zirql");
for (program, successful) in [
(source.to_owned(), true),
(
format!("{source} report contains(\"x\", [\"z\"][2]);"),
false,
),
] {
fs::write(&path, &program).unwrap();
let mut child = Command::new(env!("CARGO_BIN_EXE_zirium"))
.args(["--program-file", path.to_str().unwrap()])
.stdin(Stdio::piped())
.stdout(Stdio::piped())
.stderr(Stdio::piped())
.spawn()
.unwrap();
child
.stdin
.take()
.unwrap()
.write_all(input.as_bytes())
.unwrap();
let file = child.wait_with_output().unwrap();
if successful {
assert!(file.status.success(), "{file:?}");
assert_eq!(file.stdout, inline.stdout);
} else {
assert!(!file.status.success());
assert!(file.stdout.is_empty());
let failed = run_stdin(&program, input);
assert!(!failed.status.success());
assert!(failed.stdout.is_empty());
}
}
fs::remove_file(path).unwrap();
}
#[test]
fn report_ordering_inline_file_and_later_failure_keep_output_buffering() {
let input = include_str!("../../../examples/cli/function-report.mlir");
let source = include_str!("../../../examples/cli/report-ordering.zirql");
let inline = run_stdin(source, input);
assert!(
inline.status.success(),
"{}",
String::from_utf8_lossy(&inline.stderr)
);
let json: serde_json::Value = serde_json::from_slice(&inline.stdout).unwrap();
assert_eq!(
json["ascending"]
.as_array()
.unwrap()
.iter()
.map(|row| row["id"].as_str().unwrap())
.collect::<Vec<_>>(),
["B", "D", "A", "C"]
);
assert_eq!(json["descending"][1]["id"], "C");
assert_eq!(json["minimum"]["id"], "B");
assert_eq!(json["maximum"]["id"], "A");
assert!(json["empty_min"].is_null());
let path = temporary_path("report-ordering", "zirql");
fs::write(&path, source).unwrap();
let mut child = Command::new(env!("CARGO_BIN_EXE_zirium"))
.args(["--program-file", path.to_str().unwrap()])
.stdin(Stdio::piped())
.stdout(Stdio::piped())
.stderr(Stdio::piped())
.spawn()
.unwrap();
child
.stdin
.take()
.unwrap()
.write_all(input.as_bytes())
.unwrap();
let file = child.wait_with_output().unwrap();
assert!(
file.status.success(),
"{}",
String::from_utf8_lossy(&file.stderr)
);
assert_eq!(file.stdout, inline.stdout);
let failure = run_stdin(
&format!(r#"{source} report min_by([{{"score":1}},{{"other":2}}],"score");"#),
input,
);
assert!(!failure.status.success() && failure.stdout.is_empty());
assert!(String::from_utf8_lossy(&failure.stderr).contains("missing ordering field"));
fs::remove_file(path).unwrap();
}
#[test]
fn indexed_arrays_agree_inline_file_and_buffer_later_failures() {
let input = include_str!("../../../examples/cli/function-report.mlir");
let source = include_str!("../../../examples/cli/indexed-arrays.zirql");
let inline = run_stdin(source, input);
assert!(
inline.status.success(),
"{}",
String::from_utf8_lossy(&inline.stderr)
);
let json: serde_json::Value = serde_json::from_slice(&inline.stdout).unwrap();
assert_eq!(
json,
serde_json::json!({
"keys": [5,2,9],
"cells": [{"key":5,"value":50},{"key":2,"value":20}],
"second": 20,
"has_nine": false,
})
);
let file = Command::new(env!("CARGO_BIN_EXE_zirium"))
.args([
"--program-file",
"examples/cli/indexed-arrays.zirql",
"examples/cli/function-report.mlir",
])
.current_dir(concat!(env!("CARGO_MANIFEST_DIR"), "/../.."))
.output()
.unwrap();
assert!(
file.status.success(),
"{}",
String::from_utf8_lossy(&file.stderr)
);
assert_eq!(inline.stdout, file.stdout);
let failure = run_stdin(&format!("{source} report 1 div 0;"), input);
assert!(!failure.status.success() && failure.stdout.is_empty());
let keyed_json = run_stdin("[2:20] | json", input);
assert!(!keyed_json.status.success() && keyed_json.stdout.is_empty());
}
#[test]
fn pattern_examples_agree_inline_file_and_buffer_later_failures() {
let input = include_str!("../../../examples/cli/function-report.mlir");
for program in [
include_str!("../../../examples/cli/destructured-report.zirql"),
include_str!("../../../examples/cli/guarded-match.zirql"),
] {
let inline = run_stdin(program, input);
assert!(
inline.status.success(),
"{}",
String::from_utf8_lossy(&inline.stderr)
);
let path = temporary_path("pattern-report", "zirql");
fs::write(&path, program).unwrap();
let file = Command::new(env!("CARGO_BIN_EXE_zirium"))
.args([
"--program-file",
path.to_str().unwrap(),
"examples/cli/function-report.mlir",
])
.current_dir(concat!(env!("CARGO_MANIFEST_DIR"), "/../.."))
.output()
.unwrap();
fs::remove_file(path).unwrap();
assert!(
file.status.success(),
"{}",
String::from_utf8_lossy(&file.stderr)
);
assert_eq!(file.stdout, inline.stdout);
let failed = run_stdin(
&format!("{program} report match (1) {{ x if (1 div 0) == 0 => x, _ => 0 }};"),
input,
);
assert!(!failed.status.success());
assert!(failed.stdout.is_empty());
}
}
#[test]
fn worklist_examples_agree_inline_file_and_buffer_later_failures() {
let input = include_str!("../../../examples/cli/worklist-reachability.mlir");
for program in [
include_str!("../../../examples/cli/worklist-reachability.zirql"),
include_str!("../../../examples/cli/fold-scan.zirql"),
] {
let inline = run_stdin(program, input);
assert!(
inline.status.success(),
"{}",
String::from_utf8_lossy(&inline.stderr)
);
let path = temporary_path("worklist-report", "zirql");
fs::write(&path, program).unwrap();
let file = Command::new(env!("CARGO_BIN_EXE_zirium"))
.args([
"--program-file",
path.to_str().unwrap(),
"examples/cli/worklist-reachability.mlir",
])
.current_dir(concat!(env!("CARGO_MANIFEST_DIR"), "/../.."))
.output()
.unwrap();
fs::remove_file(path).unwrap();
assert!(
file.status.success(),
"{}",
String::from_utf8_lossy(&file.stderr)
);
assert_eq!(file.stdout, inline.stdout);
let failed = run_stdin(
&format!("{program}; report queue<Operation>([]) | json;"),
input,
);
assert!(!failed.status.success());
assert!(failed.stdout.is_empty());
assert!(String::from_utf8_lossy(&failed.stderr).contains("computational"));
}
}
#[test]
fn common_index_sets_and_tuple_positions_work_inline() {
let output = run_stdin(
include_str!("../../../examples/cli/index-sets-and-tuples.zirql"),
include_str!("../../../examples/cli/function-report.mlir"),
);
assert!(output.status.success(), "{output:?}");
assert_eq!(
serde_json::from_slice::<serde_json::Value>(&output.stdout).unwrap(),
serde_json::json!([
{"position": 1, "name": "double"},
{"position": 2, "name": "mix"},
{"position": 3, "name": "plain"},
])
);
}
#[test]
fn existence_inline_file_and_later_failure_share_buffering() {
let source = "where_exists(users) | names";
let inline = run_stdin(source, INPUT);
assert!(
inline.status.success(),
"{}",
String::from_utf8_lossy(&inline.stderr)
);
let path = temporary_path("existence", "zirql");
fs::write(&path, source).unwrap();
let mut child = Command::new(env!("CARGO_BIN_EXE_zirium"))
.args(["--program-file", path.to_str().unwrap()])
.stdin(Stdio::piped())
.stdout(Stdio::piped())
.stderr(Stdio::piped())
.spawn()
.unwrap();
child
.stdin
.take()
.unwrap()
.write_all(INPUT.as_bytes())
.unwrap();
let file = child.wait_with_output().unwrap();
assert!(
file.status.success(),
"{}",
String::from_utf8_lossy(&file.stderr)
);
assert_eq!(file.stdout, inline.stdout);
let failure = run_stdin("names; where_exists(users | check(99))", INPUT);
assert_eq!(failure.status.code(), Some(1));
assert!(failure.stdout.is_empty());
assert!(String::from_utf8_lossy(&failure.stderr).contains("check"));
fs::remove_file(path).unwrap();
}
#[test]
fn brace_reports_match_inline_files_and_buffer_duplicate_failures() {
let path = temporary_path("brace-collections", "zirql");
for (source, success) in [
(
include_str!("../../../examples/cli/brace-collections.zirql"),
true,
),
(
r#"report {3,1}; let k="pear"; report {k:1,(k ++ ""):1 div 0};"#,
false,
),
] {
let inline = run_stdin(source, INPUT);
fs::write(&path, source).unwrap();
let mut child = Command::new(env!("CARGO_BIN_EXE_zirium"))
.args(["--program-file", path.to_str().unwrap()])
.stdin(Stdio::piped())
.stdout(Stdio::piped())
.stderr(Stdio::piped())
.spawn()
.unwrap();
child
.stdin
.take()
.unwrap()
.write_all(INPUT.as_bytes())
.unwrap();
let saved = child.wait_with_output().unwrap();
assert_eq!(inline.status.success(), success, "{inline:?}");
assert_eq!(saved.status.success(), success, "{saved:?}");
assert_eq!(inline.stdout, saved.stdout);
if success {
let result: serde_json::Value = serde_json::from_slice(&inline.stdout).unwrap();
assert_eq!(result["set"], serde_json::json!([3, 1]));
assert_eq!(
result["dictionary"]
.as_object()
.unwrap()
.keys()
.map(String::as_str)
.collect::<Vec<_>>(),
["pear!", "apple!"]
);
} else {
assert!(inline.stdout.is_empty());
for result in [inline, saved] {
let message = String::from_utf8_lossy(&result.stderr);
assert!(
message.contains("duplicate dictionary key: pear"),
"{message}"
);
assert!(!message.contains("zero"));
}
}
}
fs::remove_file(path).unwrap();
}
#[test]
fn saved_collection_suffixes_agree_inline_file_and_buffer_later_errors() {
let source = r#"1+2; let n=1; n+2; let total=n+2; total;
"a" ++ "b"; not false and n+2 == 3; (n+2)*3-1; [n,2][1]+2;
let d={"n":1}; d.n; (d.n);
let numbers=[3,1,3]; sum(numbers); numbers | sort | unique | count"#;
let inline = run_stdin(source, INPUT);
assert!(
inline.status.success(),
"{}",
String::from_utf8_lossy(&inline.stderr)
);
let path = temporary_path("saved-values", "zirql");
fs::write(&path, source).unwrap();
let file = Command::new(env!("CARGO_BIN_EXE_zirium"))
.args([
"--program-file",
path.to_str().unwrap(),
"examples/cli/function-report.mlir",
])
.current_dir(concat!(env!("CARGO_MANIFEST_DIR"), "/../.."))
.output()
.unwrap();
assert!(
file.status.success(),
"{}",
String::from_utf8_lossy(&file.stderr)
);
assert_eq!(inline.stdout, file.stdout);
assert_eq!(
String::from_utf8(inline.stdout).unwrap(),
"3\n3\n3\n\"ab\"\ntrue\n8\n3\n1\n1\n7\n2\n"
);
let native_fields = run_stdin(
r#"let selected=input | filter(op("arith.constant")) | head(1);
let row=user_info(selected);
row.targets | names; (row.targets) | names; (row.targets | names);
let answer=row.targets; answer | names; (answer | names)"#,
INPUT,
);
assert!(
native_fields.status.success(),
"{}",
String::from_utf8_lossy(&native_fields.stderr)
);
assert_eq!(
native_fields.stdout,
b"arith.addi\narith.addi\narith.addi\narith.addi\narith.addi\narith.addi\narith.addi\narith.addi\narith.addi\narith.addi\n"
);
let failed = run_stdin(&format!("{source}; 1 div 0"), INPUT);
assert!(!failed.status.success());
assert!(failed.stdout.is_empty());
assert!(String::from_utf8_lossy(&failed.stderr).contains("zero"));
fs::remove_file(path).unwrap();
}