nibli_session/lib.rs
1//! The shared session core — the ONE compile/assert/query chain every runtime
2//! surface wraps with only boundary conversion.
3//!
4//! Before this crate, the compile chain (`nibli_kr::parse_checked` →
5//! `nibli_semantics::compile_from_ast` → `nibli_reason::transform_compute_nodes`)
6//! plus the compute-predicate registry and the assert/query wrappers were
7//! hand-mirrored across nibli-engine (native), nibli-pipeline (WASM component),
8//! nibli-wasm (wasm-bindgen), nibli-ui (Dioxus), and nibli-verify's battery —
9//! the pipeline's copy literally commented "the mirror of nibli-engine's
10//! compile_text, so native and WASM agree". [`CoreSession`] is that agreement
11//! BY CONSTRUCTION.
12//!
13//! What stays surface-side, deliberately:
14//! - the ERROR BOUNDARY (this crate speaks canonical [`NibliError`]; the
15//! pipeline converts to its WIT twin, nibli-wasm flattens to `String`);
16//! - verdict/proof SERIALIZATION (WIT records, JSON, rendered text);
17//! - the LINT policy (`nibli_kr::lint` — stdout `[Note:]` echoes vs UI note
18//! data vs none) and env reads (`NIBLI_QUIET`/`NIBLI_STRICT` are wasip2/host
19//! concerns; the browser has no process env);
20//! - STORE write-through (nibli-engine's durable registry mints its own ids
21//! and reaches the KB through [`CoreSession::kb`]);
22//! - compute-dispatch WIRING (the pipeline bridges to its WIT host import,
23//! the engine offers an opt-in TCP client, the browser leaves external
24//! compute unregistered) — the [`CoreSession::set_compute_dispatch`]
25//! passthrough is the seam.
26//!
27//! nibli-formalize's gates intentionally do NOT use this crate: they stop
28//! before compute-marking (the translator never needs `ComputeNode`s), keep
29//! the AST for the render round-trip gate, and carry their own `GateError`
30//! taxonomy (see nibli-formalize/src/gates.rs).
31
32use std::collections::HashSet;
33
34use nibli_types::error::NibliError;
35use nibli_types::logic::{
36 AggregateOp, FactSummary, LogicBuffer, LogicalTerm, ProofTrace, QueryResult, WitnessBinding,
37};
38
39/// Compile KR text WITHOUT compute-marking: parse + semantic compile only.
40/// For consumers that deliberately stop before `transform_compute_nodes`
41/// (display paths; nibli-formalize's gates mirror this shape independently).
42pub fn compile_unmarked(text: &str) -> Result<LogicBuffer, NibliError> {
43 let ast = nibli_kr::parse_checked(text)?;
44 nibli_semantics::compile_from_ast(ast)
45}
46
47/// THE compile chain: parse + semantic compile + compute-marking against the
48/// given predicate set. Free-fn form for per-call-set users (nibli-ui builds
49/// its set fresh each query); [`CoreSession::compile_text`] is the
50/// session-owned form.
51pub fn compile_text(
52 text: &str,
53 compute_predicates: &HashSet<String>,
54) -> Result<LogicBuffer, NibliError> {
55 let mut buf = compile_unmarked(text)?;
56 nibli_reason::transform_compute_nodes(&mut buf, compute_predicates);
57 Ok(buf)
58}
59
60/// The shared session: a [`nibli_reason::KnowledgeBase`] + the compute-predicate
61/// registry, with the compile/assert/query verbs every surface previously
62/// hand-mirrored. No env reads, no linting, no persistence — those are
63/// per-surface boundary policy (see the module doc).
64pub struct CoreSession {
65 kb: nibli_reason::KnowledgeBase,
66 compute_predicates: HashSet<String>,
67}
68
69impl Default for CoreSession {
70 fn default() -> Self {
71 Self::new()
72 }
73}
74
75impl CoreSession {
76 /// A fresh in-memory session seeded with the built-in arithmetic compute
77 /// predicates (`nibli_reason::default_compute_predicates`).
78 pub fn new() -> Self {
79 Self::with_kb(nibli_reason::KnowledgeBase::new())
80 }
81
82 /// Wrap an already-constructed KB (e.g. one built with a persistent
83 /// write-through fact store via `KnowledgeBase::with_store`).
84 pub fn with_kb(kb: nibli_reason::KnowledgeBase) -> Self {
85 CoreSession {
86 kb,
87 compute_predicates: nibli_reason::default_compute_predicates(),
88 }
89 }
90
91 /// The underlying KB, for surface-specific extras (cancel flags, predicate
92 /// tracing, contradiction scans, store replay via `assert_fact_with_id`).
93 pub fn kb(&self) -> &nibli_reason::KnowledgeBase {
94 &self.kb
95 }
96
97 /// The current compute-predicate set (the marking input).
98 pub fn compute_predicates(&self) -> &HashSet<String> {
99 &self.compute_predicates
100 }
101
102 /// Register a predicate name for external compute dispatch.
103 pub fn register_compute_predicate(&mut self, name: String) {
104 self.compute_predicates.insert(name);
105 }
106
107 /// Register this session's external compute dispatch (per-instance; see
108 /// `nibli_reason::KnowledgeBase::set_compute_dispatch` for the trust
109 /// boundary). Without it, external predicates error; built-in arithmetic
110 /// still resolves in-engine.
111 pub fn set_compute_dispatch(
112 &self,
113 eval: fn(&str, &[LogicalTerm]) -> Result<bool, String>,
114 batch_eval: fn(&[nibli_reason::ComputeRequest]) -> Vec<Result<bool, String>>,
115 ) {
116 self.kb.set_compute_dispatch(eval, batch_eval);
117 }
118
119 /// Engine stdout diagnostics (`[Rule]`/`[Skolem]`/`[Constraint]`).
120 /// Default OFF — a silent library; surfaces opt in.
121 pub fn set_verbose(&self, verbose: bool) {
122 self.kb.set_verbose(verbose);
123 }
124
125 /// STRICT MODE (default off — permissive warn-and-insert).
126 pub fn set_strict(&self, strict: bool) {
127 self.kb.set_strict(strict);
128 }
129
130 /// EXISTENTIAL-IMPORT MODE (default ON — the v0.1 xorlo behavior). OFF gives
131 /// the clean-core `some` = plain ∃ profile (no presupposition witnesses).
132 pub fn set_existential_import(&self, on: bool) {
133 self.kb.set_existential_import(on);
134 }
135
136 /// STRATUM-ORDERED MATERIALISATION (default ON). OFF sends every
137 /// negation-as-failure check back through backward chaining.
138 pub fn set_materialization(&self, on: bool) {
139 self.kb.set_materialization(on);
140 }
141
142 /// What the last query's saturation covered: `(complete, [(relation, why not)])`.
143 /// Empty until a query has run and after any KB mutation.
144 pub fn materialization_report(&self) -> (Vec<String>, Vec<(String, String)>) {
145 self.kb.materialization_report()
146 }
147
148 /// THE compile chain against this session's compute-predicate set.
149 pub fn compile_text(&self, text: &str) -> Result<LogicBuffer, NibliError> {
150 compile_text(text, &self.compute_predicates)
151 }
152
153 /// Compile KR text and assert it, splitting a multi-statement input into
154 /// one INDEPENDENT fact per root (`split_roots` — connectives compile to a
155 /// single root and stay one compound fact). The full input text is each
156 /// root's label. Returns one `(id, compiled-sub-buffer)` pair per root so
157 /// a persisting caller can store the FACT itself and replay it
158 /// recompile-free; callers that only need ids map the pairs down.
159 pub fn assert_text(&self, text: &str) -> Result<Vec<(u64, LogicBuffer)>, NibliError> {
160 let buf = self.compile_text(text)?;
161 let mut out = Vec::new();
162 for sub in buf.split_roots() {
163 let id = self.kb.assert_fact(sub.clone(), text.to_string())?;
164 out.push((id, sub));
165 }
166 Ok(out)
167 }
168
169 /// Assert a fact directly by relation name and arguments, bypassing text
170 /// parsing, under an optional caller-chosen id (store replay). The label
171 /// is `":assert {relation}"`. Event-decomposes to the SAME shape a surface
172 /// assertion produces, so the injected fact is matched by surface text
173 /// queries (not just raw-FOL / same-shape direct queries). Identity stays
174 /// flat; arity follows the injected-arity policy (fail-closed) — see
175 /// `nibli_semantics::compile_injected_fact`.
176 pub fn assert_fact_direct(
177 &self,
178 relation: &str,
179 args: &[LogicalTerm],
180 id: Option<u64>,
181 ) -> Result<u64, NibliError> {
182 let label = format!(":assert {}", relation);
183 let buf = nibli_semantics::compile_injected_fact(relation, args)?;
184 match id {
185 Some(i) => {
186 // The assert is the reasoning stage (buffer already past
187 // nibli-semantics); nibli-reason's `assert_fact_with_id`
188 // returns a String, so wrap as Reasoning.
189 self.kb
190 .assert_fact_with_id(buf, label, i)
191 .map_err(NibliError::Reasoning)?;
192 Ok(i)
193 }
194 None => self.kb.assert_fact(buf, label),
195 }
196 }
197
198 /// Compile a KR query and run the entailment check.
199 pub fn query_text(&self, text: &str) -> Result<QueryResult, NibliError> {
200 let buf = self.compile_text(text)?;
201 self.kb.query_entailment(buf)
202 }
203
204 /// Compile a KR query, run the entailment check, and return the typed
205 /// result with the canonical wire [`ProofTrace`].
206 pub fn query_text_with_proof(
207 &self,
208 text: &str,
209 ) -> Result<(QueryResult, ProofTrace), NibliError> {
210 let buf = self.compile_text(text)?;
211 self.kb.query_entailment_with_proof(buf)
212 }
213
214 /// Compile a KR query and extract all satisfying witness binding sets.
215 pub fn query_find_text(&self, text: &str) -> Result<Vec<Vec<WitnessBinding>>, NibliError> {
216 let buf = self.compile_text(text)?;
217 self.kb.query_find(buf)
218 }
219
220 /// Count the distinct witness binding sets satisfying a KR query.
221 pub fn count_witnesses_text(&self, text: &str) -> Result<usize, NibliError> {
222 let buf = self.compile_text(text)?;
223 self.kb.count_witnesses(buf)
224 }
225
226 /// Aggregate the numeric values bound to `variable` across all witness
227 /// binding sets of a KR query. `Ok(None)` when no numeric witnesses exist.
228 pub fn aggregate_text(
229 &self,
230 text: &str,
231 variable: &str,
232 op: AggregateOp,
233 ) -> Result<Option<f64>, NibliError> {
234 let buf = self.compile_text(text)?;
235 self.kb.aggregate(buf, variable, op)
236 }
237
238 /// Retract a fact by id and rebuild derived state (KB only — durable
239 /// tombstones are the persisting surface's concern).
240 pub fn retract_fact(&self, id: u64) -> Result<(), NibliError> {
241 self.kb.retract_fact(id)
242 }
243
244 /// Reset the KB, clearing all facts and rules.
245 pub fn reset(&self) -> Result<(), NibliError> {
246 self.kb.reset()
247 }
248
249 /// List all active (non-retracted) facts with their ids and labels.
250 pub fn list_facts(&self) -> Result<Vec<FactSummary>, NibliError> {
251 self.kb.list_facts()
252 }
253}