Skip to main content

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}