use crate::ast::Ast;
use crate::Diagnostics;
use delhi_syntax::{AgentId, AtomId, Interner};
use std::collections::HashMap;
#[derive(Debug)]
pub struct Sig {
pub types: HashMap<String, String>,
pub objects: HashMap<String, String>,
pub agents: Interner,
pub atoms: Interner,
pub preds: HashMap<String, Vec<String>>,
}
pub fn atom_key(pred: &str, args: &[String]) -> String {
if args.is_empty() {
pred.to_string()
} else {
format!("{pred}({})", args.join(","))
}
}
impl Sig {
pub fn build(ast: &Ast, diags: &mut Diagnostics) -> Sig {
let mut types = HashMap::new();
for t in &ast.types {
if types.insert(t.name.clone(), t.parent.clone()).is_some() {
diags.push(t.span, format!("duplicate type `{}`", t.name));
}
}
for t in &ast.types {
if t.parent != "Object" && !types.contains_key(&t.parent) {
diags.push(t.span, format!("unknown supertype `{}`", t.parent));
}
}
for t in ast.types.iter() {
let mut seen = 0usize;
let mut cur = t.name.clone();
while let Some(p) = types.get(&cur) {
seen += 1;
if seen > types.len() {
diags.push(t.span, format!("cyclic type hierarchy at `{}`", t.name));
break;
}
cur = p.clone();
}
}
let mut objects = HashMap::new();
for o in &ast.objects {
if o.ty != "Object" && !types.contains_key(&o.ty) {
diags.push(o.span, format!("unknown type `{}`", o.ty));
}
if objects.insert(o.name.clone(), o.ty.clone()).is_some() {
diags.push(o.span, format!("duplicate object `{}`", o.name));
}
}
let mut agents = Interner::default();
for (a, sp) in &ast.agents {
if !objects.contains_key(a) {
diags.push(*sp, format!("agent `{a}` is not a declared object"));
}
let before = agents.len();
agents.intern(a);
if agents.len() == before {
diags.push(*sp, format!("duplicate agent `{a}`"));
}
}
let mut preds = HashMap::new();
for p in &ast.props {
for ty in &p.params {
if ty != "Object" && !types.contains_key(ty) {
diags.push(p.span, format!("unknown type `{ty}` in predicate `{}`", p.name));
}
}
if preds.insert(p.name.clone(), p.params.clone()).is_some() {
diags.push(p.span, format!("duplicate predicate `{}`", p.name));
}
}
let mut sig = Sig { types, objects, agents, atoms: Interner::default(), preds };
let mut decls: Vec<(String, Vec<String>)> =
sig.preds.iter().map(|(k, v)| (k.clone(), v.clone())).collect();
decls.sort(); for (name, params) in decls {
for tuple in sig.tuples(¶ms) {
sig.atoms.intern(&atom_key(&name, &tuple));
}
}
sig
}
fn tuples(&self, params: &[String]) -> Vec<Vec<String>> {
let mut acc: Vec<Vec<String>> = vec![Vec::new()];
for ty in params {
let choices = self.objects_of(ty);
let mut next = Vec::with_capacity(acc.len() * choices.len());
for prefix in &acc {
for c in &choices {
let mut v = prefix.clone();
v.push(c.clone());
next.push(v);
}
}
acc = next;
}
acc
}
pub fn is_subtype(&self, sub: &str, sup: &str) -> bool {
if sub == sup || sup == "Object" {
return true;
}
let mut cur = sub.to_string();
let mut steps = 0usize;
while let Some(parent) = self.types.get(&cur) {
if parent == sup {
return true;
}
cur = parent.clone();
steps += 1;
if steps > self.types.len() {
return false; }
}
false
}
pub fn objects_of(&self, ty: &str) -> Vec<String> {
let mut out: Vec<String> = self
.objects
.iter()
.filter(|(_, oty)| self.is_subtype(oty, ty))
.map(|(name, _)| name.clone())
.collect();
out.sort();
out
}
pub fn atom_id(&self, pred: &str, args: &[String]) -> Option<AtomId> {
let key = atom_key(pred, args);
(0..self.atoms.len() as u32).find(|&i| self.atoms.name(i) == key)
}
pub fn atom_name(&self, id: AtomId) -> &str {
debug_assert!((id as usize) < self.atoms.len(), "atom id out of range");
self.atoms.name(id)
}
pub fn agent_id(&self, name: &str) -> Option<AgentId> {
(0..self.agents.len() as u32).find(|&i| self.agents.name(i) == name)
}
pub fn agent_name(&self, id: AgentId) -> &str {
debug_assert!((id as usize) < self.agents.len(), "agent id out of range");
self.agents.name(id)
}
pub fn n_atoms(&self) -> usize {
self.atoms.len()
}
pub fn n_agents(&self) -> usize {
self.agents.len()
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::{parse_file, Diagnostics};
fn sig(src: &str) -> Sig {
let mut d = Diagnostics::default();
let ast = parse_file(src, &mut d);
let s = Sig::build(&ast, &mut d);
assert!(d.is_empty(), "unexpected errors:\n{}", d.render(src));
s
}
const SRC: &str = r#"
types { Actor - Object, Location - Object, Robot - Actor, Droid - Robot }
objects { alice - Actor, r2 - Robot, study, hall - Location }
agents { alice, r2 }
props { heads, at(Actor, Location) }
initially { heads }
actions {}
"#;
#[test]
fn subtyping_is_transitive_and_rooted_at_object() {
let s = sig(SRC);
assert!(s.is_subtype("Robot", "Actor"));
assert!(s.is_subtype("Robot", "Object"), "every type reaches Object");
assert!(s.is_subtype("Droid", "Actor"), "transitive over two non-Object hops");
assert!(s.is_subtype("Actor", "Actor"), "reflexive");
assert!(!s.is_subtype("Actor", "Robot"), "not symmetric");
assert!(!s.is_subtype("Location", "Actor"));
assert!(!s.is_subtype("Actor", "Droid"));
}
#[test]
fn objects_of_a_type_include_its_subtypes() {
let s = sig(SRC);
let mut actors = s.objects_of("Actor");
actors.sort();
assert_eq!(
actors,
vec!["alice".to_string(), "r2".to_string()],
"r2 is a Robot, and Robot is an Actor"
);
}
#[test]
fn predicates_expand_over_the_objects_of_their_parameter_types() {
let s = sig(SRC);
assert_eq!(s.n_atoms(), 5);
assert!(s.atom_id("heads", &[]).is_some());
assert!(s.atom_id("at", &["alice".into(), "study".into()]).is_some());
assert!(s.atom_id("at", &["r2".into(), "hall".into()]).is_some());
assert!(
s.atom_id("at", &["study".into(), "alice".into()]).is_none(),
"arguments must respect the declared parameter types"
);
}
#[test]
fn a_predicate_over_an_empty_type_yields_no_atoms() {
let s = sig(r#"
types { Ghost - Object }
objects { }
agents { }
props { haunts(Ghost) }
initially { }
actions {}
"#);
assert_eq!(s.n_atoms(), 0);
}
#[test]
fn agents_get_dense_ids_in_declaration_order() {
let s = sig(SRC);
assert_eq!(s.n_agents(), 2);
assert_eq!(s.agent_id("alice"), Some(0));
assert_eq!(s.agent_id("r2"), Some(1));
assert_eq!(s.agent_id("nobody"), None);
}
#[test]
fn an_undeclared_supertype_is_reported() {
let mut d = Diagnostics::default();
let ast = parse_file(
"types{ Actor - Nope } objects{} agents{} props{} initially{} actions{}",
&mut d,
);
let _ = Sig::build(&ast, &mut d);
assert!(d.items().iter().any(|x| x.message.contains("Nope")));
}
#[test]
fn an_object_of_an_undeclared_type_is_reported() {
let mut d = Diagnostics::default();
let ast = parse_file(
"types{} objects{ bob - Ghost } agents{} props{} initially{} actions{}",
&mut d,
);
let _ = Sig::build(&ast, &mut d);
assert!(d.items().iter().any(|x| x.message.contains("Ghost")));
}
#[test]
fn a_duplicate_agent_is_reported() {
let mut d = Diagnostics::default();
let ast = parse_file(
"types{} objects{ alice - Object } agents{ alice, alice } props{} initially{} actions{}",
&mut d,
);
let s = Sig::build(&ast, &mut d);
assert_eq!(s.n_agents(), 1, "the id space still dedups");
assert_eq!(d.len(), 1, "exactly one complaint, and it is the duplicate");
assert!(
d.items()[0].message.contains("duplicate agent `alice`"),
"got: {}",
d.items()[0].message
);
}
#[test]
fn an_agent_that_is_not_an_object_is_reported() {
let mut d = Diagnostics::default();
let ast =
parse_file("types{} objects{} agents{ ghost } props{} initially{} actions{}", &mut d);
let _ = Sig::build(&ast, &mut d);
assert!(d.items().iter().any(|x| x.message.contains("ghost")));
}
}