#![allow(unused_assignments)]
use alloc::{sync::Arc, vec::Vec};
use smallvec::SmallVec;
use crate::{
Felt,
ast::*,
debuginfo::{SourceFile, SourceSpan, Span, Spanned},
diagnostics::{Diagnostic, RelatedLabel, miette},
parser::IntValue,
};
#[derive(Debug, thiserror::Error, Diagnostic)]
pub enum ConstEvalError {
#[error("undefined constant '{symbol}'")]
#[diagnostic(help("are you missing an import?"))]
UndefinedSymbol {
#[label("the constant referenced here is not defined in the current scope")]
symbol: Ident,
#[source_code]
source_file: Option<Arc<SourceFile>>,
},
#[error("undefined constant '{path}'")]
#[diagnostic(help(
"is the constant exported from its containing module? if the referenced module \
is in another library, make sure you provided it to the assembler"
))]
UndefinedPath {
path: Arc<Path>,
#[label("this reference is invalid: no such definition found")]
span: SourceSpan,
#[source_code]
source_file: Option<Arc<SourceFile>>,
},
#[error("invalid immediate: value is larger than expected range")]
#[diagnostic()]
ImmediateOverflow {
#[label]
span: SourceSpan,
#[source_code]
source_file: Option<Arc<SourceFile>>,
},
#[error("invalid constant expression: division by zero")]
DivisionByZero {
#[label]
span: SourceSpan,
#[source_code]
source_file: Option<Arc<SourceFile>>,
},
#[error("invalid constant")]
#[diagnostic(help("this constant does not resolve to a value of the right type"))]
InvalidConstant {
expected: &'static str,
#[label("expected {expected}")]
span: SourceSpan,
#[source_code]
source_file: Option<Arc<SourceFile>>,
},
#[error("constant evaluation failed")]
#[diagnostic(help("this constant cannot be evaluated, due to operands of incorrect type"))]
InvalidConstExprOperand {
#[label]
span: SourceSpan,
#[label("expected this operand to produce an integer value, but it does not")]
operand: SourceSpan,
#[source_code]
source_file: Option<Arc<SourceFile>>,
},
#[error("constant evaluation terminated due to infinite recursion")]
#[diagnostic(help("dependencies between constants must form an acyclic graph"))]
ConstEvalCycle {
#[label("occurs while evaluating this expression")]
start: SourceSpan,
#[source_code]
source_file: Option<Arc<SourceFile>>,
#[related]
detected: [RelatedLabel; 1],
},
}
impl ConstEvalError {
#[inline]
pub fn undefined<Env>(symbol: Ident, env: &Env) -> Self
where
Env: ?Sized + ConstEnvironment,
<Env as ConstEnvironment>::Error: From<Self>,
{
let source_file = env.get_source_file_for(symbol.span());
Self::UndefinedSymbol { symbol, source_file }
}
#[inline]
pub fn invalid_constant<Env>(span: SourceSpan, expected: &'static str, env: &Env) -> Self
where
Env: ?Sized + ConstEnvironment,
<Env as ConstEnvironment>::Error: From<Self>,
{
let source_file = env.get_source_file_for(span);
Self::InvalidConstant { expected, span, source_file }
}
#[inline]
pub fn eval_cycle<Env>(start: SourceSpan, detected: SourceSpan, env: &Env) -> Self
where
Env: ?Sized + ConstEnvironment,
<Env as ConstEnvironment>::Error: From<Self>,
{
let start_file = env.get_source_file_for(start);
let detected_file = env.get_source_file_for(detected);
let detected = [RelatedLabel::error("related error")
.with_labeled_span(
detected,
"cycle occurs because we attempt to eval this constant recursively",
)
.with_source_file(detected_file)];
Self::ConstEvalCycle { start, source_file: start_file, detected }
}
}
#[derive(Debug)]
pub enum CachedConstantValue<'a> {
Hit(&'a ConstantValue),
Miss(&'a ConstantExpr),
}
impl CachedConstantValue<'_> {
pub fn into_expr(self) -> ConstantExpr {
match self {
Self::Hit(value) => value.clone().into(),
Self::Miss(expr) => expr.clone(),
}
}
}
impl Spanned for CachedConstantValue<'_> {
fn span(&self) -> SourceSpan {
match self {
Self::Hit(value) => value.span(),
Self::Miss(expr) => expr.span(),
}
}
}
pub trait ConstEnvironment {
type Error: From<ConstEvalError>;
fn get_source_file_for(&self, span: SourceSpan) -> Option<Arc<SourceFile>>;
fn get(&mut self, name: &Ident) -> Result<Option<CachedConstantValue<'_>>, Self::Error>;
fn get_by_path(
&mut self,
path: Span<&Path>,
) -> Result<Option<CachedConstantValue<'_>>, Self::Error>;
fn get_error(&mut self, name: &Ident) -> Result<Option<Arc<str>>, Self::Error> {
let mut seen = Vec::new();
let start = name.span();
match self.get(name)?.map(CachedConstantValue::into_expr) {
Some(expr) => resolve_error_expr(self, expr, start, &mut seen),
None => Ok(None),
}
}
fn get_error_by_path(&mut self, path: Span<&Path>) -> Result<Option<Arc<str>>, Self::Error> {
let mut seen = Vec::new();
let start = path.span();
match self.get_by_path(path)?.map(CachedConstantValue::into_expr) {
Some(expr) => resolve_error_expr(self, expr, start, &mut seen),
None => Ok(None),
}
}
#[inline]
#[allow(unused_variables)]
fn on_eval_start(&mut self, path: Span<&Path>) {}
#[inline]
#[allow(unused_variables)]
fn on_eval_completed(&mut self, name: Span<&Path>, value: &ConstantExpr) {}
}
fn resolve_error_expr<Env>(
env: &mut Env,
expr: ConstantExpr,
start: SourceSpan,
seen: &mut Vec<Arc<Path>>,
) -> Result<Option<Arc<str>>, <Env as ConstEnvironment>::Error>
where
Env: ?Sized + ConstEnvironment,
<Env as ConstEnvironment>::Error: From<ConstEvalError>,
{
match expr {
ConstantExpr::String(spanned) => Ok(Some(spanned.into_inner())),
ConstantExpr::Var(path) => {
let path_ref = path.inner().as_ref();
let path_span = path.span();
resolve_error_path(env, Span::new(path_span, path_ref), start, seen)
},
other => Err(ConstEvalError::invalid_constant(other.span(), "a string", env).into()),
}
}
fn resolve_error_path<Env>(
env: &mut Env,
path: Span<&Path>,
start: SourceSpan,
seen: &mut Vec<Arc<Path>>,
) -> Result<Option<Arc<str>>, <Env as ConstEnvironment>::Error>
where
Env: ?Sized + ConstEnvironment,
<Env as ConstEnvironment>::Error: From<ConstEvalError>,
{
let path_span = path.span();
let path_ref = path.into_inner();
if seen.iter().any(|seen_path| seen_path.as_ref() == path_ref) {
return Err(ConstEvalError::eval_cycle(start, path_span, env).into());
}
seen.push(Arc::<Path>::from(path_ref));
let path = Span::new(path_span, path_ref);
match env.get_by_path(path)?.map(CachedConstantValue::into_expr) {
Some(expr) => resolve_error_expr(env, expr, start, seen),
None => Ok(None),
}
}
pub fn expr<Env>(
value: &ConstantExpr,
env: &mut Env,
) -> Result<ConstantExpr, <Env as ConstEnvironment>::Error>
where
Env: ?Sized + ConstEnvironment,
<Env as ConstEnvironment>::Error: From<ConstEvalError>,
{
enum Cont {
Eval(ConstantExpr),
Apply(Span<ConstantOp>),
Return(Span<Arc<Path>>),
}
if let Some(value) = value.as_value() {
return Ok(value.into());
}
let mut stack = Vec::with_capacity(8);
let mut continuations = Vec::with_capacity(8);
continuations.push(Cont::Eval(value.clone()));
let mut evaluating = SmallVec::<[_; 8]>::new_const();
while let Some(next) = continuations.pop() {
match next {
Cont::Eval(
expr @ (ConstantExpr::Int(_)
| ConstantExpr::String(_)
| ConstantExpr::Word(_)
| ConstantExpr::Hash(..)),
) => {
stack.push(expr);
},
Cont::Eval(ConstantExpr::Var(path)) => {
if evaluating.contains(&path) {
return Err(
ConstEvalError::eval_cycle(evaluating[0].span(), path.span(), env).into()
);
}
if let Some(name) = path.as_ident() {
let name = name.with_span(path.span());
if let Some(expr) = env.get(&name)?.map(|e| e.into_expr()) {
env.on_eval_start(path.as_deref());
evaluating.push(path.clone());
continuations.push(Cont::Return(path.clone()));
continuations.push(Cont::Eval(expr));
} else {
stack.push(ConstantExpr::Var(path));
}
} else if let Some(expr) = env.get_by_path(path.as_deref())? {
let expr = expr.into_expr();
env.on_eval_start(path.as_deref());
evaluating.push(path.clone());
continuations.push(Cont::Return(path.clone()));
continuations.push(Cont::Eval(expr));
} else {
stack.push(ConstantExpr::Var(path));
}
},
Cont::Eval(ConstantExpr::BinaryOp { span, op, lhs, rhs, .. }) => {
continuations.push(Cont::Apply(Span::new(span, op)));
continuations.push(Cont::Eval(*lhs));
continuations.push(Cont::Eval(*rhs));
},
Cont::Apply(op) => {
let lhs = stack.pop().unwrap();
let rhs = stack.pop().unwrap();
let (span, op) = op.into_parts();
match (lhs, rhs) {
(ConstantExpr::Int(lhs), ConstantExpr::Int(rhs)) => {
let lhs = lhs.into_inner();
let rhs = rhs.into_inner();
let result = match op {
ConstantOp::Add => lhs.checked_add(rhs).ok_or_else(|| {
ConstEvalError::ImmediateOverflow {
span,
source_file: env.get_source_file_for(span),
}
})?,
ConstantOp::Sub => lhs.checked_sub(rhs).ok_or_else(|| {
ConstEvalError::ImmediateOverflow {
span,
source_file: env.get_source_file_for(span),
}
})?,
ConstantOp::Mul => lhs.checked_mul(rhs).ok_or_else(|| {
ConstEvalError::ImmediateOverflow {
span,
source_file: env.get_source_file_for(span),
}
})?,
ConstantOp::IntDiv => lhs.checked_div(rhs).ok_or_else(|| {
ConstEvalError::DivisionByZero {
span,
source_file: env.get_source_file_for(span),
}
})?,
ConstantOp::Div => {
if rhs.as_int() == 0 {
return Err(ConstEvalError::DivisionByZero {
span,
source_file: env.get_source_file_for(span),
}
.into());
}
let lhs = Felt::new(lhs.as_int());
let rhs = Felt::new(rhs.as_int());
IntValue::from(lhs / rhs)
},
};
stack.push(ConstantExpr::Int(Span::new(span, result)));
},
operands @ ((
ConstantExpr::Int(_) | ConstantExpr::Var(_),
ConstantExpr::Var(_),
)
| (ConstantExpr::Var(_), ConstantExpr::Int(_))) => {
let (lhs, rhs) = operands;
stack.push(ConstantExpr::BinaryOp {
span,
op,
lhs: lhs.into(),
rhs: rhs.into(),
});
},
(ConstantExpr::Int(_) | ConstantExpr::Var(_), rhs) => {
let operand = rhs.span();
return Err(ConstEvalError::InvalidConstExprOperand {
span,
operand,
source_file: env.get_source_file_for(operand),
}
.into());
},
(lhs, _) => {
let operand = lhs.span();
return Err(ConstEvalError::InvalidConstExprOperand {
span,
operand,
source_file: env.get_source_file_for(operand),
}
.into());
},
}
},
Cont::Return(from) => {
debug_assert!(
!stack.is_empty(),
"returning from evaluating a constant reference is expected to produce at least one output"
);
evaluating.pop();
env.on_eval_completed(from.as_deref(), stack.last().unwrap());
},
}
}
assert_eq!(stack.len(), 1, "expected constant evaluation to produce exactly one output");
Ok(unsafe { stack.pop().unwrap_unchecked() })
}