summaryrefslogtreecommitdiff
path: root/crates/typst-eval/src/binding.rs
diff options
context:
space:
mode:
Diffstat (limited to 'crates/typst-eval/src/binding.rs')
-rw-r--r--crates/typst-eval/src/binding.rs210
1 files changed, 210 insertions, 0 deletions
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}",
+ )
+}