use plg_shared::{StringInterner, Term};
#[derive(Debug, Clone)]
pub struct ThrownError {
pub term: Term,
pub uncatchable: bool,
}
impl ThrownError {
pub fn from_prolog(err: PrologError, interner: &mut StringInterner) -> Self {
let uncatchable = err.is_uncatchable();
let term = err.to_term(interner);
ThrownError { term, uncatchable }
}
pub fn from_term(term: Term) -> Self {
ThrownError {
term,
uncatchable: false,
}
}
}
#[derive(Debug, Clone)]
pub enum PrologError {
Instantiation { context: String },
Type {
expected_type: &'static str,
culprit: Term,
context: String,
},
Existence {
object_type: &'static str,
culprit: Term,
context: String,
},
Domain {
expected_domain: &'static str,
culprit: Term,
context: String,
},
Evaluation {
kind: &'static str, context: String,
},
Permission {
operation: &'static str,
permission_type: &'static str,
culprit: Term,
context: String,
},
Representation {
flag: &'static str, context: String,
},
Resource {
kind: &'static str, context: String,
},
Syntax { context: String },
}
impl PrologError {
pub fn is_uncatchable(&self) -> bool {
matches!(self, PrologError::Resource { kind: "steps", .. })
}
pub fn to_term(&self, interner: &mut StringInterner) -> Term {
let formal = self.formal_term(interner);
let context_atom = interner.intern(self.context());
let error_functor = interner.intern("error");
Term::Compound {
functor: error_functor,
args: vec![formal, Term::Atom(context_atom)],
}
}
pub fn context(&self) -> &str {
match self {
PrologError::Instantiation { context }
| PrologError::Type { context, .. }
| PrologError::Existence { context, .. }
| PrologError::Domain { context, .. }
| PrologError::Evaluation { context, .. }
| PrologError::Permission { context, .. }
| PrologError::Representation { context, .. }
| PrologError::Resource { context, .. }
| PrologError::Syntax { context } => context,
}
}
fn formal_term(&self, interner: &mut StringInterner) -> Term {
match self {
PrologError::Instantiation { .. } => Term::Atom(interner.intern("instantiation_error")),
PrologError::Type {
expected_type,
culprit,
..
} => Term::Compound {
functor: interner.intern("type_error"),
args: vec![Term::Atom(interner.intern(expected_type)), culprit.clone()],
},
PrologError::Existence {
object_type,
culprit,
..
} => Term::Compound {
functor: interner.intern("existence_error"),
args: vec![Term::Atom(interner.intern(object_type)), culprit.clone()],
},
PrologError::Domain {
expected_domain,
culprit,
..
} => Term::Compound {
functor: interner.intern("domain_error"),
args: vec![
Term::Atom(interner.intern(expected_domain)),
culprit.clone(),
],
},
PrologError::Evaluation { kind, .. } => Term::Compound {
functor: interner.intern("evaluation_error"),
args: vec![Term::Atom(interner.intern(kind))],
},
PrologError::Permission {
operation,
permission_type,
culprit,
..
} => Term::Compound {
functor: interner.intern("permission_error"),
args: vec![
Term::Atom(interner.intern(operation)),
Term::Atom(interner.intern(permission_type)),
culprit.clone(),
],
},
PrologError::Representation { flag, .. } => Term::Compound {
functor: interner.intern("representation_error"),
args: vec![Term::Atom(interner.intern(flag))],
},
PrologError::Resource { kind, .. } => Term::Compound {
functor: interner.intern("resource_error"),
args: vec![Term::Atom(interner.intern(kind))],
},
PrologError::Syntax { .. } => Term::Atom(interner.intern("syntax_error")),
}
}
pub fn to_display(&self, interner: &mut StringInterner) -> String {
let term = self.to_term(interner);
let mut out = String::new();
format_term(&term, interner, &mut out);
out
}
}
pub fn format_term(term: &Term, interner: &StringInterner, out: &mut String) {
match term {
Term::Atom(id) => out.push_str(interner.resolve(*id)),
Term::Var(id) => {
out.push('_');
out.push_str(&id.to_string());
}
Term::Integer(n) => out.push_str(&n.to_string()),
Term::Float(f) => out.push_str(&f.to_string()),
Term::Compound { functor, args } => {
out.push_str(interner.resolve(*functor));
out.push('(');
for (i, a) in args.iter().enumerate() {
if i > 0 {
out.push_str(", ");
}
format_term(a, interner, out);
}
out.push(')');
}
Term::List { head, tail } => {
out.push('[');
format_term(head, interner, out);
let mut cur = tail.as_ref();
loop {
match cur {
Term::List { head, tail } => {
out.push_str(", ");
format_term(head, interner, out);
cur = tail;
}
Term::Atom(id) if interner.resolve(*id) == "[]" => break,
other => {
out.push('|');
format_term(other, interner, out);
break;
}
}
}
out.push(']');
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn instantiation_error_term_shape() {
let mut interner = StringInterner::new();
let err = PrologError::Instantiation {
context: "X must be bound".into(),
};
let term = err.to_term(&mut interner);
match term {
Term::Compound { functor, args } => {
assert_eq!(interner.resolve(functor), "error");
assert_eq!(args.len(), 2);
match &args[0] {
Term::Atom(id) => assert_eq!(interner.resolve(*id), "instantiation_error"),
_ => panic!("expected atom formal term"),
}
}
_ => panic!("expected compound error/2"),
}
}
#[test]
fn type_error_term_shape() {
let mut interner = StringInterner::new();
let foo = interner.intern("foo");
let err = PrologError::Type {
expected_type: "integer",
culprit: Term::Atom(foo),
context: "arithmetic on non-number".into(),
};
let term = err.to_term(&mut interner);
let Term::Compound { args, .. } = term else {
panic!("expected compound");
};
let Term::Compound {
functor: type_functor,
args: type_args,
} = &args[0]
else {
panic!("expected formal compound");
};
assert_eq!(interner.resolve(*type_functor), "type_error");
assert_eq!(type_args.len(), 2);
assert!(matches!(type_args[1], Term::Atom(id) if interner.resolve(id) == "foo"));
}
#[test]
fn existence_error_indicator() {
let mut interner = StringInterner::new();
let f = interner.intern("frobnicate");
let slash = interner.intern("/");
let indicator = Term::Compound {
functor: slash,
args: vec![Term::Atom(f), Term::Integer(2)],
};
let err = PrologError::Existence {
object_type: "procedure",
culprit: indicator,
context: "frobnicate/2 is undefined".into(),
};
let display = err.to_display(&mut interner);
assert!(
display.contains("existence_error(procedure, /(frobnicate, 2))"),
"got: {display}"
);
assert!(display.contains("frobnicate/2 is undefined"));
}
#[test]
fn evaluation_error_zero_divisor_renders() {
let mut interner = StringInterner::new();
let err = PrologError::Evaluation {
kind: "zero_divisor",
context: "Division by zero".into(),
};
let display = err.to_display(&mut interner);
assert!(
display.contains("evaluation_error(zero_divisor)"),
"got: {display}"
);
assert!(display.contains("zero"));
}
#[test]
fn resource_steps_is_uncatchable() {
let err = PrologError::Resource {
kind: "steps",
context: "step limit exceeded".into(),
};
assert!(err.is_uncatchable());
}
#[test]
fn other_errors_are_catchable() {
let err = PrologError::Type {
expected_type: "integer",
culprit: Term::Integer(0),
context: String::new(),
};
assert!(!err.is_uncatchable());
}
}