Skip to main content

plg_runtime/
entry.rs

1//! Process entry: `plg_rt_init` + `plg_rt_main`, called from the thin
2//! generated `main`. Owns argv parsing (hand-rolled — no clap inside
3//! compiled binaries), output, and the v1 exit-code contract:
4//!
5//!   0 = no solutions, 1 = solutions found,
6//!   2 = query parse error, 3 = runtime error
7
8use crate::core::{self, QueryResult};
9use crate::machine::{Machine, RegistryEntry, SrcLoc};
10use plg_shared::StringInterner;
11use std::ffi::CStr;
12use std::io::{self, Write};
13use std::os::raw::c_char;
14
15/// Build the Machine from the tables codegen baked into the binary.
16/// Re-interning the emitted atom names in id order reproduces the
17/// compiler's exact id space (the interner pre-seeds the same
18/// well-known atoms the compiler's did).
19///
20/// # Safety
21/// Called once from generated `main` with codegen-emitted tables.
22#[unsafe(no_mangle)]
23pub unsafe extern "C" fn plg_rt_init(
24    atom_strs: *const *const c_char,
25    atom_count: u32,
26    registry: *const RegistryEntry,
27    registry_len: u32,
28    srcmap: *const SrcLoc,
29    srcmap_len: u32,
30    files: *const *const c_char,
31    files_len: u32,
32) -> *mut Machine {
33    let mut atoms = StringInterner::new();
34    for i in 0..atom_count as usize {
35        let s = unsafe { CStr::from_ptr(*atom_strs.add(i)) };
36        let expected = i as u32;
37        let id = atoms.intern(&s.to_string_lossy());
38        debug_assert_eq!(id, expected, "atom table out of sync with interner");
39    }
40    let registry: Vec<RegistryEntry> =
41        unsafe { std::slice::from_raw_parts(registry, registry_len as usize) }.to_vec();
42    debug_assert!(
43        registry.is_sorted_by_key(|e| (e.functor, e.arity)),
44        "registry must be sorted for binary search"
45    );
46    // Source-location side-table (SPANS.md Layer 3). Both tables are empty
47    // (`len == 0`) for binaries built without provenance.
48    let srcmap: Vec<SrcLoc> = if srcmap_len == 0 {
49        Vec::new()
50    } else {
51        unsafe { std::slice::from_raw_parts(srcmap, srcmap_len as usize) }.to_vec()
52    };
53    let files: Vec<String> = (0..files_len as usize)
54        .map(|i| {
55            unsafe { CStr::from_ptr(*files.add(i)) }
56                .to_string_lossy()
57                .into_owned()
58        })
59        .collect();
60    let mut m = Machine::new(atoms, registry);
61    m.set_provenance(srcmap, files);
62    Box::into_raw(m)
63}
64
65struct Args {
66    query: String,
67    limit: Option<usize>,
68    format: String,
69}
70
71fn parse_args(argv: Vec<String>) -> Result<Args, String> {
72    let mut query = None;
73    let mut limit = None;
74    let mut format = "json".to_string(); // v1 default
75    let mut it = argv.into_iter().peekable();
76    while let Some(arg) = it.next() {
77        let (flag, inline_value) = match arg.split_once('=') {
78            Some((f, v)) => (f.to_string(), Some(v.to_string())),
79            None => (arg, None),
80        };
81        let value = |it: &mut std::iter::Peekable<std::vec::IntoIter<String>>| {
82            inline_value
83                .clone()
84                .or_else(|| it.next())
85                .ok_or(format!("missing value for {flag}"))
86        };
87        match flag.as_str() {
88            "-q" | "--query" => query = Some(value(&mut it)?),
89            "-l" | "--limit" => {
90                limit = Some(
91                    value(&mut it)?
92                        .parse::<usize>()
93                        .map_err(|_| "invalid --limit value".to_string())?,
94                )
95            }
96            "-f" | "--format" => format = value(&mut it)?,
97            "-h" | "--help" => {
98                return Err("usage: --query <goal> [--limit N] [--format json|text]".to_string());
99            }
100            other => return Err(format!("unexpected argument: {other}")),
101        }
102    }
103    let query = query.ok_or("missing required argument: --query <goal>".to_string())?;
104    Ok(Args {
105        query,
106        limit,
107        format,
108    })
109}
110
111/// v1's output_error: JSON errors go to stdout, text errors to stderr.
112/// The JSON shape is the shared core's; only the routing is CLI-specific.
113fn output_error(format: &str, message: &str) {
114    if format == "json" {
115        let mut out = io::stdout().lock();
116        let _ = core::write_error_json(&mut out, message);
117        let _ = out.write_all(b"\n");
118    } else {
119        eprintln!("Error: {message}");
120    }
121}
122
123fn output_json(m: &Machine, exhausted: bool) {
124    let mut out = io::stdout().lock();
125    // `None` output: the CLI streamed any `write/1` bytes to stdout already, so
126    // its JSON stays byte-identical to v1 (no `output` field).
127    let _ = core::write_solutions_json(&mut out, m, exhausted, None);
128    let _ = out.write_all(b"\n");
129}
130
131fn output_text(m: &Machine) {
132    if m.solutions.is_empty() {
133        println!("false.");
134        return;
135    }
136    for sol in &m.solutions {
137        if sol.bindings.is_empty() {
138            println!("true.");
139        } else {
140            for (name, _, text) in &sol.bindings {
141                println!("{name} = {text}");
142            }
143        }
144    }
145}
146
147/// Run the query named in argv against the compiled program. Returns
148/// the process exit code.
149///
150/// # Safety
151/// Called once from generated `main` with the process argc/argv.
152#[unsafe(no_mangle)]
153pub unsafe extern "C" fn plg_rt_main(
154    m: *mut Machine,
155    argc: i32,
156    argv: *const *const c_char,
157) -> i32 {
158    let m = unsafe { &mut *m };
159    let raw_args: Vec<String> = (1..argc as usize)
160        .map(|i| {
161            unsafe { CStr::from_ptr(*argv.add(i)) }
162                .to_string_lossy()
163                .into_owned()
164        })
165        .collect();
166
167    let args = match parse_args(raw_args) {
168        Ok(a) => a,
169        Err(e) => {
170            eprintln!("{e}");
171            return 2;
172        }
173    };
174    if args.format != "json" && args.format != "text" {
175        output_error("text", &format!("Unknown format: {}", args.format));
176        return 2;
177    }
178    m.solution_limit = args.limit;
179    // Documented extension over v1 (which hardcoded 10_000): the step
180    // ceiling is tunable via environment so big-but-legitimate queries
181    // can raise it without changing the CLI contract.
182    if let Ok(s) = std::env::var("PLG_MAX_STEPS")
183        && let Ok(n) = s.parse::<u64>()
184    {
185        m.step_limit = n;
186    }
187    // Same knob for the metacall depth bound (#23): tune it to the stack the
188    // binary will run under — lower it for a small `ulimit -s`, raise it on a
189    // generous stack. Mirrors `PLG_MAX_STEPS`; the default (1000) is set in
190    // `Machine::new`.
191    if let Ok(s) = std::env::var("PLG_METACALL_DEPTH")
192        && let Ok(n) = s.parse::<usize>()
193    {
194        m.metacall_depth_limit = n;
195    }
196
197    match core::run_query(m, &args.query) {
198        QueryResult::ParseError(msg) => {
199            output_error(&args.format, &msg);
200            2
201        }
202        QueryResult::RuntimeError(msg) => {
203            output_error(&args.format, &msg);
204            3
205        }
206        QueryResult::Solutions => {
207            let count = m.solutions.len();
208            let exhausted = core::exhausted(m);
209            match args.format.as_str() {
210                "json" => output_json(m, exhausted),
211                _ => output_text(m),
212            }
213            if count > 0 { 1 } else { 0 }
214        }
215    }
216}
217
218#[cfg(test)]
219mod tests {
220    use super::*;
221
222    fn args(v: &[&str]) -> Result<Args, String> {
223        parse_args(v.iter().map(|s| s.to_string()).collect())
224    }
225
226    #[test]
227    fn parses_flags_with_space_and_equals() {
228        let a = args(&["--query", "p(X)", "--limit", "3", "--format", "text"]).unwrap();
229        assert_eq!(a.query, "p(X)");
230        assert_eq!(a.limit, Some(3));
231        assert_eq!(a.format, "text");
232
233        let a = args(&["--query=p(X)", "-l", "1"]).unwrap();
234        assert_eq!(a.query, "p(X)");
235        assert_eq!(a.limit, Some(1));
236        assert_eq!(a.format, "json", "default format is json (v1)");
237    }
238
239    #[test]
240    fn missing_query_is_an_error() {
241        assert!(args(&["--format", "json"]).is_err());
242        assert!(args(&["--query"]).is_err());
243        assert!(args(&["--bogus", "x"]).is_err());
244    }
245}