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    ast::*,
10    debuginfo::{SourceFile, SourceSpan, Span, Spanned},
11    diagnostics::{Diagnostic, RelatedLabel, miette},
12};
13
14/// An error raised during evaluation of a constant expression
15#[derive(Debug, thiserror::Error, Diagnostic)]
16pub enum ConstEvalError {
17    #[error("undefined constant '{symbol}'")]
18    #[diagnostic(help("are you missing an import?"))]
19    UndefinedSymbol {
20        #[label("the constant referenced here is not defined in the current scope")]
21        symbol: Ident,
22        #[source_code]
23        source_file: Option<Arc<SourceFile>>,
24    },
25    #[error("undefined constant '{path}'")]
26    #[diagnostic(help(
27        "is the constant exported from its containing module? if the referenced module \
28        is in another library, make sure you provided it to the assembler"
29    ))]
30    UndefinedPath {
31        path: Arc<Path>,
32        #[label("this reference is invalid: no such definition found")]
33        span: SourceSpan,
34        #[source_code]
35        source_file: Option<Arc<SourceFile>>,
36    },
37    #[error("invalid immediate: value is larger than expected range")]
38    #[diagnostic()]
39    ImmediateOverflow {
40        #[label]
41        span: SourceSpan,
42        #[source_code]
43        source_file: Option<Arc<SourceFile>>,
44    },
45    #[error("invalid constant expression: division by zero")]
46    DivisionByZero {
47        #[label]
48        span: SourceSpan,
49        #[source_code]
50        source_file: Option<Arc<SourceFile>>,
51    },
52    #[error("invalid constant")]
53    #[diagnostic(help("this constant does not resolve to a value of the right type"))]
54    InvalidConstant {
55        expected: &'static str,
56        #[label("expected {expected}")]
57        span: SourceSpan,
58        #[source_code]
59        source_file: Option<Arc<SourceFile>>,
60    },
61    #[error("constant evaluation failed")]
62    #[diagnostic(help("this constant cannot be evaluated, due to operands of incorrect type"))]
63    InvalidConstExprOperand {
64        #[label]
65        span: SourceSpan,
66        #[label("expected this operand to produce an integer value, but it does not")]
67        operand: SourceSpan,
68        #[source_code]
69        source_file: Option<Arc<SourceFile>>,
70    },
71    #[error("constant evaluation terminated due to infinite recursion")]
72    #[diagnostic(help("dependencies between constants must form an acyclic graph"))]
73    ConstEvalCycle {
74        #[label("occurs while evaluating this expression")]
75        start: SourceSpan,
76        #[source_code]
77        source_file: Option<Arc<SourceFile>>,
78        #[related]
79        detected: [RelatedLabel; 1],
80    },
81}
82
83impl ConstEvalError {
84    #[inline]
85    pub fn undefined<Env>(symbol: Ident, env: &Env) -> Self
86    where
87        Env: ?Sized + ConstEnvironment,
88        <Env as ConstEnvironment>::Error: From<Self>,
89    {
90        let source_file = env.get_source_file_for(symbol.span());
91        Self::UndefinedSymbol { symbol, source_file }
92    }
93
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(start);
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(&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        &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(&self, name: &Ident) -> Result<Option<Arc<str>>, Self::Error> {
187        match self.get(name)? {
188            Some(expr) => match expr {
189                CachedConstantValue::Hit(ConstantValue::String(spanned)) => {
190                    Ok(Some(spanned.clone().into_inner()))
191                },
192                CachedConstantValue::Miss(ConstantExpr::String(spanned)) => {
193                    Ok(Some(spanned.clone().into_inner()))
194                },
195                CachedConstantValue::Miss(ConstantExpr::Var(_)) => Ok(None),
196                other => {
197                    Err(ConstEvalError::invalid_constant(other.span(), "a string", self).into())
198                },
199            },
200            None => Ok(None),
201        }
202    }
203
204    /// A specialized form of [ConstEnvironment::get_by_path], which validates that the constant
205    /// expression returned by `get_by_path` evaluates to an error string, returning that string,
206    /// or raising an error if invalid.
207    fn get_error_by_path(&self, path: Span<&Path>) -> Result<Option<Arc<str>>, Self::Error> {
208        match self.get_by_path(path)? {
209            Some(expr) => match expr {
210                CachedConstantValue::Hit(ConstantValue::String(spanned)) => {
211                    Ok(Some(spanned.clone().into_inner()))
212                },
213                CachedConstantValue::Miss(ConstantExpr::String(spanned)) => {
214                    Ok(Some(spanned.clone().into_inner()))
215                },
216                CachedConstantValue::Miss(ConstantExpr::Var(_)) => Ok(None),
217                other => {
218                    Err(ConstEvalError::invalid_constant(other.span(), "a string", self).into())
219                },
220            },
221            None => Ok(None),
222        }
223    }
224
225    /// This method is called when the evaluator begins to evaluate the constant at `path`
226    #[inline]
227    #[allow(unused_variables)]
228    fn on_eval_start(&mut self, path: Span<&Path>) {}
229
230    /// This method is called when the evaluator has finished evaluating the constant at `path`.
231    ///
232    /// The `value` here is the value produced as the result of evaluation.
233    #[inline]
234    #[allow(unused_variables)]
235    fn on_eval_completed(&mut self, name: Span<&Path>, value: &ConstantExpr) {}
236}
237
238/// Evaluate `expr` in `env`, producing a new [ConstantExpr] representing the value produced as the
239/// result of evaluation.
240///
241/// If `expr` could not be fully evaluated, e.g. due to external references which are not yet
242/// available, the returned expression may be only partially evaluated, or even entirely
243/// unevaluated.
244///
245/// It is up to `env` to determine how unresolved foreign symbols are to be handled. See the
246/// [ConstEnvironment] trait for more details.
247pub fn expr<Env>(
248    value: &ConstantExpr,
249    env: &mut Env,
250) -> Result<ConstantExpr, <Env as ConstEnvironment>::Error>
251where
252    Env: ?Sized + ConstEnvironment,
253    <Env as ConstEnvironment>::Error: From<ConstEvalError>,
254{
255    /// Represents the type of a continuation to apply during evaluation
256    enum Cont {
257        /// We have reached an anonymous expression to evaluate
258        Eval(ConstantExpr),
259        /// We have finished evaluating the operands of a constant op, and must now apply the
260        /// operation to them, pushing the result on the operand stack.
261        Apply(Span<ConstantOp>),
262        /// We have finished evaluating a reference to another constant and are returning
263        /// its value on the operand stack
264        Return(Span<Arc<Path>>),
265    }
266
267    // If we don't require evaluation, we're done
268    if let Some(value) = value.as_value() {
269        return Ok(value.into());
270    }
271
272    // The operand stack
273    let mut stack = Vec::with_capacity(8);
274    // The continuation stack
275    let mut continuations = Vec::with_capacity(8);
276    // Start evaluation from the root expression
277    continuations.push(Cont::Eval(value.clone()));
278    // Keep track of the stack of constants being expanded during evaluation
279    //
280    // Any time we reach a reference to another constant that requires evaluation, we check if
281    // we're already in the process of evaluating that constant. If so, then a cycle is present
282    // and we must raise an eval error.
283    let mut evaluating = SmallVec::<[_; 8]>::new_const();
284
285    while let Some(next) = continuations.pop() {
286        match next {
287            Cont::Eval(
288                expr @ (ConstantExpr::Int(_)
289                | ConstantExpr::String(_)
290                | ConstantExpr::Word(_)
291                | ConstantExpr::Hash(..)),
292            ) => {
293                stack.push(expr);
294            },
295            Cont::Eval(ConstantExpr::Var(path)) => {
296                if evaluating.contains(&path) {
297                    return Err(
298                        ConstEvalError::eval_cycle(evaluating[0].span(), path.span(), env).into()
299                    );
300                }
301
302                if let Some(name) = path.as_ident() {
303                    let name = name.with_span(path.span());
304                    if let Some(expr) = env.get(&name)?.map(|e| e.into_expr()) {
305                        env.on_eval_start(path.as_deref());
306                        evaluating.push(path.clone());
307                        continuations.push(Cont::Return(path.clone()));
308                        continuations.push(Cont::Eval(expr));
309                    } else {
310                        stack.push(ConstantExpr::Var(path));
311                    }
312                } else if let Some(expr) = env.get_by_path(path.as_deref())? {
313                    let expr = expr.into_expr();
314                    env.on_eval_start(path.as_deref());
315                    evaluating.push(path.clone());
316                    continuations.push(Cont::Return(path.clone()));
317                    continuations.push(Cont::Eval(expr));
318                } else {
319                    stack.push(ConstantExpr::Var(path));
320                }
321            },
322            Cont::Eval(ConstantExpr::BinaryOp { span, op, lhs, rhs, .. }) => {
323                continuations.push(Cont::Apply(Span::new(span, op)));
324                continuations.push(Cont::Eval(*lhs));
325                continuations.push(Cont::Eval(*rhs));
326            },
327            Cont::Apply(op) => {
328                let lhs = stack.pop().unwrap();
329                let rhs = stack.pop().unwrap();
330                let (span, op) = op.into_parts();
331                match (lhs, rhs) {
332                    (ConstantExpr::Int(lhs), ConstantExpr::Int(rhs)) => {
333                        let lhs = lhs.into_inner();
334                        let rhs = rhs.into_inner();
335                        let result = match op {
336                            ConstantOp::Add => lhs.checked_add(rhs).ok_or_else(|| {
337                                ConstEvalError::ImmediateOverflow {
338                                    span,
339                                    source_file: env.get_source_file_for(span),
340                                }
341                            })?,
342                            ConstantOp::Sub => lhs.checked_sub(rhs).ok_or_else(|| {
343                                ConstEvalError::ImmediateOverflow {
344                                    span,
345                                    source_file: env.get_source_file_for(span),
346                                }
347                            })?,
348                            ConstantOp::Mul => lhs.checked_mul(rhs).ok_or_else(|| {
349                                ConstEvalError::ImmediateOverflow {
350                                    span,
351                                    source_file: env.get_source_file_for(span),
352                                }
353                            })?,
354                            ConstantOp::Div | ConstantOp::IntDiv => lhs
355                                .checked_div(rhs)
356                                .ok_or_else(|| ConstEvalError::DivisionByZero {
357                                    span,
358                                    source_file: env.get_source_file_for(span),
359                                })?,
360                        };
361                        stack.push(ConstantExpr::Int(Span::new(span, result)));
362                    },
363                    operands @ ((ConstantExpr::Int(_), ConstantExpr::Var(_))
364                    | (ConstantExpr::Var(_), ConstantExpr::Int(_))
365                    | (ConstantExpr::Var(_), ConstantExpr::Var(_))) => {
366                        let (lhs, rhs) = operands;
367                        stack.push(ConstantExpr::BinaryOp {
368                            span,
369                            op,
370                            lhs: lhs.into(),
371                            rhs: rhs.into(),
372                        });
373                    },
374                    (ConstantExpr::Int(_) | ConstantExpr::Var(_), rhs) => {
375                        let operand = rhs.span();
376                        return Err(ConstEvalError::InvalidConstExprOperand {
377                            span,
378                            operand,
379                            source_file: env.get_source_file_for(operand),
380                        }
381                        .into());
382                    },
383                    (lhs, _) => {
384                        let operand = lhs.span();
385                        return Err(ConstEvalError::InvalidConstExprOperand {
386                            span,
387                            operand,
388                            source_file: env.get_source_file_for(operand),
389                        }
390                        .into());
391                    },
392                }
393            },
394            Cont::Return(from) => {
395                debug_assert!(
396                    !stack.is_empty(),
397                    "returning from evaluating a constant reference is expected to produce at least one output"
398                );
399                evaluating.pop();
400
401                env.on_eval_completed(from.as_deref(), stack.last().unwrap());
402            },
403        }
404    }
405
406    // When we reach here, we should have exactly one expression on the operand stack
407    assert_eq!(stack.len(), 1, "expected constant evaluation to produce exactly one output");
408    // SAFETY: The above assertion guarantees that the stack has an element, and that `pop` will
409    // always succeed, thus the safety requirements of `unwrap_unchecked` are upheld
410    Ok(unsafe { stack.pop().unwrap_unchecked() })
411}