1mod ops;
14
15use crate::cell::{self, Word};
16use crate::machine::Machine;
17use std::collections::HashMap;
18
19pub fn parse_query(m: &mut Machine, src: &str) -> Result<Word, String> {
20 let mut p = QueryParser {
21 chars: src.chars().collect(),
22 pos: 0,
23 vars: HashMap::new(),
24 };
25 let goal = p.parse_level(m, 1200)?;
26 p.skip_ws();
27 if p.peek() == Some('.') {
29 p.pos += 1;
30 p.skip_ws();
31 }
32 if p.pos < p.chars.len() {
33 return Err(format!("unexpected input at column {}", p.pos + 1));
34 }
35 Ok(goal)
36}
37
38pub(crate) struct QueryParser {
39 pub(crate) chars: Vec<char>,
40 pub(crate) pos: usize,
41 vars: HashMap<String, Word>,
42}
43
44impl QueryParser {
45 pub(crate) fn peek(&self) -> Option<char> {
46 self.chars.get(self.pos).copied()
47 }
48
49 pub(crate) fn skip_ws(&mut self) {
50 while let Some(c) = self.peek() {
51 if c.is_whitespace() {
52 self.pos += 1;
53 } else if c == '%' {
54 while self.peek().is_some_and(|c| c != '\n') {
55 self.pos += 1;
56 }
57 } else {
58 break;
59 }
60 }
61 }
62
63 fn expect(&mut self, c: char) -> Result<(), String> {
64 self.skip_ws();
65 if self.peek() == Some(c) {
66 self.pos += 1;
67 Ok(())
68 } else {
69 Err(format!("expected `{c}` at column {}", self.pos + 1))
70 }
71 }
72
73 pub(crate) fn make_binop(&self, m: &mut Machine, name: &str, a: Word, b: Word) -> Word {
74 let id = m.atoms.intern(name);
75 let idx = m.heap.len();
76 m.heap.push(cell::pack_functor(id, 2));
77 m.heap.push(a);
78 m.heap.push(b);
79 cell::make(cell::TAG_STR, idx as u64)
80 }
81
82 pub(crate) fn parse_primary(&mut self, m: &mut Machine) -> Result<Word, String> {
84 self.skip_ws();
85 match self.peek() {
86 None => Err("unexpected end of query".to_string()),
87 Some('(') => {
88 self.pos += 1;
89 let t = self.parse_level(m, 1200)?;
90 self.expect(')')?;
91 Ok(t)
92 }
93 Some('[') => self.parse_list(m),
94 Some('\'') => {
95 let name = self.read_quoted()?;
96 self.parse_atom_or_compound(m, name)
97 }
98 Some(c) if c.is_ascii_digit() => self.parse_number(m, false),
99 Some(c) if is_symbol_atom_char(c) && self.symbol_run_is_standalone() => {
103 let name = self.read_symbol_run();
104 self.parse_atom_or_compound(m, name)
105 }
106 Some('-')
107 if self
108 .chars
109 .get(self.pos + 1)
110 .is_some_and(|c| c.is_ascii_digit()) =>
111 {
112 self.pos += 1;
113 self.parse_number(m, true)
114 }
115 Some('-') => {
116 self.pos += 1;
118 let t = self.parse_primary(m)?;
119 self.make_prefix(m, "-", t)
120 }
121 Some('+') => {
122 self.pos += 1;
125 self.skip_ws();
126 if self.peek().is_some_and(|c| c.is_ascii_digit()) {
127 self.parse_number(m, false)
128 } else {
129 let t = self.parse_primary(m)?;
130 self.make_prefix(m, "+", t)
131 }
132 }
133 Some('\\') => {
134 let name = self.read_symbol_run();
137 if self.peek() == Some('(') {
138 self.parse_atom_or_compound(m, name)
139 } else {
140 let t = self.parse_primary(m)?;
141 self.make_prefix(m, &name, t)
142 }
143 }
144 Some('!') => {
145 self.pos += 1;
146 Ok(cell::make_atom(m.atoms.intern("!")))
147 }
148 Some(c) if c.is_uppercase() || c == '_' => {
149 let name = self.read_ident();
150 Ok(self.var_word(m, &name))
151 }
152 Some(c) if c.is_lowercase() => {
153 let name = self.read_ident();
154 self.parse_atom_or_compound(m, name)
155 }
156 Some(c) => Err(format!("unexpected `{c}` at column {}", self.pos + 1)),
157 }
158 }
159
160 fn parse_atom_or_compound(&mut self, m: &mut Machine, name: String) -> Result<Word, String> {
161 let id = m.atoms.intern(&name);
162 if self.peek() == Some('(') {
164 self.pos += 1;
165 let mut args = vec![self.parse_level(m, 999)?];
167 loop {
168 self.skip_ws();
169 match self.peek() {
170 Some(',') => {
171 self.pos += 1;
172 args.push(self.parse_level(m, 999)?);
173 }
174 Some(')') => {
175 self.pos += 1;
176 break;
177 }
178 _ => return Err(format!("expected `,` or `)` at column {}", self.pos + 1)),
179 }
180 }
181 let idx = m.heap.len();
182 m.heap.push(cell::pack_functor(id, args.len() as u32));
183 m.heap.extend_from_slice(&args);
184 Ok(cell::make(cell::TAG_STR, idx as u64))
185 } else {
186 Ok(cell::make_atom(id))
187 }
188 }
189
190 fn parse_list(&mut self, m: &mut Machine) -> Result<Word, String> {
191 self.expect('[')?;
192 self.skip_ws();
193 if self.peek() == Some(']') {
194 self.pos += 1;
195 return Ok(cell::make_atom(plg_shared::atom::ATOM_NIL));
196 }
197 let mut elements = vec![self.parse_level(m, 999)?];
198 let mut tail = None;
199 loop {
200 self.skip_ws();
201 match self.peek() {
202 Some(',') => {
203 self.pos += 1;
204 elements.push(self.parse_level(m, 999)?);
205 }
206 Some('|') => {
207 self.pos += 1;
208 tail = Some(self.parse_level(m, 999)?);
209 self.expect(']')?;
210 break;
211 }
212 Some(']') => {
213 self.pos += 1;
214 break;
215 }
216 _ => {
217 return Err(format!(
218 "expected `,`, `|` or `]` at column {}",
219 self.pos + 1
220 ));
221 }
222 }
223 }
224 let mut w = tail.unwrap_or(cell::make_atom(plg_shared::atom::ATOM_NIL));
225 for e in elements.into_iter().rev() {
226 let idx = m.heap.len();
227 m.heap.push(e);
228 m.heap.push(w);
229 w = cell::make(cell::TAG_LST, idx as u64);
230 }
231 Ok(w)
232 }
233
234 fn parse_number(&mut self, m: &mut Machine, neg: bool) -> Result<Word, String> {
238 let start = self.pos;
239 while self.peek().is_some_and(|c| c.is_ascii_digit()) {
240 self.pos += 1;
241 }
242 let is_float = self.peek() == Some('.')
243 && self
244 .chars
245 .get(self.pos + 1)
246 .is_some_and(|c| c.is_ascii_digit());
247 if is_float {
248 self.pos += 1; while self.peek().is_some_and(|c| c.is_ascii_digit()) {
250 self.pos += 1;
251 }
252 let text: String = self.chars[start..self.pos].iter().collect();
253 let f: f64 = text
254 .parse()
255 .map_err(|_| format!("invalid float `{text}`"))?;
256 let f = if neg { -f } else { f };
257 let idx = m.heap.len();
258 m.heap.push(f.to_bits());
259 return Ok(cell::make(cell::TAG_FLT, idx as u64));
260 }
261 let digits: String = self.chars[start..self.pos].iter().collect();
262 let n: i64 = digits
263 .parse()
264 .map_err(|_| format!("invalid integer `{digits}`"))?;
265 let n = if neg { -n } else { n };
266 if !(cell::INT_MIN..=cell::INT_MAX).contains(&n) {
267 let idx = m.heap.len();
269 m.heap.push(n as u64);
270 return Ok(cell::make(cell::TAG_BIG, idx as u64));
271 }
272 Ok(cell::make_int(n))
273 }
274
275 pub(crate) fn read_ident(&mut self) -> String {
276 let start = self.pos;
277 while self.peek().is_some_and(|c| c.is_alphanumeric() || c == '_') {
278 self.pos += 1;
279 }
280 self.chars[start..self.pos].iter().collect()
281 }
282
283 fn read_quoted(&mut self) -> Result<String, String> {
284 self.pos += 1; let mut out = String::new();
286 loop {
287 match self.peek() {
288 None => return Err("unterminated quoted atom".to_string()),
289 Some('\'') => {
290 self.pos += 1;
291 if self.peek() == Some('\'') {
292 out.push('\''); self.pos += 1;
294 } else {
295 return Ok(out);
296 }
297 }
298 Some(c) => {
299 out.push(c);
300 self.pos += 1;
301 }
302 }
303 }
304 }
305
306 fn read_symbol_run(&mut self) -> String {
307 let start = self.pos;
308 while self.peek().is_some_and(is_symbol_atom_char) {
309 self.pos += 1;
310 }
311 self.chars[start..self.pos].iter().collect()
312 }
313
314 fn symbol_run_is_standalone(&self) -> bool {
318 let mut p = self.pos;
319 while self.chars.get(p).copied().is_some_and(is_symbol_atom_char) {
320 p += 1;
321 }
322 if self.chars.get(p) == Some(&'(') {
323 return true; }
325 let mut q = p;
326 while self.chars.get(q).is_some_and(|c| c.is_whitespace()) {
327 q += 1;
328 }
329 match self.chars.get(q) {
330 None => {
331 true
335 }
336 Some(')' | ',' | ']' | '|') => true,
337 _ => false,
338 }
339 }
340
341 fn make_prefix(&self, m: &mut Machine, name: &str, t: Word) -> Result<Word, String> {
342 let id = m.atoms.intern(name);
343 let idx = m.heap.len();
344 m.heap.push(cell::pack_functor(id, 1));
345 m.heap.push(t);
346 Ok(cell::make(cell::TAG_STR, idx as u64))
347 }
348
349 fn var_word(&mut self, m: &mut Machine, name: &str) -> Word {
352 if name == "_" {
353 return m.new_var();
354 }
355 if let Some(&w) = self.vars.get(name) {
356 return w;
357 }
358 let w = m.new_var();
359 self.vars.insert(name.to_string(), w);
360 m.query_vars
361 .push((name.to_string(), cell::payload(w) as usize));
362 w
363 }
364}
365
366fn is_symbol_atom_char(c: char) -> bool {
368 matches!(
369 c,
370 '+' | '-' | '*' | '/' | '\\' | '<' | '>' | '=' | ':' | '@' | '^' | '.' | '?' | '&' | '~'
371 )
372}
373
374#[cfg(test)]
375mod tests {
376 use super::*;
377 use crate::cell::*;
378 use plg_shared::StringInterner;
379
380 fn machine() -> Box<Machine> {
381 Machine::new(StringInterner::new(), Vec::new())
382 }
383
384 fn functor_name(m: &Machine, w: Word) -> String {
385 let idx = payload(w) as usize;
386 let (f, _) = unpack_functor(m.heap[idx]);
387 m.atoms.resolve(f).to_string()
388 }
389
390 #[test]
391 fn parses_compound_with_vars() {
392 let mut m = machine();
393 let w = parse_query(&mut m, "parent(tom, X)").unwrap();
394 assert_eq!(tag_of(w), TAG_STR);
395 assert_eq!(functor_name(&m, w), "parent");
396 assert_eq!(m.query_vars.len(), 1);
397 assert_eq!(m.query_vars[0].0, "X");
398 }
399
400 #[test]
401 fn conjunction_shares_variables() {
402 let mut m = machine();
403 let w = parse_query(&mut m, "p(X), q(X, Y)").unwrap();
404 assert_eq!(functor_name(&m, w), ",");
405 assert_eq!(m.query_vars.len(), 2, "X shared, Y new");
406 }
407
408 #[test]
409 fn operator_goals_parse() {
410 let mut m = machine();
411 let w = parse_query(&mut m, "X is 2 + 3 * 4").unwrap();
412 assert_eq!(functor_name(&m, w), "is");
413 let w = parse_query(&mut m, "1 < 2").unwrap();
414 assert_eq!(functor_name(&m, w), "<");
415 let w = parse_query(&mut m, "(a ; b)").unwrap();
416 assert_eq!(functor_name(&m, w), ";");
417 let w = parse_query(&mut m, "(a -> b ; c)").unwrap();
418 assert_eq!(functor_name(&m, w), ";");
419 let w = parse_query(&mut m, "\\+ p(X)").unwrap();
420 assert_eq!(functor_name(&m, w), "\\+");
421 }
422
423 #[test]
424 fn precedence_multiplication_binds_tighter() {
425 let mut m = machine();
426 let w = parse_query(&mut m, "X is 2 + 3 * 4").unwrap();
428 let idx = payload(w) as usize;
429 let rhs = m.deref(m.heap[idx + 2]);
430 assert_eq!(functor_name(&m, rhs), "+");
431 let plus_idx = payload(rhs) as usize;
432 let right = m.deref(m.heap[plus_idx + 2]);
433 assert_eq!(functor_name(&m, right), "*");
434 }
435
436 #[test]
437 fn args_parse_operators_but_not_bare_comma() {
438 let mut m = machine();
439 let w = parse_query(&mut m, "p(1 + 2, X)").unwrap();
440 let idx = payload(w) as usize;
441 let (f, n) = unpack_functor(m.heap[idx]);
442 assert_eq!(m.atoms.resolve(f), "p");
443 assert_eq!(n, 2, "1 + 2 is one arg, X the other");
444 }
445
446 #[test]
447 fn lists_and_quoted_atoms() {
448 let mut m = machine();
449 let w = parse_query(&mut m, "p([1, 2 | T], 'hello world')").unwrap();
450 assert_eq!(tag_of(w), TAG_STR);
451 assert!(m.atoms.lookup("hello world").is_some());
452 assert_eq!(m.query_vars[0].0, "T");
453 }
454
455 #[test]
456 fn underscore_never_recorded() {
457 let mut m = machine();
458 parse_query(&mut m, "p(_, _)").unwrap();
459 assert!(m.query_vars.is_empty());
460 }
461
462 #[test]
463 fn negative_integers_and_trailing_dot() {
464 let mut m = machine();
465 let w = parse_query(&mut m, "p(-42).").unwrap();
466 let idx = payload(w) as usize;
467 assert_eq!(int_value(m.deref(m.heap[idx + 1])), -42);
468 }
469
470 #[test]
471 fn rejects_trailing_garbage() {
472 let mut m = machine();
473 assert!(parse_query(&mut m, "p(a) q").is_err());
474 assert!(parse_query(&mut m, "p(").is_err());
475 }
476}