nibli_semantics/semantic/compile.rs
1//! Proposition and sentence compilation: the main compilation entry points.
2//!
3//! Compiles predication nodes and sentence connectives into FOL. Handles place
4//! tags, modal tags (the `via` custom modal), quantifier closure, existential
5//! wrapping, tense wrappers, and deontic moods.
6use super::*;
7
8impl SemanticCompiler {
9 /// Compiles a proposition (predication) into FOL with quantifier scoping and tense wrapping.
10 pub fn compile_proposition(
11 &mut self,
12 proposition: &Proposition,
13 predicates: &[Predicate],
14 arguments: &[Argument],
15 sentences: &[Sentence],
16 ) -> IrForm {
17 // Frame-local checkpoint for rel clauses attached to non-quantifier
18 // argument (see `pending_matrix_conjuncts`): only conjuncts pushed by
19 // THIS proposition's argument are drained into THIS proposition's matrix; nested
20 // proposition (rel clause bodies, abstractions) drain their own.
21 let matrix_conjunct_checkpoint = self.pending_matrix_conjuncts.len();
22 // Frame-scoped `ma` closure: each `compile_proposition` frame drains only the
23 // `ma` vars pushed during ITS frame (see the drain near the end). A
24 // nested proposition (rel-clause body, abstraction body) takes its own
25 // checkpoint AFTER any ancestor pushes, so it can no longer steal an
26 // enclosing proposition's pending `ma` var (mirrors `matrix_conjunct_checkpoint`).
27 let ma_checkpoint = self.question_vars.len();
28
29 let target_arity = self.get_predicate_arity(proposition.relation, predicates);
30
31 let mut positioned: Vec<Option<IrTerm>> = vec![None; target_arity];
32
33 // A relative clause's implicit `it` subject occupies x1 (the CLL
34 // default), pushing the clause's explicit argument to x2+. Place it as the
35 // x1 ARGUMENT here — BEFORE `apply_predicate` runs any `se`/`te`/`ve`/`xe`
36 // conversion — so `poi se prami la .alis.` routes `it` through the
37 // conversion to the correct underlying role (prami_x2), exactly as an
38 // explicit subject would. One-shot: the first x1-implicit proposition
39 // (`x1_present == false`) WITHOUT its own explicit `it` consumes it;
40 // nested proposition (abstraction bodies)
41 // see `None`. Marking `ref_used` makes the caller skip the post-hoc
42 // `inject_variable`, which cannot see conversion and would refill the
43 // vacated x1 slot.
44 //
45 // SKIP RULE: when the proposition's own direct terms already carry an
46 // explicit `it` — bare, or under a `fa`..`fu` place tag (the shape the
47 // KR front-end emits for all-named args: x1 implicit + FA-tagged terms,
48 // e.g. `where loves(lover: Alis, loved: it)`) — the user has placed the
49 // clause variable, and injecting would double-fill x1: a hard "place
50 // already filled" reject for a place-tagged `it` colliding with the
51 // pre-fill, or a silently self-referring x1 for a lone tagged `it`.
52 // The explicit `it` resolves in the term loop below and sets `ref_used`
53 // itself, exactly like the positional spelling (whose explicit x1
54 // never reaches this branch) — so named ≡ positional, including leaving
55 // `pending_clause_subject` untouched. The scan is SHALLOW: a `it`
56 // nested in a Description/Restricted/Abstraction belongs to the inner
57 // clause; a BAI-modal-carried `it` (`ModalTagged`) is not place-filling
58 // and keeps the implicit-x1 default.
59 if !proposition.x1_present
60 && target_arity >= 1
61 && self.pending_clause_subject.is_some()
62 && !Self::terms_contain_explicit_kea(&proposition.terms, arguments)
63 {
64 if let Some(subject) = self.pending_clause_subject.take() {
65 positioned[0] = Some(IrTerm::Variable(subject));
66 self.ref_used = true;
67 }
68 }
69
70 // Surface-ordered scope introductions (descriptions + bare da/de/di),
71 // folded in reverse below so the leftmost binder is outermost.
72 let mut markers: Vec<ScopeMarker> = Vec::new();
73 // da/de/di already recorded as a `Bare` marker — dedups a co-referring
74 // `da` and lets the safety-net residual pass skip surface-captured vars.
75 let mut introduced: std::collections::HashSet<lasso::Spur> =
76 std::collections::HashSet::new();
77 let mut modal_entries: Vec<(ModalTag, IrTerm, Vec<QuantifierEntry>)> = Vec::new();
78 // Untagged argument that overflowed the predicate's arity (placed nowhere).
79 // Preserves the prior silent-drop behaviour for over-arity untagged
80 // argument, and drives the fail-closed `du` n-ary check below.
81 let mut overflow_untagged: usize = 0;
82 // CLL place counter (CLL ch.9, FA cmavo): `fa/fe/fi/fo/fu` set the place
83 // number; a following UNTAGGED argument fills the place AFTER the last tag,
84 // not the first free slot. Starts at x1 and skips slots already filled
85 // (a `it` x1 pre-fill, or an out-of-order tag).
86 let mut next_place: usize = 0;
87
88 for &term_id in &proposition.terms {
89 match &arguments[term_id as usize] {
90 Argument::Tagged((tag, inner_id)) => {
91 let inner = &arguments[*inner_id as usize];
92 let (term, quants) =
93 self.resolve_argument(inner, arguments, predicates, sentences);
94 self.record_bare_marker(&term, &mut introduced, &mut markers);
95 markers.extend(quants.into_iter().map(ScopeMarker::Desc));
96 let idx = *tag as usize;
97 if idx >= target_arity {
98 // FAIL CLOSED: a named argument beyond the predicate's arity
99 // has no slot to bind into. Silently dropping the tagged term
100 // loses meaning (panel finding 2026-06-10) — reject instead.
101 self.errors.push(format!(
102 "A named argument targets place x{}, but the predicate only has \
103 {} place(s); it cannot be placed.",
104 idx + 1,
105 target_arity
106 ));
107 } else if positioned[idx].is_some() {
108 // FAIL CLOSED: a named argument re-targeting an already-filled
109 // place would last-win and drop the earlier term.
110 self.errors.push(format!(
111 "A named argument targets place x{}, which is already filled; \
112 the same place cannot be set twice.",
113 idx + 1
114 ));
115 } else {
116 positioned[idx] = Some(term);
117 next_place = idx + 1; // CLL: resume AFTER the tagged place
118 }
119 }
120 Argument::ModalTagged((modal_tag, inner_id)) => {
121 let inner = &arguments[*inner_id as usize];
122 let (term, quants) =
123 self.resolve_argument(inner, arguments, predicates, sentences);
124 // A bare `da`/`de`/`di` carried by a BAI modal (`ri'a da`) is
125 // introduced at this surface position. Its description quants
126 // (rare: `ri'a lo broda`) stay innermost (appended after the
127 // loop, with the modal predicate).
128 self.record_bare_marker(&term, &mut introduced, &mut markers);
129 // BAI modals are not place-filling — they do NOT advance the
130 // place counter.
131 modal_entries.push((*modal_tag, term, quants));
132 }
133 other => {
134 let (term, quants) =
135 self.resolve_argument(other, arguments, predicates, sentences);
136 self.record_bare_marker(&term, &mut introduced, &mut markers);
137 markers.extend(quants.into_iter().map(ScopeMarker::Desc));
138 // Skip slots already filled (`it` x1, or an out-of-order tag),
139 // then fill the current place and advance.
140 while next_place < target_arity && positioned[next_place].is_some() {
141 next_place += 1;
142 }
143 if next_place < target_arity {
144 positioned[next_place] = Some(term);
145 next_place += 1;
146 } else {
147 overflow_untagged += 1;
148 }
149 }
150 }
151 }
152
153 let args: Vec<IrTerm> = positioned
154 .into_iter()
155 .map(|slot| slot.unwrap_or(IrTerm::Unspecified))
156 .collect();
157
158 // Fail-closed: untagged argument that overflow the predicate's places were
159 // dropped above (counted in `overflow_untagged`); reject rather than lose
160 // meaning. `du` (a 2-place identity, consumed binary by nibli-reason's union-find)
161 // gets a specific message; any other predicate errors too — but ONLY when its
162 // arity is KNOWN in jbovlaste (an unknown word defaults to arity 2 and its
163 // real arity may be higher, so an "overflow" there is unprovable; this also
164 // keeps the no-XML build, where many proxy words default to 2, from
165 // false-firing).
166 let head_name = self.get_predicate_head_name(proposition.relation, predicates);
167 if overflow_untagged > 0 {
168 if head_name == nibli_types::relations::IDENTITY {
169 self.errors.push(format!(
170 "`du` (identity) is a 2-place relation, but {} extra argument were supplied; \
171 n-ary identity is unsupported.",
172 overflow_untagged
173 ));
174 } else if LexiconSchema::get_arity(head_name).is_some() {
175 self.errors.push(format!(
176 "{} untagged argument overflow the predicate `{}`'s {} place(s); the extra \
177 argument cannot be placed.",
178 overflow_untagged, head_name, target_arity
179 ));
180 }
181 }
182
183 let mut final_form = self.apply_predicate(
184 proposition.relation,
185 &args,
186 predicates,
187 arguments,
188 sentences,
189 );
190
191 for (modal_tag, tagged_term, modal_quants) in modal_entries {
192 markers.extend(modal_quants.into_iter().map(ScopeMarker::Desc));
193
194 let ModalTag(predicate_id) = &modal_tag;
195 let (modal_gismu, modal_arity) = {
196 let name = self.get_predicate_head_name(*predicate_id, predicates);
197 let arity = self.get_predicate_arity(*predicate_id, predicates);
198 (self.interner.get_or_intern(name), arity)
199 };
200
201 // FAIL CLOSED: a modal relates its tagged argument (the modal predicate's x1)
202 // to the main proposition's x1 (its x2), so the modal predicate needs at least 2
203 // places. A 1-place predicate has no x2 to carry the main-proposition link — only
204 // reachable via a `via` tag over an arity-1 predicate (every curated modal is
205 // arity >= 2). Silently dropping `main_x1` loses meaning, so reject.
206 if modal_arity < 2 {
207 let modal_name = self.interner.resolve(&modal_gismu).to_string();
208 self.errors.push(format!(
209 "Modal tag `{}` maps to a {}-place predicate, but a modal needs at \
210 least 2 places (x1 = the tag's own argument, x2 = the main proposition's \
211 x1 link); the main proposition's x1 cannot be carried.",
212 modal_name, modal_arity
213 ));
214 continue;
215 }
216
217 let main_x1 = args.first().cloned().unwrap_or(IrTerm::Unspecified);
218 let mut modal_args = vec![IrTerm::Unspecified; modal_arity];
219 modal_args[0] = tagged_term;
220 modal_args[1] = main_x1;
221
222 let modal_form = IrForm::Predicate {
223 relation: modal_gismu,
224 args: modal_args,
225 };
226
227 final_form = IrForm::And(Box::new(final_form), Box::new(modal_form));
228 }
229
230 // Conjoin rel clauses attached to non-quantifier argument (la names, le
231 // descriptions, pro-argument) into the proposition matrix. These were
232 // previously compiled then silently DISCARDED (panel finding
233 // 2026-06-10), so `la .adam. poi gerku cu klama` answered TRUE with
234 // only klama(adam) known.
235 let pending: Vec<IrForm> = self
236 .pending_matrix_conjuncts
237 .split_off(matrix_conjunct_checkpoint);
238 for conj in pending {
239 final_form = IrForm::And(Box::new(final_form), Box::new(conj));
240 }
241
242 // Quantifier scope follows Lojban surface order (leftmost = outermost).
243 // `markers` recorded every scope introduction — description quantifiers
244 // AND bare logic variables (da/de/di) — in source order during the term
245 // loop above. Folding the list in REVERSE makes the first-introduced
246 // quantifier the outermost binder, so `da citka ro lo gerku` yields
247 // `∃da.∀x` (the leading bare var outscopes the universal — an
248 // Exists-over-ForAll root that nibli-reason's assertion dispatch now accepts by
249 // skolemizing the leading ∃) while `ro lo gerku cu citka da` yields
250 // `∀x.∃da` (unchanged).
251 //
252 // Safety net: a da/de/di reachable only via a merged predicate — a be/bei
253 // role arg (`klama be da`) or any var the surface loop did not capture —
254 // has no well-defined surface position, so it is collected from the built
255 // `final_form` and closed INNERMOST (the conservative default). This
256 // guarantees no bare var is ever left free; `introduced` excludes the
257 // surface-captured vars so none is double-wrapped. Binder tracking in
258 // `collect_free_logic_vars` skips abstraction-bound and prenex-bound vars,
259 // and the description bodies / rel-clause restrictors are not folded into
260 // `final_form` yet (they wrap below), so they are correctly out of scope.
261 //
262 // This innermost closure is a DELIBERATE, ACCEPTED boundary (not a
263 // deferred TODO): a be/bei-arg or restrictor-internal `da` is soundly
264 // closed innermost. A restrictor-internal `da` can never diverge (it is
265 // bound inside the very quantifier whose domain the restrictor defines);
266 // the ONLY construct where surface order would differ is an obscure
267 // be-arg `da` preceding a tail-term universal (`klama be da ro lo gerku`),
268 // where innermost gives ∀∃ vs surface ∃∀ — and even there innermost
269 // merely under-claims (sound for assertions). Surface interleaving would
270 // need source spans the flat AST does not carry, for ~zero semantic gain.
271 // Locked by `test_da_in_be_arg_closed`,
272 // `test_be_arg_da_with_universal_stays_innermost`, and
273 // `test_restrictor_internal_da_closed_innermost`.
274 let mut all_free_seen = std::collections::HashSet::new();
275 let mut all_free: Vec<lasso::Spur> = Vec::new();
276 let mut bound_vars: Vec<lasso::Spur> = Vec::new();
277 Self::collect_free_logic_vars(
278 &final_form,
279 &self.interner,
280 &self.prenex_vars,
281 &mut bound_vars,
282 &mut all_free_seen,
283 &mut all_free,
284 );
285 for var in &all_free {
286 if !introduced.contains(var) {
287 final_form = IrForm::Exists(*var, Box::new(final_form));
288 }
289 }
290
291 let has_universal_quantifier = markers.iter().any(|m| {
292 matches!(
293 m,
294 ScopeMarker::Desc(e)
295 if matches!(e.kind, QuantifierKind::Universal | QuantifierKind::UniversalLe)
296 )
297 });
298
299 for marker in markers.into_iter().rev() {
300 final_form = match marker {
301 ScopeMarker::Desc(entry) => {
302 self.close_quantifier(entry, final_form, predicates, arguments, sentences)
303 }
304 ScopeMarker::Bare(var) => IrForm::Exists(var, Box::new(final_form)),
305 };
306 }
307
308 // Rare corner: a rel clause on a non-quantifier argument nested inside a
309 // description restrictor (e.g. the be-arg in `lo gerku be la .adam.
310 // poi prenu`) pushes its conjunct while the closure loop above
311 // compiles the restrictor — too late to join the matrix. Conjoin it
312 // at the top level when sound (no universal: the root stays a ground
313 // conjunction); under a universal the root must remain ForAll for
314 // rule compilation, so FAIL CLOSED rather than silently drop.
315 let late: Vec<IrForm> = self
316 .pending_matrix_conjuncts
317 .split_off(matrix_conjunct_checkpoint);
318 if !late.is_empty() {
319 if has_universal_quantifier {
320 self.errors.push(
321 "Relative clause on a name/description inside a universal \
322 description's restrictor cannot be represented; restate it \
323 as a separate sentence."
324 .to_string(),
325 );
326 } else {
327 for conj in late {
328 final_form = IrForm::And(Box::new(final_form), Box::new(conj));
329 }
330 }
331 }
332
333 for var in self.question_vars.drain(ma_checkpoint..) {
334 final_form = IrForm::Exists(var, Box::new(final_form));
335 }
336
337 if proposition.negated {
338 final_form = IrForm::Not(Box::new(final_form));
339 }
340
341 match &proposition.tense {
342 Some(Tense::Past) => {
343 final_form = IrForm::Past(Box::new(final_form));
344 }
345 Some(Tense::Now) => {
346 final_form = IrForm::Present(Box::new(final_form));
347 }
348 Some(Tense::Future) => {
349 final_form = IrForm::Future(Box::new(final_form));
350 }
351 None => {}
352 }
353
354 match &proposition.deontic {
355 Some(DeonticMood::Obligation) => {
356 final_form = IrForm::Obligatory(Box::new(final_form));
357 }
358 Some(DeonticMood::Permission) => {
359 final_form = IrForm::Permitted(Box::new(final_form));
360 }
361 None => {}
362 }
363
364 final_form
365 }
366
367 /// Walk a compiled `IrForm` collecting free `da`/`de`/`di` logic
368 /// variables for existential closure. Tracks binders (`Exists`/`ForAll`/
369 /// `Count`) so a var already bound (e.g. by an abstraction body's own
370 /// closure) is skipped — no double-wrap — and excludes prenex-bound vars.
371 /// Dedups via `seen`; `out` preserves first-appearance order.
372 fn collect_free_logic_vars(
373 form: &IrForm,
374 interner: &Rodeo,
375 prenex: &std::collections::HashSet<lasso::Spur>,
376 bound: &mut Vec<lasso::Spur>,
377 seen: &mut std::collections::HashSet<lasso::Spur>,
378 out: &mut Vec<lasso::Spur>,
379 ) {
380 match form {
381 IrForm::Predicate { args, .. } => {
382 for arg in args {
383 if let IrTerm::Variable(spur) = arg {
384 let name = interner.resolve(spur);
385 if name.starts_with('$')
386 && !bound.contains(spur)
387 && !prenex.contains(spur)
388 && seen.insert(*spur)
389 {
390 out.push(*spur);
391 }
392 }
393 }
394 }
395 IrForm::And(l, r)
396 | IrForm::Or(l, r)
397 | IrForm::Biconditional(l, r)
398 | IrForm::Xor(l, r) => {
399 Self::collect_free_logic_vars(l, interner, prenex, bound, seen, out);
400 Self::collect_free_logic_vars(r, interner, prenex, bound, seen, out);
401 }
402 IrForm::Not(inner)
403 | IrForm::Past(inner)
404 | IrForm::Present(inner)
405 | IrForm::Future(inner)
406 | IrForm::Obligatory(inner)
407 | IrForm::Permitted(inner) => {
408 Self::collect_free_logic_vars(inner, interner, prenex, bound, seen, out);
409 }
410 IrForm::Exists(v, body) | IrForm::ForAll(v, body) => {
411 bound.push(*v);
412 Self::collect_free_logic_vars(body, interner, prenex, bound, seen, out);
413 bound.pop();
414 }
415 IrForm::Count { var, body, .. } => {
416 bound.push(*var);
417 Self::collect_free_logic_vars(body, interner, prenex, bound, seen, out);
418 bound.pop();
419 }
420 }
421 }
422
423 /// Record a surface-ordered `Bare` scope marker if `term` is a bare logic
424 /// variable (`da`/`de`/`di`) seen for the first time in this proposition frame and
425 /// not prenex-bound. Dedups a co-referring var via `introduced`. Reads only
426 /// `self.interner`/`self.prenex_vars`; mutates the caller's frame-local
427 /// `introduced`/`markers`.
428 fn record_bare_marker(
429 &self,
430 term: &IrTerm,
431 introduced: &mut std::collections::HashSet<lasso::Spur>,
432 markers: &mut Vec<ScopeMarker>,
433 ) {
434 if let IrTerm::Variable(spur) = term {
435 let spur = *spur;
436 let is_logic_var = self.interner.resolve(&spur).starts_with('$');
437 if is_logic_var && !self.prenex_vars.contains(&spur) && introduced.insert(spur) {
438 markers.push(ScopeMarker::Bare(spur));
439 }
440 }
441 }
442
443 /// Shallow scan of a proposition's direct terms for an explicit `it` (the
444 /// bound-entity marker) — bare, or under place-tag wrappers (unwrapped
445 /// transitively). Does NOT descend into Description/Restricted/Abstraction
446 /// arguments (a nested clause's `it` belongs to that clause), and does NOT
447 /// count a modal-carried `it` (`ModalTagged` is not place-filling, so it
448 /// cannot collide with the implicit-x1 injection — see the skip rule at the
449 /// call site). Mirrors the nibli-kr render-side `has_explicit_keha` scan.
450 fn terms_contain_explicit_kea(term_ids: &[u32], arguments: &[Argument]) -> bool {
451 term_ids.iter().any(|&id| {
452 let mut s = &arguments[id as usize];
453 loop {
454 match s {
455 Argument::Tagged((_, inner_id)) => s = &arguments[*inner_id as usize],
456 Argument::Marker(Marker::It) => return true,
457 _ => return false,
458 }
459 }
460 })
461 }
462
463 /// Compiles a sentence node (simple proposition or connected sentences) into FOL.
464 pub fn compile_sentence(
465 &mut self,
466 sentence_id: u32,
467 predicates: &[Predicate],
468 arguments: &[Argument],
469 sentences: &[Sentence],
470 ) -> IrForm {
471 match &sentences[sentence_id as usize] {
472 Sentence::Simple(proposition) => {
473 self.compile_proposition(proposition, predicates, arguments, sentences)
474 }
475 Sentence::Prenex((vars, body_id)) => {
476 // `ro da [ro de ...] zo'u BODY` → ∀da. ∀de. … BODY.
477 // Intern each prenex variable and mark it bound so the body's
478 // compile_proposition does NOT existentially close it; then wrap the
479 // compiled body in nested ForAll (outermost = first variable).
480 let spurs: Vec<lasso::Spur> = vars
481 .iter()
482 .map(|v| self.interner.get_or_intern(v))
483 .collect();
484 let saved: Vec<lasso::Spur> = spurs
485 .iter()
486 .filter(|s| self.prenex_vars.insert(**s))
487 .copied()
488 .collect();
489
490 let mut form = self.compile_sentence(*body_id, predicates, arguments, sentences);
491
492 // Wrap inner-to-outer so the first variable is the outermost ∀.
493 for spur in spurs.iter().rev() {
494 form = IrForm::ForAll(*spur, Box::new(form));
495 }
496
497 // Restore: only remove the vars THIS prenex introduced (a nested
498 // prenex may share a name with an outer one).
499 for s in saved {
500 self.prenex_vars.remove(&s);
501 }
502 form
503 }
504 Sentence::Quantified((kind, var, restr_id, clause_id, body_id)) => {
505 // Block binder: `exactly N [the] X $v: body` / `every the X
506 // $v: body`. The `$v` binds by the prenex mechanism (marked
507 // bound so no frame closes it existentially), the domain is
508 // built exactly like the term-position twin (close_quantifier's
509 // shapes), and any where-clause folds on the DOMAIN side.
510 let spur = self.interner.get_or_intern(var);
511 let newly_bound = self.prenex_vars.insert(spur);
512 let var_term = IrTerm::Variable(spur);
513
514 let mut domain = match kind {
515 nibli_types::ast::BlockQuant::ExactCount(_) => {
516 // Indefinite restrictor: `X($v, _, …)` — the
517 // term-position ExactCount shape.
518 let desc_arity = self.get_predicate_arity(*restr_id, predicates);
519 let mut restrictor_args = Vec::with_capacity(desc_arity);
520 restrictor_args.push(var_term.clone());
521 while restrictor_args.len() < desc_arity {
522 restrictor_args.push(IrTerm::Unspecified);
523 }
524 self.apply_predicate(
525 *restr_id,
526 &restrictor_args,
527 predicates,
528 arguments,
529 sentences,
530 )
531 }
532 nibli_types::ast::BlockQuant::ExactCountDefinite(_)
533 | nibli_types::ast::BlockQuant::UniversalDefinite => {
534 // Opaque definite domain: `the_domain_<head>($v)` —
535 // the term-position ExactCountLe/UniversalLe shape.
536 self.build_the_domain_restrictor(*restr_id, spur, predicates)
537 }
538 };
539 if let Some(cl) = clause_id {
540 let clause_form = self.compile_sentence(*cl, predicates, arguments, sentences);
541 domain = IrForm::And(Box::new(domain), Box::new(clause_form));
542 }
543
544 let body_form = self.compile_sentence(*body_id, predicates, arguments, sentences);
545
546 let form = match kind {
547 nibli_types::ast::BlockQuant::ExactCount(n)
548 | nibli_types::ast::BlockQuant::ExactCountDefinite(n) => IrForm::Count {
549 var: spur,
550 count: *n,
551 body: Box::new(IrForm::And(Box::new(domain), Box::new(body_form))),
552 },
553 nibli_types::ast::BlockQuant::UniversalDefinite => IrForm::ForAll(
554 spur,
555 Box::new(IrForm::Or(
556 Box::new(IrForm::Not(Box::new(domain))),
557 Box::new(body_form),
558 )),
559 ),
560 };
561
562 if newly_bound {
563 self.prenex_vars.remove(&spur);
564 }
565 form
566 }
567 Sentence::Connected((connective, left_id, right_id)) => {
568 let left_form = self.compile_sentence(*left_id, predicates, arguments, sentences);
569 let right_form = self.compile_sentence(*right_id, predicates, arguments, sentences);
570
571 match connective {
572 SentenceConnective::Implies => IrForm::Or(
573 Box::new(IrForm::Not(Box::new(left_form))),
574 Box::new(right_form),
575 ),
576 SentenceConnective::And => {
577 IrForm::And(Box::new(left_form), Box::new(right_form))
578 }
579 SentenceConnective::Afterthought(conn) => match conn {
580 Connective::And => IrForm::And(Box::new(left_form), Box::new(right_form)),
581 Connective::Or => IrForm::Or(Box::new(left_form), Box::new(right_form)),
582 Connective::Iff => {
583 IrForm::Biconditional(Box::new(left_form), Box::new(right_form))
584 }
585 Connective::Xor => IrForm::Xor(Box::new(left_form), Box::new(right_form)),
586 },
587 }
588 }
589 }
590 }
591}