1use std::collections::HashMap;
10
11use nibli_lexicon::get_gloss;
12use nibli_types::logic::{LogicBuffer, LogicNode, LogicalTerm};
13
14use crate::frame::{deontic_content_phrase, fill_template, frame_template};
15use crate::register::Register;
16use crate::term::{role_base, role_index};
17
18fn is_abstraction_scaffold(rel: &str) -> bool {
21 matches!(rel, "event" | "fact" | "property" | "amount" | "concept")
22}
23
24fn is_deontic_duty_rel(rel: &str) -> bool {
26 matches!(rel, "obligated_by" | "obliged")
27}
28
29pub fn render_logic_buffer(buf: &LogicBuffer, register: Register) -> String {
33 let mut ctx = Ctx::new(register);
34 let sentences: Vec<String> = buf
35 .roots
36 .iter()
37 .map(|&root| capitalize_terminate(&render_node(buf, root, &mut ctx)))
38 .filter(|s| !s.is_empty())
39 .collect();
40 sentences.join(" ")
41}
42
43pub fn render_logic_tree(buf: &LogicBuffer, _register: Register) -> String {
53 let mut out = String::with_capacity(256);
54 for (i, &root) in buf.roots.iter().enumerate() {
55 if i > 0 {
56 out.push('\n');
57 }
58 write_tree(&mut out, buf, root, 0);
59 }
60 out
61}
62
63fn write_tree(out: &mut String, buf: &LogicBuffer, id: u32, depth: usize) {
64 for _ in 0..depth {
65 out.push_str(" ");
66 }
67 let Some(node) = buf.nodes.get(id as usize) else {
68 out.push_str(&format!("[invalid node {id}]\n"));
69 return;
70 };
71 match node {
72 LogicNode::Predicate((rel, args)) => {
73 out.push_str(&format!("{rel}({})\n", render_term_list(args)));
74 }
75 LogicNode::ComputeNode((rel, args)) => {
76 out.push_str(&format!("{rel}({}) [compute]\n", render_term_list(args)));
77 }
78 LogicNode::AndNode((l, r)) => {
79 out.push_str("And:\n");
80 write_tree(out, buf, *l, depth + 1);
81 write_tree(out, buf, *r, depth + 1);
82 }
83 LogicNode::OrNode((l, r)) => {
84 out.push_str("Or:\n");
85 write_tree(out, buf, *l, depth + 1);
86 write_tree(out, buf, *r, depth + 1);
87 }
88 LogicNode::NotNode(inner) => {
89 out.push_str("\u{00ac}:\n"); write_tree(out, buf, *inner, depth + 1);
91 }
92 LogicNode::ExistsNode((v, body)) => {
93 out.push_str(&format!("\u{2203} {v}:\n")); write_tree(out, buf, *body, depth + 1);
95 }
96 LogicNode::ForAllNode((v, body)) => {
97 out.push_str(&format!("\u{2200} {v}:\n")); write_tree(out, buf, *body, depth + 1);
99 }
100 LogicNode::PastNode(inner) => {
101 out.push_str("Past:\n");
102 write_tree(out, buf, *inner, depth + 1);
103 }
104 LogicNode::PresentNode(inner) => {
105 out.push_str("Present:\n");
106 write_tree(out, buf, *inner, depth + 1);
107 }
108 LogicNode::FutureNode(inner) => {
109 out.push_str("Future:\n");
110 write_tree(out, buf, *inner, depth + 1);
111 }
112 LogicNode::ObligatoryNode(inner) => {
113 out.push_str("Obligatory:\n");
114 write_tree(out, buf, *inner, depth + 1);
115 }
116 LogicNode::PermittedNode(inner) => {
117 out.push_str("Permitted:\n");
118 write_tree(out, buf, *inner, depth + 1);
119 }
120 LogicNode::CountNode((v, count, body)) => {
121 out.push_str(&format!("Count {v} = {count}:\n"));
122 write_tree(out, buf, *body, depth + 1);
123 }
124 }
125}
126
127fn render_term_list(args: &[LogicalTerm]) -> String {
128 args.iter()
129 .map(render_tree_term)
130 .collect::<Vec<_>>()
131 .join(", ")
132}
133
134fn render_tree_term(t: &LogicalTerm) -> String {
135 match t {
136 LogicalTerm::Variable(v) => v.clone(),
137 LogicalTerm::Constant(c) => c.clone(),
138 LogicalTerm::Description(d) => format!("the {d}"),
139 LogicalTerm::Unspecified => "something".to_string(),
140 LogicalTerm::Number(n) => format_number(*n),
141 }
142}
143
144struct Ctx {
145 #[allow(dead_code)]
146 register: Register,
147 var_names: HashMap<String, String>,
148}
149
150impl Ctx {
151 fn new(register: Register) -> Self {
152 Ctx {
153 register,
154 var_names: HashMap::new(),
155 }
156 }
157
158 fn var_name(&mut self, raw: &str) -> String {
160 if let Some(v) = self.var_names.get(raw) {
161 return v.clone();
162 }
163 const LETTERS: &[&str] = &["X", "Y", "Z", "W", "V", "U", "T", "S"];
164 let n = self.var_names.len();
165 let name = LETTERS
166 .get(n)
167 .map(|s| s.to_string())
168 .unwrap_or_else(|| format!("X{n}"));
169 self.var_names.insert(raw.to_string(), name.clone());
170 name
171 }
172}
173
174fn render_node(buf: &LogicBuffer, id: u32, ctx: &mut Ctx) -> String {
175 let Some(node) = buf.nodes.get(id as usize) else {
176 return String::new();
177 };
178 match node {
179 LogicNode::ForAllNode((var, body)) => render_forall(buf, var, *body, ctx),
180 LogicNode::CountNode((var, count, body)) => {
181 let _ = ctx.var_name(var);
182 format!(
183 "exactly {} things are such that {}",
184 count,
185 render_node(buf, *body, ctx)
186 )
187 }
188 LogicNode::OrNode((l, r)) => {
189 format!(
190 "either {} or {}",
191 render_node(buf, *l, ctx),
192 render_node(buf, *r, ctx)
193 )
194 }
195 LogicNode::NotNode(inner) => {
196 format!("it is not the case that {}", render_node(buf, *inner, ctx))
197 }
198 LogicNode::PastNode(inner) => format!("in the past, {}", render_node(buf, *inner, ctx)),
199 LogicNode::PresentNode(inner) => format!("currently, {}", render_node(buf, *inner, ctx)),
200 LogicNode::FutureNode(inner) => format!("in the future, {}", render_node(buf, *inner, ctx)),
201 LogicNode::ObligatoryNode(inner) => {
202 format!("it is required that {}", render_node(buf, *inner, ctx))
203 }
204 LogicNode::PermittedNode(inner) => {
205 format!("it is permitted that {}", render_node(buf, *inner, ctx))
206 }
207 LogicNode::AndNode(_)
210 | LogicNode::ExistsNode(_)
211 | LogicNode::Predicate(_)
212 | LogicNode::ComputeNode(_) => render_conjunction(buf, id, ctx),
213 }
214}
215
216fn render_forall(buf: &LogicBuffer, var: &str, body: u32, ctx: &mut Ctx) -> String {
219 let x = ctx.var_name(var); if let Some(LogicNode::OrNode((l, r))) = buf.nodes.get(body as usize)
221 && let Some(LogicNode::NotNode(ante)) = buf.nodes.get(*l as usize)
222 {
223 let antecedent = render_node(buf, *ante, ctx);
224 let consequent = render_node(buf, *r, ctx);
225 return format!("for every {x}, if {antecedent}, then {consequent}");
226 }
227 let inner = render_node(buf, body, ctx);
228 format!("for every {x}, {inner}")
229}
230
231fn render_conjunction(buf: &LogicBuffer, id: u32, ctx: &mut Ctx) -> String {
234 let mut preds: Vec<(String, Vec<LogicalTerm>)> = Vec::new();
235 let mut extras: Vec<String> = Vec::new();
236 collect(buf, id, ctx, &mut preds, &mut extras);
237
238 let mut clauses = build_frames(&preds, ctx);
239 clauses.extend(extras);
240 join_clauses(&clauses)
241}
242
243fn collect(
244 buf: &LogicBuffer,
245 id: u32,
246 ctx: &mut Ctx,
247 preds: &mut Vec<(String, Vec<LogicalTerm>)>,
248 extras: &mut Vec<String>,
249) {
250 let Some(node) = buf.nodes.get(id as usize) else {
251 return;
252 };
253 match node {
254 LogicNode::AndNode((l, r)) => {
255 collect(buf, *l, ctx, preds, extras);
256 collect(buf, *r, ctx, preds, extras);
257 }
258 LogicNode::ExistsNode((_var, body)) => collect(buf, *body, ctx, preds, extras),
261 LogicNode::Predicate((rel, args)) | LogicNode::ComputeNode((rel, args)) => {
262 if crate::is_internal_relation(rel) {
265 return;
266 }
267 preds.push((rel.clone(), args.clone()));
268 }
269 _ => extras.push(render_node(buf, id, ctx)),
272 }
273}
274
275struct FrameAcc {
277 base: String,
278 places: HashMap<usize, LogicalTerm>,
280 flat_args: Vec<LogicalTerm>,
282 has_roles: bool,
283}
284
285fn term_key(t: Option<&LogicalTerm>) -> String {
286 match t {
287 Some(LogicalTerm::Variable(s)) => format!("v:{s}"),
288 Some(LogicalTerm::Constant(s)) => format!("c:{s}"),
289 Some(LogicalTerm::Description(s)) => format!("d:{s}"),
290 Some(LogicalTerm::Number(n)) => format!("n:{n}"),
291 Some(LogicalTerm::Unspecified) => "u".to_string(),
292 None => "_".to_string(),
293 }
294}
295
296fn build_frames(preds: &[(String, Vec<LogicalTerm>)], ctx: &mut Ctx) -> Vec<String> {
297 let mut order: Vec<(String, String)> = Vec::new();
298 let mut map: HashMap<(String, String), FrameAcc> = HashMap::new();
299
300 for (rel, args) in preds {
301 if let (Some(base), Some(idx)) = (role_base(rel), role_index(rel)) {
302 let key = (term_key(args.first()), base.to_string());
304 let acc = map.entry(key.clone()).or_insert_with(|| {
305 order.push(key.clone());
306 FrameAcc {
307 base: base.to_string(),
308 places: HashMap::new(),
309 flat_args: Vec::new(),
310 has_roles: false,
311 }
312 });
313 acc.has_roles = true;
314 if let Some(filler) = args.get(1) {
315 acc.places.insert(idx, filler.clone());
316 }
317 } else {
318 let key = (term_key(args.first()), rel.clone());
320 let acc = map.entry(key.clone()).or_insert_with(|| {
321 order.push(key.clone());
322 FrameAcc {
323 base: rel.clone(),
324 places: HashMap::new(),
325 flat_args: Vec::new(),
326 has_roles: false,
327 }
328 });
329 if acc.flat_args.is_empty() {
330 acc.flat_args = args.clone();
331 }
332 }
333 }
334
335 let accs: Vec<FrameAcc> = order
336 .into_iter()
337 .filter_map(|key| map.remove(&key))
338 .collect();
339 let accs = collapse_deontic_event_duties(accs);
340 accs.into_iter()
341 .map(|acc| render_frame(&acc, ctx))
342 .filter(|s| !s.is_empty())
343 .collect()
344}
345
346fn collapse_deontic_event_duties(accs: Vec<FrameAcc>) -> Vec<FrameAcc> {
350 let has_scaffold = accs.iter().any(|a| is_abstraction_scaffold(&a.base));
351 let has_deontic = accs.iter().any(|a| is_deontic_duty_rel(&a.base));
352 if !has_scaffold || !has_deontic {
353 return accs;
354 }
355
356 let content: Vec<String> = accs
357 .iter()
358 .filter(|a| !is_deontic_duty_rel(&a.base) && !is_abstraction_scaffold(&a.base))
359 .map(|a| deontic_content_phrase(&a.base))
360 .collect();
361 if content.is_empty() {
362 return accs;
363 }
364 let content_joined = match content.len() {
365 1 => content[0].clone(),
366 2 => format!("{} and {}", content[0], content[1]),
367 _ => {
368 let head = &content[..content.len() - 1];
369 format!("{}, and {}", head.join(", "), content[content.len() - 1])
370 }
371 };
372
373 let mut out = Vec::new();
377 for acc in &accs {
378 if !is_deontic_duty_rel(&acc.base) {
379 continue;
380 }
381 let who = acc
382 .places
383 .get(&2)
384 .cloned()
385 .or_else(|| acc.flat_args.get(1).cloned());
386 let mut places = HashMap::new();
387 places.insert(1, LogicalTerm::Constant(content_joined.clone()));
388 if let Some(w) = who {
389 places.insert(2, w);
390 }
391 out.push(FrameAcc {
392 base: "obligated_by".into(),
393 places,
394 flat_args: Vec::new(),
395 has_roles: true,
396 });
397 }
398 if out.is_empty() { accs } else { out }
400}
401
402fn render_frame(acc: &FrameAcc, ctx: &mut Ctx) -> String {
403 if is_deontic_duty_rel(&acc.base)
407 && let (Some(LogicalTerm::Constant(content)), Some(who_term)) =
408 (acc.places.get(&1), acc.places.get(&2))
409 && let Some(who) = render_term(who_term, ctx)
410 {
411 if content.starts_with("be ")
413 || !content.contains(" is ")
414 && !content.chars().next().is_some_and(|c| c.is_uppercase())
415 {
416 return format!("{who} is obligated to {content}");
417 }
418 return format!("{who} is obligated that {content}");
419 }
420 let places: Vec<Option<String>> = if acc.has_roles {
421 let max_idx = acc.places.keys().copied().max().unwrap_or(0);
422 (1..=max_idx)
423 .map(|i| acc.places.get(&i).and_then(|t| render_term(t, ctx)))
424 .collect()
425 } else {
426 acc.flat_args.iter().map(|t| render_term(t, ctx)).collect()
427 };
428 fill_template(&frame_template(&acc.base), &places)
429}
430
431fn render_term(t: &LogicalTerm, ctx: &mut Ctx) -> Option<String> {
433 match t {
434 LogicalTerm::Constant(s) => Some(display_constant(s)),
435 LogicalTerm::Description(s) => Some(format!("the {}", get_gloss(s).unwrap_or(s.as_str()))),
436 LogicalTerm::Variable(n) => Some(ctx.var_name(n)),
437 LogicalTerm::Number(n) => Some(format_number(*n)),
438 LogicalTerm::Unspecified => None,
439 }
440}
441
442fn display_constant(s: &str) -> String {
445 let mut chars = s.chars();
446 match chars.next() {
447 Some(c)
448 if c.is_ascii_lowercase() && s.bytes().all(|b| b.is_ascii_lowercase() || b == b'_') =>
449 {
450 format!("{}{}", c.to_ascii_uppercase(), chars.as_str())
452 }
453 Some(c) => format!("{c}{}", chars.as_str()),
454 None => String::new(),
455 }
456}
457
458fn format_number(n: f64) -> String {
459 if n == (n as i64) as f64 {
460 format!("{}", n as i64)
461 } else {
462 format!("{n}")
463 }
464}
465
466fn join_clauses(clauses: &[String]) -> String {
467 match clauses.len() {
468 0 => String::new(),
469 1 => clauses[0].clone(),
470 2 => format!("{} and {}", clauses[0], clauses[1]),
471 _ => {
472 let head = &clauses[..clauses.len() - 1];
473 format!("{}, and {}", head.join(", "), clauses[clauses.len() - 1])
474 }
475 }
476}
477
478fn capitalize_terminate(s: &str) -> String {
479 let s = s.trim();
480 if s.is_empty() {
481 return String::new();
482 }
483 let mut chars = s.chars();
484 let first = chars.next().unwrap().to_uppercase().to_string();
485 let mut out = format!("{first}{}", chars.as_str());
486 if !out.ends_with('.') {
487 out.push('.');
488 }
489 out
490}