1use 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#[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 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(); 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
111fn 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 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#[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 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 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}