From be7cfc85d08c545abfac08098b7b33b4bd71f37e Mon Sep 17 00:00:00 2001 From: Laurenz Date: Sun, 27 Oct 2024 19:04:55 +0100 Subject: Split out four new crates (#5302) --- crates/typst-eval/src/access.rs | 101 ++++++ crates/typst-eval/src/binding.rs | 210 +++++++++++++ crates/typst-eval/src/call.rs | 654 +++++++++++++++++++++++++++++++++++++++ crates/typst-eval/src/code.rs | 360 +++++++++++++++++++++ crates/typst-eval/src/flow.rs | 229 ++++++++++++++ crates/typst-eval/src/import.rs | 230 ++++++++++++++ crates/typst-eval/src/lib.rs | 170 ++++++++++ crates/typst-eval/src/markup.rs | 275 ++++++++++++++++ crates/typst-eval/src/math.rs | 119 +++++++ crates/typst-eval/src/methods.rs | 91 ++++++ crates/typst-eval/src/ops.rs | 91 ++++++ crates/typst-eval/src/rules.rs | 76 +++++ crates/typst-eval/src/vm.rs | 60 ++++ 13 files changed, 2666 insertions(+) create mode 100644 crates/typst-eval/src/access.rs create mode 100644 crates/typst-eval/src/binding.rs create mode 100644 crates/typst-eval/src/call.rs create mode 100644 crates/typst-eval/src/code.rs create mode 100644 crates/typst-eval/src/flow.rs create mode 100644 crates/typst-eval/src/import.rs create mode 100644 crates/typst-eval/src/lib.rs create mode 100644 crates/typst-eval/src/markup.rs create mode 100644 crates/typst-eval/src/math.rs create mode 100644 crates/typst-eval/src/methods.rs create mode 100644 crates/typst-eval/src/ops.rs create mode 100644 crates/typst-eval/src/rules.rs create mode 100644 crates/typst-eval/src/vm.rs (limited to 'crates/typst-eval/src') 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 { + 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 { + 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( + 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( + 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( + 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 { + 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::(); + 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 { + 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 { + // 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, + routines: &Routines, + world: Tracked, + introspector: Tracked, + traced: Tracked, + sink: TrackedMut, + route: Tracked, + context: Tracked, + mut args: Args, +) -> SourceResult { + let (name, params, body) = match closure.node.cast::() { + 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::(&ident)?) + } + pattern => { + crate::destructure( + &mut vm, + pattern, + args.expect::("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::(&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 { + // 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::()?; + 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 { + let mut body = Content::empty(); + for (i, arg) in args.all::()?.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::() { + 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 { + eval_code(vm, &mut self.exprs()) + } +} + +/// Evaluate a stream of expressions. +fn eval_code<'a>( + vm: &mut Vm, + exprs: &mut impl Iterator>, +) -> SourceResult { + 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 { + 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 { + vm.scopes.get(&self).cloned().at(self.span()) + } +} + +impl Eval for ast::None<'_> { + type Output = Value; + + fn eval(self, _: &mut Vm) -> SourceResult { + Ok(Value::None) + } +} + +impl Eval for ast::Auto<'_> { + type Output = Value; + + fn eval(self, _: &mut Vm) -> SourceResult { + Ok(Value::Auto) + } +} + +impl Eval for ast::Bool<'_> { + type Output = Value; + + fn eval(self, _: &mut Vm) -> SourceResult { + Ok(Value::Bool(self.get())) + } +} + +impl Eval for ast::Int<'_> { + type Output = Value; + + fn eval(self, _: &mut Vm) -> SourceResult { + Ok(Value::Int(self.get())) + } +} + +impl Eval for ast::Float<'_> { + type Output = Value; + + fn eval(self, _: &mut Vm) -> SourceResult { + Ok(Value::Float(self.get())) + } +} + +impl Eval for ast::Numeric<'_> { + type Output = Value; + + fn eval(self, _: &mut Vm) -> SourceResult { + Ok(Value::numeric(self.get())) + } +} + +impl Eval for ast::Str<'_> { + type Output = Value; + + fn eval(self, _: &mut Vm) -> SourceResult { + Ok(Value::Str(self.get().into())) + } +} + +impl Eval for ast::Array<'_> { + type Output = Array; + + fn eval(self, vm: &mut Vm) -> SourceResult { + 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 { + 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::().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 { + 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 { + 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.expr().eval(vm) + } +} + +impl Eval for ast::FieldAccess<'_> { + type Output = Value; + + fn eval(self, vm: &mut Vm) -> SourceResult { + 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 { + 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), +} + +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 { + let condition = self.condition(); + if condition.eval(vm)?.cast::().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 { + 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::().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 { + 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 { + 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 { + 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 { + 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 { + 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 { + 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 { + 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::().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 { + // 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 { + // 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, + traced: Tracked, + sink: TrackedMut, + route: Tracked, + source: &Source, +) -> SourceResult { + // 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::().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, + string: &str, + span: Span, + mode: EvalMode, + scope: Scope, +) -> SourceResult { + 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::().unwrap().eval(&mut vm)?, + EvalMode::Markup => { + Value::Content(root.cast::().unwrap().eval(&mut vm)?) + } + EvalMode::Math => Value::Content( + EquationElem::new(root.cast::().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; +} 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 { + eval_markup(vm, &mut self.exprs()) + } +} + +/// Evaluate a stream of markup. +fn eval_markup<'a>( + vm: &mut Vm, + exprs: &mut impl Iterator>, +) -> SourceResult { + 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::()) + { + 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 { + Ok(TextElem::packed(self.get().clone())) + } +} + +impl Eval for ast::Space<'_> { + type Output = Content; + + fn eval(self, _: &mut Vm) -> SourceResult { + Ok(SpaceElem::shared().clone()) + } +} + +impl Eval for ast::Linebreak<'_> { + type Output = Content; + + fn eval(self, _: &mut Vm) -> SourceResult { + Ok(LinebreakElem::shared().clone()) + } +} + +impl Eval for ast::Parbreak<'_> { + type Output = Content; + + fn eval(self, _: &mut Vm) -> SourceResult { + Ok(ParbreakElem::shared().clone()) + } +} + +impl Eval for ast::Escape<'_> { + type Output = Value; + + fn eval(self, _: &mut Vm) -> SourceResult { + Ok(Value::Symbol(Symbol::single(self.get().into()))) + } +} + +impl Eval for ast::Shorthand<'_> { + type Output = Value; + + fn eval(self, _: &mut Vm) -> SourceResult { + Ok(Value::Symbol(Symbol::single(self.get().into()))) + } +} + +impl Eval for ast::SmartQuote<'_> { + type Output = Content; + + fn eval(self, _: &mut Vm) -> SourceResult { + Ok(SmartQuoteElem::new().with_double(self.double()).pack()) + } +} + +impl Eval for ast::Strong<'_> { + type Output = Content; + + fn eval(self, vm: &mut Vm) -> SourceResult { + 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 { + 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 { + 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 { + 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 { + Ok(Value::Label(Label::new(self.get()))) + } +} + +impl Eval for ast::Ref<'_> { + type Output = Content; + + fn eval(self, vm: &mut Vm) -> SourceResult { + 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 { + 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 { + Ok(ListItem::new(self.body().eval(vm)?).pack()) + } +} + +impl Eval for ast::EnumItem<'_> { + type Output = Content; + + fn eval(self, vm: &mut Vm) -> SourceResult { + 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 { + 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 { + 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 { + Ok(Content::sequence( + self.exprs() + .map(|expr| expr.eval_display(vm)) + .collect::>>()?, + )) + } +} + +impl Eval for ast::MathIdent<'_> { + type Output = Value; + + fn eval(self, vm: &mut Vm) -> SourceResult { + vm.scopes.get_in_math(&self).cloned().at(self.span()) + } +} + +impl Eval for ast::MathShorthand<'_> { + type Output = Value; + + fn eval(self, _: &mut Vm) -> SourceResult { + Ok(Value::Symbol(Symbol::single(self.get().into()))) + } +} + +impl Eval for ast::MathAlignPoint<'_> { + type Output = Content; + + fn eval(self, _: &mut Vm) -> SourceResult { + Ok(AlignPointElem::shared().clone()) + } +} + +impl Eval for ast::MathDelimited<'_> { + type Output = Content; + + fn eval(self, vm: &mut Vm) -> SourceResult { + 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 { + 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 { + Ok(PrimesElem::new(self.count()).pack()) + } +} + +impl Eval for ast::MathFrac<'_> { + type Output = Content; + + fn eval(self, vm: &mut Vm) -> SourceResult { + 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 { + 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; +} + +impl ExprExt for ast::Expr<'_> { + fn eval_display(&self, vm: &mut Vm) -> SourceResult { + 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 { + 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::("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::("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 { + 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 { + 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, +) -> SourceResult { + 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, +) -> SourceResult { + 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 { + if let Some(condition) = self.condition() { + if !condition.eval(vm)?.cast::().at(condition.span())? { + return Ok(Styles::new()); + } + } + + let target = self.target(); + let target = target + .eval(vm)? + .cast::() + .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 { + let selector = self + .selector() + .map(|sel| sel.eval(vm)?.cast::().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::().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::(); + if let Transformation::Style(styles) = recipe.transform(); + if styles.has::(::Enum::Above as _) || + styles.has::(::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, + /// The stack of scopes. + pub(crate) scopes: Scopes<'a>, + /// A span that is currently under inspection. + pub(crate) inspected: Option, + /// 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())); + } +} -- cgit v1.2.3