1use crate::machine::{Machine, RegistryEntry, SrcLoc};
9use crate::{query, render, solve};
10use plg_shared::StringInterner;
11use std::ffi::CStr;
12use std::os::raw::c_char;
13
14#[unsafe(no_mangle)]
22pub unsafe extern "C" fn plg_rt_init(
23 atom_strs: *const *const c_char,
24 atom_count: u32,
25 registry: *const RegistryEntry,
26 registry_len: u32,
27 srcmap: *const SrcLoc,
28 srcmap_len: u32,
29 files: *const *const c_char,
30 files_len: u32,
31) -> *mut Machine {
32 let mut atoms = StringInterner::new();
33 for i in 0..atom_count as usize {
34 let s = unsafe { CStr::from_ptr(*atom_strs.add(i)) };
35 let expected = i as u32;
36 let id = atoms.intern(&s.to_string_lossy());
37 debug_assert_eq!(id, expected, "atom table out of sync with interner");
38 }
39 let registry: Vec<RegistryEntry> =
40 unsafe { std::slice::from_raw_parts(registry, registry_len as usize) }.to_vec();
41 debug_assert!(
42 registry.is_sorted_by_key(|e| (e.functor, e.arity)),
43 "registry must be sorted for binary search"
44 );
45 let srcmap: Vec<SrcLoc> = if srcmap_len == 0 {
48 Vec::new()
49 } else {
50 unsafe { std::slice::from_raw_parts(srcmap, srcmap_len as usize) }.to_vec()
51 };
52 let files: Vec<String> = (0..files_len as usize)
53 .map(|i| {
54 unsafe { CStr::from_ptr(*files.add(i)) }
55 .to_string_lossy()
56 .into_owned()
57 })
58 .collect();
59 let mut m = Machine::new(atoms, registry);
60 m.set_provenance(srcmap, files);
61 Box::into_raw(m)
62}
63
64struct Args {
65 query: String,
66 limit: Option<usize>,
67 format: String,
68}
69
70fn parse_args(argv: Vec<String>) -> Result<Args, String> {
71 let mut query = None;
72 let mut limit = None;
73 let mut format = "json".to_string(); let mut it = argv.into_iter().peekable();
75 while let Some(arg) = it.next() {
76 let (flag, inline_value) = match arg.split_once('=') {
77 Some((f, v)) => (f.to_string(), Some(v.to_string())),
78 None => (arg, None),
79 };
80 let value = |it: &mut std::iter::Peekable<std::vec::IntoIter<String>>| {
81 inline_value
82 .clone()
83 .or_else(|| it.next())
84 .ok_or(format!("missing value for {flag}"))
85 };
86 match flag.as_str() {
87 "-q" | "--query" => query = Some(value(&mut it)?),
88 "-l" | "--limit" => {
89 limit = Some(
90 value(&mut it)?
91 .parse::<usize>()
92 .map_err(|_| "invalid --limit value".to_string())?,
93 )
94 }
95 "-f" | "--format" => format = value(&mut it)?,
96 "-h" | "--help" => {
97 return Err("usage: --query <goal> [--limit N] [--format json|text]".to_string());
98 }
99 other => return Err(format!("unexpected argument: {other}")),
100 }
101 }
102 let query = query.ok_or("missing required argument: --query <goal>".to_string())?;
103 Ok(Args {
104 query,
105 limit,
106 format,
107 })
108}
109
110fn output_error(format: &str, message: &str) {
112 if format == "json" {
113 println!("{{\"error\":\"{}\"}}", render::json_escape(message));
114 } else {
115 eprintln!("Error: {message}");
116 }
117}
118
119fn output_json(m: &Machine, exhausted: bool) {
120 let solutions: Vec<String> = m
121 .solutions
122 .iter()
123 .map(|sol| {
124 let fields: Vec<String> = sol
125 .bindings
126 .iter()
127 .map(|(name, json, _)| format!("\"{}\":{}", render::json_escape(name), json))
128 .collect();
129 format!("{{{}}}", fields.join(","))
130 })
131 .collect();
132 println!(
134 "{{\"count\":{},\"exhausted\":{},\"solutions\":[{}]}}",
135 m.solutions.len(),
136 exhausted,
137 solutions.join(",")
138 );
139}
140
141fn output_text(m: &Machine) {
142 if m.solutions.is_empty() {
143 println!("false.");
144 return;
145 }
146 for sol in &m.solutions {
147 if sol.bindings.is_empty() {
148 println!("true.");
149 } else {
150 for (name, _, text) in &sol.bindings {
151 println!("{name} = {text}");
152 }
153 }
154 }
155}
156
157#[unsafe(no_mangle)]
163pub unsafe extern "C" fn plg_rt_main(
164 m: *mut Machine,
165 argc: i32,
166 argv: *const *const c_char,
167) -> i32 {
168 let m = unsafe { &mut *m };
169 let raw_args: Vec<String> = (1..argc as usize)
170 .map(|i| {
171 unsafe { CStr::from_ptr(*argv.add(i)) }
172 .to_string_lossy()
173 .into_owned()
174 })
175 .collect();
176
177 let args = match parse_args(raw_args) {
178 Ok(a) => a,
179 Err(e) => {
180 eprintln!("{e}");
181 return 2;
182 }
183 };
184 if args.format != "json" && args.format != "text" {
185 output_error("text", &format!("Unknown format: {}", args.format));
186 return 2;
187 }
188 m.solution_limit = args.limit;
189 if let Ok(s) = std::env::var("PLG_MAX_STEPS")
193 && let Ok(n) = s.parse::<u64>()
194 {
195 m.step_limit = n;
196 }
197
198 let goal = match query::parse_query(m, &args.query) {
199 Ok(g) => g,
200 Err(e) => {
201 output_error(&args.format, &format!("Parse error: {e}"));
202 return 2;
203 }
204 };
205
206 match solve::solve(m, goal) {
207 solve::Outcome::Error => {
208 let msg = m.error.take().map(|e| e.message).unwrap_or_default();
209 output_error(&args.format, &format!("Runtime error: {msg}"));
210 3
211 }
212 solve::Outcome::Done => {
213 let count = m.solutions.len();
214 let exhausted = args.limit.is_none_or(|l| count < l);
215 match args.format.as_str() {
216 "json" => output_json(m, exhausted),
217 _ => output_text(m),
218 }
219 if count > 0 { 1 } else { 0 }
220 }
221 }
222}
223
224#[cfg(test)]
225mod tests {
226 use super::*;
227
228 fn args(v: &[&str]) -> Result<Args, String> {
229 parse_args(v.iter().map(|s| s.to_string()).collect())
230 }
231
232 #[test]
233 fn parses_flags_with_space_and_equals() {
234 let a = args(&["--query", "p(X)", "--limit", "3", "--format", "text"]).unwrap();
235 assert_eq!(a.query, "p(X)");
236 assert_eq!(a.limit, Some(3));
237 assert_eq!(a.format, "text");
238
239 let a = args(&["--query=p(X)", "-l", "1"]).unwrap();
240 assert_eq!(a.query, "p(X)");
241 assert_eq!(a.limit, Some(1));
242 assert_eq!(a.format, "json", "default format is json (v1)");
243 }
244
245 #[test]
246 fn missing_query_is_an_error() {
247 assert!(args(&["--format", "json"]).is_err());
248 assert!(args(&["--query"]).is_err());
249 assert!(args(&["--bogus", "x"]).is_err());
250 }
251}