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}