1use crate::cell::*;
7use crate::copyterm::{TermBuf, copy_to_buf};
8use crate::machine::Machine;
9use plg_shared::atom::ATOM_NIL;
10
11pub struct RenderedSolution {
16 pub bindings: Vec<Binding>,
19}
20
21pub struct Binding {
26 pub name: String,
27 pub text: String,
28 pub buf: TermBuf,
29}
30
31pub fn capture_solution(m: &Machine) -> RenderedSolution {
33 let mut vars: Vec<_> = m.query_vars.iter().collect();
34 vars.sort_by(|a, b| a.0.cmp(&b.0));
35 let bindings = vars
36 .into_iter()
37 .filter(|(name, _)| name != "_")
38 .map(|(name, idx)| {
39 let w = m.deref(make_ref(*idx));
40 Binding {
41 name: name.clone(),
42 text: term_to_string(m, w),
43 buf: copy_to_buf(m, w),
44 }
45 })
46 .collect();
47 RenderedSolution { bindings }
48}
49
50fn fmt_float(f: f64) -> String {
54 if f.is_finite() && f.fract() == 0.0 && f.abs() < 1e15 {
55 format!("{f:.1}")
56 } else {
57 format!("{f}")
58 }
59}
60
61const INFIX: &[&str] = &[
63 "+", "-", "*", "/", "mod", "is", "=", "\\=", "<", ">", "=<", ">=", "=:=", "=\\=",
64];
65
66pub fn term_to_string(m: &Machine, w: Word) -> String {
67 term_to_string_v(m, w, false, &mut Vec::new())
68}
69
70pub fn term_to_string_quoted(m: &Machine, w: Word) -> String {
73 term_to_string_v(m, w, true, &mut Vec::new())
74}
75
76fn atom_is_unquoted(s: &str) -> bool {
82 if matches!(s, "[]" | "!" | ";" | "{}") {
83 return true;
84 }
85 let bytes = s.as_bytes();
86 if bytes.is_empty() {
87 return false;
88 }
89 if bytes[0].is_ascii_lowercase()
90 && bytes
91 .iter()
92 .all(|b| b.is_ascii_alphanumeric() || *b == b'_')
93 {
94 return true;
95 }
96 const SYM: &[u8] = b"+-*/\\^<>=~:.?@#&$";
97 bytes.iter().all(|b| SYM.contains(b))
98}
99
100fn quote_atom(s: &str) -> String {
103 if atom_is_unquoted(s) {
104 return s.to_string();
105 }
106 let mut out = String::with_capacity(s.len() + 2);
107 out.push('\'');
108 for c in s.chars() {
109 match c {
110 '\'' => out.push_str("\\'"),
111 '\\' => out.push_str("\\\\"),
112 '\n' => out.push_str("\\n"),
113 '\t' => out.push_str("\\t"),
114 c => out.push(c),
115 }
116 }
117 out.push('\'');
118 out
119}
120
121fn atom_name(name: &str, quoted: bool) -> String {
123 if quoted {
124 quote_atom(name)
125 } else {
126 name.to_string()
127 }
128}
129
130fn term_to_string_v(m: &Machine, w: Word, quoted: bool, visiting: &mut Vec<usize>) -> String {
131 let w = m.deref(w);
132 match tag_of(w) {
133 TAG_ATOM => atom_name(m.atoms.resolve(atom_id(w)), quoted),
134 TAG_INT => int_value(w).to_string(),
135 TAG_BIG => (m.heap[payload(w) as usize] as i64).to_string(),
136 TAG_FLT => fmt_float(f64::from_bits(m.heap[payload(w) as usize])),
140 TAG_REF => format!("_{}", payload(w)),
141 TAG_STR => {
142 let idx = payload(w) as usize;
143 if visiting.contains(&idx) {
144 return format!("_{idx}"); }
146 visiting.push(idx);
147 let (f, n) = unpack_functor(m.heap[idx]);
148 let name = m.atoms.resolve(f).to_string();
149 let out = if n == 2 && INFIX.contains(&name.as_str()) {
152 format!(
153 "{} {} {}",
154 term_to_string_v(m, m.heap[idx + 1], quoted, visiting),
155 name,
156 term_to_string_v(m, m.heap[idx + 2], quoted, visiting)
157 )
158 } else {
159 let args: Vec<String> = (0..n as usize)
160 .map(|i| term_to_string_v(m, m.heap[idx + 1 + i], quoted, visiting))
161 .collect();
162 format!("{}({})", atom_name(&name, quoted), args.join(", "))
163 };
164 visiting.pop();
165 out
166 }
167 TAG_LST => {
168 let idx = payload(w) as usize;
169 if visiting.contains(&idx) {
170 return format!("_{idx}");
171 }
172 visiting.push(idx);
173 let (elements, tail) = collect_list_v(m, w, visiting);
174 let items: Vec<String> = elements
175 .iter()
176 .map(|e| term_to_string_v(m, *e, quoted, visiting))
177 .collect();
178 let out = match tail {
179 None => format!("[{}]", items.join(", ")),
180 Some(t) => format!(
181 "[{}|{}]",
182 items.join(", "),
183 term_to_string_v(m, t, quoted, visiting)
184 ),
185 };
186 visiting.pop();
187 out
188 }
189 _ => unreachable!("bad tag"),
190 }
191}
192
193pub fn format_term(m: &Machine, w: Word, out: &mut String) {
197 format_term_v(m, w, out, &mut Vec::new())
198}
199
200fn format_term_v(m: &Machine, w: Word, out: &mut String, visiting: &mut Vec<usize>) {
201 let w = m.deref(w);
202 match tag_of(w) {
203 TAG_ATOM => out.push_str(m.atoms.resolve(atom_id(w))),
204 TAG_INT => out.push_str(&int_value(w).to_string()),
205 TAG_BIG => out.push_str(&(m.heap[payload(w) as usize] as i64).to_string()),
206 TAG_FLT => out.push_str(&fmt_float(f64::from_bits(m.heap[payload(w) as usize]))),
210 TAG_REF => {
211 out.push('_');
212 out.push_str(&payload(w).to_string());
213 }
214 TAG_STR => {
215 let idx = payload(w) as usize;
216 if visiting.contains(&idx) {
217 out.push('_');
218 out.push_str(&idx.to_string());
219 return;
220 }
221 visiting.push(idx);
222 let (f, n) = unpack_functor(m.heap[idx]);
223 out.push_str(m.atoms.resolve(f));
224 out.push('(');
225 for i in 0..n as usize {
226 if i > 0 {
227 out.push_str(", ");
228 }
229 format_term_v(m, m.heap[idx + 1 + i], out, visiting);
230 }
231 out.push(')');
232 visiting.pop();
233 }
234 TAG_LST => {
235 let idx = payload(w) as usize;
236 if visiting.contains(&idx) {
237 out.push('_');
238 out.push_str(&idx.to_string());
239 return;
240 }
241 visiting.push(idx);
242 out.push('[');
243 let (elements, tail) = collect_list_v(m, w, visiting);
244 for (i, e) in elements.iter().enumerate() {
245 if i > 0 {
246 out.push_str(", ");
247 }
248 format_term_v(m, *e, out, visiting);
249 }
250 if let Some(t) = tail {
251 out.push('|');
252 format_term_v(m, t, out, visiting);
253 }
254 out.push(']');
255 visiting.pop();
256 }
257 _ => unreachable!("bad tag"),
258 }
259}
260
261fn collect_list_v(m: &Machine, w: Word, visiting: &[usize]) -> (Vec<Word>, Option<Word>) {
266 let mut elements = Vec::new();
267 let mut cur = m.deref(w);
268 let mut seen: Vec<usize> = Vec::new();
269 loop {
270 match tag_of(cur) {
271 TAG_LST => {
272 let idx = payload(cur) as usize;
273 if seen.contains(&idx) || (visiting.contains(&idx) && !elements.is_empty()) {
274 return (elements, Some(cur));
275 }
276 seen.push(idx);
277 elements.push(m.heap[idx]);
278 cur = m.deref(m.heap[idx + 1]);
279 }
280 TAG_ATOM if atom_id(cur) == ATOM_NIL => return (elements, None),
281 _ => return (elements, Some(cur)),
282 }
283 }
284}
285
286#[cfg(test)]
287mod tests {
288 use super::*;
289 use plg_shared::StringInterner;
290
291 fn machine() -> Box<Machine> {
292 let mut atoms = StringInterner::new();
293 atoms.intern("foo");
294 atoms.intern("bar");
295 Machine::new(atoms, Vec::new())
296 }
297
298 #[test]
299 fn atoms_ints_render() {
300 let m = machine();
301 let foo = m.atoms.lookup("foo").unwrap();
302 assert_eq!(term_to_string(&m, make_atom(foo)), "foo");
303 assert_eq!(term_to_string(&m, make_int(-7)), "-7");
304 }
305
306 #[test]
307 fn compound_renders_readable() {
308 let mut m = machine();
309 let foo = m.atoms.lookup("foo").unwrap();
310 let bar = m.atoms.lookup("bar").unwrap();
311 let idx = m.heap.len();
312 m.heap.push(pack_functor(foo, 2));
313 m.heap.push(make_atom(bar));
314 m.heap.push(make_int(1));
315 let w = make(TAG_STR, idx as u64);
316 assert_eq!(term_to_string(&m, w), "foo(bar, 1)");
317 }
318
319 #[test]
320 fn whole_floats_keep_decimal_point_in_text() {
321 let mut m = machine();
324 let push_flt = |m: &mut Machine, f: f64| {
325 let idx = m.heap.len();
326 m.heap.push(f.to_bits());
327 make(TAG_FLT, idx as u64)
328 };
329 let two = push_flt(&mut m, 2.0);
330 assert_eq!(term_to_string(&m, two), "2.0");
331 let mut em = String::new();
334 format_term(&m, two, &mut em);
335 assert_eq!(em, "2.0");
336 let big = push_flt(&mut m, 1024.0);
337 assert_eq!(term_to_string(&m, big), "1024.0");
338 let half = push_flt(&mut m, 3.5);
340 assert_eq!(term_to_string(&m, half), "3.5");
341 }
342
343 #[test]
344 fn writeq_quotes_only_when_needed() {
345 let mut m = machine();
348 let atom = |m: &mut Machine, s: &str| make_atom(m.atoms.intern(s));
349
350 for s in ["foo", "fooBar", "+", "=..", "[]", "!", ";"] {
352 let w = atom(&mut m, s);
353 assert_eq!(term_to_string_quoted(&m, w), s, "{s} must stay unquoted");
354 }
355 let w = atom(&mut m, "hello world");
357 assert_eq!(term_to_string_quoted(&m, w), "'hello world'");
358 let w = atom(&mut m, "Abc");
359 assert_eq!(term_to_string_quoted(&m, w), "'Abc'");
360 let w = atom(&mut m, "");
361 assert_eq!(term_to_string_quoted(&m, w), "''");
362 let w = atom(&mut m, "it's");
363 assert_eq!(term_to_string_quoted(&m, w), "'it\\'s'");
364
365 let w = atom(&mut m, "hello world");
367 assert_eq!(term_to_string(&m, w), "hello world");
368
369 let inner = atom(&mut m, "a b");
371 let f = m.atoms.intern("my pred");
372 let idx = m.heap.len();
373 m.heap.push(pack_functor(f, 1));
374 m.heap.push(inner);
375 let s = make(TAG_STR, idx as u64);
376 assert_eq!(term_to_string_quoted(&m, s), "'my pred'('a b')");
377 }
378
379 #[test]
380 fn proper_and_partial_lists() {
381 let mut m = machine();
382 let nil = make_atom(ATOM_NIL);
383 let i2 = m.heap.len();
384 m.heap.push(make_int(2));
385 m.heap.push(nil);
386 let l2 = make(TAG_LST, i2 as u64);
387 let i1 = m.heap.len();
388 m.heap.push(make_int(1));
389 m.heap.push(l2);
390 let l1 = make(TAG_LST, i1 as u64);
391 assert_eq!(term_to_string(&m, l1), "[1, 2]");
392
393 let v = m.new_var();
394 let ip = m.heap.len();
395 m.heap.push(make_int(1));
396 m.heap.push(v);
397 let lp = make(TAG_LST, ip as u64);
398 assert!(term_to_string(&m, lp).starts_with("[1|_"));
399 }
400}