1use crate::cell::*;
10use crate::machine::Machine;
11use plg_shared::atom::ATOM_NIL;
12
13pub struct RenderedSolution {
16 pub bindings: Vec<(String, String, String)>,
18}
19
20pub fn capture_solution(m: &Machine) -> RenderedSolution {
22 let mut vars: Vec<_> = m.query_vars.iter().collect();
23 vars.sort_by(|a, b| a.0.cmp(&b.0));
24 let bindings = vars
25 .into_iter()
26 .filter(|(name, _)| name != "_")
27 .map(|(name, idx)| {
28 let w = m.deref(make_ref(*idx));
29 (name.clone(), term_to_json(m, w), term_to_string(m, w))
30 })
31 .collect();
32 RenderedSolution { bindings }
33}
34
35pub fn json_escape(s: &str) -> String {
37 let mut out = String::with_capacity(s.len() + 2);
38 for c in s.chars() {
39 match c {
40 '"' => out.push_str("\\\""),
41 '\\' => out.push_str("\\\\"),
42 '\n' => out.push_str("\\n"),
43 '\r' => out.push_str("\\r"),
44 '\t' => out.push_str("\\t"),
45 '\u{08}' => out.push_str("\\b"),
46 '\u{0c}' => out.push_str("\\f"),
47 c if (c as u32) < 0x20 => out.push_str(&format!("\\u{:04x}", c as u32)),
48 c => out.push(c),
49 }
50 }
51 out
52}
53
54fn fmt_float(f: f64) -> String {
58 if f.is_finite() && f.fract() == 0.0 && f.abs() < 1e15 {
59 format!("{f:.1}")
60 } else {
61 format!("{f}")
62 }
63}
64
65pub fn term_to_json(m: &Machine, w: Word) -> String {
66 term_to_json_v(m, w, &mut Vec::new())
67}
68
69fn term_to_json_v(m: &Machine, w: Word, visiting: &mut Vec<usize>) -> String {
75 let w = m.deref(w);
76 match tag_of(w) {
77 TAG_ATOM => format!("\"{}\"", json_escape(m.atoms.resolve(atom_id(w)))),
78 TAG_INT => int_value(w).to_string(),
79 TAG_BIG => (m.heap[payload(w) as usize] as i64).to_string(),
80 TAG_FLT => fmt_float(f64::from_bits(m.heap[payload(w) as usize])),
81 TAG_REF => format!("\"_{}\"", payload(w)),
82 TAG_STR => {
83 let idx = payload(w) as usize;
84 if visiting.contains(&idx) {
85 return format!("\"_{idx}\""); }
87 visiting.push(idx);
88 let (f, n) = unpack_functor(m.heap[idx]);
89 let args: Vec<String> = (0..n as usize)
90 .map(|i| term_to_json_v(m, m.heap[idx + 1 + i], visiting))
91 .collect();
92 visiting.pop();
93 format!(
95 "{{\"args\":[{}],\"functor\":\"{}\"}}",
96 args.join(","),
97 json_escape(m.atoms.resolve(f))
98 )
99 }
100 TAG_LST => {
101 let idx = payload(w) as usize;
102 if visiting.contains(&idx) {
103 return format!("\"_{idx}\"");
104 }
105 visiting.push(idx);
106 let (elements, tail) = collect_list_v(m, w, visiting);
107 let items: Vec<String> = elements
108 .iter()
109 .map(|e| term_to_json_v(m, *e, visiting))
110 .collect();
111 let out = match tail {
112 None => format!("[{}]", items.join(",")),
113 Some(t) => format!(
115 "{{\"list\":[{}],\"tail\":{}}}",
116 items.join(","),
117 term_to_json_v(m, t, visiting)
118 ),
119 };
120 visiting.pop();
121 out
122 }
123 _ => unreachable!("bad tag"),
124 }
125}
126
127const INFIX: &[&str] = &[
129 "+", "-", "*", "/", "mod", "is", "=", "\\=", "<", ">", "=<", ">=", "=:=", "=\\=",
130];
131
132pub fn term_to_string(m: &Machine, w: Word) -> String {
133 term_to_string_v(m, w, false, &mut Vec::new())
134}
135
136pub fn term_to_string_quoted(m: &Machine, w: Word) -> String {
139 term_to_string_v(m, w, true, &mut Vec::new())
140}
141
142fn atom_is_unquoted(s: &str) -> bool {
148 if matches!(s, "[]" | "!" | ";" | "{}") {
149 return true;
150 }
151 let bytes = s.as_bytes();
152 if bytes.is_empty() {
153 return false;
154 }
155 if bytes[0].is_ascii_lowercase()
156 && bytes
157 .iter()
158 .all(|b| b.is_ascii_alphanumeric() || *b == b'_')
159 {
160 return true;
161 }
162 const SYM: &[u8] = b"+-*/\\^<>=~:.?@#&$";
163 bytes.iter().all(|b| SYM.contains(b))
164}
165
166fn quote_atom(s: &str) -> String {
169 if atom_is_unquoted(s) {
170 return s.to_string();
171 }
172 let mut out = String::with_capacity(s.len() + 2);
173 out.push('\'');
174 for c in s.chars() {
175 match c {
176 '\'' => out.push_str("\\'"),
177 '\\' => out.push_str("\\\\"),
178 '\n' => out.push_str("\\n"),
179 '\t' => out.push_str("\\t"),
180 c => out.push(c),
181 }
182 }
183 out.push('\'');
184 out
185}
186
187fn atom_name(name: &str, quoted: bool) -> String {
189 if quoted {
190 quote_atom(name)
191 } else {
192 name.to_string()
193 }
194}
195
196fn term_to_string_v(m: &Machine, w: Word, quoted: bool, visiting: &mut Vec<usize>) -> String {
197 let w = m.deref(w);
198 match tag_of(w) {
199 TAG_ATOM => atom_name(m.atoms.resolve(atom_id(w)), quoted),
200 TAG_INT => int_value(w).to_string(),
201 TAG_BIG => (m.heap[payload(w) as usize] as i64).to_string(),
202 TAG_FLT => fmt_float(f64::from_bits(m.heap[payload(w) as usize])),
206 TAG_REF => format!("_{}", payload(w)),
207 TAG_STR => {
208 let idx = payload(w) as usize;
209 if visiting.contains(&idx) {
210 return format!("_{idx}"); }
212 visiting.push(idx);
213 let (f, n) = unpack_functor(m.heap[idx]);
214 let name = m.atoms.resolve(f).to_string();
215 let out = if n == 2 && INFIX.contains(&name.as_str()) {
218 format!(
219 "{} {} {}",
220 term_to_string_v(m, m.heap[idx + 1], quoted, visiting),
221 name,
222 term_to_string_v(m, m.heap[idx + 2], quoted, visiting)
223 )
224 } else {
225 let args: Vec<String> = (0..n as usize)
226 .map(|i| term_to_string_v(m, m.heap[idx + 1 + i], quoted, visiting))
227 .collect();
228 format!("{}({})", atom_name(&name, quoted), args.join(", "))
229 };
230 visiting.pop();
231 out
232 }
233 TAG_LST => {
234 let idx = payload(w) as usize;
235 if visiting.contains(&idx) {
236 return format!("_{idx}");
237 }
238 visiting.push(idx);
239 let (elements, tail) = collect_list_v(m, w, visiting);
240 let items: Vec<String> = elements
241 .iter()
242 .map(|e| term_to_string_v(m, *e, quoted, visiting))
243 .collect();
244 let out = match tail {
245 None => format!("[{}]", items.join(", ")),
246 Some(t) => format!(
247 "[{}|{}]",
248 items.join(", "),
249 term_to_string_v(m, t, quoted, visiting)
250 ),
251 };
252 visiting.pop();
253 out
254 }
255 _ => unreachable!("bad tag"),
256 }
257}
258
259pub fn format_term(m: &Machine, w: Word, out: &mut String) {
263 format_term_v(m, w, out, &mut Vec::new())
264}
265
266fn format_term_v(m: &Machine, w: Word, out: &mut String, visiting: &mut Vec<usize>) {
267 let w = m.deref(w);
268 match tag_of(w) {
269 TAG_ATOM => out.push_str(m.atoms.resolve(atom_id(w))),
270 TAG_INT => out.push_str(&int_value(w).to_string()),
271 TAG_BIG => out.push_str(&(m.heap[payload(w) as usize] as i64).to_string()),
272 TAG_FLT => out.push_str(&fmt_float(f64::from_bits(m.heap[payload(w) as usize]))),
276 TAG_REF => {
277 out.push('_');
278 out.push_str(&payload(w).to_string());
279 }
280 TAG_STR => {
281 let idx = payload(w) as usize;
282 if visiting.contains(&idx) {
283 out.push('_');
284 out.push_str(&idx.to_string());
285 return;
286 }
287 visiting.push(idx);
288 let (f, n) = unpack_functor(m.heap[idx]);
289 out.push_str(m.atoms.resolve(f));
290 out.push('(');
291 for i in 0..n as usize {
292 if i > 0 {
293 out.push_str(", ");
294 }
295 format_term_v(m, m.heap[idx + 1 + i], out, visiting);
296 }
297 out.push(')');
298 visiting.pop();
299 }
300 TAG_LST => {
301 let idx = payload(w) as usize;
302 if visiting.contains(&idx) {
303 out.push('_');
304 out.push_str(&idx.to_string());
305 return;
306 }
307 visiting.push(idx);
308 out.push('[');
309 let (elements, tail) = collect_list_v(m, w, visiting);
310 for (i, e) in elements.iter().enumerate() {
311 if i > 0 {
312 out.push_str(", ");
313 }
314 format_term_v(m, *e, out, visiting);
315 }
316 if let Some(t) = tail {
317 out.push('|');
318 format_term_v(m, t, out, visiting);
319 }
320 out.push(']');
321 visiting.pop();
322 }
323 _ => unreachable!("bad tag"),
324 }
325}
326
327fn collect_list_v(m: &Machine, w: Word, visiting: &[usize]) -> (Vec<Word>, Option<Word>) {
332 let mut elements = Vec::new();
333 let mut cur = m.deref(w);
334 let mut seen: Vec<usize> = Vec::new();
335 loop {
336 match tag_of(cur) {
337 TAG_LST => {
338 let idx = payload(cur) as usize;
339 if seen.contains(&idx) || (visiting.contains(&idx) && !elements.is_empty()) {
340 return (elements, Some(cur));
341 }
342 seen.push(idx);
343 elements.push(m.heap[idx]);
344 cur = m.deref(m.heap[idx + 1]);
345 }
346 TAG_ATOM if atom_id(cur) == ATOM_NIL => return (elements, None),
347 _ => return (elements, Some(cur)),
348 }
349 }
350}
351
352#[cfg(test)]
353mod tests {
354 use super::*;
355 use plg_shared::StringInterner;
356
357 fn machine() -> Box<Machine> {
358 let mut atoms = StringInterner::new();
359 atoms.intern("foo");
360 atoms.intern("bar");
361 Machine::new(atoms, Vec::new())
362 }
363
364 #[test]
365 fn json_escape_matches_serde() {
366 assert_eq!(json_escape("a\"b\\c\nd"), "a\\\"b\\\\c\\nd");
367 assert_eq!(json_escape("\u{01}"), "\\u0001");
368 }
369
370 #[test]
371 fn atoms_ints_render() {
372 let m = machine();
373 let foo = m.atoms.lookup("foo").unwrap();
374 assert_eq!(term_to_json(&m, make_atom(foo)), "\"foo\"");
375 assert_eq!(term_to_json(&m, make_int(-7)), "-7");
376 assert_eq!(term_to_string(&m, make_int(-7)), "-7");
377 }
378
379 #[test]
380 fn compound_renders_sorted_keys() {
381 let mut m = machine();
382 let foo = m.atoms.lookup("foo").unwrap();
383 let bar = m.atoms.lookup("bar").unwrap();
384 let idx = m.heap.len();
385 m.heap.push(pack_functor(foo, 2));
386 m.heap.push(make_atom(bar));
387 m.heap.push(make_int(1));
388 let w = make(TAG_STR, idx as u64);
389 assert_eq!(
390 term_to_json(&m, w),
391 "{\"args\":[\"bar\",1],\"functor\":\"foo\"}"
392 );
393 assert_eq!(term_to_string(&m, w), "foo(bar, 1)");
394 }
395
396 #[test]
397 fn whole_floats_keep_decimal_point_in_text() {
398 let mut m = machine();
401 let push_flt = |m: &mut Machine, f: f64| {
402 let idx = m.heap.len();
403 m.heap.push(f.to_bits());
404 make(TAG_FLT, idx as u64)
405 };
406 let two = push_flt(&mut m, 2.0);
407 assert_eq!(term_to_string(&m, two), "2.0");
408 assert_eq!(term_to_json(&m, two), "2.0");
409 let mut em = String::new();
412 format_term(&m, two, &mut em);
413 assert_eq!(em, "2.0");
414 let big = push_flt(&mut m, 1024.0);
415 assert_eq!(term_to_string(&m, big), "1024.0");
416 let half = push_flt(&mut m, 3.5);
418 assert_eq!(term_to_string(&m, half), "3.5");
419 }
420
421 #[test]
422 fn writeq_quotes_only_when_needed() {
423 let mut m = machine();
426 let atom = |m: &mut Machine, s: &str| make_atom(m.atoms.intern(s));
427
428 for s in ["foo", "fooBar", "+", "=..", "[]", "!", ";"] {
430 let w = atom(&mut m, s);
431 assert_eq!(term_to_string_quoted(&m, w), s, "{s} must stay unquoted");
432 }
433 let w = atom(&mut m, "hello world");
435 assert_eq!(term_to_string_quoted(&m, w), "'hello world'");
436 let w = atom(&mut m, "Abc");
437 assert_eq!(term_to_string_quoted(&m, w), "'Abc'");
438 let w = atom(&mut m, "");
439 assert_eq!(term_to_string_quoted(&m, w), "''");
440 let w = atom(&mut m, "it's");
441 assert_eq!(term_to_string_quoted(&m, w), "'it\\'s'");
442
443 let w = atom(&mut m, "hello world");
445 assert_eq!(term_to_string(&m, w), "hello world");
446
447 let inner = atom(&mut m, "a b");
449 let f = m.atoms.intern("my pred");
450 let idx = m.heap.len();
451 m.heap.push(pack_functor(f, 1));
452 m.heap.push(inner);
453 let s = make(TAG_STR, idx as u64);
454 assert_eq!(term_to_string_quoted(&m, s), "'my pred'('a b')");
455 }
456
457 #[test]
458 fn proper_and_partial_lists() {
459 let mut m = machine();
460 let nil = make_atom(ATOM_NIL);
461 let i2 = m.heap.len();
462 m.heap.push(make_int(2));
463 m.heap.push(nil);
464 let l2 = make(TAG_LST, i2 as u64);
465 let i1 = m.heap.len();
466 m.heap.push(make_int(1));
467 m.heap.push(l2);
468 let l1 = make(TAG_LST, i1 as u64);
469 assert_eq!(term_to_json(&m, l1), "[1,2]");
470 assert_eq!(term_to_string(&m, l1), "[1, 2]");
471
472 let v = m.new_var();
473 let ip = m.heap.len();
474 m.heap.push(make_int(1));
475 m.heap.push(v);
476 let lp = make(TAG_LST, ip as u64);
477 let json = term_to_json(&m, lp);
478 assert!(json.starts_with("{\"list\":[1],\"tail\":\"_"), "{json}");
479 assert!(term_to_string(&m, lp).starts_with("[1|_"));
480 }
481}