Skip to main content

miden_assembly_syntax/ast/constants/
eval.rs

1// Allow unused assignments - required by miette::Diagnostic derive macro
2#![allow(unused_assignments)]
3
4use alloc::{sync::Arc, vec::Vec};
5
6use smallvec::SmallVec;
7
8use crate::{
9    Felt,
10    ast::*,
11    debuginfo::{SourceFile, SourceSpan, Span, Spanned},
12    diagnostics::{Diagnostic, RelatedLabel, miette},
13    parser::IntValue,
14};
15
16/// An error raised during evaluation of a constant expression
17#[derive(Debug, thiserror::Error, Diagnostic)]
18pub enum ConstEvalError {
19    #[error("undefined constant '{symbol}'")]
20    #[diagnostic(help("are you missing an import?"))]
21    UndefinedSymbol {
22        #[label("the constant referenced here is not defined in the current scope")]
23        symbol: Ident,
24        #[source_code]
25        source_file: Option<Arc<SourceFile>>,
26    },
27    #[error("undefined constant '{path}'")]
28    #[diagnostic(help(
29        "is the constant exported from its containing module? if the referenced module \
30        is in another library, make sure you provided it to the assembler"
31    ))]
32    UndefinedPath {
33        path: Arc<Path>,
34        #[label("this reference is invalid: no such definition found")]
35        span: SourceSpan,
36        #[source_code]
37        source_file: Option<Arc<SourceFile>>,
38    },
39    #[error("invalid immediate: value is larger than expected range")]
40    #[diagnostic()]
41    ImmediateOverflow {
42        #[label]
43        span: SourceSpan,
44        #[source_code]
45        source_file: Option<Arc<SourceFile>>,
46    },
47    #[error("invalid constant expression: value is larger than expected range")]
48    #[diagnostic()]
49    ConstExprOverflow {
50        #[label]
51        span: SourceSpan,
52        #[source_code]
53        source_file: Option<Arc<SourceFile>>,
54    },
55    #[error("invalid constant expression: division by zero")]
56    DivisionByZero {
57        #[label]
58        span: SourceSpan,
59        #[source_code]
60        source_file: Option<Arc<SourceFile>>,
61    },
62    #[error("invalid constant")]
63    #[diagnostic(help("this constant does not resolve to a value of the right type"))]
64    InvalidConstant {
65        expected: &'static str,
66        #[label("expected {expected}")]
67        span: SourceSpan,
68        #[source_code]
69        source_file: Option<Arc<SourceFile>>,
70    },
71    #[error("constant evaluation failed")]
72    #[diagnostic(help("this constant cannot be evaluated, due to operands of incorrect type"))]
73    InvalidConstExprOperand {
74        #[label]
75        span: SourceSpan,
76        #[label("expected this operand to produce an integer value, but it does not")]
77        operand: SourceSpan,
78        #[source_code]
79        source_file: Option<Arc<SourceFile>>,
80    },
81    #[error("constant evaluation terminated due to infinite recursion")]
82    #[diagnostic(help("dependencies between constants must form an acyclic graph"))]
83    ConstEvalCycle {
84        #[label("occurs while evaluating this expression")]
85        start: SourceSpan,
86        #[source_code]
87        source_file: Option<Arc<SourceFile>>,
88        #[related]
89        detected: [RelatedLabel; 1],
90    },
91}
92
93impl ConstEvalError {
94    #[inline]
95    pub fn undefined<Env>(symbol: Ident, env: &Env) -> Self
96    where
97        Env: ?Sized + ConstEnvironment,
98        <Env as ConstEnvironment>::Error: From<Self>,
99    {
100        let source_file = env.get_source_file_for(symbol.span());
101        Self::UndefinedSymbol { symbol, source_file }
102    }
103
104    #[inline]
105    pub fn invalid_constant<Env>(span: SourceSpan, expected: &'static str, env: &Env) -> Self
106    where
107        Env: ?Sized + ConstEnvironment,
108        <Env as ConstEnvironment>::Error: From<Self>,
109    {
110        let source_file = env.get_source_file_for(span);
111        Self::InvalidConstant { expected, span, source_file }
112    }
113
114    #[inline]
115    pub fn eval_cycle<Env>(start: SourceSpan, detected: SourceSpan, env: &Env) -> Self
116    where
117        Env: ?Sized + ConstEnvironment,
118        <Env as ConstEnvironment>::Error: From<Self>,
119    {
120        let start_file = env.get_source_file_for(start);
121        let detected_file = env.get_source_file_for(detected);
122        let detected = [RelatedLabel::error("related error")
123            .with_labeled_span(
124                detected,
125                "cycle occurs because we attempt to eval this constant recursively",
126            )
127            .with_source_file(detected_file)];
128        Self::ConstEvalCycle { start, source_file: start_file, detected }
129    }
130}
131
132#[derive(Debug)]
133pub enum CachedConstantValue<'a> {
134    /// We've already evaluated a constant to a concrete value
135    Hit(&'a ConstantValue),
136    /// We've not yet evaluated a constant expression to a value
137    Miss(&'a ConstantExpr),
138}
139
140impl CachedConstantValue<'_> {
141    pub fn into_expr(self) -> ConstantExpr {
142        match self {
143            Self::Hit(value) => value.clone().into(),
144            Self::Miss(expr) => expr.clone(),
145        }
146    }
147}
148
149impl Spanned for CachedConstantValue<'_> {
150    fn span(&self) -> SourceSpan {
151        match self {
152            Self::Hit(value) => value.span(),
153            Self::Miss(expr) => expr.span(),
154        }
155    }
156}
157
158/// There are two phases to constant evaluation, one during semantic analysis, and another phase
159/// performed during linking of the final assembly, on any constant expressions that were left
160/// partially or unevaluated during semantic analysis due to external references. This trait is
161/// used to abstract over the environment in which the evaluator runs, so that we can use it in
162/// both phases by simply providing a suitable implementation.
163pub trait ConstEnvironment {
164    /// The error type used in the current evaluation phase.
165    ///
166    /// The error type must support infallible conversions from [ConstEvalError].
167    type Error: From<ConstEvalError>;
168
169    /// Map a [SourceSpan] to the [SourceFile] to which it refers
170    fn get_source_file_for(&self, span: SourceSpan) -> Option<Arc<SourceFile>>;
171
172    /// Get the constant expression/value bound to `name` in the current scope.
173    ///
174    /// Implementations should return `Ok(None)` if the symbol is defined, but not yet resolvable to
175    /// a concrete definition.
176    fn get(&mut self, name: &Ident) -> Result<Option<CachedConstantValue<'_>>, Self::Error>;
177
178    /// Get the constant expression/value defined at `path`, which is resolved using the imports
179    /// and definitions in the current scope.
180    ///
181    /// This function should return `Ok(None)` if unresolvable external references should be left
182    /// unevaluated, rather than treated as an undefined symbol error.
183    ///
184    /// This function should return `Err` if any of the following are true:
185    ///
186    /// * The path cannot be resolved, and the implementation wishes this to be treated as an error
187    /// * The definition of the constant was found, but it does not have public visibility
188    fn get_by_path(
189        &mut self,
190        path: Span<&Path>,
191    ) -> Result<Option<CachedConstantValue<'_>>, Self::Error>;
192
193    /// A specialized form of [ConstEnvironment::get], which validates that the constant expression
194    /// returned by `get` evaluates to an error string, returning that string, or raising an error
195    /// if invalid.
196    fn get_error(&mut self, name: &Ident) -> Result<Option<Arc<str>>, Self::Error> {
197        let mut seen = Vec::new();
198        let start = name.span();
199        match self.get(name)?.map(CachedConstantValue::into_expr) {
200            Some(expr) => resolve_error_expr(self, expr, start, &mut seen),
201            None => Ok(None),
202        }
203    }
204
205    /// A specialized form of [ConstEnvironment::get_by_path], which validates that the constant
206    /// expression returned by `get_by_path` evaluates to an error string, returning that string,
207    /// or raising an error if invalid.
208    fn get_error_by_path(&mut self, path: Span<&Path>) -> Result<Option<Arc<str>>, Self::Error> {
209        let mut seen = Vec::new();
210        let start = path.span();
211        match self.get_by_path(path)?.map(CachedConstantValue::into_expr) {
212            Some(expr) => resolve_error_expr(self, expr, start, &mut seen),
213            None => Ok(None),
214        }
215    }
216
217    /// This method is called when the evaluator begins to evaluate the constant at `path`
218    #[inline]
219    #[allow(unused_variables)]
220    fn on_eval_start(&mut self, path: Span<&Path>) {}
221
222    /// This method is called when the evaluator has finished evaluating the constant at `path`.
223    ///
224    /// The `value` here is the value produced as the result of evaluation.
225    #[inline]
226    #[allow(unused_variables)]
227    fn on_eval_completed(&mut self, name: Span<&Path>, value: &ConstantExpr) {}
228}
229
230fn resolve_error_expr<Env>(
231    env: &mut Env,
232    expr: ConstantExpr,
233    start: SourceSpan,
234    seen: &mut Vec<Arc<Path>>,
235) -> Result<Option<Arc<str>>, <Env as ConstEnvironment>::Error>
236where
237    Env: ?Sized + ConstEnvironment,
238    <Env as ConstEnvironment>::Error: From<ConstEvalError>,
239{
240    match expr {
241        ConstantExpr::String(spanned) => Ok(Some(spanned.into_inner())),
242        ConstantExpr::Var(path) => {
243            let path_ref = path.inner().as_ref();
244            let path_span = path.span();
245            resolve_error_path(env, Span::new(path_span, path_ref), start, seen)
246        },
247        other => Err(ConstEvalError::invalid_constant(other.span(), "a string", env).into()),
248    }
249}
250
251fn resolve_error_path<Env>(
252    env: &mut Env,
253    path: Span<&Path>,
254    start: SourceSpan,
255    seen: &mut Vec<Arc<Path>>,
256) -> Result<Option<Arc<str>>, <Env as ConstEnvironment>::Error>
257where
258    Env: ?Sized + ConstEnvironment,
259    <Env as ConstEnvironment>::Error: From<ConstEvalError>,
260{
261    let path_span = path.span();
262    let path_ref = path.into_inner();
263    if seen.iter().any(|seen_path| seen_path.as_ref() == path_ref) {
264        return Err(ConstEvalError::eval_cycle(start, path_span, env).into());
265    }
266    seen.push(Arc::<Path>::from(path_ref));
267
268    let path = Span::new(path_span, path_ref);
269    match env.get_by_path(path)?.map(CachedConstantValue::into_expr) {
270        Some(expr) => resolve_error_expr(env, expr, start, seen),
271        None => Ok(None),
272    }
273}
274
275/// Evaluate `expr` in `env`, producing a new [ConstantExpr] representing the value produced as the
276/// result of evaluation.
277///
278/// If `expr` could not be fully evaluated, e.g. due to external references which are not yet
279/// available, the returned expression may be only partially evaluated, or even entirely
280/// unevaluated.
281///
282/// It is up to `env` to determine how unresolved foreign symbols are to be handled. See the
283/// [ConstEnvironment] trait for more details.
284pub fn expr<Env>(
285    value: &ConstantExpr,
286    env: &mut Env,
287) -> Result<ConstantExpr, <Env as ConstEnvironment>::Error>
288where
289    Env: ?Sized + ConstEnvironment,
290    <Env as ConstEnvironment>::Error: From<ConstEvalError>,
291{
292    /// Represents the type of a continuation to apply during evaluation
293    enum Cont {
294        /// We have reached an anonymous expression to evaluate
295        Eval(ConstantExpr),
296        /// We have finished evaluating the operands of a constant op, and must now apply the
297        /// operation to them, pushing the result on the operand stack.
298        Apply(Span<ConstantOp>),
299        /// We have finished evaluating a reference to another constant and are returning
300        /// its value on the operand stack
301        Return(Span<Arc<Path>>),
302    }
303
304    // If we don't require evaluation, we're done
305    if let Some(value) = value.as_value() {
306        return Ok(value.into());
307    }
308
309    // The operand stack
310    let mut stack = Vec::with_capacity(8);
311    // The continuation stack
312    let mut continuations = Vec::with_capacity(8);
313    // Start evaluation from the root expression
314    continuations.push(Cont::Eval(value.clone()));
315    // Keep track of the stack of constants being expanded during evaluation
316    //
317    // Any time we reach a reference to another constant that requires evaluation, we check if
318    // we're already in the process of evaluating that constant. If so, then a cycle is present
319    // and we must raise an eval error.
320    let mut evaluating = SmallVec::<[_; 8]>::new_const();
321
322    while let Some(next) = continuations.pop() {
323        match next {
324            Cont::Eval(
325                expr @ (ConstantExpr::Int(_)
326                | ConstantExpr::String(_)
327                | ConstantExpr::Word(_)
328                | ConstantExpr::Hash(..)),
329            ) => {
330                stack.push(expr);
331            },
332            Cont::Eval(ConstantExpr::Var(path)) => {
333                if evaluating.contains(&path) {
334                    return Err(
335                        ConstEvalError::eval_cycle(evaluating[0].span(), path.span(), env).into()
336                    );
337                }
338
339                if let Some(name) = path.as_ident() {
340                    let name = name.with_span(path.span());
341                    if let Some(expr) = env.get(&name)?.map(CachedConstantValue::into_expr) {
342                        env.on_eval_start(path.as_deref());
343                        evaluating.push(path.clone());
344                        continuations.push(Cont::Return(path.clone()));
345                        continuations.push(Cont::Eval(expr));
346                    } else {
347                        stack.push(ConstantExpr::Var(path));
348                    }
349                } else if let Some(expr) = env.get_by_path(path.as_deref())? {
350                    let expr = expr.into_expr();
351                    env.on_eval_start(path.as_deref());
352                    evaluating.push(path.clone());
353                    continuations.push(Cont::Return(path.clone()));
354                    continuations.push(Cont::Eval(expr));
355                } else {
356                    stack.push(ConstantExpr::Var(path));
357                }
358            },
359            Cont::Eval(ConstantExpr::BinaryOp { span, op, lhs, rhs, .. }) => {
360                continuations.push(Cont::Apply(Span::new(span, op)));
361                continuations.push(Cont::Eval(*lhs));
362                continuations.push(Cont::Eval(*rhs));
363            },
364            Cont::Apply(op) => {
365                let lhs = stack.pop().unwrap();
366                let rhs = stack.pop().unwrap();
367                let (span, op) = op.into_parts();
368                match (lhs, rhs) {
369                    (ConstantExpr::Int(lhs), ConstantExpr::Int(rhs)) => {
370                        let lhs = lhs.into_inner();
371                        let rhs = rhs.into_inner();
372                        let result = match op {
373                            ConstantOp::Add => lhs.checked_add(rhs).ok_or_else(|| {
374                                ConstEvalError::ConstExprOverflow {
375                                    span,
376                                    source_file: env.get_source_file_for(span),
377                                }
378                            })?,
379                            ConstantOp::Sub => lhs.checked_sub(rhs).ok_or_else(|| {
380                                ConstEvalError::ConstExprOverflow {
381                                    span,
382                                    source_file: env.get_source_file_for(span),
383                                }
384                            })?,
385                            ConstantOp::Mul => lhs.checked_mul(rhs).ok_or_else(|| {
386                                ConstEvalError::ConstExprOverflow {
387                                    span,
388                                    source_file: env.get_source_file_for(span),
389                                }
390                            })?,
391                            ConstantOp::IntDiv => lhs.checked_div(rhs).ok_or_else(|| {
392                                ConstEvalError::DivisionByZero {
393                                    span,
394                                    source_file: env.get_source_file_for(span),
395                                }
396                            })?,
397                            ConstantOp::Div => {
398                                if rhs.as_int() == 0 {
399                                    return Err(ConstEvalError::DivisionByZero {
400                                        span,
401                                        source_file: env.get_source_file_for(span),
402                                    }
403                                    .into());
404                                }
405                                let lhs = Felt::new_unchecked(lhs.as_int());
406                                let rhs = Felt::new_unchecked(rhs.as_int());
407                                IntValue::from(lhs / rhs)
408                            },
409                        };
410                        stack.push(ConstantExpr::Int(Span::new(span, result)));
411                    },
412                    operands @ ((
413                        ConstantExpr::Int(_) | ConstantExpr::Var(_),
414                        ConstantExpr::Var(_),
415                    )
416                    | (ConstantExpr::Var(_), ConstantExpr::Int(_))) => {
417                        let (lhs, rhs) = operands;
418                        stack.push(ConstantExpr::BinaryOp {
419                            span,
420                            op,
421                            lhs: lhs.into(),
422                            rhs: rhs.into(),
423                        });
424                    },
425                    (ConstantExpr::Int(_) | ConstantExpr::Var(_), rhs) => {
426                        let operand = rhs.span();
427                        return Err(ConstEvalError::InvalidConstExprOperand {
428                            span,
429                            operand,
430                            source_file: env.get_source_file_for(operand),
431                        }
432                        .into());
433                    },
434                    (lhs, _) => {
435                        let operand = lhs.span();
436                        return Err(ConstEvalError::InvalidConstExprOperand {
437                            span,
438                            operand,
439                            source_file: env.get_source_file_for(operand),
440                        }
441                        .into());
442                    },
443                }
444            },
445            Cont::Return(from) => {
446                debug_assert!(
447                    !stack.is_empty(),
448                    "returning from evaluating a constant reference is expected to produce at least one output"
449                );
450                evaluating.pop();
451
452                env.on_eval_completed(from.as_deref(), stack.last().unwrap());
453            },
454        }
455    }
456
457    // When we reach here, we should have exactly one expression on the operand stack
458    assert_eq!(stack.len(), 1, "expected constant evaluation to produce exactly one output");
459    // SAFETY: The above assertion guarantees that the stack has an element, and that `pop` will
460    // always succeed, thus the safety requirements of `unwrap_unchecked` are upheld
461    Ok(unsafe { stack.pop().unwrap_unchecked() })
462}