use std::path::PathBuf;
use std::process::Command;
fn cargo_bin() -> PathBuf {
PathBuf::from(env!("CARGO_BIN_EXE_cairn"))
}
fn examples_dir() -> PathBuf {
PathBuf::from(env!("CARGO_MANIFEST_DIR"))
.join("..")
.join("..")
.join("examples")
}
fn run_synth(args: &[&str]) -> std::process::Output {
Command::new(cargo_bin())
.arg("synth")
.args(args)
.output()
.expect("failed to invoke cairn binary")
}
#[test]
fn cli_synth_requires_experimental_flag() {
let path = examples_dir().join("redstone-door.crn");
let out = run_synth(&[path.to_str().unwrap()]);
assert_eq!(out.status.code(), Some(2));
let stderr = String::from_utf8(out.stderr).expect("utf-8");
assert!(
stderr.contains("--experimental-logic-synth"),
"gate diagnostic should name the required flag, got: {stderr}",
);
}
#[test]
fn cli_synth_redstone_door_emits_or_gate_json() {
let path = examples_dir().join("redstone-door.crn");
let out = run_synth(&["--experimental-logic-synth", path.to_str().unwrap()]);
assert!(
out.status.success(),
"expected exit 0, stderr={}",
String::from_utf8_lossy(&out.stderr),
);
let stdout = String::from_utf8(out.stdout).expect("utf-8");
let value: serde_json::Value = serde_json::from_str(&stdout)
.unwrap_or_else(|err| panic!("stdout should parse as JSON: {err}\n{stdout}"));
let scopes = value.as_array().expect("top-level is a scope list");
let gatehouse = scopes
.iter()
.find(|s| s["name"] == "gatehouse")
.expect("gatehouse scope in output");
let ir = &gatehouse["ir"];
assert_eq!(ir["inputs"].as_array().expect("inputs array").len(), 2);
assert_eq!(ir["outputs"].as_array().expect("outputs array").len(), 1);
let nodes = ir["nodes"].as_array().expect("nodes array");
assert_eq!(nodes.len(), 1);
assert_eq!(nodes[0]["kind"]["kind"], "or2");
}
#[test]
fn cli_synth_stage_netlist_emits_or_cell_json() {
let path = examples_dir().join("redstone-door.crn");
let out = run_synth(&[
"--experimental-logic-synth",
"--stage",
"netlist",
path.to_str().unwrap(),
]);
assert!(
out.status.success(),
"expected exit 0, stderr={}",
String::from_utf8_lossy(&out.stderr),
);
let stdout = String::from_utf8(out.stdout).expect("utf-8");
let value: serde_json::Value = serde_json::from_str(&stdout)
.unwrap_or_else(|err| panic!("stdout should parse as JSON: {err}\n{stdout}"));
let scopes = value.as_array().expect("top-level is a scope list");
let gatehouse = scopes
.iter()
.find(|s| s["name"] == "gatehouse")
.expect("gatehouse scope in output");
let ir = &gatehouse["ir"];
assert_eq!(ir["inputs"].as_array().expect("inputs array").len(), 2);
assert_eq!(ir["outputs"].as_array().expect("outputs array").len(), 1);
let cells = ir["cells"].as_array().expect("cells array");
assert_eq!(cells.len(), 1);
assert_eq!(cells[0]["cell"], "or");
let drivers = cells[0]["drivers"].as_array().expect("drivers array");
assert_eq!(drivers.len(), 2);
assert_eq!(drivers[0]["port"], "a");
assert_eq!(drivers[1]["port"], "b");
assert_eq!(ir["outputs"][0]["driver"]["kind"], "cell");
}
#[test]
fn cli_synth_stage_edition_java_maps_or_cell_to_java_repeater_or() {
let path = examples_dir().join("redstone-door.crn");
let out = run_synth(&[
"--experimental-logic-synth",
"--stage",
"edition",
"--edition",
"java",
path.to_str().unwrap(),
]);
assert!(
out.status.success(),
"expected exit 0, stderr={}",
String::from_utf8_lossy(&out.stderr),
);
let stdout = String::from_utf8(out.stdout).expect("utf-8");
let value: serde_json::Value = serde_json::from_str(&stdout)
.unwrap_or_else(|err| panic!("stdout should parse as JSON: {err}\n{stdout}"));
let scopes = value.as_array().expect("top-level is a scope list");
let gatehouse = scopes
.iter()
.find(|s| s["name"] == "gatehouse")
.expect("gatehouse scope in output");
let ir = &gatehouse["ir"];
assert_eq!(ir["edition"], "java");
let cells = ir["cells"].as_array().expect("cells array");
assert_eq!(cells.len(), 1);
assert_eq!(cells[0]["cell"], "java_repeater_or");
let drivers = cells[0]["drivers"].as_array().expect("drivers array");
assert_eq!(drivers.len(), 2);
assert_eq!(drivers[0]["port"], "a");
assert_eq!(drivers[1]["port"], "b");
}
#[test]
fn cli_synth_stage_edition_bedrock_maps_or_cell_to_bedrock_torch_or() {
let path = examples_dir().join("redstone-door.crn");
let out = run_synth(&[
"--experimental-logic-synth",
"--stage",
"edition",
"--edition",
"bedrock",
path.to_str().unwrap(),
]);
assert!(
out.status.success(),
"expected exit 0, stderr={}",
String::from_utf8_lossy(&out.stderr),
);
let stdout = String::from_utf8(out.stdout).expect("utf-8");
let value: serde_json::Value = serde_json::from_str(&stdout)
.unwrap_or_else(|err| panic!("stdout should parse as JSON: {err}\n{stdout}"));
let gatehouse = value
.as_array()
.and_then(|s| s.iter().find(|s| s["name"] == "gatehouse"))
.expect("gatehouse scope");
let ir = &gatehouse["ir"];
assert_eq!(ir["edition"], "bedrock");
assert_eq!(ir["cells"][0]["cell"], "bedrock_torch_or");
}
#[test]
fn cli_synth_stage_logic_rejects_edition_flag() {
let path = examples_dir().join("redstone-door.crn");
let out = run_synth(&[
"--experimental-logic-synth",
"--stage",
"logic",
"--edition",
"java",
path.to_str().unwrap(),
]);
assert_eq!(out.status.code(), Some(2));
let stderr = String::from_utf8(out.stderr).expect("utf-8");
assert!(
stderr.contains("--edition"),
"usage hint should name the stray flag, got: {stderr}",
);
}
#[test]
fn cli_synth_stage_netlist_rejects_edition_flag() {
let path = examples_dir().join("redstone-door.crn");
let out = run_synth(&[
"--experimental-logic-synth",
"--stage",
"netlist",
"--edition",
"bedrock",
path.to_str().unwrap(),
]);
assert_eq!(out.status.code(), Some(2));
}
#[test]
fn cli_synth_stage_placement_java_places_or_cell_at_origin() {
let path = examples_dir().join("redstone-door.crn");
let out = run_synth(&[
"--experimental-logic-synth",
"--stage",
"placement",
"--edition",
"java",
path.to_str().unwrap(),
]);
assert!(
out.status.success(),
"expected exit 0, stderr={}",
String::from_utf8_lossy(&out.stderr),
);
let stdout = String::from_utf8(out.stdout).expect("utf-8");
let value: serde_json::Value = serde_json::from_str(&stdout)
.unwrap_or_else(|err| panic!("stdout should parse as JSON: {err}\n{stdout}"));
let gatehouse = value
.as_array()
.and_then(|s| s.iter().find(|s| s["name"] == "gatehouse"))
.expect("gatehouse scope");
let ir = &gatehouse["ir"];
assert_eq!(ir["edition"], "java");
let region = &ir["region"];
assert_eq!(region["label"], "floor");
assert_eq!(region["void"], 2);
assert_eq!(region["width"], 7);
assert_eq!(region["depth"], 5);
let cells = ir["cells"].as_array().expect("cells array");
assert_eq!(cells.len(), 1);
assert_eq!(cells[0]["cell"], "java_repeater_or");
assert_eq!(
cells[0]["stage"], "placement",
"the stage tag must echo the --stage flag that produced the dump: {stdout}",
);
let coord = &cells[0]["coord"];
assert_eq!(coord["x"], 0);
assert_eq!(coord["y"], 0);
assert_eq!(coord["z"], 0);
assert!(
cells[0].get("wire_length").is_none(),
"wire_length must be elided today: {stdout}",
);
assert!(
cells[0].get("delay_ticks").is_none(),
"delay_ticks must be elided today: {stdout}",
);
}
#[test]
fn cli_synth_stage_placement_bedrock_matches_java_layout() {
let path = examples_dir().join("redstone-door.crn");
let out = run_synth(&[
"--experimental-logic-synth",
"--stage",
"placement",
"--edition",
"bedrock",
path.to_str().unwrap(),
]);
assert!(
out.status.success(),
"expected exit 0, stderr={}",
String::from_utf8_lossy(&out.stderr),
);
let stdout = String::from_utf8(out.stdout).expect("utf-8");
let value: serde_json::Value = serde_json::from_str(&stdout)
.unwrap_or_else(|err| panic!("stdout should parse as JSON: {err}\n{stdout}"));
let gatehouse = value
.as_array()
.and_then(|s| s.iter().find(|s| s["name"] == "gatehouse"))
.expect("gatehouse scope");
let ir = &gatehouse["ir"];
assert_eq!(ir["edition"], "bedrock");
assert_eq!(ir["cells"][0]["cell"], "bedrock_torch_or");
assert_eq!(ir["cells"][0]["coord"]["x"], 0);
}
#[test]
fn cli_synth_stage_placement_requires_edition_flag() {
let path = examples_dir().join("redstone-door.crn");
let out = run_synth(&[
"--experimental-logic-synth",
"--stage",
"placement",
path.to_str().unwrap(),
]);
assert_eq!(out.status.code(), Some(2));
let stderr = String::from_utf8(out.stderr).expect("utf-8");
assert!(
stderr.contains("--edition"),
"usage hint should name the missing flag, got: {stderr}",
);
assert!(
stderr.contains("--stage placement"),
"usage hint should name the failing stage as it is spelled on \
the CLI so the mirror stays in sync with clap, got: {stderr}",
);
}
#[test]
fn cli_synth_stage_placement_missing_region_exits_one() {
let dir = tempfile::tempdir().expect("temp dir");
let path = dir.path().join("noregion.crn");
let source = "@cairn 2026.06\n@requires version>=1.20\n\n\
theme t:\n slot wall -> @oak_planks\n\n\
struct noregion size=7x5\n \
floor mat_slot=wall\n \
pressure_plate id=p at=front.outside offset=0 y=0 -> sig.a\n \
pressure_plate id=q at=inside.front offset=0 y=0 -> sig.b\n \
logic sig.open = sig.a or sig.b\n \
door id=d side=front at=center mat_slot=wall opened_by=sig.open\n";
std::fs::write(&path, source).expect("write missing-region fixture");
let out = run_synth(&[
"--experimental-logic-synth",
"--stage",
"placement",
"--edition",
"java",
path.to_str().unwrap(),
]);
assert_eq!(out.status.code(), Some(1));
let stderr = String::from_utf8(out.stderr).expect("utf-8");
assert!(
stderr.contains("E_NO_CIRCUIT_REGION"),
"expected E_NO_CIRCUIT_REGION on stderr, got: {stderr}",
);
}
#[test]
fn cli_synth_stage_placement_congestion_exits_one() {
let dir = tempfile::tempdir().expect("temp dir");
let path = dir.path().join("tiny.crn");
let source = "@cairn 2026.06\n@requires version>=1.20\n\n\
theme t:\n slot wall -> @oak_planks\n\n\
struct tiny size=3x3\n \
floor mat_slot=wall\n \
pressure_plate id=p at=front.outside offset=0 y=0 -> sig.a\n \
pressure_plate id=q at=inside.front offset=0 y=0 -> sig.b\n \
logic sig.and_ab = sig.a and sig.b\n \
logic sig.or_ab = sig.a or sig.b\n \
logic sig.combined = sig.and_ab and sig.or_ab\n \
door id=d side=front at=center mat_slot=wall opened_by=sig.combined\n \
circuit region=floor void=1\n";
std::fs::write(&path, source).expect("write congestion fixture");
let out = run_synth(&[
"--experimental-logic-synth",
"--stage",
"placement",
"--edition",
"java",
path.to_str().unwrap(),
]);
assert_eq!(out.status.code(), Some(1));
let stderr = String::from_utf8(out.stderr).expect("utf-8");
assert!(
stderr.contains("E_ROUTE_CONGESTION"),
"expected E_ROUTE_CONGESTION on stderr, got: {stderr}",
);
}
#[test]
fn cli_synth_stage_edition_requires_edition_flag() {
let path = examples_dir().join("redstone-door.crn");
let out = run_synth(&[
"--experimental-logic-synth",
"--stage",
"edition",
path.to_str().unwrap(),
]);
assert_eq!(out.status.code(), Some(2));
let stderr = String::from_utf8(out.stderr).expect("utf-8");
assert!(
stderr.contains("--edition"),
"usage hint should name the missing flag, got: {stderr}",
);
assert!(
stderr.contains("--stage edition"),
"usage hint should name the failing stage as it is spelled on \
the CLI so the mirror stays in sync with clap, got: {stderr}",
);
}
#[test]
fn cli_synth_stage_route_java_fills_wire_length() {
let path = examples_dir().join("redstone-door.crn");
let out = run_synth(&[
"--experimental-logic-synth",
"--stage",
"route",
"--edition",
"java",
path.to_str().unwrap(),
]);
assert!(
out.status.success(),
"expected exit 0, stderr={}",
String::from_utf8_lossy(&out.stderr),
);
let stdout = String::from_utf8(out.stdout).expect("utf-8");
let value: serde_json::Value = serde_json::from_str(&stdout)
.unwrap_or_else(|err| panic!("stdout should parse as JSON: {err}\n{stdout}"));
let gatehouse = value
.as_array()
.and_then(|s| s.iter().find(|s| s["name"] == "gatehouse"))
.expect("gatehouse scope");
let ir = &gatehouse["ir"];
assert_eq!(ir["edition"], "java");
let cells = ir["cells"].as_array().expect("cells array");
assert_eq!(cells.len(), 1);
assert_eq!(cells[0]["cell"], "java_repeater_or");
assert_eq!(
cells[0]["stage"], "route",
"the stage tag must echo the --stage flag that produced the dump: {stdout}",
);
assert_eq!(cells[0]["wire_length"], 3);
assert!(
cells[0].get("delay_ticks").is_none(),
"delay_ticks must be elided at this stage: {stdout}",
);
}
#[test]
fn cli_synth_stage_route_bedrock_matches_java_wire_length() {
let path = examples_dir().join("redstone-door.crn");
let out = run_synth(&[
"--experimental-logic-synth",
"--stage",
"route",
"--edition",
"bedrock",
path.to_str().unwrap(),
]);
assert!(
out.status.success(),
"expected exit 0, stderr={}",
String::from_utf8_lossy(&out.stderr),
);
let stdout = String::from_utf8(out.stdout).expect("utf-8");
let value: serde_json::Value = serde_json::from_str(&stdout)
.unwrap_or_else(|err| panic!("stdout should parse as JSON: {err}\n{stdout}"));
let gatehouse = value
.as_array()
.and_then(|s| s.iter().find(|s| s["name"] == "gatehouse"))
.expect("gatehouse scope");
let ir = &gatehouse["ir"];
assert_eq!(ir["edition"], "bedrock");
assert_eq!(ir["cells"][0]["cell"], "bedrock_torch_or");
assert_eq!(ir["cells"][0]["wire_length"], 3);
}
#[test]
fn cli_synth_stage_route_requires_edition_flag() {
let path = examples_dir().join("redstone-door.crn");
let out = run_synth(&[
"--experimental-logic-synth",
"--stage",
"route",
path.to_str().unwrap(),
]);
assert_eq!(out.status.code(), Some(2));
let stderr = String::from_utf8(out.stderr).expect("utf-8");
assert!(
stderr.contains("--edition"),
"usage hint should name the missing flag, got: {stderr}",
);
assert!(
stderr.contains("--stage route"),
"usage hint should name the tripped stage so a caller cannot mis-attribute the error, got: {stderr}",
);
}
#[test]
fn cli_synth_stage_route_congestion_exits_one() {
let dir = tempfile::tempdir().expect("temp dir");
let path = dir.path().join("pack.crn");
let source = "@cairn 2026.06\n@requires version>=1.20\n\n\
theme t:\n slot wall -> @oak_planks\n\n\
struct pack size=4x3\n \
floor mat_slot=wall\n \
pressure_plate id=p at=front.outside offset=0 y=0 -> sig.a\n \
pressure_plate id=q at=inside.front offset=0 y=0 -> sig.b\n \
logic sig.and_ab = sig.a and sig.b\n \
logic sig.or_ab = sig.a or sig.b\n \
logic sig.combined = sig.and_ab and sig.or_ab\n \
door id=d side=front at=center mat_slot=wall opened_by=sig.combined\n \
circuit region=floor void=1\n";
std::fs::write(&path, source).expect("write congestion fixture");
let out = run_synth(&[
"--experimental-logic-synth",
"--stage",
"route",
"--edition",
"java",
path.to_str().unwrap(),
]);
assert_eq!(out.status.code(), Some(1));
let stderr = String::from_utf8(out.stderr).expect("utf-8");
assert!(
stderr.contains("E_ROUTE_CONGESTION"),
"expected E_ROUTE_CONGESTION on stderr, got: {stderr}",
);
assert!(
stderr.contains("routed netlist for struct `pack`"),
"primary should name the routing origin and failed scope, got: {stderr}",
);
}
#[test]
fn cli_synth_stage_route_rejects_missing_edition_when_stage_neutral() {
let path = examples_dir().join("redstone-door.crn");
let out = run_synth(&[
"--experimental-logic-synth",
"--stage",
"netlist",
"--edition",
"java",
path.to_str().unwrap(),
]);
assert_eq!(out.status.code(), Some(2));
}
#[test]
fn cli_synth_stage_delay_java_fills_delay_ticks() {
let path = examples_dir().join("redstone-door.crn");
let out = run_synth(&[
"--experimental-logic-synth",
"--stage",
"delay",
"--edition",
"java",
path.to_str().unwrap(),
]);
assert!(
out.status.success(),
"expected exit 0, stderr={}",
String::from_utf8_lossy(&out.stderr),
);
let stdout = String::from_utf8(out.stdout).expect("utf-8");
let value: serde_json::Value = serde_json::from_str(&stdout)
.unwrap_or_else(|err| panic!("stdout should parse as JSON: {err}\n{stdout}"));
let gatehouse = value
.as_array()
.and_then(|s| s.iter().find(|s| s["name"] == "gatehouse"))
.expect("gatehouse scope");
let ir = &gatehouse["ir"];
assert_eq!(ir["edition"], "java");
let cells = ir["cells"].as_array().expect("cells array");
assert_eq!(cells.len(), 1);
assert_eq!(cells[0]["cell"], "java_repeater_or");
assert_eq!(
cells[0]["stage"], "delay",
"the stage tag must echo the --stage flag that produced the dump: {stdout}",
);
assert_eq!(cells[0]["wire_length"], 3);
assert_eq!(cells[0]["delay_ticks"], 1);
}
#[test]
fn cli_synth_stage_delay_bedrock_matches_bedrock_torch_or() {
let path = examples_dir().join("redstone-door.crn");
let out = run_synth(&[
"--experimental-logic-synth",
"--stage",
"delay",
"--edition",
"bedrock",
path.to_str().unwrap(),
]);
assert!(
out.status.success(),
"expected exit 0, stderr={}",
String::from_utf8_lossy(&out.stderr),
);
let stdout = String::from_utf8(out.stdout).expect("utf-8");
let value: serde_json::Value = serde_json::from_str(&stdout)
.unwrap_or_else(|err| panic!("stdout should parse as JSON: {err}\n{stdout}"));
let gatehouse = value
.as_array()
.and_then(|s| s.iter().find(|s| s["name"] == "gatehouse"))
.expect("gatehouse scope");
let ir = &gatehouse["ir"];
assert_eq!(ir["edition"], "bedrock");
assert_eq!(ir["cells"][0]["cell"], "bedrock_torch_or");
assert_eq!(ir["cells"][0]["wire_length"], 3);
assert_eq!(ir["cells"][0]["delay_ticks"], 0);
}
#[test]
fn cli_synth_stage_delay_requires_edition_flag() {
let path = examples_dir().join("redstone-door.crn");
let out = run_synth(&[
"--experimental-logic-synth",
"--stage",
"delay",
path.to_str().unwrap(),
]);
assert_eq!(out.status.code(), Some(2));
let stderr = String::from_utf8(out.stderr).expect("utf-8");
assert!(
stderr.contains("--edition"),
"usage hint should name the missing flag, got: {stderr}",
);
assert!(
stderr.contains("--stage delay"),
"usage hint should name the tripped stage so a caller cannot mis-attribute the error, got: {stderr}",
);
}
#[test]
fn cli_synth_stage_delay_inherits_upstream_congestion_failure() {
let dir = tempfile::tempdir().expect("temp dir");
let path = dir.path().join("pack.crn");
let source = "@cairn 2026.06\n@requires version>=1.20\n\n\
theme t:\n slot wall -> @oak_planks\n\n\
struct pack size=4x3\n \
floor mat_slot=wall\n \
pressure_plate id=p at=front.outside offset=0 y=0 -> sig.a\n \
pressure_plate id=q at=inside.front offset=0 y=0 -> sig.b\n \
logic sig.and_ab = sig.a and sig.b\n \
logic sig.or_ab = sig.a or sig.b\n \
logic sig.combined = sig.and_ab and sig.or_ab\n \
door id=d side=front at=center mat_slot=wall opened_by=sig.combined\n \
circuit region=floor void=1\n";
std::fs::write(&path, source).expect("write congestion fixture");
let out = run_synth(&[
"--experimental-logic-synth",
"--stage",
"delay",
"--edition",
"java",
path.to_str().unwrap(),
]);
assert_eq!(out.status.code(), Some(1));
let stderr = String::from_utf8(out.stderr).expect("utf-8");
assert!(
stderr.contains("E_ROUTE_CONGESTION"),
"expected E_ROUTE_CONGESTION inherited from routing on stderr, got: {stderr}",
);
}
#[test]
fn cli_synth_stage_delay_attenuation_limit_exits_one() {
let dir = tempfile::tempdir().expect("temp dir");
let path = dir.path().join("wide.crn");
let source = "@cairn 2026.06\n@requires version>=1.20\n\n\
theme t:\n slot wall -> @oak_planks\n\n\
struct wide_pack size=300x5\n \
floor mat_slot=wall\n \
pressure_plate id=p at=front.outside offset=0 y=0 -> sig.a\n \
pressure_plate id=q at=inside.front offset=0 y=0 -> sig.b\n \
logic sig.out = sig.a or sig.b\n \
door id=d side=front at=center mat_slot=wall opened_by=sig.out\n \
circuit region=floor void=3\n";
std::fs::write(&path, source).expect("write attenuation fixture");
let out = run_synth(&[
"--experimental-logic-synth",
"--stage",
"delay",
"--edition",
"java",
path.to_str().unwrap(),
]);
assert_eq!(out.status.code(), Some(1));
let stderr = String::from_utf8(out.stderr).expect("utf-8");
assert!(
stderr.contains("E_ATTENUATION_LIMIT"),
"expected E_ATTENUATION_LIMIT on stderr, got: {stderr}",
);
assert!(
stderr.contains("routed netlist for struct `wide_pack`"),
"primary should name the delay-side origin and failed scope, got: {stderr}",
);
}
#[test]
fn cli_synth_missing_file_exits_two() {
let out = run_synth(&["--experimental-logic-synth", "no-such-file.crn"]);
assert_eq!(out.status.code(), Some(2));
}
#[test]
fn cli_synth_unparseable_source_exits_one() {
let dir = tempfile::tempdir().expect("temp dir");
let path = dir.path().join("broken.crn");
std::fs::write(&path, "@cairn 2026.06\n\nstruct 3xnot-a-size\n").expect("write scratch file");
let out = run_synth(&["--experimental-logic-synth", path.to_str().unwrap()]);
assert_eq!(out.status.code(), Some(1));
let stderr = String::from_utf8(out.stderr).expect("utf-8");
assert!(
stderr.contains("error"),
"expected an error line on stderr, got: {stderr}",
);
}
#[test]
fn cli_synth_stage_crossing_java_legalizes_or_cell_scope() {
let path = examples_dir().join("redstone-door.crn");
let out = run_synth(&[
"--experimental-logic-synth",
"--stage",
"crossing",
"--edition",
"java",
path.to_str().unwrap(),
]);
let stderr = String::from_utf8_lossy(&out.stderr).into_owned();
assert!(out.status.success(), "expected exit 0, stderr={stderr}");
assert!(
stderr.is_empty(),
"a clean fixture must not spill diagnostics on stderr; a future \
deprecation notice would otherwise reach users silently. Got: {stderr}",
);
let stdout = String::from_utf8(out.stdout)
.unwrap_or_else(|err| panic!("stdout should be utf-8: {err}\nstderr={stderr}"));
let value: serde_json::Value = serde_json::from_str(&stdout).unwrap_or_else(|err| {
panic!("stdout should parse as JSON: {err}\nstdout={stdout}\nstderr={stderr}")
});
let gatehouse = value
.as_array()
.and_then(|s| s.iter().find(|s| s["name"] == "gatehouse"))
.expect("gatehouse scope");
let ir = &gatehouse["ir"];
assert_eq!(ir["edition"], "java");
let cells = ir["cells"].as_array().expect("cells array");
assert_eq!(cells.len(), 1);
assert_eq!(
cells[0]["stage"], "crossing",
"the stage tag must echo the --stage flag that produced the dump — it is what \
distinguishes this zero-buffer legalized dump from its --stage delay input: {stdout}",
);
assert!(
cells[0].get("buffer_coords").is_none(),
"empty buffer_coords must serde-skip: the stage tag, not a sentinel empty array, is what marks a dump as legalized: {stdout}",
);
assert!(
cells[0]["coord"].get("layer").is_none(),
"plane cell coord must serde-skip its layer field: {stdout}",
);
}
#[test]
fn cli_synth_stage_crossing_bedrock_legalizes_or_cell_scope() {
let path = examples_dir().join("redstone-door.crn");
let out = run_synth(&[
"--experimental-logic-synth",
"--stage",
"crossing",
"--edition",
"bedrock",
path.to_str().unwrap(),
]);
let stderr = String::from_utf8_lossy(&out.stderr).into_owned();
assert!(out.status.success(), "expected exit 0, stderr={stderr}");
assert!(
stderr.is_empty(),
"a clean fixture must not spill diagnostics on stderr; a future \
deprecation notice would otherwise reach users silently. Got: {stderr}",
);
let stdout = String::from_utf8(out.stdout)
.unwrap_or_else(|err| panic!("stdout should be utf-8: {err}\nstderr={stderr}"));
let value: serde_json::Value = serde_json::from_str(&stdout).unwrap_or_else(|err| {
panic!("stdout should parse as JSON: {err}\nstdout={stdout}\nstderr={stderr}")
});
let gatehouse = value
.as_array()
.and_then(|s| s.iter().find(|s| s["name"] == "gatehouse"))
.expect("gatehouse scope");
let ir = &gatehouse["ir"];
assert_eq!(ir["edition"], "bedrock");
let cells = ir["cells"].as_array().expect("cells array");
assert_eq!(cells.len(), 1);
assert_eq!(
cells[0]["cell"], "bedrock_torch_or",
"Bedrock edition realises `or` as the torch-based cell",
);
assert_eq!(
cells[0]["stage"], "crossing",
"the stage tag must echo the --stage flag that produced the dump: {stdout}",
);
assert!(
cells[0].get("buffer_coords").is_none(),
"empty buffer_coords must serde-skip: the stage tag, not a sentinel empty array, is what marks a dump as legalized: {stdout}",
);
assert!(
cells[0]["coord"].get("layer").is_none(),
"plane cell coord must serde-skip its layer field: {stdout}",
);
}
#[test]
fn cli_synth_stage_crossing_requires_edition_flag() {
let path = examples_dir().join("redstone-door.crn");
let out = run_synth(&[
"--experimental-logic-synth",
"--stage",
"crossing",
path.to_str().unwrap(),
]);
assert_eq!(out.status.code(), Some(2));
let stderr = String::from_utf8(out.stderr).expect("utf-8");
assert!(
stderr.contains("--edition"),
"usage hint should name the required flag, got: {stderr}",
);
assert!(
stderr.contains("--stage crossing"),
"usage hint should name the failing stage as it is spelled on \
the CLI so the mirror stays in sync with clap, got: {stderr}",
);
}
#[test]
fn cli_synth_stage_crossing_inherits_upstream_attenuation_failure() {
let dir = tempfile::tempdir().expect("temp dir");
let path = dir.path().join("wide.crn");
let source = "@cairn 2026.06\n@requires version>=1.20\n\n\
theme t:\n slot wall -> @oak_planks\n\n\
struct wide_pack size=300x5\n \
floor mat_slot=wall\n \
pressure_plate id=p at=front.outside offset=0 y=0 -> sig.a\n \
pressure_plate id=q at=inside.front offset=0 y=0 -> sig.b\n \
logic sig.out = sig.a or sig.b\n \
door id=d side=front at=center mat_slot=wall opened_by=sig.out\n \
circuit region=floor void=3\n";
std::fs::write(&path, source).expect("write attenuation fixture");
let out = run_synth(&[
"--experimental-logic-synth",
"--stage",
"crossing",
"--edition",
"java",
path.to_str().unwrap(),
]);
assert_eq!(out.status.code(), Some(1));
let stderr = String::from_utf8(out.stderr).expect("utf-8");
assert!(
stderr.contains("E_ATTENUATION_LIMIT"),
"expected upstream E_ATTENUATION_LIMIT on stderr, got: {stderr}",
);
}
#[test]
fn cli_synth_stage_crossing_congestion_exits_one() {
let source = std::fs::read_to_string(examples_dir().join("crossbar.crn"))
.expect("read crossbar fixture");
assert_eq!(
source.matches("void=2").count(),
1,
"crossbar.crn no longer carries exactly one `void=2` needle — the \
fixture drifted, and patching it would either no-op (leaving the \
crossing refusal unexercised) or rewrite an unintended second site",
);
let patched = source.replace("void=2", "void=1");
let dir = tempfile::tempdir().expect("temp dir");
let path = dir.path().join("crossbar.crn");
std::fs::write(&path, &patched).expect("write crossing congestion fixture");
let out = run_synth(&[
"--experimental-logic-synth",
"--stage",
"crossing",
"--edition",
"java",
path.to_str().unwrap(),
]);
assert_eq!(out.status.code(), Some(1));
let stderr = String::from_utf8(out.stderr).expect("utf-8");
assert!(
stderr.contains("E_CROSSING_CONGESTION"),
"expected E_CROSSING_CONGESTION on stderr, got: {stderr}",
);
assert!(
stderr.contains("routed netlist for struct `crossbar`")
&& stderr.contains("plane crossing"),
"primary should name the crossing-side origin and failed scope, got: {stderr}",
);
assert!(
!stderr.contains("E_ROUTE_CONGESTION"),
"the refusal must come from the crossing pass itself, but the fixture \
also tripped routing, got: {stderr}",
);
assert!(
!stderr.contains("E_ATTENUATION_LIMIT"),
"the refusal must come from the crossing pass itself, but the fixture \
also tripped delay-side attenuation, got: {stderr}",
);
assert!(
out.stdout.is_empty(),
"a refused scope must not reach stdout as a legalized IR dump, got: {}",
String::from_utf8_lossy(&out.stdout),
);
}
fn stage_values() -> Vec<String> {
let out = run_synth(&["--stage", "not-a-stage", "unused.crn"]);
assert_eq!(
out.status.code(),
Some(2),
"an unknown --stage value should be a usage error",
);
let stderr = String::from_utf8(out.stderr).expect("utf-8");
let Some((_, rest)) = stderr.split_once("[possible values: ") else {
panic!("clap should list --stage's values, got: {stderr}");
};
let Some((list, _)) = rest.split_once(']') else {
panic!("clap's value list should be closed, got: {stderr}");
};
list.split(", ").map(str::to_string).collect()
}
#[test]
fn cli_synth_missing_edition_reports_only_the_usage_error() {
let path = examples_dir().join("redstone-door.crn");
let mut gated = 0;
for stage in stage_values() {
let out = run_synth(&[
"--experimental-logic-synth",
"--stage",
&stage,
path.to_str().unwrap(),
]);
let stderr = String::from_utf8(out.stderr).expect("utf-8");
match out.status.code() {
Some(2) => {
gated += 1;
assert!(
out.stdout.is_empty(),
"--stage {stage} must print no IR before the usage gate, got: {}",
String::from_utf8_lossy(&out.stdout),
);
let lines: Vec<&str> = stderr.lines().filter(|l| !l.trim().is_empty()).collect();
assert_eq!(
lines.len(),
1,
"--stage {stage} stderr should be the usage error alone, got: {stderr}",
);
assert!(
lines[0].contains(&format!("--stage {stage}"))
&& lines[0].contains("--edition"),
"--stage {stage} stderr line should be the missing-edition hint, got: {stderr}",
);
}
Some(0) => assert!(
stderr.is_empty(),
"edition-neutral --stage {stage} should run clean without the flag, got: {stderr}",
),
other => panic!("--stage {stage} without --edition exited {other:?}: {stderr}"),
}
}
assert!(
gated > 0,
"no --stage value required --edition, so this test asserted nothing about the gate",
);
}