nibli_semantics/semantic.rs
1//! Semantic compiler: flat AST buffer → First-Order Logic IR.
2//!
3//! Walks the WIT AST buffer (flat arrays of `Predicate`, `Argument`, `Sentence`) and
4//! compiles each sentence into a [`IrForm`] tree. Key transformations:
5//!
6//! - **Quantifier scoping**: determiner descriptions (`lo`/`le`/`la`/`ro lo`) introduce
7//! quantified variables; scopes are closed outward after the proposition body is compiled.
8//! - **Quantifier closure**: bare logic variables (`da`/`de`/`di`) are wrapped in
9//! `Exists`; Skolemization itself happens downstream in nibli-reason at assertion time.
10//! - **Connective expansion**: argument/predicate/sentence connectives expand into FOL
11//! `And`/`Or`/`Not`/`Biconditional`/`Xor` combinations.
12//! - **Conversion**: `se`/`te`/`ve`/`xe` permute argument places.
13//! - **Abstraction**: `nu`/`du'u`/`ka`/`ni`/`si'o` reify inner proposition as 1-place predicates.
14//! - **Relative clauses**: `poi`/`voi` (restrictive) conjoin a domain restrictor;
15//! `noi` (non-restrictive) conjoins its body at the MATRIX level (consequent for
16//! universals, body conjunct for existentials/counts) so it does not narrow the
17//! quantifier domain. Residual: under exact-count quantifiers `noi` is still
18//! treated restrictively (documented limitation).
19//! - **Modal tags**: `via` tags produce conjoined modal predications.
20//! - **String interning**: all relation names and variable names use [`lasso::Rodeo`]
21//! for zero-copy comparison and deduplication.
22
23use crate::dictionary::LexiconSchema;
24use crate::ir::{IrForm, IrTerm};
25use lasso::Rodeo;
26use nibli_types::ast::{
27 AbstractionKind, Argument, Connective, Conversion, DeonticMood, Determiner, Marker, ModalTag,
28 Predicate, Proposition, RelClauseKind, Sentence, SentenceConnective, Tense,
29};
30
31mod compile;
32mod helpers;
33mod predicate;
34
35/// The kind of quantifier introduced by a determiner description.
36#[derive(Debug, Clone)]
37pub(crate) enum QuantifierKind {
38 /// lo → ∃x (veridical existential, restrictor = predicate predicate)
39 Existential,
40 /// ro lo → ∀x (veridical universal, restrictor = predicate predicate)
41 Universal,
42 /// ro le → ∀x (referential universal, restrictor = opaque the_domain predicate)
43 UniversalLe,
44 /// PA lo → exactly N (veridical, restrictor = predicate predicate)
45 ExactCount(u32),
46 /// PA le → exactly N (referential, restrictor = opaque the_domain predicate)
47 ExactCountLe(u32),
48}
49
50/// Tracks a quantifier introduced by a description (lo/le/ro lo/ro le/PA lo),
51/// with an optional relative clause restrictor.
52pub(crate) struct QuantifierEntry {
53 /// The fresh variable bound by this quantifier.
54 var: lasso::Spur,
55 /// Index into the predicate array for the description predicate (restrictor source).
56 desc_id: u32,
57 /// Optional restrictive (poi/voi) relative clause body, already compiled.
58 /// Folded on the domain side: antecedent for ∀, conjunct for ∃/Count.
59 restrictor: Option<IrForm>,
60 /// Optional non-restrictive (noi) relative clause body, already compiled.
61 /// Folded on the MATRIX side (consequent for ∀, body conjunct for ∃/Count) so
62 /// it does not narrow the quantifier domain — see `close_quantifier`.
63 incidental_restrictor: Option<IrForm>,
64 /// What kind of quantifier this description introduces.
65 kind: QuantifierKind,
66}
67
68/// One scope introduction in a proposition, recorded in left-to-right SURFACE order so
69/// quantifier nesting can follow Lojban scope (leftmost = outermost). Folding the
70/// list in reverse interleaves bare-variable existentials among the description
71/// quantifiers, so `da citka ro lo gerku` compiles `∃da.∀x` and `ro lo gerku cu
72/// citka da` compiles `∀x.∃da`.
73pub(crate) enum ScopeMarker {
74 /// A determiner description (lo/le/ro lo/ro le/PA lo) → closed via `close_quantifier`.
75 Desc(QuantifierEntry),
76 /// A bare logic variable (da/de/di), first occurrence → closed via `Exists`.
77 Bare(lasso::Spur),
78}
79
80/// Stateful compiler that transforms flat AST buffers into FOL logic forms.
81///
82/// Maintains a string interner, fresh variable counter, and context state for
83/// relative clauses, ka-abstractions, and `ma` query variables. Accumulated
84/// errors are checked after compilation.
85pub struct SemanticCompiler {
86 /// String interner for relation names and variable names.
87 pub interner: Rodeo,
88 /// Monotonically increasing counter for generating fresh variable names.
89 pub var_counter: usize,
90 /// When inside a relative clause, holds the bound variable from the
91 /// enclosing description. ke'a resolves to this variable directly.
92 rel_clause_var: Option<lasso::Spur>,
93 /// Set to true when ke'a is encountered during rel clause compilation.
94 /// When true, inject_variable is skipped (user placed the variable explicitly).
95 ref_used: bool,
96 /// When inside a ka abstraction, holds the variable that ce'u resolves to.
97 /// This is the x1 arg from the enclosing description quantifier.
98 property_open_var: Option<lasso::Spur>,
99 /// Fresh variables generated for `ma` query pro-argument. Each `ma` gets
100 /// an independent variable (unlike da/de/di which co-refer). These are
101 /// wrapped in ∃ during quantifier closure.
102 question_vars: Vec<lasso::Spur>,
103 /// Accumulated semantic errors (e.g., ambiguous inject_variable).
104 /// Checked after compilation; if non-empty, compile_buffer returns error.
105 pub errors: Vec<String>,
106 /// Monotonically increasing counter for generating fresh event variable names.
107 event_counter: usize,
108 /// Relative clause bodies attached to argument that introduce NO quantifier
109 /// (la names, le descriptions, pro-argument). The clause term is already
110 /// substituted in; `compile_proposition` drains its frame's entries and conjoins
111 /// them into the proposition matrix (previously these were silently dropped —
112 /// panel finding 2026-06-10).
113 pending_matrix_conjuncts: Vec<IrForm>,
114 /// One-shot: the implicit `ke'a` subject of a relative clause, to be placed
115 /// as the x1 ARGUMENT of the clause's main proposition BEFORE predicate conversion —
116 /// the same position an explicit subject occupies. Consumed by the first
117 /// `compile_proposition` of the clause body. This makes `poi se prami la .alis.`
118 /// route `ke'a` through `se` conversion to the correct underlying role
119 /// (prami_x2), instead of post-hoc `inject_variable` wrongly filling the
120 /// conversion-vacated `prami_x1` slot. Skipped (left in place) when the
121 /// proposition's own terms carry an explicit `ke'a` — see the skip rule in
122 /// `compile_proposition`.
123 pending_clause_subject: Option<lasso::Spur>,
124 /// Logic variables (`da`/`de`/`di`) bound by an enclosing prenex
125 /// (`ro da ... zo'u`). These are universally quantified by the prenex
126 /// lowering, so `compile_proposition` must NOT existentially close them the way it
127 /// closes free `da`/`de`/`di`.
128 prenex_vars: std::collections::HashSet<lasso::Spur>,
129}
130
131impl SemanticCompiler {
132 /// Creates a new compiler with empty interner and zeroed counters.
133 pub fn new() -> Self {
134 Self {
135 interner: Rodeo::new(),
136 var_counter: 0,
137 rel_clause_var: None,
138 ref_used: false,
139 property_open_var: None,
140 question_vars: Vec::new(),
141 errors: Vec::new(),
142 event_counter: 0,
143 pending_matrix_conjuncts: Vec::new(),
144 pending_clause_subject: None,
145 prenex_vars: std::collections::HashSet::new(),
146 }
147 }
148}
149
150#[cfg(test)]
151mod tests;