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 { context } => Term::Compound {
194                functor: interner.intern("syntax_error"),
195                args: vec![Term::Atom(interner.intern(context))],
196            },
197        }
198    }
199
200    /// Render the error term as a human-readable string.
201    /// Used by the CLI and the test helper for substring assertions.
202    pub fn to_display(&self, interner: &mut StringInterner) -> String {
203        let term = self.to_term(interner);
204        let mut out = String::new();
205        format_term(&term, interner, &mut out);
206        out
207    }
208}
209
210/// Format an arbitrary term as a Prolog-syntax string. Used by the CLI for
211/// rendering uncaught error terms. Lives here (not in a general
212/// pretty-printer module) because the only consumer is the error path.
213pub fn format_term(term: &Term, interner: &StringInterner, out: &mut String) {
214    match term {
215        Term::Atom(id) => out.push_str(interner.resolve(*id)),
216        Term::Var(id) => {
217            out.push('_');
218            out.push_str(&id.to_string());
219        }
220        Term::Integer(n) => out.push_str(&n.to_string()),
221        Term::Float(f) => out.push_str(&f.to_string()),
222        Term::Compound { functor, args } => {
223            out.push_str(interner.resolve(*functor));
224            out.push('(');
225            for (i, a) in args.iter().enumerate() {
226                if i > 0 {
227                    out.push_str(", ");
228                }
229                format_term(a, interner, out);
230            }
231            out.push(')');
232        }
233        Term::List { head, tail } => {
234            out.push('[');
235            format_term(head, interner, out);
236            let mut cur = tail.as_ref();
237            loop {
238                match cur {
239                    Term::List { head, tail } => {
240                        out.push_str(", ");
241                        format_term(head, interner, out);
242                        cur = tail;
243                    }
244                    Term::Atom(id) if interner.resolve(*id) == "[]" => break,
245                    other => {
246                        out.push('|');
247                        format_term(other, interner, out);
248                        break;
249                    }
250                }
251            }
252            out.push(']');
253        }
254    }
255}
256
257#[cfg(test)]
258mod tests {
259    use super::*;
260
261    #[test]
262    fn instantiation_error_term_shape() {
263        let mut interner = StringInterner::new();
264        let err = PrologError::Instantiation {
265            context: "X must be bound".into(),
266        };
267        let term = err.to_term(&mut interner);
268        match term {
269            Term::Compound { functor, args } => {
270                assert_eq!(interner.resolve(functor), "error");
271                assert_eq!(args.len(), 2);
272                match &args[0] {
273                    Term::Atom(id) => assert_eq!(interner.resolve(*id), "instantiation_error"),
274                    _ => panic!("expected atom formal term"),
275                }
276            }
277            _ => panic!("expected compound error/2"),
278        }
279    }
280
281    #[test]
282    fn type_error_term_shape() {
283        let mut interner = StringInterner::new();
284        let foo = interner.intern("foo");
285        let err = PrologError::Type {
286            expected_type: "integer",
287            culprit: Term::Atom(foo),
288            context: "arithmetic on non-number".into(),
289        };
290        let term = err.to_term(&mut interner);
291        // error(type_error(integer, foo), 'arithmetic on non-number')
292        let Term::Compound { args, .. } = term else {
293            panic!("expected compound");
294        };
295        let Term::Compound {
296            functor: type_functor,
297            args: type_args,
298        } = &args[0]
299        else {
300            panic!("expected formal compound");
301        };
302        assert_eq!(interner.resolve(*type_functor), "type_error");
303        assert_eq!(type_args.len(), 2);
304        assert!(matches!(type_args[1], Term::Atom(id) if interner.resolve(id) == "foo"));
305    }
306
307    #[test]
308    fn existence_error_indicator() {
309        let mut interner = StringInterner::new();
310        let f = interner.intern("frobnicate");
311        let slash = interner.intern("/");
312        let indicator = Term::Compound {
313            functor: slash,
314            args: vec![Term::Atom(f), Term::Integer(2)],
315        };
316        let err = PrologError::Existence {
317            object_type: "procedure",
318            culprit: indicator,
319            context: "frobnicate/2 is undefined".into(),
320        };
321        let display = err.to_display(&mut interner);
322        assert!(
323            display.contains("existence_error(procedure, /(frobnicate, 2))"),
324            "got: {display}"
325        );
326        assert!(display.contains("frobnicate/2 is undefined"));
327    }
328
329    #[test]
330    fn evaluation_error_zero_divisor_renders() {
331        let mut interner = StringInterner::new();
332        let err = PrologError::Evaluation {
333            kind: "zero_divisor",
334            context: "Division by zero".into(),
335        };
336        let display = err.to_display(&mut interner);
337        assert!(
338            display.contains("evaluation_error(zero_divisor)"),
339            "got: {display}"
340        );
341        assert!(display.contains("zero"));
342    }
343
344    #[test]
345    fn resource_steps_is_uncatchable() {
346        let err = PrologError::Resource {
347            kind: "steps",
348            context: "step limit exceeded".into(),
349        };
350        assert!(err.is_uncatchable());
351    }
352
353    #[test]
354    fn other_errors_are_catchable() {
355        let err = PrologError::Type {
356            expected_type: "integer",
357            culprit: Term::Integer(0),
358            context: String::new(),
359        };
360        assert!(!err.is_uncatchable());
361    }
362}