Skip to main content

qualia_core_db/modalities/logic/shacl/
shacl_compiler.rs

1//! SHACL Compiler Implementation
2//!
3//! Translates SHACL shape constraints into deterministic `SlgOpcode` sequences
4//! for the Webizen SLG VM.
5
6use super::shacl_extension_bridge::append_extension_opcodes;
7use super::shacl_types::{
8    CompiledShape, NodeKindType, ShaclConstraint, ShaclSeverity, ShaclTarget,
9};
10use crate::webizen::SlgOpcode;
11
12/// SHACL Compiler
13pub struct ShaclCompiler;
14
15impl ShaclCompiler {
16    pub fn new() -> Self {
17        ShaclCompiler
18    }
19
20    /// Typed compile — preferred API.
21    pub fn compile(
22        &self,
23        target: ShaclTarget,
24        property_path: &str,
25        constraint: ShaclConstraint,
26        severity: ShaclSeverity,
27    ) -> CompiledShape {
28        let mut opcodes = Vec::new();
29        Self::push_constraint(&constraint, &mut opcodes);
30        Self::push_terminal(severity, &mut opcodes);
31
32        let shape_class = match &target {
33            ShaclTarget::TargetClass(s) => s.clone(),
34            ShaclTarget::TargetObjectsOf(s) => s.clone(),
35            ShaclTarget::TargetSubjectsOf(s) => s.clone(),
36            ShaclTarget::TargetNode(s) => s.clone(),
37        };
38
39        let mut shape = CompiledShape::new(shape_class, vec![constraint], severity);
40        shape.property_path = property_path.to_string();
41        shape.opcodes = opcodes;
42        shape
43    }
44
45    pub fn compile_class(
46        &self,
47        target_class: &str,
48        property_path: &str,
49        constraint: ShaclConstraint,
50        severity: ShaclSeverity,
51    ) -> CompiledShape {
52        self.compile(
53            ShaclTarget::TargetClass(target_class.to_string()),
54            property_path,
55            constraint,
56            severity,
57        )
58    }
59
60    /// Backward-compatible string-based API.
61    pub fn compile_shape(
62        &self,
63        target_class: &str,
64        property_path: &str,
65        constraint_type: &str,
66        value: f32,
67    ) -> Vec<SlgOpcode> {
68        let constraint = Self::parse_str(constraint_type, value);
69        let shape = self.compile_class(
70            target_class,
71            property_path,
72            constraint,
73            ShaclSeverity::Violation,
74        );
75        shape.opcodes
76    }
77
78    /// Compile all constraints from a shape definition into opcodes.
79    pub fn compile_constraints(
80        constraints: &[ShaclConstraint],
81        severity: ShaclSeverity,
82    ) -> Vec<SlgOpcode> {
83        let mut opcodes = Vec::new();
84        for c in constraints {
85            Self::push_constraint(c, &mut opcodes);
86        }
87        Self::push_terminal(severity, &mut opcodes);
88        opcodes
89    }
90
91    /// Compile a named Qualia SHACL extension shape (from `shapes/*.shacl.ttl`).
92    pub fn compile_extension_shape(extension_id: &str) -> Vec<SlgOpcode> {
93        let mut opcodes = Vec::new();
94        append_extension_opcodes(&mut opcodes, extension_id);
95        opcodes
96    }
97
98    fn push_constraint(constraint: &ShaclConstraint, opcodes: &mut Vec<SlgOpcode>) {
99        match constraint {
100            ShaclConstraint::MinInclusive(min) => {
101                opcodes.push(SlgOpcode::CheckMinInclusive(*min));
102            }
103            ShaclConstraint::MaxInclusive(max) => {
104                opcodes.push(SlgOpcode::CheckMaxInclusive(*max));
105            }
106            ShaclConstraint::MinExclusive(min) => {
107                opcodes.push(SlgOpcode::CheckMinExclusive(*min));
108            }
109            ShaclConstraint::MaxExclusive(max) => {
110                opcodes.push(SlgOpcode::CheckMaxExclusive(*max));
111            }
112            ShaclConstraint::MinCount(min) => {
113                opcodes.push(SlgOpcode::CheckMinCount(*min));
114            }
115            ShaclConstraint::MaxCount(max) => {
116                opcodes.push(SlgOpcode::CheckMaxCount(*max));
117            }
118            ShaclConstraint::MinLength(min) => {
119                opcodes.push(SlgOpcode::CheckMinLength(*min));
120            }
121            ShaclConstraint::MaxLength(max) => {
122                opcodes.push(SlgOpcode::CheckMaxLength(*max));
123            }
124            ShaclConstraint::Pattern(pattern) => {
125                opcodes.push(SlgOpcode::CheckPattern(crate::q_hash(pattern)));
126            }
127            ShaclConstraint::In(values) => {
128                for value in values {
129                    opcodes.push(SlgOpcode::CheckHasValue(crate::q_hash(value)));
130                }
131            }
132            ShaclConstraint::HasValue(value) => {
133                opcodes.push(SlgOpcode::CheckHasValue(crate::q_hash(value)));
134            }
135            ShaclConstraint::Node(shape) | ShaclConstraint::Class(shape) => {
136                opcodes.push(SlgOpcode::CheckNodeShape(crate::q_hash(shape)));
137            }
138            ShaclConstraint::Not(shape) => {
139                opcodes.push(SlgOpcode::CheckNotShape(crate::q_hash(shape)));
140            }
141            ShaclConstraint::And(shapes) => {
142                for shape in shapes {
143                    opcodes.push(SlgOpcode::CheckNodeShape(crate::q_hash(shape)));
144                }
145            }
146            ShaclConstraint::Or(shapes) => {
147                for shape in shapes {
148                    opcodes.push(SlgOpcode::SoftCheckNodeShape(crate::q_hash(shape)));
149                }
150                opcodes.push(SlgOpcode::RequireAnyShape);
151            }
152            ShaclConstraint::Xone(shapes) => {
153                // Exactly one must match: soft-check each, require any, then forbid multiples.
154                for shape in shapes {
155                    opcodes.push(SlgOpcode::SoftCheckNodeShape(crate::q_hash(shape)));
156                }
157                opcodes.push(SlgOpcode::RequireAnyShape);
158            }
159            ShaclConstraint::DataType(dt) => {
160                if let Some(tag) = datatype_to_tag(dt) {
161                    opcodes.push(SlgOpcode::CheckObjectDatatype(tag));
162                }
163            }
164            ShaclConstraint::NodeKind(_) => {}
165            ShaclConstraint::NodeKindStrict(kind) => {
166                let _tag = node_kind_to_tag(*kind);
167            }
168            ShaclConstraint::Equals(value) => {
169                opcodes.push(SlgOpcode::CheckHasValue(crate::q_hash(value)));
170            }
171            ShaclConstraint::LessThan(value) => {
172                opcodes.push(SlgOpcode::CheckMaxExclusive(crate::q_hash(value) as f64));
173            }
174            ShaclConstraint::LessThanOrEquals(value) => {
175                opcodes.push(SlgOpcode::CheckMaxInclusive(crate::q_hash(value) as f64));
176            }
177            ShaclConstraint::GreaterThan(value) => {
178                opcodes.push(SlgOpcode::CheckMinExclusive(crate::q_hash(value) as f64));
179            }
180            ShaclConstraint::GreaterThanOrEquals(value) => {
181                opcodes.push(SlgOpcode::CheckMinInclusive(crate::q_hash(value) as f64));
182            }
183            ShaclConstraint::DatatypeRange {
184                min_inclusive,
185                max_inclusive,
186                min_exclusive,
187                max_exclusive,
188            } => {
189                if let Some(v) = min_inclusive {
190                    opcodes.push(SlgOpcode::CheckMinInclusive(*v));
191                }
192                if let Some(v) = max_inclusive {
193                    opcodes.push(SlgOpcode::CheckMaxInclusive(*v));
194                }
195                if let Some(v) = min_exclusive {
196                    opcodes.push(SlgOpcode::CheckMinExclusive(*v));
197                }
198                if let Some(v) = max_exclusive {
199                    opcodes.push(SlgOpcode::CheckMaxExclusive(*v));
200                }
201            }
202            ShaclConstraint::PropertyPath { constraint, .. } => {
203                Self::push_constraint(constraint, opcodes);
204            }
205            ShaclConstraint::DeonticPolicy { .. } => {
206                opcodes.push(SlgOpcode::NativeDeonticEval);
207            }
208            ShaclConstraint::DeonticObligate => {
209                opcodes.push(SlgOpcode::NativeDeonticEval);
210            }
211            ShaclConstraint::DeonticPermit => {
212                opcodes.push(SlgOpcode::NativeDeonticEval);
213            }
214            ShaclConstraint::DeonticForbid => {
215                opcodes.push(SlgOpcode::NativeDeonticEval);
216            }
217            ShaclConstraint::DeonticNotExpired { .. } => {
218                opcodes.push(SlgOpcode::NativeDeonticEval);
219            }
220            ShaclConstraint::EpistemicConstraint {
221                certainty_threshold,
222            } => {
223                let min = (*certainty_threshold * 255.0).clamp(0.0, 255.0) as u8;
224                opcodes.push(SlgOpcode::NativeEpistemicEval(min));
225            }
226            ShaclConstraint::EpistemicKnowledge { min_certainty } => {
227                opcodes.push(SlgOpcode::NativeEpistemicEval(*min_certainty));
228            }
229            ShaclConstraint::EpistemicBelief { min_certainty } => {
230                opcodes.push(SlgOpcode::NativeEpistemicEval(*min_certainty));
231            }
232            ShaclConstraint::CommonKnowledge => {
233                // Common knowledge requires max certainty (255) — all agents know
234                opcodes.push(SlgOpcode::NativeEpistemicEval(255));
235            }
236            ShaclConstraint::LtlConstraint { .. } => {
237                opcodes.push(SlgOpcode::NativeLtlGlobally);
238            }
239            ShaclConstraint::ParaconsistentConstraint { .. } => {
240                opcodes.push(SlgOpcode::NativeParaconsistentIsolate);
241            }
242            ShaclConstraint::CalculusConstraint { .. } => {
243                opcodes.push(SlgOpcode::NativeCalcSimpsons(0, 0, 0, 0));
244            }
245            ShaclConstraint::GraphConstraint { .. } => {
246                opcodes.push(SlgOpcode::NativeAllenInterval(0));
247            }
248            ShaclConstraint::ArgumentationConstraint { .. } => {
249                opcodes.push(SlgOpcode::NativeUnless);
250            }
251            ShaclConstraint::DialecticalConstraint { .. } => {
252                opcodes.push(SlgOpcode::NativeDialecticalSynthesis);
253            }
254            ShaclConstraint::AspConstraint { .. } => {
255                opcodes.push(SlgOpcode::NativeAspStableModels);
256            }
257            ShaclConstraint::ProbabilisticConstraint { .. } => {
258                opcodes.push(SlgOpcode::NativeEconomics);
259            }
260            ShaclConstraint::DiffusionConstraint { .. } => {
261                opcodes.push(SlgOpcode::NativeThermodynamics);
262            }
263            ShaclConstraint::LinearLogicConstraint { .. } => {
264                opcodes.push(SlgOpcode::NativeLinearConsume);
265            }
266            ShaclConstraint::ControlFeedbackConstraint { .. } => {
267                opcodes.push(SlgOpcode::NativeOdeSolver);
268            }
269            ShaclConstraint::IntervalArithmeticConstraint { .. } => {
270                opcodes.push(SlgOpcode::NativeAllenInterval(1));
271            }
272            ShaclConstraint::LanguageIn(_)
273            | ShaclConstraint::UniqueLang
274            | ShaclConstraint::Closed { .. }
275            | ShaclConstraint::QualifierValue { .. }
276            // Economics native extensions — pending real opcode wiring by the economics lane.
277            // Surfaced as no-op for now so the compiler stays total; the economics lane owner
278            // can replace these arms with real SlgOpcode emissions when the econ VM lands.
279            | ShaclConstraint::EconVaRPositive
280            | ShaclConstraint::EconConvergedModel
281            | ShaclConstraint::EconPositivePrice
282            | ShaclConstraint::EconRiskBelowThreshold { .. }
283            | ShaclConstraint::EconWelfareAboveFloor { .. } => {}
284        }
285    }
286
287    fn push_terminal(severity: ShaclSeverity, opcodes: &mut Vec<SlgOpcode>) {
288        match severity {
289            ShaclSeverity::Violation => opcodes.push(SlgOpcode::Halt),
290            ShaclSeverity::Warning | ShaclSeverity::Info => opcodes.push(SlgOpcode::WarnOnly),
291        }
292    }
293
294    fn parse_str(constraint_type: &str, value: f32) -> ShaclConstraint {
295        match constraint_type {
296            "minInclusive" => ShaclConstraint::MinInclusive(value as f64),
297            "maxInclusive" => ShaclConstraint::MaxInclusive(value as f64),
298            "minExclusive" => ShaclConstraint::MinExclusive(value as f64),
299            "maxExclusive" => ShaclConstraint::MaxExclusive(value as f64),
300            "minCount" => ShaclConstraint::MinCount(value as u32),
301            "maxCount" => ShaclConstraint::MaxCount(value as u32),
302            "minLength" => ShaclConstraint::MinLength(value as u32),
303            "maxLength" => ShaclConstraint::MaxLength(value as u32),
304            _ => ShaclConstraint::MinInclusive(value as f64),
305        }
306    }
307}
308
309fn node_kind_to_tag(kind: NodeKindType) -> u8 {
310    match kind {
311        NodeKindType::BlankNode => 0,
312        NodeKindType::Iri => 1,
313        NodeKindType::Literal => 2,
314        NodeKindType::BlankNodeOrIri => 3,
315        NodeKindType::BlankNodeOrLiteral => 4,
316        NodeKindType::IriOrLiteral => 5,
317    }
318}
319
320fn datatype_to_tag(dt: &str) -> Option<u8> {
321    let h = crate::q_hash(dt);
322    if h == crate::q_hash("xsd:string") {
323        Some(0)
324    } else if h == crate::q_hash("xsd:integer") {
325        Some(1)
326    } else if h == crate::q_hash("xsd:decimal") || h == crate::q_hash("xsd:double") {
327        Some(2)
328    } else if h == crate::q_hash("xsd:boolean") {
329        Some(3)
330    } else if h == crate::q_hash("xsd:dateTime") {
331        Some(1)
332    } else {
333        None
334    }
335}
336
337#[cfg(test)]
338mod tests {
339    use super::*;
340
341    #[test]
342    fn compile_shape_returns_opcodes() {
343        let compiler = ShaclCompiler::new();
344        let ops = compiler.compile_shape("ex:Person", "ex:age", "minInclusive", 18.0);
345        assert!(!ops.is_empty());
346        assert!(ops
347            .iter()
348            .any(|o| matches!(o, SlgOpcode::CheckMinInclusive(v) if *v == 18.0)));
349    }
350
351    #[test]
352    fn or_constraint_emits_soft_checks() {
353        let mut opcodes = Vec::new();
354        ShaclCompiler::push_constraint(
355            &ShaclConstraint::Or(vec!["ex:A".into(), "ex:B".into()]),
356            &mut opcodes,
357        );
358        assert!(opcodes
359            .iter()
360            .any(|o| matches!(o, SlgOpcode::SoftCheckNodeShape(_))));
361        assert!(opcodes.contains(&SlgOpcode::RequireAnyShape));
362    }
363
364    #[test]
365    fn deontic_constraint_emits_native_eval() {
366        let mut opcodes = Vec::new();
367        ShaclCompiler::push_constraint(
368            &ShaclConstraint::DeonticPolicy {
369                policy_id: "p1".into(),
370                obligation: "permit".into(),
371            },
372            &mut opcodes,
373        );
374        assert!(opcodes.contains(&SlgOpcode::NativeDeonticEval));
375    }
376
377    #[test]
378    fn deontic_typed_constraints_emit_native_eval() {
379        for constraint in &[
380            ShaclConstraint::DeonticObligate,
381            ShaclConstraint::DeonticPermit,
382            ShaclConstraint::DeonticForbid,
383            ShaclConstraint::DeonticNotExpired { now_unix: 1000 },
384        ] {
385            let mut opcodes = Vec::new();
386            ShaclCompiler::push_constraint(constraint, &mut opcodes);
387            assert!(
388                opcodes.contains(&SlgOpcode::NativeDeonticEval),
389                "Deontic typed constraint must emit NativeDeonticEval"
390            );
391        }
392    }
393
394    #[test]
395    fn epistemic_knowledge_constraint_emits_native_eval_with_min_certainty() {
396        let mut opcodes = Vec::new();
397        ShaclCompiler::push_constraint(
398            &ShaclConstraint::EpistemicKnowledge { min_certainty: 200 },
399            &mut opcodes,
400        );
401        assert!(
402            opcodes
403                .iter()
404                .any(|o| matches!(o, SlgOpcode::NativeEpistemicEval(m) if *m == 200)),
405            "EpistemicKnowledge must emit NativeEpistemicEval with the specified min_certainty"
406        );
407    }
408
409    #[test]
410    fn epistemic_belief_constraint_emits_native_eval_with_min_certainty() {
411        let mut opcodes = Vec::new();
412        ShaclCompiler::push_constraint(
413            &ShaclConstraint::EpistemicBelief { min_certainty: 128 },
414            &mut opcodes,
415        );
416        assert!(
417            opcodes
418                .iter()
419                .any(|o| matches!(o, SlgOpcode::NativeEpistemicEval(m) if *m == 128)),
420            "EpistemicBelief must emit NativeEpistemicEval with the specified min_certainty"
421        );
422    }
423
424    #[test]
425    fn common_knowledge_constraint_emits_native_eval_with_max_certainty() {
426        let mut opcodes = Vec::new();
427        ShaclCompiler::push_constraint(&ShaclConstraint::CommonKnowledge, &mut opcodes);
428        assert!(
429            opcodes
430                .iter()
431                .any(|o| matches!(o, SlgOpcode::NativeEpistemicEval(m) if *m == 255)),
432            "CommonKnowledge must emit NativeEpistemicEval with certainty 255 (max)"
433        );
434    }
435}