1mod collapse;
16mod corpus_overlay;
17mod fact;
18mod frame;
19mod logic;
20mod overlay;
21mod proof;
22mod register;
23mod summary;
24mod term;
25
26pub use collapse::{
27 collapse_proof, collapse_proof_with, render_collapsed_text, render_collapsed_text_with,
28 render_node_text,
29};
30pub use corpus_overlay::{DRUG_INTERACTIONS_OVERLAY, GDPR_OVERLAY, UTOPIA_OVERLAY};
31pub use fact::humanize_fact;
32pub use logic::{render_logic_buffer, render_logic_tree};
33pub use overlay::DomainGloss;
34pub use proof::{
35 RenderedNode, css_class, icon, label, render_proof, render_proof_text,
36 render_proof_text_indented, trace_display,
37};
38pub use register::Register;
39pub use summary::{fact_to_english, summarize_proof, summarize_proof_with};
40
41pub(crate) fn is_internal_relation(rel: &str) -> bool {
46 rel.starts_with("__abs_")
47}
48
49#[cfg(test)]
50mod tests {
51 use super::*;
52 use nibli_types::logic::{LogicBuffer, LogicNode, LogicalTerm};
53
54 fn syllogism_buffer() -> LogicBuffer {
58 let v0 = || LogicalTerm::Variable("_v0".to_string());
59 let ev0 = || LogicalTerm::Variable("_ev0".to_string());
60 let ev1 = || LogicalTerm::Variable("_ev1".to_string());
61 let nodes = vec![
62 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)), ];
78 LogicBuffer {
79 nodes,
80 roots: vec![14],
81 }
82 }
83
84 #[test]
85 fn syllogism_back_translation_is_readable_and_scope_exposing() {
86 let out = render_logic_buffer(&syllogism_buffer(), Register::Spec);
87 assert_eq!(out, "For every X, if X is a dog, then X is an animal.");
88 assert_ne!(out, "all the dog animal");
90 }
91
92 #[test]
93 fn utopia_floor_obligation_is_not_event_word_salad() {
94 let ast = nibli_kr::parse_checked("obligated_by(every person, event { secure() }).")
97 .expect("parse");
98 let buf = nibli_semantics::compile_from_ast(ast).expect("compile");
99 let out = render_logic_buffer(&buf, Register::Spec);
100 assert!(
101 out.to_lowercase().contains("obligated to be secure")
102 || out.to_lowercase().contains("obligated to be safe"),
103 "expected deontic+content collapse, got: {out}"
104 );
105 assert!(
106 !out.to_lowercase().contains("is event"),
107 "scaffolding 'event' leaked: {out}"
108 );
109 }
110
111 #[test]
115 fn entitled_keeps_the_holder_in_subject_position_and_hides_the_standard() {
116 let render = |t: &str| {
117 let ast = nibli_kr::parse_checked(t).expect("parse");
118 let buf = nibli_semantics::compile_from_ast(ast).expect("compile");
119 render_logic_buffer(&buf, Register::Spec)
120 };
121
122 assert_eq!(
128 render("entitled(Adam, Bread)."),
129 "Adam is entitled to Bread."
130 );
131 assert_eq!(
132 render("entitled(Adam, Bread, Law)."),
133 "Adam is entitled to Bread.",
134 "the `standard` place must not leak into the back-translation"
135 );
136
137 let floor = render("entitled(every person, event { eats() }).");
141 assert!(
142 floor.contains("X is entitled to"),
143 "the holder must stay in subject position, got: {floor}"
144 );
145 assert!(
146 !floor.contains("entitles"),
147 "the floor must not invert into an entitle-the-party reading: {floor}"
148 );
149 }
158
159 #[test]
160 fn lose_and_building_place_order_read_naturally() {
161 let lose = {
162 let ast = nibli_kr::parse_checked("lose(Points, Bela).").unwrap();
163 let buf = nibli_semantics::compile_from_ast(ast).unwrap();
164 render_logic_buffer(&buf, Register::Spec)
165 };
166 assert!(
167 lose.to_lowercase().contains("bela loses points"),
168 "got: {lose}"
169 );
170 let bld = {
171 let ast = nibli_kr::parse_checked("building(HighSec, Lalo).").unwrap();
172 let buf = nibli_semantics::compile_from_ast(ast).unwrap();
173 render_logic_buffer(&buf, Register::Spec)
174 };
175 assert!(
176 bld.to_lowercase().contains("lalo") && bld.to_lowercase().contains("highsec"),
177 "got: {bld}"
178 );
179 assert!(
180 bld.to_lowercase().contains("placed") || bld.to_lowercase().contains("housed"),
181 "got: {bld}"
182 );
183 }
184
185 #[test]
186 fn back_translation_exposes_scope() {
187 let out = render_logic_buffer(&syllogism_buffer(), Register::Spec);
188 assert!(
189 out.to_lowercase().contains("for every"),
190 "scope hidden: {out}"
191 );
192 assert!(
193 out.contains("if") && out.contains("then"),
194 "implication hidden: {out}"
195 );
196 assert!(out.contains("dog") && out.contains("animal"));
197 }
198
199 #[test]
200 fn spec_and_fluent_share_binder_order() {
201 let buf = syllogism_buffer();
202 let spec = render_logic_buffer(&buf, Register::Spec);
203 let fluent = render_logic_buffer(&buf, Register::Fluent);
204 let dog_before_animal = |s: &str| {
207 s.find("dog")
208 .zip(s.find("animal"))
209 .is_some_and(|(d, a)| d < a)
210 };
211 assert!(dog_before_animal(&spec), "spec: {spec}");
212 assert!(dog_before_animal(&fluent), "fluent: {fluent}");
213 }
214
215 #[test]
216 fn flat_fact_back_translation() {
217 let buf = LogicBuffer {
219 nodes: vec![LogicNode::Predicate((
220 "animal".into(),
221 vec![LogicalTerm::Constant("adam".into())],
222 ))],
223 roots: vec![0],
224 };
225 assert_eq!(
226 render_logic_buffer(&buf, Register::Spec),
227 "Adam is an animal."
228 );
229 }
230
231 #[test]
232 fn interior_unspecified_place_is_not_dropped() {
233 let ev0 = || LogicalTerm::Variable("_ev0".to_string());
237 let buf = LogicBuffer {
238 nodes: vec![
239 LogicNode::Predicate(("goes".into(), vec![ev0()])), LogicNode::Predicate(("goes_x1".into(), vec![ev0(), LogicalTerm::Unspecified])), LogicNode::Predicate((
242 "goes_x3".into(),
243 vec![ev0(), LogicalTerm::Description("market".into())],
244 )), LogicNode::AndNode((0, 1)), LogicNode::AndNode((3, 2)), LogicNode::ExistsNode(("_ev0".into(), 4)), ],
249 roots: vec![5],
250 };
251 let out = render_logic_buffer(&buf, Register::Spec);
252 assert!(
253 !out.is_empty(),
254 "an interior-unspecified frame must not render an empty gloss"
255 );
256 assert!(
257 out.to_lowercase().contains("something"),
258 "the unspecified x1 should render a generic filler, got: {out}"
259 );
260 }
261
262 #[test]
263 fn logic_tree_exposes_every_node_with_indentation() {
264 let tree = render_logic_tree(&syllogism_buffer(), Register::Spec);
265 assert!(tree.starts_with("\u{2200} _v0:\n"), "tree:\n{tree}");
268 assert!(tree.contains("\n Or:\n"), "tree:\n{tree}");
269 assert!(tree.contains("\n \u{00ac}:\n"), "tree:\n{tree}");
270 assert!(tree.contains("\n \u{2203} _ev0:\n"), "tree:\n{tree}");
271 assert!(tree.contains("dog(_ev0)\n"), "tree:\n{tree}");
273 assert!(tree.contains("dog_x1(_ev0, _v0)\n"), "tree:\n{tree}");
274 assert!(tree.contains("dog_x2(_ev0, something)\n"), "tree:\n{tree}");
275 assert!(!tree.contains("(Pred"), "S-expr leaked: {tree}");
276 assert!(!tree.contains("(Cons"), "S-expr leaked: {tree}");
277 }
278
279 #[test]
280 fn logic_tree_renders_compute_and_integers() {
281 let ev0 = || LogicalTerm::Variable("_ev0".to_string());
286 let buf = LogicBuffer {
287 nodes: vec![
288 LogicNode::ComputeNode(("exponential".into(), vec![ev0()])), LogicNode::Predicate((
290 "exponential_x1".into(),
291 vec![ev0(), LogicalTerm::Number(1024.0)],
292 )), LogicNode::AndNode((0, 1)), LogicNode::Predicate((
295 "exponential_x2".into(),
296 vec![ev0(), LogicalTerm::Number(2.0)],
297 )), LogicNode::AndNode((2, 3)), LogicNode::Predicate((
300 "exponential_x3".into(),
301 vec![ev0(), LogicalTerm::Number(10.0)],
302 )), LogicNode::AndNode((4, 5)), LogicNode::ExistsNode(("_ev0".into(), 6)), ],
306 roots: vec![7],
307 };
308 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";
309 assert_eq!(render_logic_tree(&buf, Register::Spec), expected);
310 }
311
312 #[test]
313 fn logic_tree_flat_fact() {
314 let buf = LogicBuffer {
315 nodes: vec![LogicNode::Predicate((
316 "animal".into(),
317 vec![LogicalTerm::Constant("adam".into())],
318 ))],
319 roots: vec![0],
320 };
321 assert_eq!(render_logic_tree(&buf, Register::Spec), "animal(adam)\n");
322 }
323
324 #[test]
325 fn logic_tree_invalid_root_is_reported() {
326 let buf = LogicBuffer {
327 nodes: vec![],
328 roots: vec![5],
329 };
330 assert_eq!(
331 render_logic_tree(&buf, Register::Spec),
332 "[invalid node 5]\n"
333 );
334 }
335}