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