Skip to main content

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}