summaryrefslogtreecommitdiff
path: root/crates/typst-eval/src
diff options
context:
space:
mode:
authorLaurenz <laurmaedje@gmail.com>2024-10-27 19:04:55 +0100
committerGitHub <noreply@github.com>2024-10-27 18:04:55 +0000
commitbe7cfc85d08c545abfac08098b7b33b4bd71f37e (patch)
treef4137fa2aaa57babae1f7603a9b2ed7e688f43d8 /crates/typst-eval/src
parentb8034a343831e8609aec2ec81eb7eeda57aa5d81 (diff)
Split out four new crates (#5302)
Diffstat (limited to 'crates/typst-eval/src')
-rw-r--r--crates/typst-eval/src/access.rs101
-rw-r--r--crates/typst-eval/src/binding.rs210
-rw-r--r--crates/typst-eval/src/call.rs654
-rw-r--r--crates/typst-eval/src/code.rs360
-rw-r--r--crates/typst-eval/src/flow.rs229
-rw-r--r--crates/typst-eval/src/import.rs230
-rw-r--r--crates/typst-eval/src/lib.rs170
-rw-r--r--crates/typst-eval/src/markup.rs275
-rw-r--r--crates/typst-eval/src/math.rs119
-rw-r--r--crates/typst-eval/src/methods.rs91
-rw-r--r--crates/typst-eval/src/ops.rs91
-rw-r--r--crates/typst-eval/src/rules.rs76
-rw-r--r--crates/typst-eval/src/vm.rs60
13 files changed, 2666 insertions, 0 deletions
diff --git a/crates/typst-eval/src/access.rs b/crates/typst-eval/src/access.rs
new file mode 100644
index 00000000..9bcac4d6
--- /dev/null
+++ b/crates/typst-eval/src/access.rs
@@ -0,0 +1,101 @@
+use ecow::eco_format;
+use typst_library::diag::{bail, At, Hint, SourceResult, Trace, Tracepoint};
+use typst_library::foundations::{Dict, Value};
+use typst_syntax::ast::{self, AstNode};
+
+use crate::{call_method_access, is_accessor_method, Eval, Vm};
+
+/// Access an expression mutably.
+pub(crate) trait Access {
+ /// Access the expression's evaluated value mutably.
+ fn access<'a>(self, vm: &'a mut Vm) -> SourceResult<&'a mut Value>;
+}
+
+impl Access for ast::Expr<'_> {
+ fn access<'a>(self, vm: &'a mut Vm) -> SourceResult<&'a mut Value> {
+ match self {
+ Self::Ident(v) => v.access(vm),
+ Self::Parenthesized(v) => v.access(vm),
+ Self::FieldAccess(v) => v.access(vm),
+ Self::FuncCall(v) => v.access(vm),
+ _ => {
+ let _ = self.eval(vm)?;
+ bail!(self.span(), "cannot mutate a temporary value");
+ }
+ }
+ }
+}
+
+impl Access for ast::Ident<'_> {
+ fn access<'a>(self, vm: &'a mut Vm) -> SourceResult<&'a mut Value> {
+ let span = self.span();
+ if vm.inspected == Some(span) {
+ if let Ok(value) = vm.scopes.get(&self).cloned() {
+ vm.trace(value);
+ }
+ }
+ let value = vm.scopes.get_mut(&self).at(span)?;
+ Ok(value)
+ }
+}
+
+impl Access for ast::Parenthesized<'_> {
+ fn access<'a>(self, vm: &'a mut Vm) -> SourceResult<&'a mut Value> {
+ self.expr().access(vm)
+ }
+}
+
+impl Access for ast::FieldAccess<'_> {
+ fn access<'a>(self, vm: &'a mut Vm) -> SourceResult<&'a mut Value> {
+ access_dict(vm, self)?.at_mut(self.field().get()).at(self.span())
+ }
+}
+
+impl Access for ast::FuncCall<'_> {
+ fn access<'a>(self, vm: &'a mut Vm) -> SourceResult<&'a mut Value> {
+ if let ast::Expr::FieldAccess(access) = self.callee() {
+ let method = access.field();
+ if is_accessor_method(&method) {
+ let span = self.span();
+ let world = vm.world();
+ let args = self.args().eval(vm)?.spanned(span);
+ let value = access.target().access(vm)?;
+ let result = call_method_access(value, &method, args, span);
+ let point = || Tracepoint::Call(Some(method.get().clone()));
+ return result.trace(world, point, span);
+ }
+ }
+
+ let _ = self.eval(vm)?;
+ bail!(self.span(), "cannot mutate a temporary value");
+ }
+}
+
+pub(crate) fn access_dict<'a>(
+ vm: &'a mut Vm,
+ access: ast::FieldAccess,
+) -> SourceResult<&'a mut Dict> {
+ match access.target().access(vm)? {
+ Value::Dict(dict) => Ok(dict),
+ value => {
+ let ty = value.ty();
+ let span = access.target().span();
+ if matches!(
+ value, // those types have their own field getters
+ Value::Symbol(_) | Value::Content(_) | Value::Module(_) | Value::Func(_)
+ ) {
+ bail!(span, "cannot mutate fields on {ty}");
+ } else if typst_library::foundations::fields_on(ty).is_empty() {
+ bail!(span, "{ty} does not have accessible fields");
+ } else {
+ // type supports static fields, which don't yet have
+ // setters
+ Err(eco_format!("fields on {ty} are not yet mutable"))
+ .hint(eco_format!(
+ "try creating a new {ty} with the updated field value instead"
+ ))
+ .at(span)
+ }
+ }
+ }
+}
diff --git a/crates/typst-eval/src/binding.rs b/crates/typst-eval/src/binding.rs
new file mode 100644
index 00000000..f3802f07
--- /dev/null
+++ b/crates/typst-eval/src/binding.rs
@@ -0,0 +1,210 @@
+use std::collections::HashSet;
+
+use ecow::eco_format;
+use typst_library::diag::{bail, error, At, SourceDiagnostic, SourceResult};
+use typst_library::foundations::{Array, Dict, Value};
+use typst_syntax::ast::{self, AstNode};
+
+use crate::{Access, Eval, Vm};
+
+impl Eval for ast::LetBinding<'_> {
+ type Output = Value;
+
+ fn eval(self, vm: &mut Vm) -> SourceResult<Self::Output> {
+ let value = match self.init() {
+ Some(expr) => expr.eval(vm)?,
+ None => Value::None,
+ };
+ if vm.flow.is_some() {
+ return Ok(Value::None);
+ }
+
+ match self.kind() {
+ ast::LetBindingKind::Normal(pattern) => destructure(vm, pattern, value)?,
+ ast::LetBindingKind::Closure(ident) => vm.define(ident, value),
+ }
+
+ Ok(Value::None)
+ }
+}
+
+impl Eval for ast::DestructAssignment<'_> {
+ type Output = Value;
+
+ fn eval(self, vm: &mut Vm) -> SourceResult<Self::Output> {
+ let value = self.value().eval(vm)?;
+ destructure_impl(vm, self.pattern(), value, &mut |vm, expr, value| {
+ let location = expr.access(vm)?;
+ *location = value;
+ Ok(())
+ })?;
+ Ok(Value::None)
+ }
+}
+
+/// Destructures a value into a pattern.
+pub(crate) fn destructure(
+ vm: &mut Vm,
+ pattern: ast::Pattern,
+ value: Value,
+) -> SourceResult<()> {
+ destructure_impl(vm, pattern, value, &mut |vm, expr, value| match expr {
+ ast::Expr::Ident(ident) => {
+ vm.define(ident, value);
+ Ok(())
+ }
+ _ => bail!(expr.span(), "cannot assign to this expression"),
+ })
+}
+
+/// Destruct the given value into the pattern and apply the function to each binding.
+fn destructure_impl<F>(
+ vm: &mut Vm,
+ pattern: ast::Pattern,
+ value: Value,
+ f: &mut F,
+) -> SourceResult<()>
+where
+ F: Fn(&mut Vm, ast::Expr, Value) -> SourceResult<()>,
+{
+ match pattern {
+ ast::Pattern::Normal(expr) => f(vm, expr, value)?,
+ ast::Pattern::Placeholder(_) => {}
+ ast::Pattern::Parenthesized(parenthesized) => {
+ destructure_impl(vm, parenthesized.pattern(), value, f)?
+ }
+ ast::Pattern::Destructuring(destruct) => match value {
+ Value::Array(value) => destructure_array(vm, destruct, value, f)?,
+ Value::Dict(value) => destructure_dict(vm, destruct, value, f)?,
+ _ => bail!(pattern.span(), "cannot destructure {}", value.ty()),
+ },
+ }
+ Ok(())
+}
+
+fn destructure_array<F>(
+ vm: &mut Vm,
+ destruct: ast::Destructuring,
+ value: Array,
+ f: &mut F,
+) -> SourceResult<()>
+where
+ F: Fn(&mut Vm, ast::Expr, Value) -> SourceResult<()>,
+{
+ let len = value.as_slice().len();
+ let mut i = 0;
+
+ for p in destruct.items() {
+ match p {
+ ast::DestructuringItem::Pattern(pattern) => {
+ let Ok(v) = value.at(i as i64, None) else {
+ bail!(wrong_number_of_elements(destruct, len));
+ };
+ destructure_impl(vm, pattern, v, f)?;
+ i += 1;
+ }
+ ast::DestructuringItem::Spread(spread) => {
+ let sink_size = (1 + len).checked_sub(destruct.items().count());
+ let sink = sink_size.and_then(|s| value.as_slice().get(i..i + s));
+ let (Some(sink_size), Some(sink)) = (sink_size, sink) else {
+ bail!(wrong_number_of_elements(destruct, len));
+ };
+ if let Some(expr) = spread.sink_expr() {
+ f(vm, expr, Value::Array(sink.into()))?;
+ }
+ i += sink_size;
+ }
+ ast::DestructuringItem::Named(named) => {
+ bail!(named.span(), "cannot destructure named pattern from an array")
+ }
+ }
+ }
+
+ if i < len {
+ bail!(wrong_number_of_elements(destruct, len));
+ }
+
+ Ok(())
+}
+
+fn destructure_dict<F>(
+ vm: &mut Vm,
+ destruct: ast::Destructuring,
+ dict: Dict,
+ f: &mut F,
+) -> SourceResult<()>
+where
+ F: Fn(&mut Vm, ast::Expr, Value) -> SourceResult<()>,
+{
+ let mut sink = None;
+ let mut used = HashSet::new();
+
+ for p in destruct.items() {
+ match p {
+ // Shorthand for a direct identifier.
+ ast::DestructuringItem::Pattern(ast::Pattern::Normal(ast::Expr::Ident(
+ ident,
+ ))) => {
+ let v = dict.get(&ident).at(ident.span())?;
+ f(vm, ast::Expr::Ident(ident), v.clone())?;
+ used.insert(ident.get().clone());
+ }
+ ast::DestructuringItem::Named(named) => {
+ let name = named.name();
+ let v = dict.get(&name).at(name.span())?;
+ destructure_impl(vm, named.pattern(), v.clone(), f)?;
+ used.insert(name.get().clone());
+ }
+ ast::DestructuringItem::Spread(spread) => sink = spread.sink_expr(),
+ ast::DestructuringItem::Pattern(expr) => {
+ bail!(expr.span(), "cannot destructure unnamed pattern from dictionary");
+ }
+ }
+ }
+
+ if let Some(expr) = sink {
+ let mut sink = Dict::new();
+ for (key, value) in dict {
+ if !used.contains(key.as_str()) {
+ sink.insert(key, value);
+ }
+ }
+ f(vm, expr, Value::Dict(sink))?;
+ }
+
+ Ok(())
+}
+
+/// The error message when the number of elements of the destructuring and the
+/// array is mismatched.
+#[cold]
+fn wrong_number_of_elements(
+ destruct: ast::Destructuring,
+ len: usize,
+) -> SourceDiagnostic {
+ let mut count = 0;
+ let mut spread = false;
+
+ for p in destruct.items() {
+ match p {
+ ast::DestructuringItem::Pattern(_) => count += 1,
+ ast::DestructuringItem::Spread(_) => spread = true,
+ ast::DestructuringItem::Named(_) => {}
+ }
+ }
+
+ let quantifier = if len > count { "too many" } else { "not enough" };
+ let expected = match (spread, count) {
+ (true, 1) => "at least 1 element".into(),
+ (true, c) => eco_format!("at least {c} elements"),
+ (false, 0) => "an empty array".into(),
+ (false, 1) => "a single element".into(),
+ (false, c) => eco_format!("{c} elements",),
+ };
+
+ error!(
+ destruct.span(), "{quantifier} elements to destructure";
+ hint: "the provided array has a length of {len}, \
+ but the pattern expects {expected}",
+ )
+}
diff --git a/crates/typst-eval/src/call.rs b/crates/typst-eval/src/call.rs
new file mode 100644
index 00000000..9dfb7693
--- /dev/null
+++ b/crates/typst-eval/src/call.rs
@@ -0,0 +1,654 @@
+use comemo::{Tracked, TrackedMut};
+use ecow::{eco_format, EcoString, EcoVec};
+use typst_library::diag::{
+ bail, error, At, HintedStrResult, HintedString, SourceDiagnostic, SourceResult,
+ Trace, Tracepoint,
+};
+use typst_library::engine::{Engine, Sink, Traced};
+use typst_library::foundations::{
+ Arg, Args, Bytes, Capturer, Closure, Content, Context, Func, IntoValue,
+ NativeElement, Scope, Scopes, Value,
+};
+use typst_library::introspection::Introspector;
+use typst_library::math::LrElem;
+use typst_library::routines::Routines;
+use typst_library::text::TextElem;
+use typst_library::World;
+use typst_syntax::ast::{self, AstNode, Ident};
+use typst_syntax::{Span, Spanned, SyntaxNode};
+use typst_utils::LazyHash;
+
+use crate::{call_method_mut, is_mutating_method, Access, Eval, FlowEvent, Route, Vm};
+
+impl Eval for ast::FuncCall<'_> {
+ type Output = Value;
+
+ fn eval(self, vm: &mut Vm) -> SourceResult<Self::Output> {
+ let span = self.span();
+ let callee = self.callee();
+ let in_math = in_math(callee);
+ let callee_span = callee.span();
+ let args = self.args();
+ let trailing_comma = args.trailing_comma();
+
+ vm.engine.route.check_call_depth().at(span)?;
+
+ // Try to evaluate as a call to an associated function or field.
+ let (callee, args) = if let ast::Expr::FieldAccess(access) = callee {
+ let target = access.target();
+ let field = access.field();
+ match eval_field_call(target, field, args, span, vm)? {
+ FieldCall::Normal(callee, args) => (callee, args),
+ FieldCall::Resolved(value) => return Ok(value),
+ }
+ } else {
+ // Function call order: we evaluate the callee before the arguments.
+ (callee.eval(vm)?, args.eval(vm)?.spanned(span))
+ };
+
+ let func_result = callee.clone().cast::<Func>();
+ if in_math && func_result.is_err() {
+ return wrap_args_in_math(callee, callee_span, args, trailing_comma);
+ }
+
+ let func = func_result
+ .map_err(|err| hint_if_shadowed_std(vm, &self.callee(), err))
+ .at(callee_span)?;
+
+ let point = || Tracepoint::Call(func.name().map(Into::into));
+ let f = || {
+ func.call(&mut vm.engine, vm.context, args)
+ .trace(vm.world(), point, span)
+ };
+
+ // Stacker is broken on WASM.
+ #[cfg(target_arch = "wasm32")]
+ return f();
+
+ #[cfg(not(target_arch = "wasm32"))]
+ stacker::maybe_grow(32 * 1024, 2 * 1024 * 1024, f)
+ }
+}
+
+impl Eval for ast::Args<'_> {
+ type Output = Args;
+
+ fn eval(self, vm: &mut Vm) -> SourceResult<Self::Output> {
+ let mut items = EcoVec::with_capacity(self.items().count());
+
+ for arg in self.items() {
+ let span = arg.span();
+ match arg {
+ ast::Arg::Pos(expr) => {
+ items.push(Arg {
+ span,
+ name: None,
+ value: Spanned::new(expr.eval(vm)?, expr.span()),
+ });
+ }
+ ast::Arg::Named(named) => {
+ let expr = named.expr();
+ items.push(Arg {
+ span,
+ name: Some(named.name().get().clone().into()),
+ value: Spanned::new(expr.eval(vm)?, expr.span()),
+ });
+ }
+ ast::Arg::Spread(spread) => match spread.expr().eval(vm)? {
+ Value::None => {}
+ Value::Array(array) => {
+ items.extend(array.into_iter().map(|value| Arg {
+ span,
+ name: None,
+ value: Spanned::new(value, span),
+ }));
+ }
+ Value::Dict(dict) => {
+ items.extend(dict.into_iter().map(|(key, value)| Arg {
+ span,
+ name: Some(key),
+ value: Spanned::new(value, span),
+ }));
+ }
+ Value::Args(args) => items.extend(args.items),
+ v => bail!(spread.span(), "cannot spread {}", v.ty()),
+ },
+ }
+ }
+
+ // We do *not* use the `self.span()` here because we want the callsite
+ // span to be one level higher (the whole function call).
+ Ok(Args { span: Span::detached(), items })
+ }
+}
+
+impl Eval for ast::Closure<'_> {
+ type Output = Value;
+
+ fn eval(self, vm: &mut Vm) -> SourceResult<Self::Output> {
+ // Evaluate default values of named parameters.
+ let mut defaults = Vec::new();
+ for param in self.params().children() {
+ if let ast::Param::Named(named) = param {
+ defaults.push(named.expr().eval(vm)?);
+ }
+ }
+
+ // Collect captured variables.
+ let captured = {
+ let mut visitor = CapturesVisitor::new(Some(&vm.scopes), Capturer::Function);
+ visitor.visit(self.to_untyped());
+ visitor.finish()
+ };
+
+ // Define the closure.
+ let closure = Closure {
+ node: self.to_untyped().clone(),
+ defaults,
+ captured,
+ num_pos_params: self
+ .params()
+ .children()
+ .filter(|p| matches!(p, ast::Param::Pos(_)))
+ .count(),
+ };
+
+ Ok(Value::Func(Func::from(closure).spanned(self.params().span())))
+ }
+}
+
+/// Call the function in the context with the arguments.
+#[comemo::memoize]
+#[allow(clippy::too_many_arguments)]
+pub fn eval_closure(
+ func: &Func,
+ closure: &LazyHash<Closure>,
+ routines: &Routines,
+ world: Tracked<dyn World + '_>,
+ introspector: Tracked<Introspector>,
+ traced: Tracked<Traced>,
+ sink: TrackedMut<Sink>,
+ route: Tracked<Route>,
+ context: Tracked<Context>,
+ mut args: Args,
+) -> SourceResult<Value> {
+ let (name, params, body) = match closure.node.cast::<ast::Closure>() {
+ Some(node) => (node.name(), node.params(), node.body()),
+ None => (None, ast::Params::default(), closure.node.cast().unwrap()),
+ };
+
+ // Don't leak the scopes from the call site. Instead, we use the scope
+ // of captured variables we collected earlier.
+ let mut scopes = Scopes::new(None);
+ scopes.top = closure.captured.clone();
+
+ // Prepare the engine.
+ let engine = Engine {
+ routines,
+ world,
+ introspector,
+ traced,
+ sink,
+ route: Route::extend(route),
+ };
+
+ // Prepare VM.
+ let mut vm = Vm::new(engine, context, scopes, body.span());
+
+ // Provide the closure itself for recursive calls.
+ if let Some(name) = name {
+ vm.define(name, Value::Func(func.clone()));
+ }
+
+ let num_pos_args = args.to_pos().len();
+ let sink_size = num_pos_args.checked_sub(closure.num_pos_params);
+
+ let mut sink = None;
+ let mut sink_pos_values = None;
+ let mut defaults = closure.defaults.iter();
+ for p in params.children() {
+ match p {
+ ast::Param::Pos(pattern) => match pattern {
+ ast::Pattern::Normal(ast::Expr::Ident(ident)) => {
+ vm.define(ident, args.expect::<Value>(&ident)?)
+ }
+ pattern => {
+ crate::destructure(
+ &mut vm,
+ pattern,
+ args.expect::<Value>("pattern parameter")?,
+ )?;
+ }
+ },
+ ast::Param::Spread(spread) => {
+ sink = Some(spread.sink_ident());
+ if let Some(sink_size) = sink_size {
+ sink_pos_values = Some(args.consume(sink_size)?);
+ }
+ }
+ ast::Param::Named(named) => {
+ let name = named.name();
+ let default = defaults.next().unwrap();
+ let value =
+ args.named::<Value>(&name)?.unwrap_or_else(|| default.clone());
+ vm.define(name, value);
+ }
+ }
+ }
+
+ if let Some(sink) = sink {
+ // Remaining args are captured regardless of whether the sink is named.
+ let mut remaining_args = args.take();
+ if let Some(sink_name) = sink {
+ if let Some(sink_pos_values) = sink_pos_values {
+ remaining_args.items.extend(sink_pos_values);
+ }
+ vm.define(sink_name, remaining_args);
+ }
+ }
+
+ // Ensure all arguments have been used.
+ args.finish()?;
+
+ // Handle control flow.
+ let output = body.eval(&mut vm)?;
+ match vm.flow {
+ Some(FlowEvent::Return(_, Some(explicit))) => return Ok(explicit),
+ Some(FlowEvent::Return(_, None)) => {}
+ Some(flow) => bail!(flow.forbidden()),
+ None => {}
+ }
+
+ Ok(output)
+}
+
+/// This used only as the return value of `eval_field_call`.
+/// - `Normal` means that we have a function to call and the arguments to call it with.
+/// - `Resolved` means that we have already resolved the call and have the value.
+enum FieldCall {
+ Normal(Value, Args),
+ Resolved(Value),
+}
+
+/// Evaluate a field call's callee and arguments.
+///
+/// This follows the normal function call order: we evaluate the callee before the
+/// arguments.
+///
+/// Prioritize associated functions on the value's type (e.g., methods) over its fields.
+/// A function call on a field is only allowed for functions, types, modules (because
+/// they are scopes), and symbols (because they have modifiers or associated functions).
+///
+/// For dictionaries, it is not allowed because it would be ambiguous - prioritizing
+/// associated functions would make an addition of a new associated function a breaking
+/// change and prioritizing fields would break associated functions for certain
+/// dictionaries.
+fn eval_field_call(
+ target_expr: ast::Expr,
+ field: Ident,
+ args: ast::Args,
+ span: Span,
+ vm: &mut Vm,
+) -> SourceResult<FieldCall> {
+ // Evaluate the field-call's target and overall arguments.
+ let (target, mut args) = if is_mutating_method(&field) {
+ // If `field` looks like a mutating method, we evaluate the arguments first,
+ // because `target_expr.access(vm)` mutably borrows the `vm`, so that we can't
+ // evaluate the arguments after it.
+ let args = args.eval(vm)?.spanned(span);
+ // However, this difference from the normal call order is not observable because
+ // expressions like `(1, arr.len(), 2, 3).push(arr.pop())` evaluate the target to
+ // a temporary which we disallow mutation on (returning an error).
+ // Theoretically this could be observed if a method matching `is_mutating_method`
+ // was added to some type in the future and we didn't update this function.
+ match target_expr.access(vm)? {
+ // Only arrays and dictionaries have mutable methods.
+ target @ (Value::Array(_) | Value::Dict(_)) => {
+ let value = call_method_mut(target, &field, args, span);
+ let point = || Tracepoint::Call(Some(field.get().clone()));
+ return Ok(FieldCall::Resolved(value.trace(vm.world(), point, span)?));
+ }
+ target => (target.clone(), args),
+ }
+ } else {
+ let target = target_expr.eval(vm)?;
+ let args = args.eval(vm)?.spanned(span);
+ (target, args)
+ };
+
+ if let Value::Plugin(plugin) = &target {
+ // Call plugins by converting args to bytes.
+ let bytes = args.all::<Bytes>()?;
+ args.finish()?;
+ let value = plugin.call(&field, bytes).at(span)?.into_value();
+ Ok(FieldCall::Resolved(value))
+ } else if let Some(callee) = target.ty().scope().get(&field) {
+ args.insert(0, target_expr.span(), target);
+ Ok(FieldCall::Normal(callee.clone(), args))
+ } else if matches!(
+ target,
+ Value::Symbol(_) | Value::Func(_) | Value::Type(_) | Value::Module(_)
+ ) {
+ // Certain value types may have their own ways to access method fields.
+ // e.g. `$arrow.r(v)$`, `table.cell[..]`
+ let value = target.field(&field).at(field.span())?;
+ Ok(FieldCall::Normal(value, args))
+ } else {
+ // Otherwise we cannot call this field.
+ bail!(missing_field_call_error(target, field))
+ }
+}
+
+/// Produce an error when we cannot call the field.
+fn missing_field_call_error(target: Value, field: Ident) -> SourceDiagnostic {
+ let mut error =
+ error!(field.span(), "type {} has no method `{}`", target.ty(), field.as_str());
+
+ match target {
+ Value::Dict(ref dict) if matches!(dict.get(&field), Ok(Value::Func(_))) => {
+ error.hint(eco_format!(
+ "to call the function stored in the dictionary, surround \
+ the field access with parentheses, e.g. `(dict.{})(..)`",
+ field.as_str(),
+ ));
+ }
+ _ if target.field(&field).is_ok() => {
+ error.hint(eco_format!(
+ "did you mean to access the field `{}`?",
+ field.as_str(),
+ ));
+ }
+ _ => {}
+ }
+ error
+}
+
+/// Check if the expression is in a math context.
+fn in_math(expr: ast::Expr) -> bool {
+ match expr {
+ ast::Expr::MathIdent(_) => true,
+ ast::Expr::FieldAccess(access) => in_math(access.target()),
+ _ => false,
+ }
+}
+
+/// For non-functions in math, we wrap the arguments in parentheses.
+fn wrap_args_in_math(
+ callee: Value,
+ callee_span: Span,
+ mut args: Args,
+ trailing_comma: bool,
+) -> SourceResult<Value> {
+ let mut body = Content::empty();
+ for (i, arg) in args.all::<Content>()?.into_iter().enumerate() {
+ if i > 0 {
+ body += TextElem::packed(',');
+ }
+ body += arg;
+ }
+ if trailing_comma {
+ body += TextElem::packed(',');
+ }
+ Ok(Value::Content(
+ callee.display().spanned(callee_span)
+ + LrElem::new(TextElem::packed('(') + body + TextElem::packed(')')).pack(),
+ ))
+}
+
+/// Provide a hint if the callee is a shadowed standard library function.
+fn hint_if_shadowed_std(
+ vm: &mut Vm,
+ callee: &ast::Expr,
+ mut err: HintedString,
+) -> HintedString {
+ if let ast::Expr::Ident(ident) = callee {
+ let ident = ident.get();
+ if vm.scopes.check_std_shadowed(ident) {
+ err.hint(eco_format!(
+ "use `std.{ident}` to access the shadowed standard library function",
+ ));
+ }
+ }
+ err
+}
+
+/// A visitor that determines which variables to capture for a closure.
+pub struct CapturesVisitor<'a> {
+ external: Option<&'a Scopes<'a>>,
+ internal: Scopes<'a>,
+ captures: Scope,
+ capturer: Capturer,
+}
+
+impl<'a> CapturesVisitor<'a> {
+ /// Create a new visitor for the given external scopes.
+ pub fn new(external: Option<&'a Scopes<'a>>, capturer: Capturer) -> Self {
+ Self {
+ external,
+ internal: Scopes::new(None),
+ captures: Scope::new(),
+ capturer,
+ }
+ }
+
+ /// Return the scope of captured variables.
+ pub fn finish(self) -> Scope {
+ self.captures
+ }
+
+ /// Visit any node and collect all captured variables.
+ pub fn visit(&mut self, node: &SyntaxNode) {
+ match node.cast() {
+ // Every identifier is a potential variable that we need to capture.
+ // Identifiers that shouldn't count as captures because they
+ // actually bind a new name are handled below (individually through
+ // the expressions that contain them).
+ Some(ast::Expr::Ident(ident)) => {
+ self.capture(ident.get(), ident.span(), Scopes::get)
+ }
+ Some(ast::Expr::MathIdent(ident)) => {
+ self.capture(ident.get(), ident.span(), Scopes::get_in_math)
+ }
+
+ // Code and content blocks create a scope.
+ Some(ast::Expr::Code(_) | ast::Expr::Content(_)) => {
+ self.internal.enter();
+ for child in node.children() {
+ self.visit(child);
+ }
+ self.internal.exit();
+ }
+
+ // Don't capture the field of a field access.
+ Some(ast::Expr::FieldAccess(access)) => {
+ self.visit(access.target().to_untyped());
+ }
+
+ // A closure contains parameter bindings, which are bound before the
+ // body is evaluated. Care must be taken so that the default values
+ // of named parameters cannot access previous parameter bindings.
+ Some(ast::Expr::Closure(expr)) => {
+ for param in expr.params().children() {
+ if let ast::Param::Named(named) = param {
+ self.visit(named.expr().to_untyped());
+ }
+ }
+
+ self.internal.enter();
+ if let Some(name) = expr.name() {
+ self.bind(name);
+ }
+
+ for param in expr.params().children() {
+ match param {
+ ast::Param::Pos(pattern) => {
+ for ident in pattern.bindings() {
+ self.bind(ident);
+ }
+ }
+ ast::Param::Named(named) => self.bind(named.name()),
+ ast::Param::Spread(spread) => {
+ if let Some(ident) = spread.sink_ident() {
+ self.bind(ident);
+ }
+ }
+ }
+ }
+
+ self.visit(expr.body().to_untyped());
+ self.internal.exit();
+ }
+
+ // A let expression contains a binding, but that binding is only
+ // active after the body is evaluated.
+ Some(ast::Expr::Let(expr)) => {
+ if let Some(init) = expr.init() {
+ self.visit(init.to_untyped());
+ }
+
+ for ident in expr.kind().bindings() {
+ self.bind(ident);
+ }
+ }
+
+ // A for loop contains one or two bindings in its pattern. These are
+ // active after the iterable is evaluated but before the body is
+ // evaluated.
+ Some(ast::Expr::For(expr)) => {
+ self.visit(expr.iterable().to_untyped());
+ self.internal.enter();
+
+ let pattern = expr.pattern();
+ for ident in pattern.bindings() {
+ self.bind(ident);
+ }
+
+ self.visit(expr.body().to_untyped());
+ self.internal.exit();
+ }
+
+ // An import contains items, but these are active only after the
+ // path is evaluated.
+ Some(ast::Expr::Import(expr)) => {
+ self.visit(expr.source().to_untyped());
+ if let Some(ast::Imports::Items(items)) = expr.imports() {
+ for item in items.iter() {
+ self.bind(item.bound_name());
+ }
+ }
+ }
+
+ _ => {
+ // Never capture the name part of a named pair.
+ if let Some(named) = node.cast::<ast::Named>() {
+ self.visit(named.expr().to_untyped());
+ return;
+ }
+
+ // Everything else is traversed from left to right.
+ for child in node.children() {
+ self.visit(child);
+ }
+ }
+ }
+ }
+
+ /// Bind a new internal variable.
+ fn bind(&mut self, ident: ast::Ident) {
+ self.internal.top.define_ident(ident, Value::None);
+ }
+
+ /// Capture a variable if it isn't internal.
+ fn capture(
+ &mut self,
+ ident: &EcoString,
+ span: Span,
+ getter: impl FnOnce(&'a Scopes<'a>, &str) -> HintedStrResult<&'a Value>,
+ ) {
+ if self.internal.get(ident).is_err() {
+ let Some(value) = self
+ .external
+ .map(|external| getter(external, ident).ok())
+ .unwrap_or(Some(&Value::None))
+ else {
+ return;
+ };
+
+ self.captures.define_captured(
+ ident.clone(),
+ value.clone(),
+ self.capturer,
+ span,
+ );
+ }
+ }
+}
+
+#[cfg(test)]
+mod tests {
+ use typst_syntax::parse;
+
+ use super::*;
+
+ #[track_caller]
+ fn test(text: &str, result: &[&str]) {
+ let mut scopes = Scopes::new(None);
+ scopes.top.define("f", 0);
+ scopes.top.define("x", 0);
+ scopes.top.define("y", 0);
+ scopes.top.define("z", 0);
+
+ let mut visitor = CapturesVisitor::new(Some(&scopes), Capturer::Function);
+ let root = parse(text);
+ visitor.visit(&root);
+
+ let captures = visitor.finish();
+ let mut names: Vec<_> = captures.iter().map(|(k, ..)| k).collect();
+ names.sort();
+
+ assert_eq!(names, result);
+ }
+
+ #[test]
+ fn test_captures() {
+ // Let binding and function definition.
+ test("#let x = x", &["x"]);
+ test("#let x; #(x + y)", &["y"]);
+ test("#let f(x, y) = x + y", &[]);
+ test("#let f(x, y) = f", &[]);
+ test("#let f = (x, y) => f", &["f"]);
+
+ // Closure with different kinds of params.
+ test("#((x, y) => x + z)", &["z"]);
+ test("#((x: y, z) => x + z)", &["y"]);
+ test("#((..x) => x + y)", &["y"]);
+ test("#((x, y: x + z) => x + y)", &["x", "z"]);
+ test("#{x => x; x}", &["x"]);
+
+ // Show rule.
+ test("#show y: x => x", &["y"]);
+ test("#show y: x => x + z", &["y", "z"]);
+ test("#show x: x => x", &["x"]);
+
+ // For loop.
+ test("#for x in y { x + z }", &["y", "z"]);
+ test("#for (x, y) in y { x + y }", &["y"]);
+ test("#for x in y {} #x", &["x", "y"]);
+
+ // Import.
+ test("#import z: x, y", &["z"]);
+ test("#import x + y: x, y, z", &["x", "y"]);
+
+ // Blocks.
+ test("#{ let x = 1; { let y = 2; y }; x + y }", &["y"]);
+ test("#[#let x = 1]#x", &["x"]);
+
+ // Field access.
+ test("#foo(body: 1)", &[]);
+ test("#(body: 1)", &[]);
+ test("#(body = 1)", &[]);
+ test("#(body += y)", &["y"]);
+ test("#{ (body, a) = (y, 1) }", &["y"]);
+ test("#(x.at(y) = 5)", &["x", "y"])
+ }
+}
diff --git a/crates/typst-eval/src/code.rs b/crates/typst-eval/src/code.rs
new file mode 100644
index 00000000..918d9d2a
--- /dev/null
+++ b/crates/typst-eval/src/code.rs
@@ -0,0 +1,360 @@
+use ecow::{eco_vec, EcoVec};
+use typst_library::diag::{bail, error, At, SourceResult};
+use typst_library::foundations::{
+ ops, Array, Capturer, Closure, Content, ContextElem, Dict, Func, NativeElement, Str,
+ Value,
+};
+use typst_syntax::ast::{self, AstNode};
+
+use crate::{CapturesVisitor, Eval, Vm};
+
+impl Eval for ast::Code<'_> {
+ type Output = Value;
+
+ fn eval(self, vm: &mut Vm) -> SourceResult<Self::Output> {
+ eval_code(vm, &mut self.exprs())
+ }
+}
+
+/// Evaluate a stream of expressions.
+fn eval_code<'a>(
+ vm: &mut Vm,
+ exprs: &mut impl Iterator<Item = ast::Expr<'a>>,
+) -> SourceResult<Value> {
+ let flow = vm.flow.take();
+ let mut output = Value::None;
+
+ while let Some(expr) = exprs.next() {
+ let span = expr.span();
+ let value = match expr {
+ ast::Expr::Set(set) => {
+ let styles = set.eval(vm)?;
+ if vm.flow.is_some() {
+ break;
+ }
+
+ let tail = eval_code(vm, exprs)?.display();
+ Value::Content(tail.styled_with_map(styles))
+ }
+ ast::Expr::Show(show) => {
+ let recipe = show.eval(vm)?;
+ if vm.flow.is_some() {
+ break;
+ }
+
+ let tail = eval_code(vm, exprs)?.display();
+ Value::Content(tail.styled_with_recipe(
+ &mut vm.engine,
+ vm.context,
+ recipe,
+ )?)
+ }
+ _ => expr.eval(vm)?,
+ };
+
+ output = ops::join(output, value).at(span)?;
+
+ if vm.flow.is_some() {
+ break;
+ }
+ }
+
+ if flow.is_some() {
+ vm.flow = flow;
+ }
+
+ Ok(output)
+}
+
+impl Eval for ast::Expr<'_> {
+ type Output = Value;
+
+ fn eval(self, vm: &mut Vm) -> SourceResult<Self::Output> {
+ let span = self.span();
+ let forbidden = |name| {
+ error!(span, "{} is only allowed directly in code and content blocks", name)
+ };
+
+ let v = match self {
+ Self::Text(v) => v.eval(vm).map(Value::Content),
+ Self::Space(v) => v.eval(vm).map(Value::Content),
+ Self::Linebreak(v) => v.eval(vm).map(Value::Content),
+ Self::Parbreak(v) => v.eval(vm).map(Value::Content),
+ Self::Escape(v) => v.eval(vm),
+ Self::Shorthand(v) => v.eval(vm),
+ Self::SmartQuote(v) => v.eval(vm).map(Value::Content),
+ Self::Strong(v) => v.eval(vm).map(Value::Content),
+ Self::Emph(v) => v.eval(vm).map(Value::Content),
+ Self::Raw(v) => v.eval(vm).map(Value::Content),
+ Self::Link(v) => v.eval(vm).map(Value::Content),
+ Self::Label(v) => v.eval(vm),
+ Self::Ref(v) => v.eval(vm).map(Value::Content),
+ Self::Heading(v) => v.eval(vm).map(Value::Content),
+ Self::List(v) => v.eval(vm).map(Value::Content),
+ Self::Enum(v) => v.eval(vm).map(Value::Content),
+ Self::Term(v) => v.eval(vm).map(Value::Content),
+ Self::Equation(v) => v.eval(vm).map(Value::Content),
+ Self::Math(v) => v.eval(vm).map(Value::Content),
+ Self::MathIdent(v) => v.eval(vm),
+ Self::MathShorthand(v) => v.eval(vm),
+ Self::MathAlignPoint(v) => v.eval(vm).map(Value::Content),
+ Self::MathDelimited(v) => v.eval(vm).map(Value::Content),
+ Self::MathAttach(v) => v.eval(vm).map(Value::Content),
+ Self::MathPrimes(v) => v.eval(vm).map(Value::Content),
+ Self::MathFrac(v) => v.eval(vm).map(Value::Content),
+ Self::MathRoot(v) => v.eval(vm).map(Value::Content),
+ Self::Ident(v) => v.eval(vm),
+ Self::None(v) => v.eval(vm),
+ Self::Auto(v) => v.eval(vm),
+ Self::Bool(v) => v.eval(vm),
+ Self::Int(v) => v.eval(vm),
+ Self::Float(v) => v.eval(vm),
+ Self::Numeric(v) => v.eval(vm),
+ Self::Str(v) => v.eval(vm),
+ Self::Code(v) => v.eval(vm),
+ Self::Content(v) => v.eval(vm).map(Value::Content),
+ Self::Array(v) => v.eval(vm).map(Value::Array),
+ Self::Dict(v) => v.eval(vm).map(Value::Dict),
+ Self::Parenthesized(v) => v.eval(vm),
+ Self::FieldAccess(v) => v.eval(vm),
+ Self::FuncCall(v) => v.eval(vm),
+ Self::Closure(v) => v.eval(vm),
+ Self::Unary(v) => v.eval(vm),
+ Self::Binary(v) => v.eval(vm),
+ Self::Let(v) => v.eval(vm),
+ Self::DestructAssign(v) => v.eval(vm),
+ Self::Set(_) => bail!(forbidden("set")),
+ Self::Show(_) => bail!(forbidden("show")),
+ Self::Contextual(v) => v.eval(vm).map(Value::Content),
+ Self::Conditional(v) => v.eval(vm),
+ Self::While(v) => v.eval(vm),
+ Self::For(v) => v.eval(vm),
+ Self::Import(v) => v.eval(vm),
+ Self::Include(v) => v.eval(vm).map(Value::Content),
+ Self::Break(v) => v.eval(vm),
+ Self::Continue(v) => v.eval(vm),
+ Self::Return(v) => v.eval(vm),
+ }?
+ .spanned(span);
+
+ if vm.inspected == Some(span) {
+ vm.trace(v.clone());
+ }
+
+ Ok(v)
+ }
+}
+
+impl Eval for ast::Ident<'_> {
+ type Output = Value;
+
+ fn eval(self, vm: &mut Vm) -> SourceResult<Self::Output> {
+ vm.scopes.get(&self).cloned().at(self.span())
+ }
+}
+
+impl Eval for ast::None<'_> {
+ type Output = Value;
+
+ fn eval(self, _: &mut Vm) -> SourceResult<Self::Output> {
+ Ok(Value::None)
+ }
+}
+
+impl Eval for ast::Auto<'_> {
+ type Output = Value;
+
+ fn eval(self, _: &mut Vm) -> SourceResult<Self::Output> {
+ Ok(Value::Auto)
+ }
+}
+
+impl Eval for ast::Bool<'_> {
+ type Output = Value;
+
+ fn eval(self, _: &mut Vm) -> SourceResult<Self::Output> {
+ Ok(Value::Bool(self.get()))
+ }
+}
+
+impl Eval for ast::Int<'_> {
+ type Output = Value;
+
+ fn eval(self, _: &mut Vm) -> SourceResult<Self::Output> {
+ Ok(Value::Int(self.get()))
+ }
+}
+
+impl Eval for ast::Float<'_> {
+ type Output = Value;
+
+ fn eval(self, _: &mut Vm) -> SourceResult<Self::Output> {
+ Ok(Value::Float(self.get()))
+ }
+}
+
+impl Eval for ast::Numeric<'_> {
+ type Output = Value;
+
+ fn eval(self, _: &mut Vm) -> SourceResult<Self::Output> {
+ Ok(Value::numeric(self.get()))
+ }
+}
+
+impl Eval for ast::Str<'_> {
+ type Output = Value;
+
+ fn eval(self, _: &mut Vm) -> SourceResult<Self::Output> {
+ Ok(Value::Str(self.get().into()))
+ }
+}
+
+impl Eval for ast::Array<'_> {
+ type Output = Array;
+
+ fn eval(self, vm: &mut Vm) -> SourceResult<Self::Output> {
+ let items = self.items();
+
+ let mut vec = EcoVec::with_capacity(items.size_hint().0);
+ for item in items {
+ match item {
+ ast::ArrayItem::Pos(expr) => vec.push(expr.eval(vm)?),
+ ast::ArrayItem::Spread(spread) => match spread.expr().eval(vm)? {
+ Value::None => {}
+ Value::Array(array) => vec.extend(array.into_iter()),
+ v => bail!(spread.span(), "cannot spread {} into array", v.ty()),
+ },
+ }
+ }
+
+ Ok(vec.into())
+ }
+}
+
+impl Eval for ast::Dict<'_> {
+ type Output = Dict;
+
+ fn eval(self, vm: &mut Vm) -> SourceResult<Self::Output> {
+ let mut map = indexmap::IndexMap::new();
+ let mut invalid_keys = eco_vec![];
+
+ for item in self.items() {
+ match item {
+ ast::DictItem::Named(named) => {
+ map.insert(named.name().get().clone().into(), named.expr().eval(vm)?);
+ }
+ ast::DictItem::Keyed(keyed) => {
+ let raw_key = keyed.key();
+ let key = raw_key.eval(vm)?;
+ let key =
+ key.cast::<Str>().at(raw_key.span()).unwrap_or_else(|errors| {
+ invalid_keys.extend(errors);
+ Str::default()
+ });
+ map.insert(key, keyed.expr().eval(vm)?);
+ }
+ ast::DictItem::Spread(spread) => match spread.expr().eval(vm)? {
+ Value::None => {}
+ Value::Dict(dict) => map.extend(dict.into_iter()),
+ v => bail!(spread.span(), "cannot spread {} into dictionary", v.ty()),
+ },
+ }
+ }
+
+ if !invalid_keys.is_empty() {
+ return Err(invalid_keys);
+ }
+
+ Ok(map.into())
+ }
+}
+
+impl Eval for ast::CodeBlock<'_> {
+ type Output = Value;
+
+ fn eval(self, vm: &mut Vm) -> SourceResult<Self::Output> {
+ vm.scopes.enter();
+ let output = self.body().eval(vm)?;
+ vm.scopes.exit();
+ Ok(output)
+ }
+}
+
+impl Eval for ast::ContentBlock<'_> {
+ type Output = Content;
+
+ fn eval(self, vm: &mut Vm) -> SourceResult<Self::Output> {
+ vm.scopes.enter();
+ let content = self.body().eval(vm)?;
+ vm.scopes.exit();
+ Ok(content)
+ }
+}
+
+impl Eval for ast::Parenthesized<'_> {
+ type Output = Value;
+
+ fn eval(self, vm: &mut Vm) -> SourceResult<Self::Output> {
+ self.expr().eval(vm)
+ }
+}
+
+impl Eval for ast::FieldAccess<'_> {
+ type Output = Value;
+
+ fn eval(self, vm: &mut Vm) -> SourceResult<Self::Output> {
+ let value = self.target().eval(vm)?;
+ let field = self.field();
+
+ let err = match value.field(&field).at(field.span()) {
+ Ok(value) => return Ok(value),
+ Err(err) => err,
+ };
+
+ // Check whether this is a get rule field access.
+ if_chain::if_chain! {
+ if let Value::Func(func) = &value;
+ if let Some(element) = func.element();
+ if let Some(id) = element.field_id(&field);
+ let styles = vm.context.styles().at(field.span());
+ if let Ok(value) = element.field_from_styles(
+ id,
+ styles.as_ref().map(|&s| s).unwrap_or_default(),
+ );
+ then {
+ // Only validate the context once we know that this is indeed
+ // a field from the style chain.
+ let _ = styles?;
+ return Ok(value);
+ }
+ }
+
+ Err(err)
+ }
+}
+
+impl Eval for ast::Contextual<'_> {
+ type Output = Content;
+
+ fn eval(self, vm: &mut Vm) -> SourceResult<Self::Output> {
+ let body = self.body();
+
+ // Collect captured variables.
+ let captured = {
+ let mut visitor = CapturesVisitor::new(Some(&vm.scopes), Capturer::Context);
+ visitor.visit(body.to_untyped());
+ visitor.finish()
+ };
+
+ // Define the closure.
+ let closure = Closure {
+ node: self.body().to_untyped().clone(),
+ defaults: vec![],
+ captured,
+ num_pos_params: 0,
+ };
+
+ let func = Func::from(closure).spanned(body.span());
+ Ok(ContextElem::new(func).pack())
+ }
+}
diff --git a/crates/typst-eval/src/flow.rs b/crates/typst-eval/src/flow.rs
new file mode 100644
index 00000000..5c9d2a00
--- /dev/null
+++ b/crates/typst-eval/src/flow.rs
@@ -0,0 +1,229 @@
+use typst_library::diag::{bail, error, At, SourceDiagnostic, SourceResult};
+use typst_library::foundations::{ops, IntoValue, Value};
+use typst_syntax::ast::{self, AstNode};
+use typst_syntax::{Span, SyntaxKind, SyntaxNode};
+use unicode_segmentation::UnicodeSegmentation;
+
+use crate::{destructure, Eval, Vm};
+
+/// The maximum number of loop iterations.
+const MAX_ITERATIONS: usize = 10_000;
+
+/// A control flow event that occurred during evaluation.
+#[derive(Debug, Clone, PartialEq)]
+pub(crate) enum FlowEvent {
+ /// Stop iteration in a loop.
+ Break(Span),
+ /// Skip the remainder of the current iteration in a loop.
+ Continue(Span),
+ /// Stop execution of a function early, optionally returning an explicit
+ /// value.
+ Return(Span, Option<Value>),
+}
+
+impl FlowEvent {
+ /// Return an error stating that this control flow is forbidden.
+ pub fn forbidden(&self) -> SourceDiagnostic {
+ match *self {
+ Self::Break(span) => {
+ error!(span, "cannot break outside of loop")
+ }
+ Self::Continue(span) => {
+ error!(span, "cannot continue outside of loop")
+ }
+ Self::Return(span, _) => {
+ error!(span, "cannot return outside of function")
+ }
+ }
+ }
+}
+
+impl Eval for ast::Conditional<'_> {
+ type Output = Value;
+
+ fn eval(self, vm: &mut Vm) -> SourceResult<Self::Output> {
+ let condition = self.condition();
+ if condition.eval(vm)?.cast::<bool>().at(condition.span())? {
+ self.if_body().eval(vm)
+ } else if let Some(else_body) = self.else_body() {
+ else_body.eval(vm)
+ } else {
+ Ok(Value::None)
+ }
+ }
+}
+
+impl Eval for ast::WhileLoop<'_> {
+ type Output = Value;
+
+ #[typst_macros::time(name = "while loop", span = self.span())]
+ fn eval(self, vm: &mut Vm) -> SourceResult<Self::Output> {
+ let flow = vm.flow.take();
+ let mut output = Value::None;
+ let mut i = 0;
+
+ let condition = self.condition();
+ let body = self.body();
+
+ while condition.eval(vm)?.cast::<bool>().at(condition.span())? {
+ if i == 0
+ && is_invariant(condition.to_untyped())
+ && !can_diverge(body.to_untyped())
+ {
+ bail!(condition.span(), "condition is always true");
+ } else if i >= MAX_ITERATIONS {
+ bail!(self.span(), "loop seems to be infinite");
+ }
+
+ let value = body.eval(vm)?;
+ output = ops::join(output, value).at(body.span())?;
+
+ match vm.flow {
+ Some(FlowEvent::Break(_)) => {
+ vm.flow = None;
+ break;
+ }
+ Some(FlowEvent::Continue(_)) => vm.flow = None,
+ Some(FlowEvent::Return(..)) => break,
+ None => {}
+ }
+
+ i += 1;
+ }
+
+ if flow.is_some() {
+ vm.flow = flow;
+ }
+
+ Ok(output)
+ }
+}
+
+impl Eval for ast::ForLoop<'_> {
+ type Output = Value;
+
+ #[typst_macros::time(name = "for loop", span = self.span())]
+ fn eval(self, vm: &mut Vm) -> SourceResult<Self::Output> {
+ let flow = vm.flow.take();
+ let mut output = Value::None;
+
+ macro_rules! iter {
+ (for $pat:ident in $iterable:expr) => {{
+ vm.scopes.enter();
+
+ #[allow(unused_parens)]
+ for value in $iterable {
+ destructure(vm, $pat, value.into_value())?;
+
+ let body = self.body();
+ let value = body.eval(vm)?;
+ output = ops::join(output, value).at(body.span())?;
+
+ match vm.flow {
+ Some(FlowEvent::Break(_)) => {
+ vm.flow = None;
+ break;
+ }
+ Some(FlowEvent::Continue(_)) => vm.flow = None,
+ Some(FlowEvent::Return(..)) => break,
+ None => {}
+ }
+ }
+
+ vm.scopes.exit();
+ }};
+ }
+
+ let pattern = self.pattern();
+ let iterable = self.iterable().eval(vm)?;
+ let iterable_type = iterable.ty();
+
+ use ast::Pattern;
+ match (pattern, iterable) {
+ (_, Value::Array(array)) => {
+ // Iterate over values of array.
+ iter!(for pattern in array);
+ }
+ (_, Value::Dict(dict)) => {
+ // Iterate over key-value pairs of dict.
+ iter!(for pattern in dict.iter());
+ }
+ (Pattern::Normal(_) | Pattern::Placeholder(_), Value::Str(str)) => {
+ // Iterate over graphemes of string.
+ iter!(for pattern in str.as_str().graphemes(true));
+ }
+ (Pattern::Normal(_) | Pattern::Placeholder(_), Value::Bytes(bytes)) => {
+ // Iterate over the integers of bytes.
+ iter!(for pattern in bytes.as_slice());
+ }
+ (Pattern::Destructuring(_), Value::Str(_) | Value::Bytes(_)) => {
+ bail!(pattern.span(), "cannot destructure values of {}", iterable_type);
+ }
+ _ => {
+ bail!(self.iterable().span(), "cannot loop over {}", iterable_type);
+ }
+ }
+
+ if flow.is_some() {
+ vm.flow = flow;
+ }
+
+ Ok(output)
+ }
+}
+
+impl Eval for ast::LoopBreak<'_> {
+ type Output = Value;
+
+ fn eval(self, vm: &mut Vm) -> SourceResult<Self::Output> {
+ if vm.flow.is_none() {
+ vm.flow = Some(FlowEvent::Break(self.span()));
+ }
+ Ok(Value::None)
+ }
+}
+
+impl Eval for ast::LoopContinue<'_> {
+ type Output = Value;
+
+ fn eval(self, vm: &mut Vm) -> SourceResult<Self::Output> {
+ if vm.flow.is_none() {
+ vm.flow = Some(FlowEvent::Continue(self.span()));
+ }
+ Ok(Value::None)
+ }
+}
+
+impl Eval for ast::FuncReturn<'_> {
+ type Output = Value;
+
+ fn eval(self, vm: &mut Vm) -> SourceResult<Self::Output> {
+ let value = self.body().map(|body| body.eval(vm)).transpose()?;
+ if vm.flow.is_none() {
+ vm.flow = Some(FlowEvent::Return(self.span(), value));
+ }
+ Ok(Value::None)
+ }
+}
+
+/// Whether the expression always evaluates to the same value.
+fn is_invariant(expr: &SyntaxNode) -> bool {
+ match expr.cast() {
+ Some(ast::Expr::Ident(_)) => false,
+ Some(ast::Expr::MathIdent(_)) => false,
+ Some(ast::Expr::FieldAccess(access)) => {
+ is_invariant(access.target().to_untyped())
+ }
+ Some(ast::Expr::FuncCall(call)) => {
+ is_invariant(call.callee().to_untyped())
+ && is_invariant(call.args().to_untyped())
+ }
+ _ => expr.children().all(is_invariant),
+ }
+}
+
+/// Whether the expression contains a break or return.
+fn can_diverge(expr: &SyntaxNode) -> bool {
+ matches!(expr.kind(), SyntaxKind::Break | SyntaxKind::Return)
+ || expr.children().any(can_diverge)
+}
diff --git a/crates/typst-eval/src/import.rs b/crates/typst-eval/src/import.rs
new file mode 100644
index 00000000..316fbf87
--- /dev/null
+++ b/crates/typst-eval/src/import.rs
@@ -0,0 +1,230 @@
+use comemo::TrackedMut;
+use ecow::{eco_format, eco_vec, EcoString};
+use typst_library::diag::{
+ bail, error, warning, At, FileError, SourceResult, Trace, Tracepoint,
+};
+use typst_library::foundations::{Content, Module, Value};
+use typst_library::World;
+use typst_syntax::ast::{self, AstNode};
+use typst_syntax::package::{PackageManifest, PackageSpec};
+use typst_syntax::{FileId, Span, VirtualPath};
+
+use crate::{eval, Eval, Vm};
+
+impl Eval for ast::ModuleImport<'_> {
+ type Output = Value;
+
+ fn eval(self, vm: &mut Vm) -> SourceResult<Self::Output> {
+ let source = self.source();
+ let source_span = source.span();
+ let mut source = source.eval(vm)?;
+ let new_name = self.new_name();
+ let imports = self.imports();
+
+ match &source {
+ Value::Func(func) => {
+ if func.scope().is_none() {
+ bail!(source_span, "cannot import from user-defined functions");
+ }
+ }
+ Value::Type(_) => {}
+ other => {
+ source = Value::Module(import(vm, other.clone(), source_span, true)?);
+ }
+ }
+
+ if let Some(new_name) = new_name {
+ if let ast::Expr::Ident(ident) = self.source() {
+ if ident.as_str() == new_name.as_str() {
+ // Warn on `import x as x`
+ vm.engine.sink.warn(warning!(
+ new_name.span(),
+ "unnecessary import rename to same name",
+ ));
+ }
+ }
+
+ // Define renamed module on the scope.
+ vm.scopes.top.define_ident(new_name, source.clone());
+ }
+
+ let scope = source.scope().unwrap();
+ match imports {
+ None => {
+ // Only import here if there is no rename.
+ if new_name.is_none() {
+ let name: EcoString = source.name().unwrap().into();
+ vm.scopes.top.define(name, source);
+ }
+ }
+ Some(ast::Imports::Wildcard) => {
+ for (var, value, span) in scope.iter() {
+ vm.scopes.top.define_spanned(var.clone(), value.clone(), span);
+ }
+ }
+ Some(ast::Imports::Items(items)) => {
+ let mut errors = eco_vec![];
+ for item in items.iter() {
+ let mut path = item.path().iter().peekable();
+ let mut scope = scope;
+
+ while let Some(component) = &path.next() {
+ let Some(value) = scope.get(component) else {
+ errors.push(error!(component.span(), "unresolved import"));
+ break;
+ };
+
+ if path.peek().is_some() {
+ // Nested import, as this is not the last component.
+ // This must be a submodule.
+ let Some(submodule) = value.scope() else {
+ let error = if matches!(value, Value::Func(function) if function.scope().is_none())
+ {
+ error!(
+ component.span(),
+ "cannot import from user-defined functions"
+ )
+ } else if !matches!(
+ value,
+ Value::Func(_) | Value::Module(_) | Value::Type(_)
+ ) {
+ error!(
+ component.span(),
+ "expected module, function, or type, found {}",
+ value.ty()
+ )
+ } else {
+ panic!("unexpected nested import failure")
+ };
+ errors.push(error);
+ break;
+ };
+
+ // Walk into the submodule.
+ scope = submodule;
+ } else {
+ // Now that we have the scope of the innermost submodule
+ // in the import path, we may extract the desired item from
+ // it.
+
+ // Warn on `import ...: x as x`
+ if let ast::ImportItem::Renamed(renamed_item) = &item {
+ if renamed_item.original_name().as_str()
+ == renamed_item.new_name().as_str()
+ {
+ vm.engine.sink.warn(warning!(
+ renamed_item.new_name().span(),
+ "unnecessary import rename to same name",
+ ));
+ }
+ }
+
+ vm.define(item.bound_name(), value.clone());
+ }
+ }
+ }
+ if !errors.is_empty() {
+ return Err(errors);
+ }
+ }
+ }
+
+ Ok(Value::None)
+ }
+}
+
+impl Eval for ast::ModuleInclude<'_> {
+ type Output = Content;
+
+ fn eval(self, vm: &mut Vm) -> SourceResult<Self::Output> {
+ let span = self.source().span();
+ let source = self.source().eval(vm)?;
+ let module = import(vm, source, span, false)?;
+ Ok(module.content())
+ }
+}
+
+/// Process an import of a module relative to the current location.
+pub fn import(
+ vm: &mut Vm,
+ source: Value,
+ span: Span,
+ allow_scopes: bool,
+) -> SourceResult<Module> {
+ let path = match source {
+ Value::Str(path) => path,
+ Value::Module(module) => return Ok(module),
+ v if allow_scopes => {
+ bail!(span, "expected path, module, function, or type, found {}", v.ty())
+ }
+ v => bail!(span, "expected path or module, found {}", v.ty()),
+ };
+
+ // Handle package and file imports.
+ let path = path.as_str();
+ if path.starts_with('@') {
+ let spec = path.parse::<PackageSpec>().at(span)?;
+ import_package(vm, spec, span)
+ } else {
+ import_file(vm, path, span)
+ }
+}
+
+/// Import an external package.
+fn import_package(vm: &mut Vm, spec: PackageSpec, span: Span) -> SourceResult<Module> {
+ // Evaluate the manifest.
+ let world = vm.world();
+ let manifest_id = FileId::new(Some(spec.clone()), VirtualPath::new("typst.toml"));
+ let bytes = world.file(manifest_id).at(span)?;
+ let string = std::str::from_utf8(&bytes).map_err(FileError::from).at(span)?;
+ let manifest: PackageManifest = toml::from_str(string)
+ .map_err(|err| eco_format!("package manifest is malformed ({})", err.message()))
+ .at(span)?;
+ manifest.validate(&spec).at(span)?;
+
+ // Evaluate the entry point.
+ let entrypoint_id = manifest_id.join(&manifest.package.entrypoint);
+ let source = world.source(entrypoint_id).at(span)?;
+
+ // Prevent cyclic importing.
+ if vm.engine.route.contains(source.id()) {
+ bail!(span, "cyclic import");
+ }
+
+ let point = || Tracepoint::Import;
+ Ok(eval(
+ vm.engine.routines,
+ vm.engine.world,
+ vm.engine.traced,
+ TrackedMut::reborrow_mut(&mut vm.engine.sink),
+ vm.engine.route.track(),
+ &source,
+ )
+ .trace(world, point, span)?
+ .with_name(manifest.package.name))
+}
+
+/// Import a file from a path.
+fn import_file(vm: &mut Vm, path: &str, span: Span) -> SourceResult<Module> {
+ // Load the source file.
+ let world = vm.world();
+ let id = span.resolve_path(path).at(span)?;
+ let source = world.source(id).at(span)?;
+
+ // Prevent cyclic importing.
+ if vm.engine.route.contains(source.id()) {
+ bail!(span, "cyclic import");
+ }
+
+ // Evaluate the file.
+ let point = || Tracepoint::Import;
+ eval(
+ vm.engine.routines,
+ vm.engine.world,
+ vm.engine.traced,
+ TrackedMut::reborrow_mut(&mut vm.engine.sink),
+ vm.engine.route.track(),
+ &source,
+ )
+ .trace(world, point, span)
+}
diff --git a/crates/typst-eval/src/lib.rs b/crates/typst-eval/src/lib.rs
new file mode 100644
index 00000000..a5c0c7e3
--- /dev/null
+++ b/crates/typst-eval/src/lib.rs
@@ -0,0 +1,170 @@
+//! Typst's code interpreter.
+
+pub(crate) mod ops;
+
+mod access;
+mod binding;
+mod call;
+mod code;
+mod flow;
+mod import;
+mod markup;
+mod math;
+mod methods;
+mod rules;
+mod vm;
+
+pub use self::call::*;
+pub use self::import::*;
+pub use self::vm::*;
+pub use typst_library::routines::EvalMode;
+
+use self::access::*;
+use self::binding::*;
+use self::flow::*;
+use self::methods::*;
+
+use comemo::{Track, Tracked, TrackedMut};
+use typst_library::diag::{bail, SourceResult};
+use typst_library::engine::{Engine, Route, Sink, Traced};
+use typst_library::foundations::{Context, Module, NativeElement, Scope, Scopes, Value};
+use typst_library::introspection::Introspector;
+use typst_library::math::EquationElem;
+use typst_library::routines::Routines;
+use typst_library::World;
+use typst_syntax::{ast, parse, parse_code, parse_math, Source, Span};
+
+/// Evaluate a source file and return the resulting module.
+#[comemo::memoize]
+#[typst_macros::time(name = "eval", span = source.root().span())]
+pub fn eval(
+ routines: &Routines,
+ world: Tracked<dyn World + '_>,
+ traced: Tracked<Traced>,
+ sink: TrackedMut<Sink>,
+ route: Tracked<Route>,
+ source: &Source,
+) -> SourceResult<Module> {
+ // Prevent cyclic evaluation.
+ let id = source.id();
+ if route.contains(id) {
+ panic!("Tried to cyclicly evaluate {:?}", id.vpath());
+ }
+
+ // Prepare the engine.
+ let introspector = Introspector::default();
+ let engine = Engine {
+ routines,
+ world,
+ introspector: introspector.track(),
+ traced,
+ sink,
+ route: Route::extend(route).with_id(id),
+ };
+
+ // Prepare VM.
+ let context = Context::none();
+ let scopes = Scopes::new(Some(world.library()));
+ let root = source.root();
+ let mut vm = Vm::new(engine, context.track(), scopes, root.span());
+
+ // Check for well-formedness unless we are in trace mode.
+ let errors = root.errors();
+ if !errors.is_empty() && vm.inspected.is_none() {
+ return Err(errors.into_iter().map(Into::into).collect());
+ }
+
+ // Evaluate the module.
+ let markup = root.cast::<ast::Markup>().unwrap();
+ let output = markup.eval(&mut vm)?;
+
+ // Handle control flow.
+ if let Some(flow) = vm.flow {
+ bail!(flow.forbidden());
+ }
+
+ // Assemble the module.
+ let name = id
+ .vpath()
+ .as_rootless_path()
+ .file_stem()
+ .unwrap_or_default()
+ .to_string_lossy();
+
+ Ok(Module::new(name, vm.scopes.top).with_content(output).with_file_id(id))
+}
+
+/// Evaluate a string as code and return the resulting value.
+///
+/// Everything in the output is associated with the given `span`.
+#[comemo::memoize]
+pub fn eval_string(
+ routines: &Routines,
+ world: Tracked<dyn World + '_>,
+ string: &str,
+ span: Span,
+ mode: EvalMode,
+ scope: Scope,
+) -> SourceResult<Value> {
+ let mut root = match mode {
+ EvalMode::Code => parse_code(string),
+ EvalMode::Markup => parse(string),
+ EvalMode::Math => parse_math(string),
+ };
+
+ root.synthesize(span);
+
+ // Check for well-formedness.
+ let errors = root.errors();
+ if !errors.is_empty() {
+ return Err(errors.into_iter().map(Into::into).collect());
+ }
+
+ // Prepare the engine.
+ let mut sink = Sink::new();
+ let introspector = Introspector::default();
+ let traced = Traced::default();
+ let engine = Engine {
+ routines,
+ world,
+ introspector: introspector.track(),
+ traced: traced.track(),
+ sink: sink.track_mut(),
+ route: Route::default(),
+ };
+
+ // Prepare VM.
+ let context = Context::none();
+ let scopes = Scopes::new(Some(world.library()));
+ let mut vm = Vm::new(engine, context.track(), scopes, root.span());
+ vm.scopes.scopes.push(scope);
+
+ // Evaluate the code.
+ let output = match mode {
+ EvalMode::Code => root.cast::<ast::Code>().unwrap().eval(&mut vm)?,
+ EvalMode::Markup => {
+ Value::Content(root.cast::<ast::Markup>().unwrap().eval(&mut vm)?)
+ }
+ EvalMode::Math => Value::Content(
+ EquationElem::new(root.cast::<ast::Math>().unwrap().eval(&mut vm)?)
+ .with_block(false)
+ .pack(),
+ ),
+ };
+
+ // Handle control flow.
+ if let Some(flow) = vm.flow {
+ bail!(flow.forbidden());
+ }
+
+ Ok(output)
+}
+
+/// Evaluate an expression.
+pub trait Eval {
+ /// The output of evaluating the expression.
+ type Output;
+
+ /// Evaluate the expression to the output value.
+ fn eval(self, vm: &mut Vm) -> SourceResult<Self::Output>;
+}
diff --git a/crates/typst-eval/src/markup.rs b/crates/typst-eval/src/markup.rs
new file mode 100644
index 00000000..e28eb9dd
--- /dev/null
+++ b/crates/typst-eval/src/markup.rs
@@ -0,0 +1,275 @@
+use typst_library::diag::{warning, At, SourceResult};
+use typst_library::foundations::{
+ Content, Label, NativeElement, Repr, Smart, Symbol, Unlabellable, Value,
+};
+use typst_library::math::EquationElem;
+use typst_library::model::{
+ EmphElem, EnumItem, HeadingElem, LinkElem, ListItem, ParbreakElem, RefElem,
+ StrongElem, Supplement, TermItem, Url,
+};
+use typst_library::text::{
+ LinebreakElem, RawContent, RawElem, SmartQuoteElem, SpaceElem, TextElem,
+};
+use typst_syntax::ast::{self, AstNode};
+
+use crate::{Eval, Vm};
+
+impl Eval for ast::Markup<'_> {
+ type Output = Content;
+
+ fn eval(self, vm: &mut Vm) -> SourceResult<Self::Output> {
+ eval_markup(vm, &mut self.exprs())
+ }
+}
+
+/// Evaluate a stream of markup.
+fn eval_markup<'a>(
+ vm: &mut Vm,
+ exprs: &mut impl Iterator<Item = ast::Expr<'a>>,
+) -> SourceResult<Content> {
+ let flow = vm.flow.take();
+ let mut seq = Vec::with_capacity(exprs.size_hint().1.unwrap_or_default());
+
+ while let Some(expr) = exprs.next() {
+ match expr {
+ ast::Expr::Set(set) => {
+ let styles = set.eval(vm)?;
+ if vm.flow.is_some() {
+ break;
+ }
+
+ seq.push(eval_markup(vm, exprs)?.styled_with_map(styles))
+ }
+ ast::Expr::Show(show) => {
+ let recipe = show.eval(vm)?;
+ if vm.flow.is_some() {
+ break;
+ }
+
+ let tail = eval_markup(vm, exprs)?;
+ seq.push(tail.styled_with_recipe(&mut vm.engine, vm.context, recipe)?)
+ }
+ expr => match expr.eval(vm)? {
+ Value::Label(label) => {
+ if let Some(elem) =
+ seq.iter_mut().rev().find(|node| !node.can::<dyn Unlabellable>())
+ {
+ if elem.label().is_some() {
+ vm.engine.sink.warn(warning!(
+ elem.span(), "content labelled multiple times";
+ hint: "only the last label is used, the rest are ignored",
+ ));
+ }
+
+ *elem = std::mem::take(elem).labelled(label);
+ } else {
+ vm.engine.sink.warn(warning!(
+ expr.span(),
+ "label `{}` is not attached to anything",
+ label.repr()
+ ));
+ }
+ }
+ value => seq.push(value.display().spanned(expr.span())),
+ },
+ }
+
+ if vm.flow.is_some() {
+ break;
+ }
+ }
+
+ if flow.is_some() {
+ vm.flow = flow;
+ }
+
+ Ok(Content::sequence(seq))
+}
+
+impl Eval for ast::Text<'_> {
+ type Output = Content;
+
+ fn eval(self, _: &mut Vm) -> SourceResult<Self::Output> {
+ Ok(TextElem::packed(self.get().clone()))
+ }
+}
+
+impl Eval for ast::Space<'_> {
+ type Output = Content;
+
+ fn eval(self, _: &mut Vm) -> SourceResult<Self::Output> {
+ Ok(SpaceElem::shared().clone())
+ }
+}
+
+impl Eval for ast::Linebreak<'_> {
+ type Output = Content;
+
+ fn eval(self, _: &mut Vm) -> SourceResult<Self::Output> {
+ Ok(LinebreakElem::shared().clone())
+ }
+}
+
+impl Eval for ast::Parbreak<'_> {
+ type Output = Content;
+
+ fn eval(self, _: &mut Vm) -> SourceResult<Self::Output> {
+ Ok(ParbreakElem::shared().clone())
+ }
+}
+
+impl Eval for ast::Escape<'_> {
+ type Output = Value;
+
+ fn eval(self, _: &mut Vm) -> SourceResult<Self::Output> {
+ Ok(Value::Symbol(Symbol::single(self.get().into())))
+ }
+}
+
+impl Eval for ast::Shorthand<'_> {
+ type Output = Value;
+
+ fn eval(self, _: &mut Vm) -> SourceResult<Self::Output> {
+ Ok(Value::Symbol(Symbol::single(self.get().into())))
+ }
+}
+
+impl Eval for ast::SmartQuote<'_> {
+ type Output = Content;
+
+ fn eval(self, _: &mut Vm) -> SourceResult<Self::Output> {
+ Ok(SmartQuoteElem::new().with_double(self.double()).pack())
+ }
+}
+
+impl Eval for ast::Strong<'_> {
+ type Output = Content;
+
+ fn eval(self, vm: &mut Vm) -> SourceResult<Self::Output> {
+ let body = self.body();
+ if body.exprs().next().is_none() {
+ vm.engine
+ .sink
+ .warn(warning!(
+ self.span(), "no text within stars";
+ hint: "using multiple consecutive stars (e.g. **) has no additional effect",
+ ));
+ }
+
+ Ok(StrongElem::new(body.eval(vm)?).pack())
+ }
+}
+
+impl Eval for ast::Emph<'_> {
+ type Output = Content;
+
+ fn eval(self, vm: &mut Vm) -> SourceResult<Self::Output> {
+ let body = self.body();
+ if body.exprs().next().is_none() {
+ vm.engine
+ .sink
+ .warn(warning!(
+ self.span(), "no text within underscores";
+ hint: "using multiple consecutive underscores (e.g. __) has no additional effect"
+ ));
+ }
+
+ Ok(EmphElem::new(body.eval(vm)?).pack())
+ }
+}
+
+impl Eval for ast::Raw<'_> {
+ type Output = Content;
+
+ fn eval(self, _: &mut Vm) -> SourceResult<Self::Output> {
+ let lines = self.lines().map(|line| (line.get().clone(), line.span())).collect();
+ let mut elem = RawElem::new(RawContent::Lines(lines)).with_block(self.block());
+ if let Some(lang) = self.lang() {
+ elem.push_lang(Some(lang.get().clone()));
+ }
+ Ok(elem.pack())
+ }
+}
+
+impl Eval for ast::Link<'_> {
+ type Output = Content;
+
+ fn eval(self, _: &mut Vm) -> SourceResult<Self::Output> {
+ let url = Url::new(self.get().clone()).at(self.span())?;
+ Ok(LinkElem::from_url(url).pack())
+ }
+}
+
+impl Eval for ast::Label<'_> {
+ type Output = Value;
+
+ fn eval(self, _: &mut Vm) -> SourceResult<Self::Output> {
+ Ok(Value::Label(Label::new(self.get())))
+ }
+}
+
+impl Eval for ast::Ref<'_> {
+ type Output = Content;
+
+ fn eval(self, vm: &mut Vm) -> SourceResult<Self::Output> {
+ let target = Label::new(self.target());
+ let mut elem = RefElem::new(target);
+ if let Some(supplement) = self.supplement() {
+ elem.push_supplement(Smart::Custom(Some(Supplement::Content(
+ supplement.eval(vm)?,
+ ))));
+ }
+ Ok(elem.pack())
+ }
+}
+
+impl Eval for ast::Heading<'_> {
+ type Output = Content;
+
+ fn eval(self, vm: &mut Vm) -> SourceResult<Self::Output> {
+ let depth = self.depth();
+ let body = self.body().eval(vm)?;
+ Ok(HeadingElem::new(body).with_depth(depth).pack())
+ }
+}
+
+impl Eval for ast::ListItem<'_> {
+ type Output = Content;
+
+ fn eval(self, vm: &mut Vm) -> SourceResult<Self::Output> {
+ Ok(ListItem::new(self.body().eval(vm)?).pack())
+ }
+}
+
+impl Eval for ast::EnumItem<'_> {
+ type Output = Content;
+
+ fn eval(self, vm: &mut Vm) -> SourceResult<Self::Output> {
+ let body = self.body().eval(vm)?;
+ let mut elem = EnumItem::new(body);
+ if let Some(number) = self.number() {
+ elem.push_number(Some(number));
+ }
+ Ok(elem.pack())
+ }
+}
+
+impl Eval for ast::TermItem<'_> {
+ type Output = Content;
+
+ fn eval(self, vm: &mut Vm) -> SourceResult<Self::Output> {
+ let term = self.term().eval(vm)?;
+ let description = self.description().eval(vm)?;
+ Ok(TermItem::new(term, description).pack())
+ }
+}
+
+impl Eval for ast::Equation<'_> {
+ type Output = Content;
+
+ fn eval(self, vm: &mut Vm) -> SourceResult<Self::Output> {
+ let body = self.body().eval(vm)?;
+ let block = self.block();
+ Ok(EquationElem::new(body).with_block(block).pack())
+ }
+}
diff --git a/crates/typst-eval/src/math.rs b/crates/typst-eval/src/math.rs
new file mode 100644
index 00000000..c61a3251
--- /dev/null
+++ b/crates/typst-eval/src/math.rs
@@ -0,0 +1,119 @@
+use ecow::eco_format;
+use typst_library::diag::{At, SourceResult};
+use typst_library::foundations::{Content, NativeElement, Symbol, Value};
+use typst_library::math::{
+ AlignPointElem, AttachElem, FracElem, LrElem, PrimesElem, RootElem,
+};
+use typst_library::text::TextElem;
+use typst_syntax::ast::{self, AstNode};
+
+use crate::{Eval, Vm};
+
+impl Eval for ast::Math<'_> {
+ type Output = Content;
+ fn eval(self, vm: &mut Vm) -> SourceResult<Self::Output> {
+ Ok(Content::sequence(
+ self.exprs()
+ .map(|expr| expr.eval_display(vm))
+ .collect::<SourceResult<Vec<_>>>()?,
+ ))
+ }
+}
+
+impl Eval for ast::MathIdent<'_> {
+ type Output = Value;
+
+ fn eval(self, vm: &mut Vm) -> SourceResult<Self::Output> {
+ vm.scopes.get_in_math(&self).cloned().at(self.span())
+ }
+}
+
+impl Eval for ast::MathShorthand<'_> {
+ type Output = Value;
+
+ fn eval(self, _: &mut Vm) -> SourceResult<Self::Output> {
+ Ok(Value::Symbol(Symbol::single(self.get().into())))
+ }
+}
+
+impl Eval for ast::MathAlignPoint<'_> {
+ type Output = Content;
+
+ fn eval(self, _: &mut Vm) -> SourceResult<Self::Output> {
+ Ok(AlignPointElem::shared().clone())
+ }
+}
+
+impl Eval for ast::MathDelimited<'_> {
+ type Output = Content;
+
+ fn eval(self, vm: &mut Vm) -> SourceResult<Self::Output> {
+ let open = self.open().eval_display(vm)?;
+ let body = self.body().eval(vm)?;
+ let close = self.close().eval_display(vm)?;
+ Ok(LrElem::new(open + body + close).pack())
+ }
+}
+
+impl Eval for ast::MathAttach<'_> {
+ type Output = Content;
+
+ fn eval(self, vm: &mut Vm) -> SourceResult<Self::Output> {
+ let base = self.base().eval_display(vm)?;
+ let mut elem = AttachElem::new(base);
+
+ if let Some(expr) = self.top() {
+ elem.push_t(Some(expr.eval_display(vm)?));
+ }
+
+ // Always attach primes in scripts style (not limits style),
+ // i.e. at the top-right corner.
+ if let Some(primes) = self.primes() {
+ elem.push_tr(Some(primes.eval(vm)?));
+ }
+
+ if let Some(expr) = self.bottom() {
+ elem.push_b(Some(expr.eval_display(vm)?));
+ }
+
+ Ok(elem.pack())
+ }
+}
+
+impl Eval for ast::MathPrimes<'_> {
+ type Output = Content;
+
+ fn eval(self, _: &mut Vm) -> SourceResult<Self::Output> {
+ Ok(PrimesElem::new(self.count()).pack())
+ }
+}
+
+impl Eval for ast::MathFrac<'_> {
+ type Output = Content;
+
+ fn eval(self, vm: &mut Vm) -> SourceResult<Self::Output> {
+ let num = self.num().eval_display(vm)?;
+ let denom = self.denom().eval_display(vm)?;
+ Ok(FracElem::new(num, denom).pack())
+ }
+}
+
+impl Eval for ast::MathRoot<'_> {
+ type Output = Content;
+
+ fn eval(self, vm: &mut Vm) -> SourceResult<Self::Output> {
+ let index = self.index().map(|i| TextElem::packed(eco_format!("{i}")));
+ let radicand = self.radicand().eval_display(vm)?;
+ Ok(RootElem::new(radicand).with_index(index).pack())
+ }
+}
+
+trait ExprExt {
+ fn eval_display(&self, vm: &mut Vm) -> SourceResult<Content>;
+}
+
+impl ExprExt for ast::Expr<'_> {
+ fn eval_display(&self, vm: &mut Vm) -> SourceResult<Content> {
+ Ok(self.eval(vm)?.display().spanned(self.span()))
+ }
+}
diff --git a/crates/typst-eval/src/methods.rs b/crates/typst-eval/src/methods.rs
new file mode 100644
index 00000000..7cb36a00
--- /dev/null
+++ b/crates/typst-eval/src/methods.rs
@@ -0,0 +1,91 @@
+//! Handles special built-in methods on values.
+
+use typst_library::diag::{At, SourceResult};
+use typst_library::foundations::{Args, Str, Type, Value};
+use typst_syntax::Span;
+
+/// Whether a specific method is mutating.
+pub(crate) fn is_mutating_method(method: &str) -> bool {
+ matches!(method, "push" | "pop" | "insert" | "remove")
+}
+
+/// Whether a specific method is an accessor.
+pub(crate) fn is_accessor_method(method: &str) -> bool {
+ matches!(method, "first" | "last" | "at")
+}
+
+/// Call a mutating method on a value.
+pub(crate) fn call_method_mut(
+ value: &mut Value,
+ method: &str,
+ mut args: Args,
+ span: Span,
+) -> SourceResult<Value> {
+ let ty = value.ty();
+ let missing = || Err(missing_method(ty, method)).at(span);
+ let mut output = Value::None;
+
+ match value {
+ Value::Array(array) => match method {
+ "push" => array.push(args.expect("value")?),
+ "pop" => output = array.pop().at(span)?,
+ "insert" => {
+ array.insert(args.expect("index")?, args.expect("value")?).at(span)?
+ }
+ "remove" => {
+ output = array
+ .remove(args.expect("index")?, args.named("default")?)
+ .at(span)?
+ }
+ _ => return missing(),
+ },
+
+ Value::Dict(dict) => match method {
+ "insert" => dict.insert(args.expect::<Str>("key")?, args.expect("value")?),
+ "remove" => {
+ output =
+ dict.remove(args.expect("key")?, args.named("default")?).at(span)?
+ }
+ _ => return missing(),
+ },
+
+ _ => return missing(),
+ }
+
+ args.finish()?;
+ Ok(output)
+}
+
+/// Call an accessor method on a value.
+pub(crate) fn call_method_access<'a>(
+ value: &'a mut Value,
+ method: &str,
+ mut args: Args,
+ span: Span,
+) -> SourceResult<&'a mut Value> {
+ let ty = value.ty();
+ let missing = || Err(missing_method(ty, method)).at(span);
+
+ let slot = match value {
+ Value::Array(array) => match method {
+ "first" => array.first_mut().at(span)?,
+ "last" => array.last_mut().at(span)?,
+ "at" => array.at_mut(args.expect("index")?).at(span)?,
+ _ => return missing(),
+ },
+ Value::Dict(dict) => match method {
+ "at" => dict.at_mut(&args.expect::<Str>("key")?).at(span)?,
+ _ => return missing(),
+ },
+ _ => return missing(),
+ };
+
+ args.finish()?;
+ Ok(slot)
+}
+
+/// The missing method error message.
+#[cold]
+fn missing_method(ty: Type, method: &str) -> String {
+ format!("type {ty} has no method `{method}`")
+}
diff --git a/crates/typst-eval/src/ops.rs b/crates/typst-eval/src/ops.rs
new file mode 100644
index 00000000..ebbd6743
--- /dev/null
+++ b/crates/typst-eval/src/ops.rs
@@ -0,0 +1,91 @@
+use typst_library::diag::{At, HintedStrResult, SourceResult};
+use typst_library::foundations::{ops, IntoValue, Value};
+use typst_syntax::ast::{self, AstNode};
+
+use crate::{access_dict, Access, Eval, Vm};
+
+impl Eval for ast::Unary<'_> {
+ type Output = Value;
+
+ fn eval(self, vm: &mut Vm) -> SourceResult<Self::Output> {
+ let value = self.expr().eval(vm)?;
+ let result = match self.op() {
+ ast::UnOp::Pos => ops::pos(value),
+ ast::UnOp::Neg => ops::neg(value),
+ ast::UnOp::Not => ops::not(value),
+ };
+ result.at(self.span())
+ }
+}
+
+impl Eval for ast::Binary<'_> {
+ type Output = Value;
+
+ fn eval(self, vm: &mut Vm) -> SourceResult<Self::Output> {
+ match self.op() {
+ ast::BinOp::Add => apply_binary(self, vm, ops::add),
+ ast::BinOp::Sub => apply_binary(self, vm, ops::sub),
+ ast::BinOp::Mul => apply_binary(self, vm, ops::mul),
+ ast::BinOp::Div => apply_binary(self, vm, ops::div),
+ ast::BinOp::And => apply_binary(self, vm, ops::and),
+ ast::BinOp::Or => apply_binary(self, vm, ops::or),
+ ast::BinOp::Eq => apply_binary(self, vm, ops::eq),
+ ast::BinOp::Neq => apply_binary(self, vm, ops::neq),
+ ast::BinOp::Lt => apply_binary(self, vm, ops::lt),
+ ast::BinOp::Leq => apply_binary(self, vm, ops::leq),
+ ast::BinOp::Gt => apply_binary(self, vm, ops::gt),
+ ast::BinOp::Geq => apply_binary(self, vm, ops::geq),
+ ast::BinOp::In => apply_binary(self, vm, ops::in_),
+ ast::BinOp::NotIn => apply_binary(self, vm, ops::not_in),
+ ast::BinOp::Assign => apply_assignment(self, vm, |_, b| Ok(b)),
+ ast::BinOp::AddAssign => apply_assignment(self, vm, ops::add),
+ ast::BinOp::SubAssign => apply_assignment(self, vm, ops::sub),
+ ast::BinOp::MulAssign => apply_assignment(self, vm, ops::mul),
+ ast::BinOp::DivAssign => apply_assignment(self, vm, ops::div),
+ }
+ }
+}
+
+/// Apply a basic binary operation.
+fn apply_binary(
+ binary: ast::Binary,
+ vm: &mut Vm,
+ op: fn(Value, Value) -> HintedStrResult<Value>,
+) -> SourceResult<Value> {
+ let lhs = binary.lhs().eval(vm)?;
+
+ // Short-circuit boolean operations.
+ if (binary.op() == ast::BinOp::And && lhs == false.into_value())
+ || (binary.op() == ast::BinOp::Or && lhs == true.into_value())
+ {
+ return Ok(lhs);
+ }
+
+ let rhs = binary.rhs().eval(vm)?;
+ op(lhs, rhs).at(binary.span())
+}
+
+/// Apply an assignment operation.
+fn apply_assignment(
+ binary: ast::Binary,
+ vm: &mut Vm,
+ op: fn(Value, Value) -> HintedStrResult<Value>,
+) -> SourceResult<Value> {
+ let rhs = binary.rhs().eval(vm)?;
+ let lhs = binary.lhs();
+
+ // An assignment to a dictionary field is different from a normal access
+ // since it can create the field instead of just modifying it.
+ if binary.op() == ast::BinOp::Assign {
+ if let ast::Expr::FieldAccess(access) = lhs {
+ let dict = access_dict(vm, access)?;
+ dict.insert(access.field().get().clone().into(), rhs);
+ return Ok(Value::None);
+ }
+ }
+
+ let location = binary.lhs().access(vm)?;
+ let lhs = std::mem::take(&mut *location);
+ *location = op(lhs, rhs).at(binary.span())?;
+ Ok(Value::None)
+}
diff --git a/crates/typst-eval/src/rules.rs b/crates/typst-eval/src/rules.rs
new file mode 100644
index 00000000..646354d4
--- /dev/null
+++ b/crates/typst-eval/src/rules.rs
@@ -0,0 +1,76 @@
+use typst_library::diag::{warning, At, SourceResult};
+use typst_library::foundations::{
+ Element, Fields, Func, Recipe, Selector, ShowableSelector, Styles, Transformation,
+};
+use typst_library::layout::BlockElem;
+use typst_library::model::ParElem;
+use typst_syntax::ast::{self, AstNode};
+
+use crate::{Eval, Vm};
+
+impl Eval for ast::SetRule<'_> {
+ type Output = Styles;
+
+ fn eval(self, vm: &mut Vm) -> SourceResult<Self::Output> {
+ if let Some(condition) = self.condition() {
+ if !condition.eval(vm)?.cast::<bool>().at(condition.span())? {
+ return Ok(Styles::new());
+ }
+ }
+
+ let target = self.target();
+ let target = target
+ .eval(vm)?
+ .cast::<Func>()
+ .and_then(|func| {
+ func.element().ok_or_else(|| {
+ "only element functions can be used in set rules".into()
+ })
+ })
+ .at(target.span())?;
+ let args = self.args().eval(vm)?.spanned(self.span());
+ Ok(target.set(&mut vm.engine, args)?.spanned(self.span()).liftable())
+ }
+}
+
+impl Eval for ast::ShowRule<'_> {
+ type Output = Recipe;
+
+ fn eval(self, vm: &mut Vm) -> SourceResult<Self::Output> {
+ let selector = self
+ .selector()
+ .map(|sel| sel.eval(vm)?.cast::<ShowableSelector>().at(sel.span()))
+ .transpose()?
+ .map(|selector| selector.0);
+
+ let transform = self.transform();
+ let transform = match transform {
+ ast::Expr::Set(set) => Transformation::Style(set.eval(vm)?),
+ expr => expr.eval(vm)?.cast::<Transformation>().at(transform.span())?,
+ };
+
+ let recipe = Recipe::new(selector, transform, self.span());
+ check_show_par_set_block(vm, &recipe);
+
+ Ok(recipe)
+ }
+}
+
+/// Migration hint for `show par: set block(spacing: ..)`.
+fn check_show_par_set_block(vm: &mut Vm, recipe: &Recipe) {
+ if_chain::if_chain! {
+ if let Some(Selector::Elem(elem, _)) = recipe.selector();
+ if *elem == Element::of::<ParElem>();
+ if let Transformation::Style(styles) = recipe.transform();
+ if styles.has::<BlockElem>(<BlockElem as Fields>::Enum::Above as _) ||
+ styles.has::<BlockElem>(<BlockElem as Fields>::Enum::Below as _);
+ then {
+ vm.engine.sink.warn(warning!(
+ recipe.span(),
+ "`show par: set block(spacing: ..)` has no effect anymore";
+ hint: "write `set par(spacing: ..)` instead";
+ hint: "this is specific to paragraphs as they are not considered blocks anymore"
+ ))
+ }
+ }
+}
diff --git a/crates/typst-eval/src/vm.rs b/crates/typst-eval/src/vm.rs
new file mode 100644
index 00000000..1c8331b6
--- /dev/null
+++ b/crates/typst-eval/src/vm.rs
@@ -0,0 +1,60 @@
+use comemo::Tracked;
+use typst_library::engine::Engine;
+use typst_library::foundations::{Context, IntoValue, Scopes, Value};
+use typst_library::World;
+use typst_syntax::ast::{self, AstNode};
+use typst_syntax::Span;
+
+use crate::FlowEvent;
+
+/// A virtual machine.
+///
+/// Holds the state needed to [evaluate](crate::eval()) Typst sources. A
+/// new virtual machine is created for each module evaluation and function call.
+pub struct Vm<'a> {
+ /// The underlying virtual typesetter.
+ pub(crate) engine: Engine<'a>,
+ /// A control flow event that is currently happening.
+ pub(crate) flow: Option<FlowEvent>,
+ /// The stack of scopes.
+ pub(crate) scopes: Scopes<'a>,
+ /// A span that is currently under inspection.
+ pub(crate) inspected: Option<Span>,
+ /// Data that is contextually made accessible to code behind the scenes.
+ pub(crate) context: Tracked<'a, Context<'a>>,
+}
+
+impl<'a> Vm<'a> {
+ /// Create a new virtual machine.
+ pub fn new(
+ engine: Engine<'a>,
+ context: Tracked<'a, Context<'a>>,
+ scopes: Scopes<'a>,
+ target: Span,
+ ) -> Self {
+ let inspected = target.id().and_then(|id| engine.traced.get(id));
+ Self { engine, context, flow: None, scopes, inspected }
+ }
+
+ /// Access the underlying world.
+ pub fn world(&self) -> Tracked<'a, dyn World + 'a> {
+ self.engine.world
+ }
+
+ /// Define a variable in the current scope.
+ pub fn define(&mut self, var: ast::Ident, value: impl IntoValue) {
+ let value = value.into_value();
+ if self.inspected == Some(var.span()) {
+ self.trace(value.clone());
+ }
+ self.scopes.top.define_ident(var, value);
+ }
+
+ /// Trace a value.
+ #[cold]
+ pub fn trace(&mut self, value: Value) {
+ self.engine
+ .sink
+ .value(value.clone(), self.context.styles().ok().map(|s| s.to_map()));
+ }
+}