diff --git a/crates/typst/src/eval/call.rs b/crates/typst/src/eval/call.rs index a143c8ac2..ed972c005 100644 --- a/crates/typst/src/eval/call.rs +++ b/crates/typst/src/eval/call.rs @@ -1,7 +1,10 @@ use comemo::{Tracked, TrackedMut}; use ecow::{eco_format, EcoVec}; -use crate::diag::{bail, error, At, HintedStrResult, SourceResult, Trace, Tracepoint}; +use crate::diag::{ + bail, error, At, HintedStrResult, HintedString, SourceDiagnostic, SourceResult, + Trace, Tracepoint, +}; use crate::engine::{Engine, Sink, Traced}; use crate::eval::{Access, Eval, FlowEvent, Route, Vm}; use crate::foundations::{ @@ -10,7 +13,7 @@ use crate::foundations::{ }; use crate::introspection::Introspector; use crate::math::LrElem; -use crate::syntax::ast::{self, AstNode}; +use crate::syntax::ast::{self, AstNode, Ident}; use crate::syntax::{Span, Spanned, SyntaxNode}; use crate::text::TextElem; use crate::utils::LazyHash; @@ -32,135 +35,25 @@ impl Eval for ast::FuncCall<'_> { } // Try to evaluate as a call to an associated function or field. - let (callee, mut args) = if let ast::Expr::FieldAccess(access) = callee { + let (callee, args) = if let ast::Expr::FieldAccess(access) = callee { let target = access.target(); - let target_span = target.span(); let field = access.field(); - let field_span = field.span(); - - let target = if is_mutating_method(&field) { - let mut args = args.eval(vm)?.spanned(span); - let target = target.access(vm)?; - - // Only arrays and dictionaries have mutable methods. - if matches!(target, Value::Array(_) | Value::Dict(_)) { - args.span = span; - let point = || Tracepoint::Call(Some(field.get().clone())); - return call_method_mut(target, &field, args, span).trace( - vm.world(), - point, - span, - ); - } - - target.clone() - } else { - access.target().eval(vm)? - }; - - let mut args = args.eval(vm)?.spanned(span); - - // Handle plugins. - if let Value::Plugin(plugin) = &target { - let bytes = args.all::()?; - args.finish()?; - return Ok(plugin.call(&field, bytes).at(span)?.into_value()); - } - - // Prioritize associated functions on the value's type (i.e., - // 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). - // - // 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). - if let Some(callee) = target.ty().scope().get(&field) { - let this = Arg { - span: target_span, - name: None, - value: Spanned::new(target, target_span), - }; - args.span = span; - args.items.insert(0, this); - (callee.clone(), args) - } else if matches!( - target, - Value::Symbol(_) | Value::Func(_) | Value::Type(_) | Value::Module(_) - ) { - (target.field(&field).at(field_span)?, args) - } else { - let mut error = error!( - field_span, - "type {} has no method `{}`", - target.ty(), - field.as_str() - ); - - let mut field_hint = || { - if target.field(&field).is_ok() { - error.hint(eco_format!( - "did you mean to access the field `{}`?", - 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(), - )); - } else { - field_hint(); - } - } - _ => field_hint(), - } - - bail!(error); + 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() { - // For non-functions in math, we wrap the arguments in parentheses. - 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(','); - } - return Ok(Value::Content( - callee.display().spanned(callee_span) - + LrElem::new(TextElem::packed('(') + body + TextElem::packed(')')) - .pack(), - )); + return wrap_args_in_math(callee, callee_span, args, trailing_comma); } let func = func_result - .map_err(|mut err| { - if let ast::Expr::Ident(ident) = self.callee() { - let ident = ident.get(); - if vm.scopes.check_std_shadowed(ident) { - err.hint(eco_format!( - "use `std.{}` to access the shadowed standard library function", - ident, - )); - } - } - err - }) + .map_err(|err| hint_if_shadowed_std(vm, &self.callee(), err)) .at(callee_span)?; let point = || Tracepoint::Call(func.name().map(Into::into)); @@ -368,6 +261,108 @@ pub(crate) fn call_closure( 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, @@ -376,6 +371,46 @@ fn in_math(expr: ast::Expr) -> bool { } } +/// 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>>, diff --git a/crates/typst/src/foundations/args.rs b/crates/typst/src/foundations/args.rs index c59e49855..d580be3ce 100644 --- a/crates/typst/src/foundations/args.rs +++ b/crates/typst/src/foundations/args.rs @@ -76,6 +76,18 @@ impl Args { self.items.iter().filter(|slot| slot.name.is_none()).count() } + /// Insert a positional argument at a specific index. + pub fn insert(&mut self, index: usize, span: Span, value: Value) { + self.items.insert( + index, + Arg { + span: self.span, + name: None, + value: Spanned::new(value, span), + }, + ) + } + /// Push a positional argument. pub fn push(&mut self, span: Span, value: Value) { self.items.push(Arg {