1use 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
195pub 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
242pub 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
306pub 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 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
341pub 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
354pub 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 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}