Skip to main content

urge_core/
engine.rs

1//! Logic engine trait and paradigm enumeration.
2//!
3//! Every logic paradigm in the system is represented by a [`Paradigm`] variant
4//! and implemented by a type that satisfies [`LogicEngine`]. The meta-engine
5//! (Figure 26) uses the engine registry to route AST nodes to the correct
6//! evaluator at runtime without allocation.
7
8use crate::ast::AstNode;
9use crate::decision::Verdict;
10
11/// All supported logic paradigms, ordered for stable bit-mapping.
12///
13/// The order here defines the bit positions in [`crate::symbol::ParadigmSet`].
14/// **Do not reorder without a semver bump.**
15#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
16#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
17#[repr(u8)]
18pub enum Paradigm {
19    Boolean = 0,
20    Modal = 1,
21    Epistemic = 2,
22    Deontic = 3,
23    Temporal = 4,
24    Fuzzy = 5,
25    Probabilistic = 6,
26    Paraconsistent = 7,
27    // Room for up to 8 more within a u16 ParadigmSet
28}
29
30impl Paradigm {
31    pub const ALL: &'static [Paradigm] = &[
32        Paradigm::Boolean,
33        Paradigm::Modal,
34        Paradigm::Epistemic,
35        Paradigm::Deontic,
36        Paradigm::Temporal,
37        Paradigm::Fuzzy,
38        Paradigm::Probabilistic,
39        Paradigm::Paraconsistent,
40    ];
41
42    pub fn name(self) -> &'static str {
43        match self {
44            Paradigm::Boolean => "boolean",
45            Paradigm::Modal => "modal",
46            Paradigm::Epistemic => "epistemic",
47            Paradigm::Deontic => "deontic",
48            Paradigm::Temporal => "temporal",
49            Paradigm::Fuzzy => "fuzzy",
50            Paradigm::Probabilistic => "probabilistic",
51            Paradigm::Paraconsistent => "paraconsistent",
52        }
53    }
54}
55
56/// Stable integer identifier for an engine instance.
57/// Used in logic traces so the trace is self-describing.
58#[derive(Debug, Clone, Copy, PartialEq, Eq)]
59pub struct EngineId(pub u8);
60
61/// Errors that a logic engine can return.
62#[derive(Debug, Clone, PartialEq, Eq)]
63pub enum EngineError {
64    /// The AST node type is not supported by this engine.
65    UnsupportedNode,
66    /// A required context value is absent.
67    MissingContext(&'static str),
68    /// The expression is syntactically malformed for this paradigm.
69    MalformedExpression,
70    /// The engine encountered a contradiction it cannot resolve.
71    ContradictionDetected,
72    /// Evaluation exceeded the allowed recursion depth.
73    DepthLimitExceeded,
74}
75
76impl EngineError {
77    pub fn as_str(&self) -> &'static str {
78        match self {
79            EngineError::UnsupportedNode => "unsupported_node",
80            EngineError::MissingContext(_) => "missing_context",
81            EngineError::MalformedExpression => "malformed_expression",
82            EngineError::ContradictionDetected => "contradiction_detected",
83            EngineError::DepthLimitExceeded => "depth_limit_exceeded",
84        }
85    }
86}
87
88#[cfg(feature = "std")]
89impl std::fmt::Display for EngineError {
90    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
91        match self {
92            EngineError::UnsupportedNode => write!(f, "UnsupportedNode"),
93            EngineError::MissingContext(key) => write!(f, "MissingContext({key})"),
94            EngineError::MalformedExpression => write!(f, "MalformedExpression"),
95            EngineError::ContradictionDetected => write!(f, "ContradictionDetected"),
96            EngineError::DepthLimitExceeded => write!(f, "DepthLimitExceeded"),
97        }
98    }
99}
100
101#[cfg(feature = "std")]
102impl std::error::Error for EngineError {}
103
104/// Opaque evaluation context passed through the pipeline.
105///
106/// In an embedded context this can be a fixed-size struct on the stack.
107/// In a `std` context it wraps a hash map for arbitrary key-value state.
108pub struct EvalContext<'a> {
109    /// Flat key-value pairs — avoids HashMap for `no_std` compatibility.
110    pub slots: &'a [(&'static str, ContextValue)],
111    /// Current logical time (for temporal engines). Nanoseconds since epoch,
112    /// or a monotonic counter on embedded targets.
113    pub logical_time: u64,
114    /// Maximum recursion depth allowed for this evaluation.
115    pub depth_limit: u8,
116}
117
118impl<'a> EvalContext<'a> {
119    pub fn get(&self, key: &'static str) -> Option<&ContextValue> {
120        self.slots.iter().find(|(k, _)| *k == key).map(|(_, v)| v)
121    }
122}
123
124/// Values that can appear in evaluation context slots.
125#[derive(Debug, Clone, PartialEq)]
126pub enum ContextValue {
127    Bool(bool),
128    Integer(i64),
129    Float(f64),
130    Str(&'static str),
131    #[cfg(feature = "alloc")]
132    OwnedStr(alloc::string::String),
133}
134
135impl ContextValue {
136    pub fn as_bool(&self) -> Option<bool> {
137        if let ContextValue::Bool(b) = self {
138            Some(*b)
139        } else {
140            None
141        }
142    }
143    pub fn as_i64(&self) -> Option<i64> {
144        if let ContextValue::Integer(n) = self {
145            Some(*n)
146        } else {
147            None
148        }
149    }
150    pub fn as_f64(&self) -> Option<f64> {
151        if let ContextValue::Float(f) = self {
152            Some(*f)
153        } else {
154            None
155        }
156    }
157}
158
159/// The core trait every logic engine must implement.
160///
161/// Implementations are stateless by design — all state lives in `EvalContext`.
162/// This makes engines safe to share across threads and suitable for ROM.
163pub trait LogicEngine {
164    /// Stable identifier for this engine instance.
165    fn id(&self) -> EngineId;
166
167    /// Which paradigm this engine primarily implements.
168    fn paradigm(&self) -> Paradigm;
169
170    /// Human-readable name for logic traces.
171    fn name(&self) -> &'static str;
172
173    /// Whether this engine can handle the given AST node.
174    /// Called by the router before `evaluate` to avoid wasted work.
175    fn can_handle(&self, node: &AstNode) -> bool;
176
177    /// Evaluate an AST node and return a [`Verdict`].
178    ///
179    /// Engines must be **deterministic**: same `node` + same `ctx` → same `Verdict`.
180    fn evaluate(&self, node: &AstNode, ctx: &EvalContext<'_>) -> Result<Verdict, EngineError>;
181}