use plg_shared::{AtomId, Span, Spanned, StringInterner, Term};
pub const ARITH_OPS: &[(&str, i32)] = &[
("<", 0),
(">", 1),
("=<", 2),
(">=", 3),
("=:=", 4),
("=\\=", 5),
];
pub const ORDER_OPS: &[(&str, i32)] = &[
("==", 0),
("\\==", 1),
("@<", 2),
("@>", 3),
("@=<", 4),
("@>=", 5),
];
pub const DET_BUILTINS: &[(&str, u32, &str, bool)] = &[
("var", 1, "plg_rt_b_var_1", false),
("nonvar", 1, "plg_rt_b_nonvar_1", false),
("atom", 1, "plg_rt_b_atom_1", false),
("number", 1, "plg_rt_b_number_1", false),
("integer", 1, "plg_rt_b_integer_1", false),
("float", 1, "plg_rt_b_float_1", false),
("compound", 1, "plg_rt_b_compound_1", false),
("is_list", 1, "plg_rt_b_is_list_1", false),
("functor", 3, "plg_rt_b_functor_3", true),
("arg", 3, "plg_rt_b_arg_3", true),
("=..", 2, "plg_rt_b_univ_2", true),
("copy_term", 2, "plg_rt_b_copy_term_2", false),
("atom_length", 2, "plg_rt_b_atom_length_2", true),
("atom_concat", 3, "plg_rt_b_atom_concat_3", true),
("atom_chars", 2, "plg_rt_b_atom_chars_2", true),
("number_chars", 2, "plg_rt_b_number_chars_2", true),
("number_codes", 2, "plg_rt_b_number_codes_2", true),
("msort", 2, "plg_rt_b_msort_2", true),
("sort", 2, "plg_rt_b_sort_2", true),
("succ", 2, "plg_rt_b_succ_2", true),
("plus", 3, "plg_rt_b_plus_3", true),
(
"unify_with_occurs_check",
2,
"plg_rt_b_unify_with_occurs_check_2",
false,
),
("write", 1, "plg_rt_b_write_1", false),
("writeln", 1, "plg_rt_b_writeln_1", false),
("nl", 0, "plg_rt_b_nl_0", false),
];
pub type LGoal = Spanned<LGoalKind>;
#[derive(Clone)]
pub enum LGoalKind {
Call {
functor: AtomId,
args: Vec<Term>,
},
Metacall(Term),
RtDet {
sym: &'static str,
args: Vec<Term>,
raises: bool,
},
True,
Fail,
Cut,
Unify(Term, Term),
NotUnify(Term, Term),
TermCmp(i32, Term, Term),
Compare(Term, Term, Term),
Is(Term, Term),
ArithCmp(i32, Term, Term),
Disj(Box<LGoal>, Box<LGoal>),
IfThenElse(Box<LGoal>, Box<LGoal>, Box<LGoal>),
IfThen(Box<LGoal>, Box<LGoal>),
Naf(Box<LGoal>),
Once(Box<LGoal>),
Conj(Vec<LGoal>),
}
pub fn lower_goal(t: &Term, span: Span, interner: &StringInterner) -> Result<LGoal, String> {
let (name, args): (&str, &[Term]) = match t {
Term::Atom(id) => (interner.resolve(*id), &[]),
Term::Compound { functor, args } => (interner.resolve(*functor), args),
Term::Var(_) => return Ok(Spanned::new(LGoalKind::Metacall(t.clone()), span)),
other => return Err(format!("goal is not callable: {other:?}")),
};
let kind = match (name, args.len()) {
("true", 0) => LGoalKind::True,
("fail", 0) | ("false", 0) => LGoalKind::Fail,
("!", 0) => LGoalKind::Cut,
(",", 2) => {
let mut goals = Vec::new();
flatten_conj(t, span, interner, &mut goals)?;
LGoalKind::Conj(goals)
}
(";", 2) => {
if let Term::Compound {
functor,
args: ite_args,
} = &args[0]
&& interner.resolve(*functor) == "->"
&& ite_args.len() == 2
{
LGoalKind::IfThenElse(
Box::new(lower_goal(&ite_args[0], span, interner)?),
Box::new(lower_goal(&ite_args[1], span, interner)?),
Box::new(lower_goal(&args[1], span, interner)?),
)
} else {
LGoalKind::Disj(
Box::new(lower_goal(&args[0], span, interner)?),
Box::new(lower_goal(&args[1], span, interner)?),
)
}
}
("->", 2) => LGoalKind::IfThen(
Box::new(lower_goal(&args[0], span, interner)?),
Box::new(lower_goal(&args[1], span, interner)?),
),
("\\+", 1) => LGoalKind::Naf(Box::new(lower_goal(&args[0], span, interner)?)),
("once", 1) if !matches!(args[0], Term::Var(_)) => {
LGoalKind::Once(Box::new(lower_goal(&args[0], span, interner)?))
}
("once", 1) => LGoalKind::Metacall(t.clone()),
("=", 2) => LGoalKind::Unify(args[0].clone(), args[1].clone()),
("\\=", 2) => LGoalKind::NotUnify(args[0].clone(), args[1].clone()),
("compare", 3) => LGoalKind::Compare(args[0].clone(), args[1].clone(), args[2].clone()),
("is", 2) => LGoalKind::Is(args[0].clone(), args[1].clone()),
_ => {
if let Some(&(_, op)) = ARITH_OPS.iter().find(|(n, _)| *n == name)
&& args.len() == 2
{
LGoalKind::ArithCmp(op, args[0].clone(), args[1].clone())
} else if let Some(&(_, op)) = ORDER_OPS.iter().find(|(n, _)| *n == name)
&& args.len() == 2
{
LGoalKind::TermCmp(op, args[0].clone(), args[1].clone())
} else if let Some(&(_, _, sym, raises)) = DET_BUILTINS
.iter()
.find(|(n, a, _, _)| *n == name && *a as usize == args.len())
{
LGoalKind::RtDet {
sym,
args: args.to_vec(),
raises,
}
} else {
let functor = match t {
Term::Atom(id) => *id,
Term::Compound { functor, .. } => *functor,
_ => unreachable!(),
};
LGoalKind::Call {
functor,
args: args.to_vec(),
}
}
}
};
Ok(Spanned::new(kind, span))
}
fn flatten_conj(
t: &Term,
span: Span,
interner: &StringInterner,
out: &mut Vec<LGoal>,
) -> Result<(), String> {
if let Term::Compound { functor, args } = t
&& args.len() == 2
&& interner.resolve(*functor) == ","
{
flatten_conj(&args[0], span, interner, out)?;
flatten_conj(&args[1], span, interner, out)?;
return Ok(());
}
splice_lowered(lower_goal(t, span, interner)?, out);
Ok(())
}
fn splice_lowered(lowered: LGoal, out: &mut Vec<LGoal>) {
let Spanned { node, span } = lowered;
match node {
LGoalKind::True => {}
LGoalKind::Conj(inner) => out.extend(inner),
kind => out.push(Spanned::new(kind, span)),
}
}
pub fn lower_body(body: &[Spanned<Term>], interner: &StringInterner) -> Result<Vec<LGoal>, String> {
let mut goals = Vec::new();
for sp in body {
splice_lowered(lower_goal(&sp.node, sp.span, interner)?, &mut goals);
}
Ok(goals)
}
pub fn count_scratch(goals: &[LGoal]) -> usize {
goals.iter().map(scratch_in).sum()
}
fn scratch_in(g: &LGoal) -> usize {
match &g.node {
LGoalKind::IfThenElse(c, t, e) => 2 + scratch_in(c) + scratch_in(t) + scratch_in(e),
LGoalKind::Naf(g) => 2 + scratch_in(g),
LGoalKind::IfThen(c, t) => 1 + scratch_in(c) + scratch_in(t),
LGoalKind::Once(g) => 1 + scratch_in(g),
LGoalKind::Disj(a, b) => scratch_in(a) + scratch_in(b),
LGoalKind::Conj(gs) => gs.iter().map(scratch_in).sum(),
_ => 0,
}
}
pub fn collect_goal_vars(g: &LGoal, out: &mut Vec<plg_shared::term::VarId>) {
use super::term_emit::collect_vars;
match &g.node {
LGoalKind::Call { args, .. } => {
for a in args {
collect_vars(a, out);
}
}
LGoalKind::Unify(a, b) | LGoalKind::NotUnify(a, b) | LGoalKind::Is(a, b) => {
collect_vars(a, out);
collect_vars(b, out);
}
LGoalKind::TermCmp(_, a, b) | LGoalKind::ArithCmp(_, a, b) => {
collect_vars(a, out);
collect_vars(b, out);
}
LGoalKind::Compare(o, a, b) => {
collect_vars(o, out);
collect_vars(a, out);
collect_vars(b, out);
}
LGoalKind::Disj(a, b) | LGoalKind::IfThen(a, b) => {
collect_goal_vars(a, out);
collect_goal_vars(b, out);
}
LGoalKind::IfThenElse(c, t, e) => {
collect_goal_vars(c, out);
collect_goal_vars(t, out);
collect_goal_vars(e, out);
}
LGoalKind::Naf(g) | LGoalKind::Once(g) => collect_goal_vars(g, out),
LGoalKind::Conj(gs) => {
for g in gs {
collect_goal_vars(g, out);
}
}
LGoalKind::Metacall(t) => collect_vars(t, out),
LGoalKind::RtDet { args, .. } => {
for a in args {
collect_vars(a, out);
}
}
LGoalKind::True | LGoalKind::Fail | LGoalKind::Cut => {}
}
}
#[cfg(test)]
mod vocab_invariant {
use super::{ARITH_OPS, DET_BUILTINS, ORDER_OPS};
use plg_shared::{BUILTINS, builtins::BuiltinKind};
use std::collections::BTreeSet;
#[rustfmt::skip]
const STRUCTURAL: &[(&str, u32)] = &[
("=", 2), ("\\=", 2), ("is", 2), ("compare", 3),
(",", 2), (";", 2), ("->", 2), ("\\+", 1), ("once", 1),
("catch", 3), ("throw", 1), ("findall", 3), ("call", 1), ("between", 3),
("true", 0), ("fail", 0), ("false", 0), ("!", 0),
];
#[test]
fn det_builtins_are_det_rows_in_shared() {
for &(name, arity, _sym, _raises) in DET_BUILTINS {
let row = BUILTINS
.iter()
.find(|s| s.name == name && s.arity == arity)
.unwrap_or_else(|| panic!("DET_BUILTINS {name}/{arity} missing from BUILTINS"));
assert_eq!(
row.kind,
BuiltinKind::Det,
"{name}/{arity} is in DET_BUILTINS but not kind Det in BUILTINS"
);
}
}
#[test]
fn recognized_names_equal_shared_vocabulary() {
let mut covered: BTreeSet<(&str, u32)> = BTreeSet::new();
for &(n, a, _, _) in DET_BUILTINS {
covered.insert((n, a));
}
for &(n, _) in ARITH_OPS {
covered.insert((n, 2));
}
for &(n, _) in ORDER_OPS {
covered.insert((n, 2));
}
covered.extend(STRUCTURAL.iter().copied());
let vocab: BTreeSet<(&str, u32)> = BUILTINS.iter().map(|s| (s.name, s.arity)).collect();
assert_eq!(
covered, vocab,
"codegen-recognized names diverge from BUILTINS \
(left = codegen, right = shared table)"
);
}
}
#[cfg(test)]
mod span_invariant {
use super::lower_body;
use plg_frontend::Parser;
use plg_shared::StringInterner;
#[test]
fn lowered_goals_have_non_degenerate_spans() {
let mut interner = StringInterner::new();
let (clauses, _) =
Parser::parse_program_cg("p :- a, b(X), X is 1 + 2.\n", &mut interner, 0).unwrap();
let goals = lower_body(&clauses[0].body, &interner).unwrap();
assert_eq!(goals.len(), 3);
for g in &goals {
assert!(g.span.hi > g.span.lo, "degenerate span: {:?}", g.span);
assert_eq!(g.span.file, 0);
}
}
}