Skip to main content

qualia_core_db/modalities/logic/
n3_compiler.rs

1//! N3Logic → SHACL → Sentinel Bytecode compiler (CogAI symbolic layer).
2//!
3//! The LLM emits N3 assertions on the cold path; this module validates them against
4//! compiled SHACL shapes from [`crate::modalities::logic::shacl`] and lowers surviving rules to
5//! [`SlgOpcode`] sequences for the Core-1 Webizen VM. Hot-path execution uses only
6//! fixed caller-supplied buffers.
7
8use crate::modalities::logic::n3_parser::{Formula, Rule, RuleType, Term, Triple};
9use crate::modalities::logic::shacl::{
10    CompiledShape, ShaclCompiler, ShaclConstraint, ShaclSeverity,
11};
12use crate::q_hash;
13use crate::webizen::{execute_vm_frame, SlgArena, SlgOpcode, VmFrame};
14use crate::NQuin;
15
16#[derive(Debug, Clone, Copy, PartialEq, Eq)]
17pub enum CompiledTerm {
18    Uri(u64),
19    Variable(u64),
20    Literal(u64),
21}
22
23#[derive(Debug, Clone, Copy, PartialEq, Eq)]
24pub struct CompiledTriple {
25    pub subject: CompiledTerm,
26    pub predicate: CompiledTerm,
27    pub object: CompiledTerm,
28}
29
30#[derive(Debug, Clone, PartialEq, Eq)]
31pub struct CompiledFormula {
32    pub triples: [CompiledTriple; 8],
33    pub len: usize,
34}
35
36impl Default for CompiledFormula {
37    fn default() -> Self {
38        Self {
39            triples: [CompiledTriple {
40                subject: CompiledTerm::Uri(0),
41                predicate: CompiledTerm::Uri(0),
42                object: CompiledTerm::Uri(0),
43            }; 8],
44            len: 0,
45        }
46    }
47}
48
49#[derive(Debug, Clone, PartialEq)]
50pub struct CompiledRule {
51    pub id_hash: Option<u64>,
52    pub rule_type: RuleType,
53    pub weight: Option<f32>,
54    pub premise: CompiledFormula,
55    pub conclusion: CompiledFormula,
56}
57
58impl CompiledTerm {
59    pub fn as_u64(&self) -> u64 {
60        match self {
61            CompiledTerm::Uri(h) => *h,
62            CompiledTerm::Variable(h) => *h,
63            CompiledTerm::Literal(h) => *h,
64        }
65    }
66
67    pub fn is_variable(&self) -> bool {
68        matches!(self, CompiledTerm::Variable(_))
69    }
70}
71
72pub fn compile_term(term: &Term<'_>) -> CompiledTerm {
73    let hash =
74        crate::modalities::logic::n3_parser::term_uri_hash(term).unwrap_or_else(|| match term {
75            Term::Variable(name) => q_hash(name),
76            _ => q_hash("?"),
77        });
78    match term {
79        Term::Variable(_) => CompiledTerm::Variable(hash),
80        Term::Literal(_) => CompiledTerm::Literal(hash),
81        _ => CompiledTerm::Uri(hash),
82    }
83}
84
85pub fn compile_triple(triple: &Triple<'_>) -> CompiledTriple {
86    CompiledTriple {
87        subject: compile_term(&triple.subject),
88        predicate: compile_term(&triple.predicate),
89        object: compile_term(&triple.object),
90    }
91}
92
93pub fn compile_formula(formula: &Formula<'_>) -> CompiledFormula {
94    let mut comp = CompiledFormula::default();
95    for (i, t) in formula.triples.iter().enumerate().take(8) {
96        comp.triples[i] = compile_triple(t);
97        comp.len += 1;
98    }
99    comp
100}
101
102pub fn compile_rule_to_zero_heap(rule: &Rule<'_>) -> CompiledRule {
103    CompiledRule {
104        id_hash: rule.id.map(q_hash),
105        rule_type: rule.rule_type,
106        weight: rule.weight,
107        premise: compile_formula(&rule.premise),
108        conclusion: compile_formula(&rule.conclusion),
109    }
110}
111
112pub const MAX_COMPILED_OPCODES: usize = 256;
113pub const MAX_COMPILED_QUINS: usize = 64;
114pub const MAX_INTENT_SCOPE_SLOTS: usize = 16;
115pub const MAX_CONTEXT_NAMESPACE_SLOTS: usize = 16;
116
117#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
118pub enum N3OutputMode {
119    FreeText,
120    N3Assertions,
121    GraphMutation,
122    SummarizeOnly,
123}
124
125impl Default for N3OutputMode {
126    fn default() -> Self {
127        Self::FreeText
128    }
129}
130
131#[derive(Debug, PartialEq, Eq)]
132pub enum N3CompileError {
133    EmptyRule,
134    MalformedTriple,
135    UnsupportedRuleType,
136    ShapeViolation,
137    OpcodeBufferFull,
138    QuinBufferFull,
139    SentinelMemoryOverflow,
140}
141
142#[derive(Debug, Clone, Copy, PartialEq, Eq)]
143pub enum SentinelError {
144    MemoryOverflow,
145}
146
147#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
148pub struct N3CompiledProgram {
149    pub opcode_count: usize,
150    pub quin_count: usize,
151}
152
153#[repr(C)]
154#[derive(Debug, Clone, Copy, PartialEq, Eq)]
155pub struct AgentIntentFrame {
156    pub intent_predicate: u64,
157    pub principal_did_hash: u64,
158    pub mcp_intent_frame_hash: u64,
159    pub ilp_offer_micro_cents: u64,
160    pub scope_count: u8,
161    pub context_namespace_count: u8,
162    pub requires_network: bool,
163    pub output_mode: N3OutputMode,
164    pub clearance_ceiling: u8,
165    pub max_sentinel_depth: u8,
166    pub graph_scope: [u64; MAX_INTENT_SCOPE_SLOTS],
167    pub context_namespaces: [u64; MAX_CONTEXT_NAMESPACE_SLOTS],
168}
169
170fn term_hash(term: &Term<'_>) -> Result<u64, N3CompileError> {
171    match term {
172        Term::Uri(uri) => Ok(q_hash(uri)),
173        Term::Literal(lit) => Ok(q_hash(lit)),
174        Term::Formula(s) => Ok(crate::modalities::logic::n3_parser::q_hash_formula(s)),
175        Term::Variable(_) => Err(N3CompileError::MalformedTriple),
176    }
177}
178
179pub fn triple_to_quin(triple: &CompiledTriple, context: u64) -> Result<NQuin, N3CompileError> {
180    let mut quin = NQuin::default();
181    quin.subject = triple.subject.as_u64();
182    quin.predicate = triple.predicate.as_u64();
183    quin.object = triple.object.as_u64();
184    quin.context = context;
185    quin.parity = quin.subject ^ quin.predicate ^ quin.object ^ quin.context;
186    Ok(quin)
187}
188
189fn first_triple<'a>(
190    f: &'a crate::modalities::logic::n3_compiler::CompiledFormula,
191) -> Result<&'a crate::modalities::logic::n3_compiler::CompiledTriple, N3CompileError> {
192    f.triples.first().ok_or(N3CompileError::MalformedTriple)
193}
194
195/// Returns true when every conclusion triple property path matches a compiled SHACL shape.
196pub fn validate_rule_against_shapes(
197    rule: &Rule<'_>,
198    shapes: &[&CompiledShape],
199) -> Result<(), N3CompileError> {
200    if shapes.is_empty() {
201        return Ok(());
202    }
203    let conclusion = rule
204        .conclusion
205        .triples
206        .first()
207        .ok_or(N3CompileError::MalformedTriple)?;
208    let property_hash = term_hash(&conclusion.predicate)?;
209    let mut matched = false;
210    for shape in shapes {
211        if q_hash(&shape.property_path) == property_hash {
212            matched = true;
213            if let Term::Literal(lit) = &conclusion.object {
214                if let Ok(value) = lit.parse::<f64>() {
215                    if !shape.evaluate_numeric(value) {
216                        return Err(N3CompileError::ShapeViolation);
217                    }
218                }
219            }
220        }
221    }
222    if matched {
223        Ok(())
224    } else {
225        Err(N3CompileError::ShapeViolation)
226    }
227}
228
229fn push_opcode(
230    out: &mut [SlgOpcode],
231    count: &mut usize,
232    opcode: SlgOpcode,
233) -> Result<(), N3CompileError> {
234    if *count >= out.len() {
235        return Err(N3CompileError::OpcodeBufferFull);
236    }
237    out[*count] = opcode;
238    *count += 1;
239    Ok(())
240}
241
242/// Lower one N3 rule into Sentinel opcodes (reuses SHACL terminal semantics).
243pub fn compile_rule_to_opcodes(
244    rule: &CompiledRule,
245    out: &mut [SlgOpcode],
246) -> Result<usize, N3CompileError> {
247    let mut count = 0usize;
248    match rule.rule_type {
249        RuleType::Strict => {
250            push_opcode(out, &mut count, SlgOpcode::Unify)?;
251            push_opcode(out, &mut count, SlgOpcode::Call)?;
252            push_opcode(out, &mut count, SlgOpcode::Halt)?;
253        }
254        RuleType::Defeasible => {
255            push_opcode(out, &mut count, SlgOpcode::CheckDefeaters)?;
256            push_opcode(out, &mut count, SlgOpcode::Unify)?;
257            push_opcode(out, &mut count, SlgOpcode::Call)?;
258            push_opcode(out, &mut count, SlgOpcode::WarnOnly)?;
259        }
260        RuleType::Defeater => {
261            push_opcode(out, &mut count, SlgOpcode::NativeUnless)?;
262            push_opcode(out, &mut count, SlgOpcode::Halt)?;
263        }
264        RuleType::Linear => {
265            push_opcode(out, &mut count, SlgOpcode::NativeLinearConsume)?;
266            push_opcode(out, &mut count, SlgOpcode::Unify)?;
267            push_opcode(out, &mut count, SlgOpcode::Call)?;
268            push_opcode(out, &mut count, SlgOpcode::Halt)?;
269        }
270    }
271    Ok(count)
272}
273
274pub fn compile_rule_to_quin(
275    rule: &CompiledRule,
276    contract_hash: u64,
277    out: &mut [NQuin],
278) -> Result<usize, N3CompileError> {
279    if let Some(norm) =
280        crate::modalities::logic::deontic::compile_n3_rule_to_norm(rule, contract_hash, 0)
281    {
282        if out.is_empty() {
283            return Err(N3CompileError::QuinBufferFull);
284        }
285        out[0] = norm;
286        return Ok(1);
287    }
288
289    let mut count = 0usize;
290    let triples = [&rule.premise, &rule.conclusion];
291    for formula in triples {
292        if let Ok(triple) = first_triple(formula) {
293            if count >= out.len() {
294                return Err(N3CompileError::QuinBufferFull);
295            }
296            out[count] = triple_to_quin(triple, contract_hash)?;
297            count += 1;
298        }
299    }
300    if count == 0 {
301        return Err(N3CompileError::EmptyRule);
302    }
303    Ok(count)
304}
305
306/// SHACL-gated batch compile: validate each rule, then emit opcodes into a fixed buffer.
307pub fn compile_rules_with_shacl_gate(
308    rules: &[Rule<'_>],
309    shapes: &[&CompiledShape],
310    opcodes_out: &mut [SlgOpcode],
311    quins_out: &mut [NQuin],
312    contract_hash: u64,
313) -> Result<N3CompiledProgram, N3CompileError> {
314    let mut opcode_offset = 0usize;
315    let mut quin_offset = 0usize;
316
317    for rule in rules {
318        // SHACL firewall: validate each rule against the routed shapes BEFORE compiling.
319        // `compile_term` hashes literals away ("12" -> u64), so a numeric range check is
320        // only possible here, on the Rule. Fail closed on any violation. Validation reads
321        // the existing Rule (no allocation); the compile step below is stack-only, so the
322        // gate itself remains zero-heap.
323        validate_rule_against_shapes(rule, shapes)?;
324
325        let compiled = compile_rule_to_zero_heap(rule);
326
327        let written = compile_rule_to_opcodes(&compiled, &mut opcodes_out[opcode_offset..])?;
328        opcode_offset += written;
329
330        let quins_written =
331            compile_rule_to_quin(&compiled, contract_hash, &mut quins_out[quin_offset..])?;
332        quin_offset += quins_written;
333    }
334
335    Ok(N3CompiledProgram {
336        opcode_count: opcode_offset,
337        quin_count: quin_offset,
338    })
339}
340
341/// Execute compiled opcodes inside the 42 MB `SlgArena` without heap growth in the eval loop.
342pub fn execute_compiled_program(
343    arena: &mut SlgArena,
344    opcodes: &[SlgOpcode],
345    frame: &mut VmFrame,
346    max_depth: u8,
347) -> Result<Option<NQuin>, SentinelError> {
348    if opcodes.len() > max_depth as usize {
349        return Err(SentinelError::MemoryOverflow);
350    }
351    Ok(execute_vm_frame(arena, opcodes, frame))
352}
353
354/// Build a default health-observation SHACL gate for LLM-emitted N3 (cold path helper).
355pub fn default_observation_shape() -> CompiledShape {
356    ShaclCompiler::new().compile_class(
357        "fhir:Observation",
358        "health:restingHeartRate",
359        ShaclConstraint::MinInclusive(20.0),
360        ShaclSeverity::Violation,
361    )
362}
363
364#[cfg(test)]
365mod tests {
366    use super::*;
367    use crate::modalities::logic::n3_parser::{Formula, Rule, RuleType, Triple};
368
369    fn sample_strict_rule() -> Rule<'static> {
370        Rule {
371            id: Some("hr-observation".into()),
372            rule_type: RuleType::Strict,
373            weight: None,
374            premise: Formula {
375                triples: vec![Triple {
376                    subject: Term::Uri("ex:Patient1".into()),
377                    predicate: Term::Uri("health:restingHeartRate".into()),
378                    object: Term::Literal("72".into()),
379                }],
380            },
381            conclusion: Formula {
382                triples: vec![Triple {
383                    subject: Term::Uri("ex:Patient1".into()),
384                    predicate: Term::Uri("health:restingHeartRate".into()),
385                    object: Term::Literal("72".into()),
386                }],
387            },
388        }
389    }
390
391    #[test]
392    fn compiles_strict_rule_to_opcodes() {
393        let mut opcodes = [SlgOpcode::Call; MAX_COMPILED_OPCODES];
394        let count =
395            compile_rule_to_opcodes(
396                &crate::modalities::logic::n3_compiler::compile_rule_to_zero_heap(
397                    &sample_strict_rule(),
398                ),
399                &mut opcodes,
400            )
401            .unwrap();
402        assert_eq!(count, 3);
403        assert_eq!(opcodes[0], SlgOpcode::Unify);
404        assert_eq!(opcodes[1], SlgOpcode::Call);
405        assert_eq!(opcodes[2], SlgOpcode::Halt);
406    }
407
408    #[test]
409    fn shacl_gate_rejects_out_of_range_numeric() {
410        let mut rule = sample_strict_rule();
411        rule.conclusion.triples[0].object = Term::Literal("12".into());
412        let shape = default_observation_shape();
413        let shapes = [&shape];
414        assert_eq!(
415            validate_rule_against_shapes(&rule, &shapes),
416            Err(N3CompileError::ShapeViolation)
417        );
418    }
419
420    #[test]
421    fn zero_heap_compile_rules_with_shacl_gate() {
422        // Build inputs OUTSIDE the measured region: parser-owned `Rule`s carry heap Vecs;
423        // this test asserts the GATE ITSELF allocates nothing (validate + stack compile).
424        let rules = [sample_strict_rule()];
425        let shape = default_observation_shape();
426        let shapes = [&shape];
427        let mut opcodes = [SlgOpcode::Call; MAX_COMPILED_OPCODES];
428        let mut quins = [NQuin::default(); MAX_COMPILED_QUINS];
429
430        let _profiler = dhat::Profiler::builder().testing().build();
431        let result = compile_rules_with_shacl_gate(
432            &rules,
433            &shapes,
434            &mut opcodes,
435            &mut quins,
436            q_hash("did:test:contract"),
437        );
438        assert!(result.is_ok());
439        assert!(result.unwrap().opcode_count > 0);
440
441        let stats = dhat::HeapStats::get();
442        assert_eq!(
443            stats.curr_blocks, 0,
444            "compile_rules_with_shacl_gate must not allocate"
445        );
446        assert_eq!(stats.curr_bytes, 0);
447    }
448}