qualia_core_db/governance/webizen/arena.rs
1use super::*;
2
3/// A fast, non-cryptographic bitwise hash to lookup sub-goals in the SLG Arena
4/// without wasting CPU cycles on cryptographic overhead.
5#[inline(always)]
6fn fast_hash_goal(subject: u64, predicate: u64, object: u64) -> usize {
7 let mut hash = subject.wrapping_add(0x9E3779B97F4A7C15);
8 hash = (hash ^ (hash >> 30)).wrapping_mul(0xBF58476D1CE4E5B9);
9 hash = (hash ^ predicate).wrapping_mul(0x94D049BB133111EB);
10 hash = (hash ^ object).wrapping_mul(0x9E3779B97F4A7C15);
11 (hash ^ (hash >> 31)) as usize
12}
13
14/// True when any triple in the rule's premise or conclusion carries a variable.
15/// Such rules cannot compile to a single ground deontic norm and are instead
16/// grounded by forward-chaining (`SlgArena::fire_guard_rules`).
17fn rule_has_variables(rule: &CompiledRule) -> bool {
18 let term_is_var = |t: &crate::modalities::logic::n3_compiler::CompiledTerm| t.is_variable();
19 rule.premise
20 .triples
21 .iter()
22 .chain(rule.conclusion.triples.iter())
23 .any(|tr| term_is_var(&tr.subject) || term_is_var(&tr.predicate) || term_is_var(&tr.object))
24}
25
26/// Unify one premise-triple field against a fact field under the current bindings.
27///
28/// * IRI / literal term → matches iff `q_hash(term) == field`.
29/// * variable term → matches the existing binding, or (if unbound) binds it to
30/// `field` and grows `nbound`.
31///
32/// Hashing uses the same `q_hash` as `n3_compiler::triple_to_quin`, so a premise
33/// term and an ingested fact agree.
34fn unify_field(
35 t: &CompiledTerm,
36 field: u64,
37 bindings: &mut [(u64, u64)],
38 nbound: &mut usize,
39) -> bool {
40 match t {
41 CompiledTerm::Uri(s) | CompiledTerm::Literal(s) => *s == field,
42 CompiledTerm::Variable(key) => {
43 for i in 0..*nbound {
44 if bindings[i].0 == *key {
45 return bindings[i].1 == field;
46 }
47 }
48 if *nbound < bindings.len() {
49 bindings[*nbound] = (*key, field);
50 *nbound += 1;
51 true
52 } else {
53 false // binding table exhausted — refuse rather than mis-bind
54 }
55 }
56 }
57}
58
59/// Resolve a conclusion-triple field to a concrete hash under the bindings.
60/// Returns `None` for an unbound conclusion variable (a fresh variable not
61/// constrained by the premise) — such conclusions are skipped, never guessed.
62#[allow(clippy::ptr_arg)]
63fn resolve_term(term: &CompiledTerm, bindings: &[(u64, u64)]) -> Option<u64> {
64 match term {
65 CompiledTerm::Uri(s) | CompiledTerm::Literal(s) => Some(*s),
66 CompiledTerm::Variable(key) => bindings.iter().find(|(k, _)| *k == *key).map(|(_, v)| *v),
67 }
68}
69
70/// Conjunctive backtracking join of a rule's premise triples against the facts.
71///
72/// At full depth (every premise triple satisfied), each conclusion triple is
73/// instantiated with the bound variables and staged into `pending`. Backtracking
74/// is implicit: each fact iteration restarts binding from `nbound`, so bindings a
75/// failed branch added are simply overwritten on the next branch.
76#[allow(clippy::too_many_arguments)]
77fn join_premise(
78 premise: &[CompiledTriple],
79 conclusion: &[CompiledTriple],
80 facts: &[NQuin],
81 idx: usize,
82 bindings: &mut [(u64, u64)],
83 nbound: usize,
84 pending: &mut [NQuin],
85 pending_count: &mut usize,
86) {
87 if idx >= MAX_PREMISE_DEPTH {
88 return; // guard against pathologically deep premises
89 }
90 if idx == premise.len() {
91 for ct in conclusion {
92 if *pending_count >= pending.len() {
93 return;
94 }
95 let (Some(s), Some(p), Some(o)) = (
96 resolve_term(&ct.subject, &bindings[..nbound]),
97 resolve_term(&ct.predicate, &bindings[..nbound]),
98 resolve_term(&ct.object, &bindings[..nbound]),
99 ) else {
100 continue;
101 };
102 let mut q = NQuin {
103 subject: s,
104 predicate: p,
105 object: o,
106 context: 0,
107 metadata: 1,
108 parity: 0,
109 };
110 q.parity = q.subject ^ q.predicate ^ q.object ^ q.context;
111 pending[*pending_count] = q;
112 *pending_count += 1;
113 }
114 return;
115 }
116
117 let t = &premise[idx];
118 for fact in facts {
119 let mut local_nbound = nbound;
120 if unify_field(&t.subject, fact.subject, bindings, &mut local_nbound)
121 && unify_field(&t.predicate, fact.predicate, bindings, &mut local_nbound)
122 && unify_field(&t.object, fact.object, bindings, &mut local_nbound)
123 {
124 join_premise(
125 premise,
126 conclusion,
127 facts,
128 idx + 1,
129 bindings,
130 local_nbound,
131 pending,
132 pending_count,
133 );
134 }
135 }
136}
137
138pub struct SlgArena {
139 // We will use a safe Vec wrapper here since it is allocated strictly once and never grown.
140 #[cfg(feature = "alloc_buffers")]
141 buffer: alloc::vec::Vec<NQuin>,
142 #[cfg(not(feature = "alloc_buffers"))]
143 buffer: std::vec::Vec<NQuin>,
144 head_pointer: usize,
145 recent_slots: [usize; RECENT_SLOT_RING],
146 recent_slot_head: usize,
147 // Native Rule Registry to hold N3 Logical Implications
148 #[cfg(feature = "alloc_buffers")]
149 rule_registry: alloc::vec::Vec<CompiledRule>,
150 #[cfg(not(feature = "alloc_buffers"))]
151 rule_registry: std::vec::Vec<CompiledRule>,
152 /// Pre-parsed rules indexed by id; activated via `NativeRegisterRule`.
153 #[cfg(feature = "alloc_buffers")]
154 staged_rules: alloc::vec::Vec<CompiledRule>,
155 #[cfg(not(feature = "alloc_buffers"))]
156 staged_rules: std::vec::Vec<CompiledRule>,
157}
158
159#[cfg(feature = "alloc_buffers")]
160extern crate alloc;
161
162impl SlgArena {
163 pub fn new() -> Self {
164 #[cfg(feature = "alloc_buffers")]
165 extern crate alloc;
166
167 #[cfg(feature = "alloc_buffers")]
168 let mut buffer = alloc::vec::Vec::with_capacity(MAX_SLOTS);
169 #[cfg(not(feature = "alloc_buffers"))]
170 let mut buffer = std::vec::Vec::with_capacity(MAX_SLOTS);
171
172 // Pre-fill the ring buffer with empty Quins
173 for _ in 0..MAX_SLOTS {
174 buffer.push(NQuin {
175 subject: 0,
176 predicate: 0,
177 object: 0,
178 context: 0,
179 metadata: 0,
180 parity: 0,
181 });
182 }
183
184 #[cfg(feature = "alloc_buffers")]
185 let rule_registry = alloc::vec::Vec::new();
186 #[cfg(not(feature = "alloc_buffers"))]
187 let rule_registry = std::vec::Vec::new();
188 #[cfg(feature = "alloc_buffers")]
189 let staged_rules = alloc::vec::Vec::new();
190 #[cfg(not(feature = "alloc_buffers"))]
191 let staged_rules = std::vec::Vec::new();
192
193 Self {
194 buffer,
195 head_pointer: 0,
196 recent_slots: [0; RECENT_SLOT_RING],
197 recent_slot_head: 0,
198 rule_registry,
199 staged_rules,
200 }
201 }
202
203 /// Registers a logical implication rule into the Webizen VM
204 pub fn register_rule(&mut self, rule: &Rule<'_>) {
205 vm_log!("🧠 Webizen registered new Compiled N3 Rule");
206 self.rule_registry.push(compile_rule_to_zero_heap(rule));
207 }
208
209 /// Stage a parsed rule for later activation via `NativeRegisterRule`.
210 /// Returns the rule id (`object_reg` value) used to activate it.
211 pub fn stage_rule(&mut self, rule: &Rule<'_>) -> u64 {
212 let id = self.staged_rules.len() as u64;
213 self.staged_rules.push(compile_rule_to_zero_heap(rule));
214 id
215 }
216
217 /// Activate a staged rule by id into the live rule registry.
218 pub fn activate_staged_rule(&mut self, rule_id: u64) -> bool {
219 let idx = rule_id as usize;
220 if idx >= self.staged_rules.len() {
221 return false;
222 }
223 self.rule_registry.push(self.staged_rules[idx].clone());
224 true
225 }
226
227 pub fn rule_count(&self) -> usize {
228 self.rule_registry.len()
229 }
230
231 pub fn staged_rule_count(&self) -> usize {
232 self.staged_rules.len()
233 }
234
235 /// Collect recently written Quins with valid ECC parity (bounded scan, zero heap).
236 pub fn collect_active_quins(&self, out: &mut [NQuin]) -> usize {
237 let mut n = 0usize;
238 let scan = RECENT_SLOT_RING.min(self.recent_slot_head);
239 for off in 0..scan {
240 let ring_idx = (self.recent_slot_head + RECENT_SLOT_RING - 1 - off) % RECENT_SLOT_RING;
241 let idx = self.recent_slots[ring_idx];
242 let q = self.buffer[idx];
243 if q.subject == 0 {
244 continue;
245 }
246 let expected = q.subject ^ q.predicate ^ q.object ^ q.context;
247 if q.parity != expected {
248 continue;
249 }
250 if n < out.len() {
251 out[n] = q;
252 n += 1;
253 }
254 }
255 n
256 }
257
258 /// Compile registered N3 rules to norms + bytecode and execute on Core 1 (cold path).
259 pub fn fire_registered_rules(&mut self, contract_hash: u64) -> usize {
260 // Zero-heap: move the registry out (pointer swap, not a clone of the rules) so
261 // the loop can call `&mut self` methods (`write_table` / `execute_vm_frame`)
262 // while reading the rules; restored below before `fire_guard_rules` needs it.
263 // Safe: nothing in the loop reads or mutates `rule_registry` (only
264 // `register_rule` does, and it is never called during firing).
265 let rules = core::mem::take(&mut self.rule_registry);
266 let mut fired = 0usize;
267 for rule in &rules {
268 // Only ground rules compile to a single deontic norm; variable rules
269 // are grounded by forward-chaining (`fire_guard_rules`) below, so
270 // compiling them here would write a spurious norm keyed on a
271 // variable-name hash.
272 if !rule_has_variables(rule) {
273 if let Some(norm) = crate::modalities::logic::deontic::compile_n3_rule_to_norm(
274 rule,
275 contract_hash,
276 0,
277 ) {
278 self.write_table(norm);
279 }
280 }
281 let mut opcodes = [SlgOpcode::Halt; 64];
282 if let Ok(count) =
283 crate::modalities::logic::n3_compiler::compile_rule_to_opcodes(rule, &mut opcodes)
284 {
285 let mut frame = VmFrame::default();
286 if execute_vm_frame(self, &opcodes[..count], &mut frame).is_some() {
287 fired += 1;
288 }
289 }
290 }
291 // Restore the registry before `fire_guard_rules` forward-chains over it.
292 self.rule_registry = rules;
293 // Variable (non-ground) rules — e.g. the agency.n3 G1 corporate-capture
294 // guard — cannot compile to a single ground norm; they are grounded by
295 // forward-chaining their premise over the facts live in the arena.
296 fired += self.fire_guard_rules();
297 fired
298 }
299
300 /// Forward-chaining grounding pass for variable (non-ground) N3 guard rules.
301 ///
302 /// `fire_registered_rules` handles *ground* deontic rules via
303 /// `compile_n3_rule_to_norm`. This is its complement: for every registered rule
304 /// whose premise contains variables, it performs a conjunctive backtracking join
305 /// of the premise triples against the facts currently live in the arena, and for
306 /// each satisfying binding instantiates and asserts the (variable-substituted)
307 /// conclusion triples back into the arena. Returns the number of conclusion
308 /// triples newly asserted.
309 ///
310 /// Worked example — agency.n3 **G1**:
311 /// ```text
312 /// { ?c a values:CorporatePerson ; values:claims ?r .
313 /// ?r a values:Right ; values:heldBy values:NaturalPerson }
314 /// => { ?c values:flag values:PersonhoodCategoryError } .
315 /// ```
316 /// Given facts that a corporate person claims a natural-person right, the guard
317 /// asserts `(?c, values:flag, values:PersonhoodCategoryError)` — the personhood
318 /// category error (a Deny) — observable via [`Self::has_quin`].
319 ///
320 /// Cold path: bounded stack buffers, no hot-path heap growth. Hashing is uniform
321 /// `q_hash` over IRIs/variables/literals (matching `n3_compiler::triple_to_quin`),
322 /// so grounding matches facts ingested through the standard triple path.
323 ///
324 /// Forward chaining asserts the conclusion of any satisfied premise. Defeasible
325 /// *override* of a deontic norm (e.g. a marked corporate overlay defeating a
326 /// prohibition) is handled in the deontic-norm lane via the `q42:unless`
327 /// defeater path (`evaluate_deontic_contract`), not here — the agency.n3 guards
328 /// (G1/G1') are strict `=>` rules.
329 pub fn fire_guard_rules(&mut self) -> usize {
330 // Zero-heap: see `fire_registered_rules` — move the registry out (no clone),
331 // iterate across the fixpoint rounds, restore at the end. Safe: the loop only
332 // reads facts and calls `write_table` / `has_quin`, never touching the registry.
333 let rules = core::mem::take(&mut self.rule_registry);
334 let mut total = 0usize;
335
336 // Forward-chain to a bounded fixpoint: a conclusion asserted in one round
337 // may satisfy another guard's premise next round (e.g. overclaim → flag →
338 // requiresHumanReview). `has_quin` idempotency guarantees termination.
339 for _round in 0..MAX_FIXPOINT_ROUNDS {
340 // Snapshot the live facts (immutable borrow ends before we assert).
341 let mut facts = [NQuin::default(); 1024];
342 let fact_count = self.collect_active_quins(&mut facts);
343
344 let mut pending = [NQuin::default(); MAX_GUARD_CONCLUSIONS];
345 let mut pending_count = 0usize;
346
347 for rule in &rules {
348 if !rule_has_variables(rule) {
349 continue; // ground rules go through the deontic-norm path
350 }
351 // `triples` is a fixed `[_; 8]` array, so `.is_empty()` is always
352 // false - gate on the populated `len` instead.
353 if rule.premise.len == 0 || rule.conclusion.len == 0 {
354 continue;
355 }
356 let mut bindings = [(0u64, 0u64); MAX_RULE_VARS];
357 join_premise(
358 &rule.premise.triples[..rule.premise.len],
359 // Slice the conclusion by `len` too: passing the full array
360 // would stage (8 - len) junk (0,0,0) triples per match, which
361 // - being skipped by `has_quin` (subject==0) - get re-asserted
362 // every round, never reaching the fixpoint and flooding the
363 // recent-write ring until real conclusions are evicted.
364 &rule.conclusion.triples[..rule.conclusion.len],
365 &facts[..fact_count],
366 0,
367 &mut bindings,
368 0,
369 &mut pending,
370 &mut pending_count,
371 );
372 }
373
374 let mut asserted = 0usize;
375 for q in pending.iter().take(pending_count) {
376 // Idempotent: don't re-assert a conclusion already present.
377 if !self.has_quin(q.subject, q.predicate, q.object) {
378 self.write_table(*q);
379 asserted += 1;
380 }
381 }
382 total += asserted;
383 if asserted == 0 {
384 break; // fixpoint reached
385 }
386 }
387 self.rule_registry = rules;
388 total
389 }
390
391 /// True when a fact `(subject, predicate, object)` is live in the arena with
392 /// valid ECC parity. Used to observe forward-chained guard conclusions.
393 pub fn has_quin(&self, subject: u64, predicate: u64, object: u64) -> bool {
394 let mut scratch = [NQuin::default(); 1024];
395 let n = self.collect_active_quins(&mut scratch);
396 scratch[..n]
397 .iter()
398 .any(|q| q.subject == subject && q.predicate == predicate && q.object == object)
399 }
400
401 /// Checks the SLG Arena for a previously proven sub-goal.
402 pub fn check_table(&self, subject: u64, predicate: u64, object: u64) -> Option<NQuin> {
403 let slot = fast_hash_goal(subject, predicate, object) % MAX_SLOTS;
404
405 let cached = self.buffer[slot];
406 if cached.subject == subject && cached.predicate == predicate && cached.object == object {
407 Some(cached)
408 } else {
409 None
410 }
411 }
412
413 /// Writes a proven sub-goal into the SLG Arena.
414 /// If the slot is occupied (hash collision) or we hit the boundary,
415 /// it acts as a FIFO ring-buffer and strictly overwrites the oldest cache entries.
416 pub fn write_table(&mut self, result: NQuin) {
417 let slot = fast_hash_goal(result.subject, result.predicate, result.object) % MAX_SLOTS;
418
419 // Cyclic Eviction Policy: Overwrite whatever is in the slot natively
420 self.buffer[slot] = result;
421 self.recent_slots[self.recent_slot_head % RECENT_SLOT_RING] = slot;
422 self.recent_slot_head = self.recent_slot_head.saturating_add(1);
423
424 // Increment global ring-buffer pointer (used if we wanted strict sequential FIFO instead of hashed slots)
425 self.head_pointer = (self.head_pointer + 1) % MAX_SLOTS;
426 }
427
428 pub(crate) fn find_mutable_quin(
429 &mut self,
430 subject: u64,
431 predicate: u64,
432 object: u64,
433 ) -> Option<&mut NQuin> {
434 let scan = RECENT_SLOT_RING.min(self.recent_slot_head);
435 for off in 0..scan {
436 let ring_idx = (self.recent_slot_head + RECENT_SLOT_RING - 1 - off) % RECENT_SLOT_RING;
437 let idx = self.recent_slots[ring_idx];
438 let matches = self.buffer[idx].subject == subject
439 && self.buffer[idx].predicate == predicate
440 && (object == 0 || self.buffer[idx].object == object);
441 if matches {
442 return Some(&mut self.buffer[idx]);
443 }
444 }
445 None
446 }
447}