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