Skip to main content

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;