zerodds_ast/node/constant.rs
1// SPDX-License-Identifier: Apache-2.0
2// Copyright 2026 ZeroDDS Contributors
3//! Constants and expression trees. Spec §3.5.
4
5use crate::facet::Facets;
6use crate::node::member::EnumeratorNode;
7use crate::node::module::ModuleNode;
8use crate::node::type_::TypeNode;
9use alloc::string::String;
10use alloc::vec::Vec;
11use core::fmt;
12use zerodds_ast_core::{Id, NodeId, Ref};
13
14/// A named constant. Spec §3.5.
15#[derive(Debug, Clone, PartialEq, Eq)]
16pub struct ConstNode {
17 /// Stable identity.
18 pub node_id: NodeId,
19 /// Simple name.
20 pub name: String,
21 /// Enclosing module.
22 pub module: Id<ModuleNode>,
23 /// The interface or value type this is declared in, when it is not
24 /// declared directly in a module (OMG IDL 4.2 §7.4.3.4: an interface
25 /// body holds type, constant and exception declarations). Its name is
26 /// a segment of the scoped name and of the repository id:
27 /// `CosNaming::NamingContext::NotFound`.
28 pub scope: Option<String>,
29 /// Declared type.
30 pub type_: Ref<TypeNode>,
31 /// The defining expression, retained as a tree.
32 pub expr: Id<ConstExprNode>,
33 /// Per-source information. Spec §4.
34 pub facets: Facets,
35}
36
37/// A literal value.
38///
39/// Floating-point and fixed-point literals retain their written form:
40/// `1.` and `1.0` are the same number but not the same source text, and
41/// the formatter has to render back what was written. Spec §3.5.
42#[derive(Debug, Clone, PartialEq, Eq)]
43pub enum Literal {
44 /// `TRUE` / `FALSE`.
45 Boolean(bool),
46 /// Integer literal, in its evaluated form; the written radix lives
47 /// in the source facet.
48 Integer(i128),
49 /// Floating-point literal, as written.
50 Float {
51 /// Mantissa exactly as written, including any trailing point.
52 mantissa: String,
53 /// Exponent as written, without the `e`.
54 exponent: Option<String>,
55 },
56 /// Fixed-point literal, as written.
57 Fixed {
58 /// Digits as written, without the point.
59 digits: String,
60 /// Digits after the point.
61 scale: u8,
62 },
63 /// Narrow character literal.
64 Char(char),
65 /// Wide character literal, as a code point.
66 WChar(u32),
67 /// Narrow string literal, with escapes resolved.
68 String(String),
69 /// Wide string literal, as code points.
70 WString(Vec<u32>),
71 /// An enumerator used as a value.
72 Enumerator(Ref<EnumeratorNode>),
73}
74
75/// Unary operator. Spec §3.5, IDL 4.2 §7.4.1.4.
76#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
77pub enum UnaryOp {
78 /// `+`
79 Plus,
80 /// `-`
81 Minus,
82 /// `~`
83 Not,
84}
85
86impl UnaryOp {
87 /// Every unary operator IDL 4.2 defines.
88 pub const ALL: [Self; 3] = [Self::Plus, Self::Minus, Self::Not];
89
90 /// Source spelling.
91 #[must_use]
92 pub const fn as_str(self) -> &'static str {
93 match self {
94 Self::Plus => "+",
95 Self::Minus => "-",
96 Self::Not => "~",
97 }
98 }
99}
100
101/// Binary operator. Spec §3.5, IDL 4.2 §7.4.1.4.
102#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
103pub enum BinaryOp {
104 /// `|`
105 Or,
106 /// `^`
107 Xor,
108 /// `&`
109 And,
110 /// `<<`
111 Shl,
112 /// `>>`
113 Shr,
114 /// `+`
115 Add,
116 /// `-`
117 Sub,
118 /// `*`
119 Mul,
120 /// `/`
121 Div,
122 /// `%`
123 Mod,
124}
125
126impl BinaryOp {
127 /// Every binary operator IDL 4.2 defines, in precedence order from
128 /// loosest to tightest.
129 pub const ALL: [Self; 10] = [
130 Self::Or,
131 Self::Xor,
132 Self::And,
133 Self::Shl,
134 Self::Shr,
135 Self::Add,
136 Self::Sub,
137 Self::Mul,
138 Self::Div,
139 Self::Mod,
140 ];
141
142 /// Source spelling.
143 #[must_use]
144 pub const fn as_str(self) -> &'static str {
145 match self {
146 Self::Or => "|",
147 Self::Xor => "^",
148 Self::And => "&",
149 Self::Shl => "<<",
150 Self::Shr => ">>",
151 Self::Add => "+",
152 Self::Sub => "-",
153 Self::Mul => "*",
154 Self::Div => "/",
155 Self::Mod => "%",
156 }
157 }
158}
159
160/// One node of a constant expression tree.
161#[derive(Debug, Clone, PartialEq, Eq)]
162pub enum ConstExpr {
163 /// A literal value.
164 Literal(Literal),
165 /// A reference to another constant by name.
166 NameRef(Ref<ConstNode>),
167 /// Unary application.
168 Unary {
169 /// The operator.
170 op: UnaryOp,
171 /// Its operand.
172 operand: Id<ConstExprNode>,
173 },
174 /// Binary application.
175 Binary {
176 /// The operator.
177 op: BinaryOp,
178 /// Left operand.
179 lhs: Id<ConstExprNode>,
180 /// Right operand.
181 rhs: Id<ConstExprNode>,
182 },
183}
184
185/// An expression node.
186///
187/// Expressions live in their own arena, so an expression referenced from
188/// a bound, an array dimension, a case label, a `@default` or an
189/// enumerator value is a first-class node with its own identity and its
190/// own source facet. Spec §3.5.
191#[derive(Debug, Clone, PartialEq, Eq)]
192pub struct ConstExprNode {
193 /// Stable identity.
194 pub node_id: NodeId,
195 /// The expression at this node.
196 pub expr: ConstExpr,
197 /// Per-source information. Spec §4.
198 pub facets: Facets,
199}
200
201/// Why an expression could not be evaluated.
202///
203/// Evaluation of a partially discovered model fails loudly; it never
204/// substitutes a default. Spec §3.5.
205#[derive(Debug, Clone, PartialEq, Eq)]
206#[non_exhaustive]
207pub enum EvalError {
208 /// The tree reached a reference that is not resolved.
209 Unresolved {
210 /// The expression node that could not be followed.
211 at: NodeId,
212 },
213 /// A handle in the tree did not address a live expression node.
214 DanglingExpr,
215 /// Division or remainder by zero.
216 DivisionByZero {
217 /// The expression node performing the division.
218 at: NodeId,
219 },
220 /// The computation left the range of `i128`.
221 Overflow {
222 /// The expression node that overflowed.
223 at: NodeId,
224 },
225 /// A shift distance was negative or wider than the value.
226 InvalidShift {
227 /// The expression node performing the shift.
228 at: NodeId,
229 },
230 /// The literal is not integral, so integral evaluation cannot apply.
231 NotIntegral {
232 /// The expression node holding the literal.
233 at: NodeId,
234 },
235 /// The expression nests deeper than evaluation will follow. Bounds
236 /// the work a hostile snapshot can demand.
237 TooDeep,
238 /// An infix operator combines a floating-point operand with one that
239 /// is not. IDL 4.2 §7.4.1.4.2: "An infix operator may combine two
240 /// integer types, floating point types or fixed point types, but not
241 /// mixtures of these."
242 MixedOperands {
243 /// The expression node with the mixed operands.
244 at: NodeId,
245 },
246 /// An operator other than unary `+ -` and binary `* / + -` over
247 /// floating-point operands. IDL 4.2 §7.4.1.4.2 admits only those.
248 NotFloatingOperator {
249 /// The expression node performing the operation.
250 at: NodeId,
251 },
252 /// An operator other than unary `+ -` and binary `* / + -` over
253 /// fixed-point operands. IDL 4.2 §7.4.1.4.2 admits only those.
254 NotFixedOperator {
255 /// The expression node performing the operation.
256 at: NodeId,
257 },
258 /// The arithmetic would have to run in `long double`: the constant
259 /// is one, an operand is one, or a literal is too large for `double`
260 /// and so is one (IDL 4.2 §7.4.1.4.2). This build has no IEEE
261 /// double-extended or binary128 arithmetic to run it in, and doing it
262 /// in `double` instead would round where the clause says not to.
263 LongDouble {
264 /// The expression node that needs it.
265 at: NodeId,
266 },
267}
268
269impl fmt::Display for EvalError {
270 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
271 match self {
272 Self::Unresolved { at } => write!(f, "expression {at:?} reaches an unresolved name"),
273 Self::DanglingExpr => write!(f, "expression tree contains a dangling handle"),
274 Self::DivisionByZero { at } => write!(f, "division by zero in expression {at:?}"),
275 Self::Overflow { at } => write!(f, "arithmetic overflow in expression {at:?}"),
276 Self::InvalidShift { at } => write!(f, "invalid shift distance in expression {at:?}"),
277 Self::NotIntegral { at } => write!(f, "expression {at:?} is not integral"),
278 Self::TooDeep => write!(f, "expression nests too deeply to evaluate"),
279 Self::MixedOperands { at } => write!(
280 f,
281 "expression {at:?} combines a floating-point operand with one that is not"
282 ),
283 Self::NotFloatingOperator { at } => write!(
284 f,
285 "expression {at:?} applies an operator floating-point values do not take"
286 ),
287 Self::NotFixedOperator { at } => write!(
288 f,
289 "expression {at:?} applies an operator fixed-point values do not take"
290 ),
291 Self::LongDouble { at } => write!(
292 f,
293 "expression {at:?} needs long double arithmetic, which this build does not have"
294 ),
295 }
296 }
297}
298
299impl core::error::Error for EvalError {}
300
301/// How deep integral evaluation will follow an expression tree.
302///
303/// The limit exists because a snapshot can be hostile; without it, a
304/// crafted tree would make evaluation run for an unbounded time.
305pub const MAX_EXPR_DEPTH: usize = 256;
306
307/// A constant expression's value, in the form the expression had.
308///
309/// [`Ast::eval_integral`] answers for the constants that are numbers a
310/// wire format cares about, and returns [`EvalError::NotIntegral`] for the
311/// rest. That is the right answer where an integer is required — an array
312/// bound, a `@bit_bound`, a case label — and the wrong one where the
313/// question is only "what does this constant say": `const string
314/// MODULE_ID = "spatial.sensing.lidar/1.5";` is a perfectly evaluated
315/// constant that no integer represents, and a backend that asked the
316/// integral question could not emit it at all.
317///
318/// [`Ast::eval_integral`]: crate::Ast::eval_integral
319/// [`EvalError::NotIntegral`]: crate::EvalError::NotIntegral
320#[derive(Debug, Clone, PartialEq)]
321#[non_exhaustive]
322pub enum ConstValue {
323 /// `TRUE` / `FALSE`.
324 Boolean(bool),
325 /// An integer, including a character or an enumerator evaluated as
326 /// one, and every arithmetic expression over them.
327 Integer(i128),
328 /// A floating-point literal, in the two halves it was written in.
329 ///
330 /// Kept as text: the decimal a source wrote and the nearest `f64` are
331 /// two different values, and which one a backend needs is the
332 /// backend's business.
333 Float {
334 /// Mantissa exactly as written, including any trailing point.
335 mantissa: String,
336 /// Exponent as written, without the `e`.
337 exponent: Option<String>,
338 },
339 /// A fixed-point literal — a decimal, and never a binary float.
340 Fixed {
341 /// Digits as written, without the point.
342 digits: String,
343 /// Digits after the point.
344 scale: u8,
345 },
346 /// A narrow character.
347 Char(char),
348 /// A wide character, as a code point.
349 WChar(u32),
350 /// A narrow string, with escapes resolved.
351 String(String),
352 /// A wide string, as code points.
353 WString(Vec<u32>),
354}