Skip to main content

plg_runtime/
core.rs

1//! I/O-free query core: parse + solve, producing the message bytes for the
2//! two failure classes. The envelope *shape* and its plural *encodings* live
3//! in [`crate::wire`] — this module is the solve side plus the shared
4//! `exhausted` rule, with no commitment to *where* output bytes go or how
5//! they're encoded. Both the CLI shell (`entry.rs`) and the Tier-2 reactor
6//! (`reactor.rs`) call `run_query` here, then build a [`crate::wire::Envelope`]
7//! and hand it to a chosen [`crate::wire::EncoderDesc`] — so the shape has one
8//! source and can't drift between transports.
9
10use crate::machine::Machine;
11use crate::{query, solve};
12
13/// Outcome of running one query, with the prefixed message the wire contract
14/// puts on the wire for the two failure classes. The caller maps these to its
15/// own surface — exit codes 2/3 for the CLI, an error document for the reactor
16/// — but the message bytes are produced once, here.
17pub enum QueryResult {
18    /// Solved; solutions live in `m.solutions`.
19    Solutions,
20    /// Query failed to parse — `"Parse error: …"` (CLI exit 2).
21    ParseError(String),
22    /// A runtime error was raised — `"Runtime error: …"` (CLI exit 3).
23    RuntimeError(String),
24}
25
26/// Parse `q` against the program in `m`, then solve it. The caller must have
27/// already reset per-query state and set the per-query limits; this consumes
28/// `m.error` on the error path so the message can be returned.
29pub fn run_query(m: &mut Machine, q: &str) -> QueryResult {
30    let goal = match query::parse_query(m, q) {
31        Ok(g) => g,
32        Err(e) => return QueryResult::ParseError(format!("Parse error: {e}")),
33    };
34    match solve::solve(m, goal) {
35        solve::Outcome::Error => {
36            let msg = m.error.take().map(|e| e.message).unwrap_or_default();
37            QueryResult::RuntimeError(format!("Runtime error: {msg}"))
38        }
39        solve::Outcome::Done => QueryResult::Solutions,
40    }
41}
42
43/// The `exhausted` flag: the search ran to completion unless a `--limit`
44/// stopped it exactly at the cap. Single-sourced so the CLI and the reactor
45/// compute it identically.
46pub fn exhausted(m: &Machine) -> bool {
47    m.solution_limit.is_none_or(|l| m.solutions.len() < l)
48}
49
50#[cfg(test)]
51mod tests {
52    use super::*;
53    use crate::render::RenderedSolution;
54    use plg_shared::StringInterner;
55
56    fn machine() -> Box<Machine> {
57        Machine::new(StringInterner::new(), Vec::new())
58    }
59
60    #[test]
61    fn exhausted_follows_the_limit() {
62        let mut m = machine();
63        assert!(exhausted(&m), "no limit => exhausted");
64        m.solution_limit = Some(2);
65        assert!(exhausted(&m), "under the limit => exhausted");
66        m.solutions.push(RenderedSolution { bindings: vec![] });
67        m.solutions.push(RenderedSolution { bindings: vec![] });
68        assert!(!exhausted(&m), "limit hit exactly => not exhausted");
69    }
70}