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