Skip to main content

patch_prolog_frontend/
error.rs

1//! ISO Prolog error terms.
2//!
3//! Built-ins and the solver construct `PrologError` values when something goes
4//! wrong. Each variant maps 1:1 to an ISO 13211-1 §7.12 formal error term;
5//! `to_term` renders the value as `error(Formal, Context)` so user-level
6//! `catch/3` recovery clauses can pattern-match against the structured term.
7//!
8//! Construction is cheap — no allocation on the success path. The optional
9//! `context` string lives in the second argument of `error/2` and is the
10//! human-readable message format-style; existing tests grep for substrings
11//! in this field, so it's where helpful detail goes.
12//!
13//! Ported from patch-prolog's `error.rs`; the only change is sourcing
14//! `Term` / `StringInterner` from `plg_shared` instead of the old in-crate
15//! `term` module.
16
17use plg_shared::{StringInterner, Term};
18
19/// A thrown error as it flows through the solver. The term is what `catch/3`
20/// pattern-matches against; the `uncatchable` flag is set for safety-ceiling
21/// errors (step limit) so user `catch/3` clauses cannot trap them.
22#[derive(Debug, Clone)]
23pub struct ThrownError {
24    pub term: Term,
25    pub uncatchable: bool,
26}
27
28impl ThrownError {
29    /// Build from a structured `PrologError` — `uncatchable` is derived from
30    /// the variant.
31    pub fn from_prolog(err: PrologError, interner: &mut StringInterner) -> Self {
32        let uncatchable = err.is_uncatchable();
33        let term = err.to_term(interner);
34        ThrownError { term, uncatchable }
35    }
36
37    /// Build from a raw term (used by `throw/1`). Always catchable.
38    pub fn from_term(term: Term) -> Self {
39        ThrownError {
40            term,
41            uncatchable: false,
42        }
43    }
44}
45
46/// ISO formal-error vocabulary.
47#[derive(Debug, Clone)]
48pub enum PrologError {
49    /// An argument that must be bound was unbound.
50    Instantiation { context: String },
51    /// An argument has the wrong type. `expected_type` is an atom name
52    /// (e.g. "integer", "atom", "callable"); `culprit` is the offending term.
53    Type {
54        expected_type: &'static str,
55        culprit: Term,
56        context: String,
57    },
58    /// An object referred to by the goal does not exist.
59    /// `object_type` is e.g. "procedure"; `culprit` is the indicator.
60    Existence {
61        object_type: &'static str,
62        culprit: Term,
63        context: String,
64    },
65    /// An argument is outside the valid domain.
66    Domain {
67        expected_domain: &'static str,
68        culprit: Term,
69        context: String,
70    },
71    /// Arithmetic evaluation failed (zero divisor, overflow, etc.).
72    Evaluation {
73        kind: &'static str, // "zero_divisor", "int_overflow", "float_overflow", ...
74        context: String,
75    },
76    /// Operation not permitted.
77    Permission {
78        operation: &'static str,
79        permission_type: &'static str,
80        culprit: Term,
81        context: String,
82    },
83    /// Implementation-defined representation limit exceeded.
84    Representation {
85        flag: &'static str, // "max_arity", "character_code", ...
86        context: String,
87    },
88    /// Resource limit exhausted. Note: `Resource { kind: "steps", ... }` is
89    /// the step-limit error and is intentionally uncatchable — see
90    /// `is_uncatchable`.
91    Resource {
92        kind: &'static str, // "steps", "memory", ...
93        context: String,
94    },
95    /// Syntax error during runtime term construction (e.g. `number_chars/2`).
96    Syntax { context: String },
97}
98
99impl PrologError {
100    /// Step-limit and other safety-ceiling errors must not be catchable by
101    /// `catch/3` — otherwise a malicious rule could loop indefinitely by
102    /// trapping its own timeout. The solver checks this before consulting
103    /// the catch stack.
104    pub fn is_uncatchable(&self) -> bool {
105        matches!(self, PrologError::Resource { kind: "steps", .. })
106    }
107
108    /// Render as the ISO term `error(Formal, Context)`.
109    /// `Formal` matches the variant (e.g. `type_error(integer, foo)`);
110    /// `Context` is the human-readable atom (the message string interned
111    /// as an atom). Test helpers grep this string for substrings.
112    pub fn to_term(&self, interner: &mut StringInterner) -> Term {
113        let formal = self.formal_term(interner);
114        let context_atom = interner.intern(self.context());
115        let error_functor = interner.intern("error");
116        Term::Compound {
117            functor: error_functor,
118            args: vec![formal, Term::Atom(context_atom)],
119        }
120    }
121
122    /// The human-readable context message. This is what test helpers
123    /// substring-match against and what the CLI shows.
124    pub fn context(&self) -> &str {
125        match self {
126            PrologError::Instantiation { context }
127            | PrologError::Type { context, .. }
128            | PrologError::Existence { context, .. }
129            | PrologError::Domain { context, .. }
130            | PrologError::Evaluation { context, .. }
131            | PrologError::Permission { context, .. }
132            | PrologError::Representation { context, .. }
133            | PrologError::Resource { context, .. }
134            | PrologError::Syntax { context } => context,
135        }
136    }
137
138    fn formal_term(&self, interner: &mut StringInterner) -> Term {
139        match self {
140            PrologError::Instantiation { .. } => Term::Atom(interner.intern("instantiation_error")),
141            PrologError::Type {
142                expected_type,
143                culprit,
144                ..
145            } => Term::Compound {
146                functor: interner.intern("type_error"),
147                args: vec![Term::Atom(interner.intern(expected_type)), culprit.clone()],
148            },
149            PrologError::Existence {
150                object_type,
151                culprit,
152                ..
153            } => Term::Compound {
154                functor: interner.intern("existence_error"),
155                args: vec![Term::Atom(interner.intern(object_type)), culprit.clone()],
156            },
157            PrologError::Domain {
158                expected_domain,
159                culprit,
160                ..
161            } => Term::Compound {
162                functor: interner.intern("domain_error"),
163                args: vec![
164                    Term::Atom(interner.intern(expected_domain)),
165                    culprit.clone(),
166                ],
167            },
168            PrologError::Evaluation { kind, .. } => Term::Compound {
169                functor: interner.intern("evaluation_error"),
170                args: vec![Term::Atom(interner.intern(kind))],
171            },
172            PrologError::Permission {
173                operation,
174                permission_type,
175                culprit,
176                ..
177            } => Term::Compound {
178                functor: interner.intern("permission_error"),
179                args: vec![
180                    Term::Atom(interner.intern(operation)),
181                    Term::Atom(interner.intern(permission_type)),
182                    culprit.clone(),
183                ],
184            },
185            PrologError::Representation { flag, .. } => Term::Compound {
186                functor: interner.intern("representation_error"),
187                args: vec![Term::Atom(interner.intern(flag))],
188            },
189            PrologError::Resource { kind, .. } => Term::Compound {
190                functor: interner.intern("resource_error"),
191                args: vec![Term::Atom(interner.intern(kind))],
192            },
193            PrologError::Syntax { .. } => Term::Atom(interner.intern("syntax_error")),
194        }
195    }
196
197    /// Render the error term as a human-readable string.
198    /// Used by the CLI and the test helper for substring assertions.
199    pub fn to_display(&self, interner: &mut StringInterner) -> String {
200        let term = self.to_term(interner);
201        let mut out = String::new();
202        format_term(&term, interner, &mut out);
203        out
204    }
205}
206
207/// Format an arbitrary term as a Prolog-syntax string. Used by the CLI for
208/// rendering uncaught error terms. Lives here (not in a general
209/// pretty-printer module) because the only consumer is the error path.
210pub fn format_term(term: &Term, interner: &StringInterner, out: &mut String) {
211    match term {
212        Term::Atom(id) => out.push_str(interner.resolve(*id)),
213        Term::Var(id) => {
214            out.push('_');
215            out.push_str(&id.to_string());
216        }
217        Term::Integer(n) => out.push_str(&n.to_string()),
218        Term::Float(f) => out.push_str(&f.to_string()),
219        Term::Compound { functor, args } => {
220            out.push_str(interner.resolve(*functor));
221            out.push('(');
222            for (i, a) in args.iter().enumerate() {
223                if i > 0 {
224                    out.push_str(", ");
225                }
226                format_term(a, interner, out);
227            }
228            out.push(')');
229        }
230        Term::List { head, tail } => {
231            out.push('[');
232            format_term(head, interner, out);
233            let mut cur = tail.as_ref();
234            loop {
235                match cur {
236                    Term::List { head, tail } => {
237                        out.push_str(", ");
238                        format_term(head, interner, out);
239                        cur = tail;
240                    }
241                    Term::Atom(id) if interner.resolve(*id) == "[]" => break,
242                    other => {
243                        out.push('|');
244                        format_term(other, interner, out);
245                        break;
246                    }
247                }
248            }
249            out.push(']');
250        }
251    }
252}
253
254#[cfg(test)]
255mod tests {
256    use super::*;
257
258    #[test]
259    fn instantiation_error_term_shape() {
260        let mut interner = StringInterner::new();
261        let err = PrologError::Instantiation {
262            context: "X must be bound".into(),
263        };
264        let term = err.to_term(&mut interner);
265        match term {
266            Term::Compound { functor, args } => {
267                assert_eq!(interner.resolve(functor), "error");
268                assert_eq!(args.len(), 2);
269                match &args[0] {
270                    Term::Atom(id) => assert_eq!(interner.resolve(*id), "instantiation_error"),
271                    _ => panic!("expected atom formal term"),
272                }
273            }
274            _ => panic!("expected compound error/2"),
275        }
276    }
277
278    #[test]
279    fn type_error_term_shape() {
280        let mut interner = StringInterner::new();
281        let foo = interner.intern("foo");
282        let err = PrologError::Type {
283            expected_type: "integer",
284            culprit: Term::Atom(foo),
285            context: "arithmetic on non-number".into(),
286        };
287        let term = err.to_term(&mut interner);
288        // error(type_error(integer, foo), 'arithmetic on non-number')
289        let Term::Compound { args, .. } = term else {
290            panic!("expected compound");
291        };
292        let Term::Compound {
293            functor: type_functor,
294            args: type_args,
295        } = &args[0]
296        else {
297            panic!("expected formal compound");
298        };
299        assert_eq!(interner.resolve(*type_functor), "type_error");
300        assert_eq!(type_args.len(), 2);
301        assert!(matches!(type_args[1], Term::Atom(id) if interner.resolve(id) == "foo"));
302    }
303
304    #[test]
305    fn existence_error_indicator() {
306        let mut interner = StringInterner::new();
307        let f = interner.intern("frobnicate");
308        let slash = interner.intern("/");
309        let indicator = Term::Compound {
310            functor: slash,
311            args: vec![Term::Atom(f), Term::Integer(2)],
312        };
313        let err = PrologError::Existence {
314            object_type: "procedure",
315            culprit: indicator,
316            context: "frobnicate/2 is undefined".into(),
317        };
318        let display = err.to_display(&mut interner);
319        assert!(
320            display.contains("existence_error(procedure, /(frobnicate, 2))"),
321            "got: {display}"
322        );
323        assert!(display.contains("frobnicate/2 is undefined"));
324    }
325
326    #[test]
327    fn evaluation_error_zero_divisor_renders() {
328        let mut interner = StringInterner::new();
329        let err = PrologError::Evaluation {
330            kind: "zero_divisor",
331            context: "Division by zero".into(),
332        };
333        let display = err.to_display(&mut interner);
334        assert!(
335            display.contains("evaluation_error(zero_divisor)"),
336            "got: {display}"
337        );
338        assert!(display.contains("zero"));
339    }
340
341    #[test]
342    fn resource_steps_is_uncatchable() {
343        let err = PrologError::Resource {
344            kind: "steps",
345            context: "step limit exceeded".into(),
346        };
347        assert!(err.is_uncatchable());
348    }
349
350    #[test]
351    fn other_errors_are_catchable() {
352        let err = PrologError::Type {
353            expected_type: "integer",
354            culprit: Term::Integer(0),
355            context: String::new(),
356        };
357        assert!(!err.is_uncatchable());
358    }
359}