1use crate::term::{collapse_role_name, humanize_skolem, is_event_skolem};
16
17pub fn humanize_fact(input: &str) -> String {
22 let trimmed = input.trim();
23 if trimmed.is_empty() {
24 return String::new();
25 }
26 if trimmed.starts_with('(') {
30 return trimmed.to_string();
31 }
32 let tokens = tokenize(trimmed);
33 let mut pos = 0;
34 match parse_fact(&tokens, &mut pos) {
35 Some(rendered) if pos == tokens.len() => rendered,
36 _ => trimmed.to_string(),
37 }
38}
39
40#[derive(Clone, Debug, PartialEq)]
41enum Tok {
42 Ident(String),
43 LParen,
44 RParen,
45 Comma,
46}
47
48fn tokenize(s: &str) -> Vec<Tok> {
49 let mut toks = Vec::new();
50 let mut buf = String::new();
51 for c in s.chars() {
52 match c {
53 '(' | ')' | ',' => {
54 let t = buf.trim();
55 if !t.is_empty() {
56 toks.push(Tok::Ident(t.to_string()));
57 }
58 buf.clear();
59 toks.push(match c {
60 '(' => Tok::LParen,
61 ')' => Tok::RParen,
62 _ => Tok::Comma,
63 });
64 }
65 _ => buf.push(c),
66 }
67 }
68 let t = buf.trim();
69 if !t.is_empty() {
70 toks.push(Tok::Ident(t.to_string()));
71 }
72 toks
73}
74
75fn is_wrapper(name: &str) -> Option<&'static str> {
76 match name {
77 "Past" => Some("past"),
78 "Present" => Some("present"),
79 "Future" => Some("future"),
80 "Obligatory" => Some("obligatory"),
81 "Permitted" => Some("permitted"),
82 _ => None,
83 }
84}
85
86fn parse_fact(tokens: &[Tok], pos: &mut usize) -> Option<String> {
88 if let Some(Tok::Ident(name)) = tokens.get(*pos)
89 && let Some(label) = is_wrapper(name)
90 && tokens.get(*pos + 1) == Some(&Tok::LParen)
91 {
92 *pos += 2; let inner = parse_fact(tokens, pos)?;
94 if tokens.get(*pos) != Some(&Tok::RParen) {
95 return None;
96 }
97 *pos += 1;
98 return Some(format!("[{label}] {inner}"));
99 }
100 parse_predicate(tokens, pos)
101}
102
103fn parse_predicate(tokens: &[Tok], pos: &mut usize) -> Option<String> {
105 let Some(Tok::Ident(name)) = tokens.get(*pos) else {
106 return None;
107 };
108 let name = name.clone();
109 *pos += 1;
110 let args = if tokens.get(*pos) == Some(&Tok::LParen) {
111 *pos += 1;
112 let items = parse_term_list(tokens, pos)?;
113 if tokens.get(*pos) != Some(&Tok::RParen) {
114 return None;
115 }
116 *pos += 1;
117 items
118 } else {
119 Vec::new()
120 };
121 Some(render_predicate(&name, &args))
122}
123
124fn parse_term_list(tokens: &[Tok], pos: &mut usize) -> Option<Vec<String>> {
126 let mut items = Vec::new();
127 if tokens.get(*pos) == Some(&Tok::RParen) {
128 return Some(items);
129 }
130 loop {
131 items.push(parse_term(tokens, pos)?);
132 match tokens.get(*pos) {
133 Some(Tok::Comma) => {
134 *pos += 1;
135 }
136 Some(Tok::RParen) => break,
137 _ => return None,
138 }
139 }
140 Some(items)
141}
142
143fn parse_term(tokens: &[Tok], pos: &mut usize) -> Option<String> {
150 match tokens.get(*pos) {
151 Some(Tok::LParen) => {
152 *pos += 1;
153 let items = parse_term_list(tokens, pos)?;
154 if tokens.get(*pos) != Some(&Tok::RParen) {
155 return None;
156 }
157 *pos += 1;
158 Some(format!("({})", items.join(", ")))
159 }
160 Some(Tok::Ident(name)) => {
161 let name = name.clone();
162 *pos += 1;
163 if tokens.get(*pos) == Some(&Tok::LParen) {
164 *pos += 1;
165 let items = parse_term_list(tokens, pos)?;
166 if tokens.get(*pos) != Some(&Tok::RParen) {
167 return None;
168 }
169 *pos += 1;
170 Some(format!("{}({})", name, items.join(", ")))
171 } else {
172 Some(name)
173 }
174 }
175 _ => None,
176 }
177}
178
179fn render_predicate(name: &str, raw_args: &[String]) -> String {
182 let display_name = collapse_role_name(name).unwrap_or_else(|| name.to_string());
183 let kept: Vec<String> = raw_args
184 .iter()
185 .filter(|a| !is_event_skolem(a))
186 .map(|a| humanize_skolem(a))
187 .collect();
188 if kept.is_empty() {
189 display_name
190 } else {
191 format!("{}({})", display_name, kept.join(", "))
192 }
193}
194
195#[cfg(test)]
196mod tests {
197 use super::*;
198
199 #[test]
200 fn simple_flat_predicate() {
201 assert_eq!(humanize_fact("animal(adam)"), "animal(adam)");
202 }
203
204 #[test]
205 fn type_predicate_hides_event_skolem() {
206 assert_eq!(humanize_fact("dog(sk_2)"), "dog");
208 }
209
210 #[test]
211 fn role_predicate_collapses_and_keeps_filler() {
212 assert_eq!(humanize_fact("dog_x1(sk_2, adam)"), "dog.dog(adam)");
214 }
215
216 #[test]
217 fn witness_skolem_becomes_hash() {
218 assert_eq!(humanize_fact("animal(sk_1(adam))"), "animal(#1(adam))");
219 }
220
221 #[test]
222 fn dep_pair_skolem() {
223 assert_eq!(
224 humanize_fact("nelci_x2(sk_5, sk_2(adam, bob))"),
225 "nelci.x2(#2(adam, bob))"
226 );
227 }
228
229 #[test]
230 fn tense_wrapper() {
231 assert_eq!(
232 humanize_fact("Past(goes(adam, paris))"),
233 "[past] goes(adam, paris)"
234 );
235 }
236
237 #[test]
238 fn deontic_wrapper() {
239 assert_eq!(
240 humanize_fact("Obligatory(permits(adam))"),
241 "[obligatory] permits(adam)"
242 );
243 }
244
245 #[test]
246 fn unspecified_and_description_args() {
247 assert_eq!(humanize_fact("goes(adam, _)"), "goes(adam, _)");
248 assert_eq!(humanize_fact("goes(the dog)"), "goes(the dog)");
249 }
250
251 #[test]
252 fn zero_arg_predicate() {
253 assert_eq!(humanize_fact("dog"), "dog");
254 }
255
256 #[test]
257 fn stray_s_expr_passes_through() {
258 let s = r#"(Pred "dog" (Cons (Const "adam") (Nil)))"#;
259 assert_eq!(humanize_fact(s), s);
260 }
261
262 #[test]
263 fn empty_is_empty() {
264 assert_eq!(humanize_fact(""), "");
265 }
266}