Skip to main content

urge_core/
ast.rs

1//! Abstract Syntax Tree for multi-paradigm logical expressions.
2//!
3//! The AST is built by the tokenizer/parser stage of Figure 26.
4//! Each node is annotated with the paradigm(s) it belongs to so the router
5//! can dispatch without re-scanning the tree.
6//!
7//! ## Memory model
8//!
9//! - With `alloc`: nodes box their children (heap-allocated, unbounded depth).
10//! - Without `alloc`: the `Shallow` variants use fixed-size inline children
11//!   bounded by the `heapless` structures. This is suitable for embedded.
12
13use crate::symbol::{ParadigmSet, SemanticClass};
14
15// ── Literal values ─────────────────────────────────────────────────────────────
16
17/// A literal value in an expression.
18#[derive(Debug, Clone, PartialEq)]
19#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
20pub enum Literal {
21    Bool(bool),
22    Integer(i64),
23    Float(f64),
24    /// Short string that fits in 32 bytes without allocation.
25    ShortStr(heapless::String<32>),
26    #[cfg(feature = "alloc")]
27    Str(alloc::string::String),
28    /// Unix timestamp (nanoseconds) or monotonic counter for temporal engines.
29    Time(u64),
30    /// Probability in [0.0, 1.0].
31    Probability(f32),
32    /// Fuzzy membership degree in [0.0, 1.0].
33    Membership(f32),
34}
35
36impl Literal {
37    pub fn as_bool(&self) -> Option<bool> {
38        match self {
39            Literal::Bool(b) => Some(*b),
40            Literal::Integer(n) => Some(*n != 0),
41            _ => None,
42        }
43    }
44
45    pub fn as_f64(&self) -> Option<f64> {
46        match self {
47            Literal::Float(f) => Some(*f),
48            Literal::Integer(n) => Some(*n as f64),
49            Literal::Probability(p) => Some(*p as f64),
50            Literal::Membership(m) => Some(*m as f64),
51            _ => None,
52        }
53    }
54}
55
56// ── AST node ───────────────────────────────────────────────────────────────────
57
58/// A node in the multi-paradigm AST.
59///
60/// The tree mixes paradigms freely: a Deontic `Obligatory` node can contain
61/// a Temporal `Globally` subtree. The cross-validation stage checks that such
62/// mixtures are coherent.
63#[derive(Debug, Clone)]
64#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
65pub enum Expr {
66    // ── Terminals ──────────────────────────────────────────────────────────
67    Lit(Literal),
68    Var {
69        name: heapless::String<32>,
70        paradigms: ParadigmSet,
71    },
72
73    // ── Unary operators ────────────────────────────────────────────────────
74    Unary {
75        op: SemanticClass,
76        operand: AstNode,
77        paradigms: ParadigmSet,
78    },
79
80    // ── Binary operators ───────────────────────────────────────────────────
81    Binary {
82        op: SemanticClass,
83        left: AstNode,
84        right: AstNode,
85        paradigms: ParadigmSet,
86    },
87
88    // ── Ternary (e.g., temporal Until: φ U ψ) ─────────────────────────────
89    Ternary {
90        op: SemanticClass,
91        first: AstNode,
92        second: AstNode,
93        third: AstNode,
94        paradigms: ParadigmSet,
95    },
96
97    // ── Quantified formula ─────────────────────────────────────────────────
98    Quantified {
99        quantifier: SemanticClass,
100        variable: heapless::String<32>,
101        body: AstNode,
102        paradigms: ParadigmSet,
103    },
104
105    // ── Application (agent · predicate in epistemic logic) ────────────────
106    Apply {
107        op: SemanticClass,
108        agent: heapless::String<16>,
109        body: AstNode,
110        paradigms: ParadigmSet,
111    },
112
113    // ── Obligation / Permission / Prohibition with metadata ────────────────
114    DeonticStatement {
115        modality: SemanticClass, // Obligatory | Permitted | Forbidden
116        agent: heapless::String<16>,
117        action: heapless::String<32>,
118        /// Optional deadline for obligation lifecycle tracking.
119        deadline_ns: Option<u64>,
120        /// Policy source citation (regulatory anchor).
121        source: Option<heapless::String<64>>,
122        paradigms: ParadigmSet,
123    },
124
125    // ── Temporal constraint ────────────────────────────────────────────────
126    TemporalConstraint {
127        op: SemanticClass, // Globally | Finally | Until | …
128        body: AstNode,
129        /// Absolute time bound in logical-time units.
130        bound_ns: Option<u64>,
131        paradigms: ParadigmSet,
132    },
133}
134
135impl Expr {
136    /// The set of paradigms this expression node participates in.
137    pub fn paradigms(&self) -> ParadigmSet {
138        match self {
139            Expr::Lit(_) => ParadigmSet::empty(),
140            Expr::Var { paradigms, .. } => *paradigms,
141            Expr::Unary { paradigms, .. } => *paradigms,
142            Expr::Binary { paradigms, .. } => *paradigms,
143            Expr::Ternary { paradigms, .. } => *paradigms,
144            Expr::Quantified { paradigms, .. } => *paradigms,
145            Expr::Apply { paradigms, .. } => *paradigms,
146            Expr::DeonticStatement { paradigms, .. } => *paradigms,
147            Expr::TemporalConstraint { paradigms, .. } => *paradigms,
148        }
149    }
150}
151
152// ── Box alias for heap/no-heap compatibility ───────────────────────────────────
153
154#[cfg(feature = "alloc")]
155pub type AstNode = alloc::boxed::Box<Expr>;
156
157/// Without `alloc` we can still represent leaf/single-level nodes inline.
158/// Deep nesting requires the `alloc` feature.
159#[cfg(not(feature = "alloc"))]
160#[derive(Debug, Clone)]
161pub struct AstNode(pub Expr);
162
163#[cfg(feature = "alloc")]
164pub fn node(expr: Expr) -> AstNode {
165    alloc::boxed::Box::new(expr)
166}
167
168#[cfg(not(feature = "alloc"))]
169pub fn node(expr: Expr) -> AstNode {
170    AstNode(expr)
171}
172
173// ── Token (pre-AST) ────────────────────────────────────────────────────────────
174
175/// A raw token produced by the tokenizer before AST construction.
176#[derive(Debug, Clone, PartialEq)]
177pub struct Token {
178    /// The semantic class resolved from the Unicode dictionary.
179    pub class: SemanticClass,
180    /// Raw text of the token for trace reconstruction.
181    pub raw: heapless::String<32>,
182    /// Character offset in the original input.
183    pub offset: u32,
184}
185
186impl Token {
187    pub fn new(class: SemanticClass, raw: &str, offset: u32) -> Self {
188        let mut s = heapless::String::new();
189        for ch in raw.chars().take(32) {
190            let _ = s.push(ch);
191        }
192        Token {
193            class,
194            raw: s,
195            offset,
196        }
197    }
198}