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