Skip to main content

plg_runtime/
errors.rs

1//! Structured runtime errors (ISO error terms).
2//!
3//! An error is carried as a relocatable ball (`TermBuf`) so it survives
4//! the heap rewinding that unwinds to a catch frame, plus a
5//! pre-rendered message for top-level output. The message format is
6//! v1's `format_term` rendering of the ball — byte-compatible with the
7//! M2/M3 preformatted strings.
8
9use crate::cell::*;
10use crate::copyterm::copy_to_buf;
11use crate::machine::{Machine, RtError};
12use crate::render::format_term;
13
14/// Build `error(Formal, 'Context')`, snapshot it, and set `m.error`.
15/// The formal term is built on the heap transiently and trimmed back.
16pub fn set_formal(m: &mut Machine, formal: Word, context: &str, uncatchable: bool) {
17    let mark = m.heap.len().min(payload_start(formal, m));
18    let ctx_atom = make_atom(m.atoms.intern(context));
19    let err_f = m.atoms.intern("error");
20    let idx = m.heap.len();
21    m.heap.push(pack_functor(err_f, 2));
22    m.heap.push(formal);
23    m.heap.push(ctx_atom);
24    let ball_word = make(TAG_STR, idx as u64);
25    let ball = copy_to_buf(m, ball_word);
26    let mut message = String::new();
27    format_term(m, ball_word, &mut message);
28    // SPANS.md Layer 3: append source provenance for the in-flight raise, if
29    // one is set. Centralized here so every error constructor gets it; the
30    // ISO ball above is unchanged, so the suffix lives only in `message`.
31    // `NO_SITE` (the default) resolves to `None` without allocating.
32    //
33    // CONTRACT: this reads `m.error_site`. Every caller must ensure it is
34    // either `NO_SITE` or the site of the raise in flight — a stale value is
35    // a wrong-provenance bug. Raising builtins enforce this with
36    // `ErrorSiteGuard` (set on entry, restore on drop).
37    if let Some((file, line, col)) = m.site_location(m.error_site) {
38        use std::fmt::Write as _;
39        let _ = write!(message, " at {file}:{line}:{col}");
40    }
41    m.heap.truncate(mark.max(idx)); // drop the wrapper (and formal if transient)
42    m.error = Some(RtError {
43        ball,
44        message,
45        uncatchable,
46    });
47}
48
49/// Heap index where `formal` starts if heap-allocated (for trimming).
50fn payload_start(w: Word, m: &Machine) -> usize {
51    match tag_of(w) {
52        TAG_STR | TAG_LST | TAG_FLT | TAG_BIG => payload(w) as usize,
53        _ => m.heap.len(),
54    }
55}
56
57/// `instantiation_error`
58pub fn instantiation(m: &mut Machine, context: &str) {
59    let f = make_atom(m.atoms.intern("instantiation_error"));
60    set_formal(m, f, context, false);
61}
62
63/// `type_error(Type, Culprit)`
64pub fn type_error(m: &mut Machine, expected: &str, culprit: Word, context: &str) {
65    let te = m.atoms.intern("type_error");
66    let ty = make_atom(m.atoms.intern(expected));
67    let idx = m.heap.len();
68    m.heap.push(pack_functor(te, 2));
69    m.heap.push(ty);
70    m.heap.push(culprit);
71    set_formal(m, make(TAG_STR, idx as u64), context, false);
72}
73
74/// `domain_error(Domain, Culprit)`
75pub fn domain_error(m: &mut Machine, domain: &str, culprit: Word, context: &str) {
76    let de = m.atoms.intern("domain_error");
77    let d = make_atom(m.atoms.intern(domain));
78    let idx = m.heap.len();
79    m.heap.push(pack_functor(de, 2));
80    m.heap.push(d);
81    m.heap.push(culprit);
82    set_formal(m, make(TAG_STR, idx as u64), context, false);
83}
84
85/// `evaluation_error(Kind)`
86pub fn evaluation(m: &mut Machine, kind: &str, context: &str) {
87    let ee = m.atoms.intern("evaluation_error");
88    let k = make_atom(m.atoms.intern(kind));
89    let idx = m.heap.len();
90    m.heap.push(pack_functor(ee, 1));
91    m.heap.push(k);
92    set_formal(m, make(TAG_STR, idx as u64), context, false);
93}
94
95/// `resource_error(Kind)` — the steps variant is uncatchable (v1 rule).
96pub fn resource(m: &mut Machine, kind: &str, context: &str, uncatchable: bool) {
97    let re = m.atoms.intern("resource_error");
98    let k = make_atom(m.atoms.intern(kind));
99    let idx = m.heap.len();
100    m.heap.push(pack_functor(re, 1));
101    m.heap.push(k);
102    set_formal(m, make(TAG_STR, idx as u64), context, uncatchable);
103}
104
105/// `existence_error(procedure, Name/Arity)` with v1's exact context. Source
106/// provenance (SPANS.md Layer 3) comes from `m.error_site`, appended by
107/// `set_formal` — the caller sets that around the raise.
108pub fn existence_procedure(m: &mut Machine, name: &str, arity: u32) {
109    let ee = m.atoms.intern("existence_error");
110    let proc = make_atom(m.atoms.intern("procedure"));
111    let slash = m.atoms.intern("/");
112    let name_atom = make_atom(m.atoms.intern(name));
113    let pi = m.heap.len();
114    m.heap.push(pack_functor(slash, 2));
115    m.heap.push(name_atom);
116    m.heap.push(make_int(arity as i64));
117    let idx = m.heap.len();
118    m.heap.push(pack_functor(ee, 2));
119    m.heap.push(proc);
120    m.heap.push(make(TAG_STR, pi as u64));
121    let context = format!("Undefined procedure: {name}/{arity}");
122    set_formal(m, make(TAG_STR, idx as u64), &context, false);
123}
124
125/// `throw/1` of an arbitrary user term: the ball IS the term; the
126/// top-level message is its rendering (v1 runner behavior).
127pub fn throw_term(m: &mut Machine, ball_word: Word) {
128    let ball_word = m.deref(ball_word);
129    if tag_of(ball_word) == TAG_REF {
130        // ISO: throw/1 of an unbound variable is an instantiation error.
131        instantiation(m, "throw/1 requires a bound argument");
132        return;
133    }
134    let ball = copy_to_buf(m, ball_word);
135    let mut message = String::new();
136    format_term(m, ball_word, &mut message);
137    m.error = Some(RtError {
138        ball,
139        message,
140        uncatchable: false,
141    });
142}
143
144#[cfg(test)]
145mod tests {
146    use super::*;
147    use plg_shared::StringInterner;
148
149    fn machine() -> Box<Machine> {
150        Machine::new(StringInterner::new(), Vec::new())
151    }
152
153    #[test]
154    fn existence_message_matches_v1_bytes() {
155        let mut m = machine();
156        existence_procedure(&mut m, "nosuch", 1);
157        assert_eq!(
158            m.error.as_ref().unwrap().message,
159            "error(existence_error(procedure, /(nosuch, 1)), Undefined procedure: nosuch/1)"
160        );
161    }
162
163    #[test]
164    fn evaluation_message_matches_v1_bytes() {
165        let mut m = machine();
166        evaluation(
167            &mut m,
168            "zero_divisor",
169            "Division by zero (integer division)",
170        );
171        assert_eq!(
172            m.error.as_ref().unwrap().message,
173            "error(evaluation_error(zero_divisor), Division by zero (integer division))"
174        );
175    }
176
177    #[test]
178    fn existence_suffix_appears_when_site_resolves() {
179        use crate::machine::SrcLoc;
180        let mut m = machine();
181        m.set_provenance(
182            vec![SrcLoc {
183                file: 0,
184                line: 12,
185                col: 7,
186            }],
187            vec!["examples/family.pl".to_string()],
188        );
189        m.error_site = 0;
190        existence_procedure(&mut m, "foo", 1);
191        // Ball/term shape unchanged; the suffix lives only in the message.
192        assert_eq!(
193            m.error.as_ref().unwrap().message,
194            "error(existence_error(procedure, /(foo, 1)), Undefined procedure: foo/1) \
195             at examples/family.pl:12:7"
196        );
197    }
198
199    #[test]
200    fn ball_survives_heap_truncation() {
201        let mut m = machine();
202        let heap_mark = m.heap.len();
203        existence_procedure(&mut m, "gone", 2);
204        m.heap.truncate(heap_mark); // simulate backtrack rewind
205        let err = m.error.take().unwrap();
206        let w = crate::copyterm::restore_from_buf(&mut m, &err.ball);
207        let mut s = String::new();
208        format_term(&m, w, &mut s);
209        assert!(s.starts_with("error(existence_error(procedure"), "{s}");
210    }
211}