mod collapse;
mod corpus_overlay;
mod fact;
mod frame;
mod logic;
mod overlay;
mod proof;
mod register;
mod summary;
mod term;
pub use collapse::{
collapse_proof, collapse_proof_with, render_collapsed_text, render_collapsed_text_with,
render_node_text,
};
pub use corpus_overlay::{DRUG_INTERACTIONS_OVERLAY, GDPR_OVERLAY, UTOPIA_OVERLAY};
pub use fact::humanize_fact;
pub use logic::{render_logic_buffer, render_logic_tree};
pub use overlay::DomainGloss;
pub use proof::{
RenderedNode, css_class, icon, label, render_proof, render_proof_text,
render_proof_text_indented, trace_display,
};
pub use register::Register;
pub use summary::{fact_to_english, summarize_proof, summarize_proof_with};
pub(crate) fn is_internal_relation(rel: &str) -> bool {
rel.starts_with("__abs_")
}
#[cfg(test)]
mod tests {
use super::*;
use nibli_types::logic::{LogicBuffer, LogicNode, LogicalTerm};
fn syllogism_buffer() -> LogicBuffer {
let v0 = || LogicalTerm::Variable("_v0".to_string());
let ev0 = || LogicalTerm::Variable("_ev0".to_string());
let ev1 = || LogicalTerm::Variable("_ev1".to_string());
let nodes = vec![
LogicNode::Predicate(("dog".into(), vec![ev0()])), LogicNode::Predicate(("dog_x1".into(), vec![ev0(), v0()])), LogicNode::Predicate(("dog_x2".into(), vec![ev0(), LogicalTerm::Unspecified])), LogicNode::AndNode((0, 1)), LogicNode::AndNode((3, 2)), LogicNode::ExistsNode(("_ev0".into(), 4)), LogicNode::NotNode(5), LogicNode::Predicate(("animal".into(), vec![ev1()])), LogicNode::Predicate(("animal_x1".into(), vec![ev1(), v0()])), LogicNode::Predicate(("animal_x2".into(), vec![ev1(), LogicalTerm::Unspecified])), LogicNode::AndNode((7, 8)), LogicNode::AndNode((10, 9)), LogicNode::ExistsNode(("_ev1".into(), 11)), LogicNode::OrNode((6, 12)), LogicNode::ForAllNode(("_v0".into(), 13)), ];
LogicBuffer {
nodes,
roots: vec![14],
}
}
#[test]
fn syllogism_back_translation_is_readable_and_scope_exposing() {
let out = render_logic_buffer(&syllogism_buffer(), Register::Spec);
assert_eq!(out, "For every X, if X is a dog, then X is an animal.");
assert_ne!(out, "all the dog animal");
}
#[test]
fn utopia_floor_obligation_is_not_event_word_salad() {
let ast = nibli_kr::parse_checked("obligated_by(every person, event { secure() }).")
.expect("parse");
let buf = nibli_semantics::compile_from_ast(ast).expect("compile");
let out = render_logic_buffer(&buf, Register::Spec);
assert!(
out.to_lowercase().contains("obligated to be secure")
|| out.to_lowercase().contains("obligated to be safe"),
"expected deontic+content collapse, got: {out}"
);
assert!(
!out.to_lowercase().contains("is event"),
"scaffolding 'event' leaked: {out}"
);
}
#[test]
fn entitled_keeps_the_holder_in_subject_position_and_hides_the_standard() {
let render = |t: &str| {
let ast = nibli_kr::parse_checked(t).expect("parse");
let buf = nibli_semantics::compile_from_ast(ast).expect("compile");
render_logic_buffer(&buf, Register::Spec)
};
assert_eq!(
render("entitled(Adam, Bread)."),
"Adam is entitled to Bread."
);
assert_eq!(
render("entitled(Adam, Bread, Law)."),
"Adam is entitled to Bread.",
"the `standard` place must not leak into the back-translation"
);
let floor = render("entitled(every person, event { eats() }).");
assert!(
floor.contains("X is entitled to"),
"the holder must stay in subject position, got: {floor}"
);
assert!(
!floor.contains("entitles"),
"the floor must not invert into an entitle-the-party reading: {floor}"
);
}
#[test]
fn lose_and_building_place_order_read_naturally() {
let lose = {
let ast = nibli_kr::parse_checked("lose(Points, Bela).").unwrap();
let buf = nibli_semantics::compile_from_ast(ast).unwrap();
render_logic_buffer(&buf, Register::Spec)
};
assert!(
lose.to_lowercase().contains("bela loses points"),
"got: {lose}"
);
let bld = {
let ast = nibli_kr::parse_checked("building(HighSec, Lalo).").unwrap();
let buf = nibli_semantics::compile_from_ast(ast).unwrap();
render_logic_buffer(&buf, Register::Spec)
};
assert!(
bld.to_lowercase().contains("lalo") && bld.to_lowercase().contains("highsec"),
"got: {bld}"
);
assert!(
bld.to_lowercase().contains("placed") || bld.to_lowercase().contains("housed"),
"got: {bld}"
);
}
#[test]
fn back_translation_exposes_scope() {
let out = render_logic_buffer(&syllogism_buffer(), Register::Spec);
assert!(
out.to_lowercase().contains("for every"),
"scope hidden: {out}"
);
assert!(
out.contains("if") && out.contains("then"),
"implication hidden: {out}"
);
assert!(out.contains("dog") && out.contains("animal"));
}
#[test]
fn spec_and_fluent_share_binder_order() {
let buf = syllogism_buffer();
let spec = render_logic_buffer(&buf, Register::Spec);
let fluent = render_logic_buffer(&buf, Register::Fluent);
let dog_before_animal = |s: &str| {
s.find("dog")
.zip(s.find("animal"))
.is_some_and(|(d, a)| d < a)
};
assert!(dog_before_animal(&spec), "spec: {spec}");
assert!(dog_before_animal(&fluent), "fluent: {fluent}");
}
#[test]
fn flat_fact_back_translation() {
let buf = LogicBuffer {
nodes: vec![LogicNode::Predicate((
"animal".into(),
vec![LogicalTerm::Constant("adam".into())],
))],
roots: vec![0],
};
assert_eq!(
render_logic_buffer(&buf, Register::Spec),
"Adam is an animal."
);
}
#[test]
fn interior_unspecified_place_is_not_dropped() {
let ev0 = || LogicalTerm::Variable("_ev0".to_string());
let buf = LogicBuffer {
nodes: vec![
LogicNode::Predicate(("goes".into(), vec![ev0()])), LogicNode::Predicate(("goes_x1".into(), vec![ev0(), LogicalTerm::Unspecified])), LogicNode::Predicate((
"goes_x3".into(),
vec![ev0(), LogicalTerm::Description("market".into())],
)), LogicNode::AndNode((0, 1)), LogicNode::AndNode((3, 2)), LogicNode::ExistsNode(("_ev0".into(), 4)), ],
roots: vec![5],
};
let out = render_logic_buffer(&buf, Register::Spec);
assert!(
!out.is_empty(),
"an interior-unspecified frame must not render an empty gloss"
);
assert!(
out.to_lowercase().contains("something"),
"the unspecified x1 should render a generic filler, got: {out}"
);
}
#[test]
fn logic_tree_exposes_every_node_with_indentation() {
let tree = render_logic_tree(&syllogism_buffer(), Register::Spec);
assert!(tree.starts_with("\u{2200} _v0:\n"), "tree:\n{tree}");
assert!(tree.contains("\n Or:\n"), "tree:\n{tree}");
assert!(tree.contains("\n \u{00ac}:\n"), "tree:\n{tree}");
assert!(tree.contains("\n \u{2203} _ev0:\n"), "tree:\n{tree}");
assert!(tree.contains("dog(_ev0)\n"), "tree:\n{tree}");
assert!(tree.contains("dog_x1(_ev0, _v0)\n"), "tree:\n{tree}");
assert!(tree.contains("dog_x2(_ev0, something)\n"), "tree:\n{tree}");
assert!(!tree.contains("(Pred"), "S-expr leaked: {tree}");
assert!(!tree.contains("(Cons"), "S-expr leaked: {tree}");
}
#[test]
fn logic_tree_renders_compute_and_integers() {
let ev0 = || LogicalTerm::Variable("_ev0".to_string());
let buf = LogicBuffer {
nodes: vec![
LogicNode::ComputeNode(("exponential".into(), vec![ev0()])), LogicNode::Predicate((
"exponential_x1".into(),
vec![ev0(), LogicalTerm::Number(1024.0)],
)), LogicNode::AndNode((0, 1)), LogicNode::Predicate((
"exponential_x2".into(),
vec![ev0(), LogicalTerm::Number(2.0)],
)), LogicNode::AndNode((2, 3)), LogicNode::Predicate((
"exponential_x3".into(),
vec![ev0(), LogicalTerm::Number(10.0)],
)), LogicNode::AndNode((4, 5)), LogicNode::ExistsNode(("_ev0".into(), 6)), ],
roots: vec![7],
};
let expected = "\u{2203} _ev0:\n And:\n And:\n And:\n exponential(_ev0) [compute]\n exponential_x1(_ev0, 1024)\n exponential_x2(_ev0, 2)\n exponential_x3(_ev0, 10)\n";
assert_eq!(render_logic_tree(&buf, Register::Spec), expected);
}
#[test]
fn logic_tree_flat_fact() {
let buf = LogicBuffer {
nodes: vec![LogicNode::Predicate((
"animal".into(),
vec![LogicalTerm::Constant("adam".into())],
))],
roots: vec![0],
};
assert_eq!(render_logic_tree(&buf, Register::Spec), "animal(adam)\n");
}
#[test]
fn logic_tree_invalid_root_is_reported() {
let buf = LogicBuffer {
nodes: vec![],
roots: vec![5],
};
assert_eq!(
render_logic_tree(&buf, Register::Spec),
"[invalid node 5]\n"
);
}
}