///|
/// Deterministic JSON summary; false boundedness is unknown when truncated.
pub fn AnalysisReport::to_json(self : AnalysisReport) -> String {
"{\"states\":\{self.states},\"edges\":\{self.edges},\"deadlocks\":\{self.deadlocks},\"max_tokens\":\{self.max_tokens},\"truncated\":\{self.truncated}}"
}
///|
/// Pure CLI engine: file access and process exit live in cmd/moonpetri.
pub fn run_command(
command : String,
input : String,
args : Array[String],
) -> Result[String, Array[PetriError]] {
let net = match parse_pnml(input) {
Ok(n) => n
Err(e) => return Err(e)
}
match net.validate() {
Ok(_) => ()
Err(e) => return Err(e)
}
let start = net.initial_marking()
match command {
"validate" => {
if !args.is_empty() {
return Err([ParseError("validate takes no options")])
}
Ok(
"valid places=\{net.place_count()} transitions=\{net.transition_count()} enabled=\{enabled_transitions(net,start).length()}",
)
}
"fire" => {
let seq = []
for name in args {
let found = match net.find_pnml_transition(name) {
Some(t) => Some(t)
None => net.find_transition(name)
}
match found {
Some(t) => seq.push(t)
None => return Err([ParseError("unknown transition: \{name}")])
}
}
match fire_sequence(net, start, seq) {
Ok(m) => Ok("marking=[\{marking_fingerprint(m)}]")
Err(e) => Err([e])
}
}
"explore" | "report" => {
let max = if args.is_empty() {
1000
} else if args.length() == 2 && args[0] == "--max-states" {
natural(args[1]) catch {
_ => return Err([InvalidMaxStates])
}
} else {
return Err([ParseError("expected --max-states INTEGER")])
}
let result = match reachable(net, start, max) {
Ok(r) => r
Err(e) => return Err([e])
}
let report = analysis_report(net, result)
if command == "report" {
Ok(report.to_json())
} else {
Ok(
"states=\{report.states} edges=\{report.edges} deadlocks=\{report.deadlocks} max_tokens=\{report.max_tokens} truncated=\{report.truncated}",
)
}
}
_ => Err([ParseError("expected validate, explore, fire or report")])
}
}
///|
test "command engine validates options and emits stable structured output" {
let input = ""
assert_eq(
run_command("validate", input, []),
Ok("valid places=1 transitions=0 enabled=0"),
)
assert_eq(run_command("fire", input, []), Ok("marking=[0]"))
assert_eq(
run_command("report", input, []),
Ok(
"{\"states\":1,\"edges\":0,\"deadlocks\":1,\"max_tokens\":0,\"truncated\":false}",
),
)
assert_true(run_command("fire", input, ["missing"]) is Err(_))
assert_true(run_command("bad", input, []) is Err(_))
assert_true(run_command("report", input, ["--max-states", "0"]) is Err(_))
assert_true(run_command("explore", input, ["--max-states", "-1"]) is Err(_))
assert_true(run_command("validate", "", []) is Err(_))
}
///|
test "CLI resolves PNML transition IDs before human-readable labels" {
let input = "1消费"
assert_eq(run_command("fire", input, ["t_consume"]), Ok("marking=[0]"))
assert_eq(run_command("fire", input, ["消费"]), Ok("marking=[0]"))
let n = match parse_pnml(input) {
Ok(n) => n
Err(_) => panic()
}
assert_eq(n.find_pnml_transition("t_consume"), Some(0))
assert_eq(n.find_transition("消费"), Some(0))
assert_eq(n.find_transition("t_consume"), None)
}