From 0f8219b392e96d3cf7d784ee5d474274169d9918 Mon Sep 17 00:00:00 2001 From: Marmare314 <49279081+Marmare314@users.noreply.github.com> Date: Thu, 6 Apr 2023 15:26:09 +0200 Subject: [PATCH] Unpacking syntax (#532) Closes #341 --- docs/src/reference/scripting.md | 53 ++++++-- library/src/compute/data.rs | 2 +- library/src/layout/terms.rs | 2 +- src/eval/array.rs | 11 ++ src/eval/func.rs | 14 +- src/eval/methods.rs | 2 + src/eval/mod.rs | 151 ++++++++++++++++----- src/ide/complete.rs | 9 +- src/ide/highlight.rs | 1 + src/syntax/ast.rs | 147 +++++++++++++++------ src/syntax/kind.rs | 3 + src/syntax/parser.rs | 136 ++++++++++++++++--- tests/typ/compiler/for.typ | 21 ++- tests/typ/compiler/let.typ | 153 ++++++++++++++++++++++ tests/typ/layout/stack-1.typ | 2 +- tests/typ/visualize/shape-fill-stroke.typ | 4 +- 16 files changed, 586 insertions(+), 125 deletions(-) diff --git a/docs/src/reference/scripting.md b/docs/src/reference/scripting.md index 8f72c2193..ca56103c4 100644 --- a/docs/src/reference/scripting.md +++ b/docs/src/reference/scripting.md @@ -81,6 +81,41 @@ It explains #name. Sum is #add(2, 3). ``` +Let bindings can be used to destructure arrays and dictionaries. + +```example +#let (x, y) = (1, 2) +The coordinates are #x, #y. + +#let (a, .., b) = (1, 2, 3, 4) +The first element is #a. +The last element is #b. + +#let books = ( + "Shakespeare": "Hamlet", + "Homer": "The Odyssey", + "Austen": "Persuasion", +) +#let (Austen) = books +Austen wrote #Austen. + +#let (Homer: h) = books +Homer wrote #h. + +#let (Homer, ..other) = books +#for (author, title) in other [ + #author wrote #title, +] +``` + +Note that the underscore `_` is the only identifier that can +be used multiple times in the same assignment. + +``` +#let (_, y, _) = (1, 2, 3) +The y coordinate is #y. +``` + ## Conditionals { #conditionals } With a conditional, you can display or compute different things depending on whether some condition is fulfilled. Typst supports `{if}`, `{else if}` and @@ -136,20 +171,12 @@ For loops can iterate over a variety of collections: one cluster.) - `{for value in array {..}}` \ - `{for index, value in array {..}}`\ - Iterates over the items in the [array]($type/array). Can also provide the - index of each item. + Iterates over the items in the [array]($type/array). The destructuring syntax + described in [Let binding]($scripting/bindings) can also be used here. -- `{for value in dict {..}}` \ - `{for key, value in dict {..}}` \ - Iterates over the values or keys and values of the - [dictionary]($type/dictionary). - -- `{for value in args {..}}` \ - `{for name, value in args {..}}` \ - Iterates over the values or names and values of the - [arguments]($type/arguments). For positional arguments, the `name` is - `{none}`. +- `{for pair in dict {..}}` \ + Iterates over the key-value pairs of the [dictionary]($type/dictionary). + The pairs can also be destructured by using `{for (key, value) in dict {..}}`. To control the execution of the loop, Typst provides the `{break}` and `{continue}` statements. The former performs an early exit from the loop while diff --git a/library/src/compute/data.rs b/library/src/compute/data.rs index 1e0edebb5..3b30efacf 100644 --- a/library/src/compute/data.rs +++ b/library/src/compute/data.rs @@ -232,7 +232,7 @@ fn format_json_error(error: serde_json::Error) -> String { /// ## Example /// ```example /// #let bookshelf(contents) = { -/// for author, works in contents { +/// for (author, works) in contents { /// author /// for work in works [ /// - #work.title (#work.published) diff --git a/library/src/layout/terms.rs b/library/src/layout/terms.rs index e51280f9d..45ba82b56 100644 --- a/library/src/layout/terms.rs +++ b/library/src/layout/terms.rs @@ -81,7 +81,7 @@ pub struct TermsElem { /// collected into term lists, even through constructs like for loops. /// /// ```example - /// #for year, product in ( + /// #for (year, product) in ( /// "1978": "TeX", /// "1984": "LaTeX", /// "2019": "Typst", diff --git a/src/eval/array.rs b/src/eval/array.rs index 8dcbd5a9b..394191ead 100644 --- a/src/eval/array.rs +++ b/src/eval/array.rs @@ -322,6 +322,17 @@ impl Array { usize::try_from(if index >= 0 { index } else { self.len().checked_add(index)? }) .ok() } + + /// Enumerate all items in the array. + pub fn enumerate(&self) -> Self { + let v = self + .iter() + .enumerate() + .map(|(i, value)| array![i, value.clone()]) + .map(Value::Array) + .collect(); + Self::from_vec(v) + } } impl Debug for Array { diff --git a/src/eval/func.rs b/src/eval/func.rs index e6402e87f..93da6d7b5 100644 --- a/src/eval/func.rs +++ b/src/eval/func.rs @@ -441,7 +441,10 @@ impl<'a> CapturesVisitor<'a> { if let Some(init) = expr.init() { self.visit(init.as_untyped()); } - self.bind(expr.binding()); + + for ident in expr.kind().idents() { + self.bind(ident); + } } // A for loop contains one or two bindings in its pattern. These are @@ -450,11 +453,12 @@ impl<'a> CapturesVisitor<'a> { Some(ast::Expr::For(expr)) => { self.visit(expr.iter().as_untyped()); self.internal.enter(); + let pattern = expr.pattern(); - if let Some(key) = pattern.key() { - self.bind(key); + for ident in pattern.idents() { + self.bind(ident); } - self.bind(pattern.value()); + self.visit(expr.body().as_untyped()); self.internal.exit(); } @@ -550,7 +554,7 @@ mod tests { // For loop. test("#for x in y { x + z }", &["y", "z"]); - test("#for x, y in y { x + y }", &["y"]); + test("#for (x, y) in y { x + y }", &["y"]); test("#for x in y {} #x", &["x", "y"]); // Import. diff --git a/src/eval/methods.rs b/src/eval/methods.rs index b88bca509..8b364fcba 100644 --- a/src/eval/methods.rs +++ b/src/eval/methods.rs @@ -116,6 +116,7 @@ pub fn call( array.join(sep, last).at(span)? } "sorted" => Value::Array(array.sorted().at(span)?), + "enumerate" => Value::Array(array.enumerate()), _ => return missing(), }, @@ -297,6 +298,7 @@ pub fn methods_on(type_name: &str) -> &[(&'static str, bool)] { ("rev", false), ("slice", true), ("sorted", false), + ("enumerate", false), ], "dictionary" => &[ ("at", true), diff --git a/src/eval/mod.rs b/src/eval/mod.rs index e278d787b..ca69b2d8c 100644 --- a/src/eval/mod.rs +++ b/src/eval/mod.rs @@ -37,7 +37,7 @@ pub use self::value::*; pub(crate) use self::methods::methods_on; -use std::collections::BTreeMap; +use std::collections::{BTreeMap, HashSet}; use std::mem; use std::path::{Path, PathBuf}; @@ -1184,6 +1184,97 @@ impl Eval for ast::Closure { } } +impl ast::Pattern { + // Destruct the given value into the pattern. + pub fn define(&self, vm: &mut Vm, value: Value) -> SourceResult { + match self.kind() { + ast::PatternKind::Ident(ident) => { + vm.define(ident, value); + Ok(Value::None) + } + ast::PatternKind::Destructure(pattern) => { + match value { + Value::Array(value) => { + let mut i = 0; + for p in &pattern { + match p { + ast::DestructuringKind::Ident(ident) => { + let Ok(v) = value.at(i) else { + bail!(ident.span(), "not enough elements to destructure"); + }; + vm.define(ident.clone(), v.clone()); + i += 1; + } + ast::DestructuringKind::Sink(ident) => { + (1 + value.len() as usize).checked_sub(pattern.len()).and_then(|sink_size| { + let Ok(sink) = value.slice(i, Some(i + sink_size as i64)) else { + return None; + }; + if let Some(ident) = ident { + vm.define(ident.clone(), sink.clone()); + } + i += sink_size as i64; + Some(()) + }).ok_or("not enough elements to destructure").at(self.span())?; + } + ast::DestructuringKind::Named(key, _) => { + bail!( + key.span(), + "cannot destructure named elements from an array" + ) + } + } + } + if i < value.len() as i64 { + bail!(self.span(), "too many elements to destructure"); + } + } + Value::Dict(value) => { + let mut sink = None; + let mut used = HashSet::new(); + for p in &pattern { + match p { + ast::DestructuringKind::Ident(ident) => { + let Ok(v) = value.at(ident) else { + bail!(ident.span(), "destructuring key not found in dictionary"); + }; + vm.define(ident.clone(), v.clone()); + used.insert(ident.clone().take()); + } + ast::DestructuringKind::Sink(ident) => { + sink = ident.clone() + } + ast::DestructuringKind::Named(key, ident) => { + let Ok(v) = value.at(key) else { + bail!(ident.span(), "destructuring key not found in dictionary"); + }; + vm.define(ident.clone(), v.clone()); + used.insert(key.clone().take()); + } + } + } + + if let Some(ident) = sink { + let mut sink = Dict::new(); + for (key, value) in value { + if !used.contains(key.as_str()) { + sink.insert(key, value); + } + } + vm.define(ident, Value::Dict(sink)); + } + } + _ => { + bail!(self.span(), "cannot destructure {}", value.type_name()); + } + } + + Ok(Value::None) + } + } + } +} + impl Eval for ast::LetBinding { type Output = Value; @@ -1192,8 +1283,14 @@ impl Eval for ast::LetBinding { Some(expr) => expr.eval(vm)?, None => Value::None, }; - vm.define(self.binding(), value); - Ok(Value::None) + + match self.kind() { + ast::LetBindingKind::Normal(pattern) => pattern.define(vm, value), + ast::LetBindingKind::Closure(ident) => { + vm.define(ident, value); + Ok(Value::None) + } + } } } @@ -1333,12 +1430,12 @@ impl Eval for ast::ForLoop { let mut output = Value::None; macro_rules! iter { - (for ($($binding:ident => $value:ident),*) in $iter:expr) => {{ + (for $pat:ident in $iter:expr) => {{ vm.scopes.enter(); #[allow(unused_parens)] - for ($($value),*) in $iter { - $(vm.define($binding.clone(), $value);)* + for value in $iter { + $pat.define(vm, Value::from(value))?; let body = self.body(); let value = body.eval(vm)?; @@ -1361,40 +1458,26 @@ impl Eval for ast::ForLoop { let iter = self.iter().eval(vm)?; let pattern = self.pattern(); - let key = pattern.key(); - let value = pattern.value(); - match (key, value, iter) { - (None, v, Value::Str(string)) => { - iter!(for (v => value) in string.as_str().graphemes(true)); + match (pattern.kind(), iter.clone()) { + (ast::PatternKind::Ident(_), Value::Str(string)) => { + // iterate over characters of string + iter!(for pattern in string.as_str().graphemes(true)); } - (None, v, Value::Array(array)) => { - iter!(for (v => value) in array.into_iter()); + (_, Value::Dict(dict)) => { + // iterate over keys of dict + iter!(for pattern in dict.pairs()); } - (Some(i), v, Value::Array(array)) => { - iter!(for (i => idx, v => value) in array.into_iter().enumerate()); + (_, Value::Array(array)) => { + // iterate over values of array and allow destructuring + iter!(for pattern in array.into_iter()); } - (None, v, Value::Dict(dict)) => { - iter!(for (v => value) in dict.into_iter().map(|p| p.1)); - } - (Some(k), v, Value::Dict(dict)) => { - iter!(for (k => key, v => value) in dict.into_iter()); - } - (None, v, Value::Args(args)) => { - iter!(for (v => value) in args.items.into_iter() - .filter(|arg| arg.name.is_none()) - .map(|arg| arg.value.v)); - } - (Some(k), v, Value::Args(args)) => { - iter!(for (k => key, v => value) in args.items.into_iter() - .map(|arg| (arg.name.map_or(Value::None, Value::Str), arg.value.v))); - } - (_, _, Value::Str(_)) => { - bail!(pattern.span(), "mismatched pattern"); - } - (_, _, iter) => { + (ast::PatternKind::Ident(_), _) => { bail!(self.iter().span(), "cannot loop over {}", iter.type_name()); } + (_, _) => { + bail!(pattern.span(), "cannot destructure values of {}", iter.type_name()) + } } if flow.is_some() { diff --git a/src/ide/complete.rs b/src/ide/complete.rs index da9e07251..0c80c8df7 100644 --- a/src/ide/complete.rs +++ b/src/ide/complete.rs @@ -1096,7 +1096,9 @@ impl<'a> CompletionContext<'a> { let mut sibling = Some(node.clone()); while let Some(node) = &sibling { if let Some(v) = node.cast::() { - defined.insert(v.binding().take()); + for ident in v.kind().idents() { + defined.insert(ident.take()); + } } sibling = node.prev_sibling(); } @@ -1105,10 +1107,9 @@ impl<'a> CompletionContext<'a> { if let Some(v) = parent.cast::() { if node.prev_sibling_kind() != Some(SyntaxKind::In) { let pattern = v.pattern(); - if let Some(key) = pattern.key() { - defined.insert(key.take()); + for ident in pattern.idents() { + defined.insert(ident.take()); } - defined.insert(pattern.value().take()); } } diff --git a/src/ide/highlight.rs b/src/ide/highlight.rs index e948975b3..cf8fdd109 100644 --- a/src/ide/highlight.rs +++ b/src/ide/highlight.rs @@ -246,6 +246,7 @@ pub fn highlight(node: &LinkedNode) -> Option { SyntaxKind::LoopBreak => None, SyntaxKind::LoopContinue => None, SyntaxKind::FuncReturn => None, + SyntaxKind::Pattern => None, SyntaxKind::LineComment => Some(Tag::Comment), SyntaxKind::BlockComment => Some(Tag::Comment), diff --git a/src/syntax/ast.rs b/src/syntax/ast.rs index 780c61647..941149580 100644 --- a/src/syntax/ast.rs +++ b/src/syntax/ast.rs @@ -1533,7 +1533,10 @@ impl Closure { /// /// This only exists if you use the function syntax sugar: `let f(x) = y`. pub fn name(&self) -> Option { - self.0.children().next()?.cast() + match self.0.cast_first_match::()?.kind() { + PatternKind::Ident(ident) => Some(ident), + _ => Option::None, + } } /// The parameter bindings. @@ -1589,29 +1592,122 @@ impl AstNode for Param { } } +node! { + /// A destructuring pattern: `x` or `(x, _, ..y)`. + Pattern +} + +/// The kind of a pattern. +#[derive(Debug, Clone, Hash)] +pub enum PatternKind { + /// A single identifier: `x`. + Ident(Ident), + /// A destructuring pattern: `(x, _, ..y)`. + Destructure(Vec), +} + +/// The kind of an element in a destructuring pattern. +#[derive(Debug, Clone, Hash)] +pub enum DestructuringKind { + /// An identifier: `x`. + Ident(Ident), + /// An argument sink: `..y`. + Sink(Option), + /// Named arguments: `x: 1`. + Named(Ident, Ident), +} + +impl Pattern { + /// The kind of the pattern. + pub fn kind(&self) -> PatternKind { + if self.0.children().len() <= 1 { + return PatternKind::Ident(self.0.cast_first_match().unwrap_or_default()); + } + + let mut bindings = Vec::new(); + for child in self.0.children() { + match child.kind() { + SyntaxKind::Ident => { + bindings + .push(DestructuringKind::Ident(child.cast().unwrap_or_default())); + } + SyntaxKind::Spread => { + bindings.push(DestructuringKind::Sink(child.cast_first_match())); + } + SyntaxKind::Named => { + let mut filtered = child.children().filter_map(SyntaxNode::cast); + let key = filtered.next().unwrap_or_default(); + let ident = filtered.next().unwrap_or_default(); + bindings.push(DestructuringKind::Named(key, ident)); + } + _ => (), + } + } + + PatternKind::Destructure(bindings) + } + + // Returns a list of all identifiers in the pattern. + pub fn idents(&self) -> Vec { + match self.kind() { + PatternKind::Ident(ident) => vec![ident], + PatternKind::Destructure(bindings) => bindings + .into_iter() + .filter_map(|binding| match binding { + DestructuringKind::Ident(ident) => Some(ident), + DestructuringKind::Sink(ident) => ident, + DestructuringKind::Named(_, ident) => Some(ident), + }) + .collect(), + } + } +} + node! { /// A let binding: `let x = 1`. LetBinding } +pub enum LetBindingKind { + /// A normal binding: `let x = 1`. + Normal(Pattern), + /// A closure binding: `let f(x) = 1`. + Closure(Ident), +} + +impl LetBindingKind { + // Returns a list of all identifiers in the pattern. + pub fn idents(&self) -> Vec { + match self { + LetBindingKind::Normal(pattern) => pattern.idents(), + LetBindingKind::Closure(ident) => { + vec![ident.clone()] + } + } + } +} + impl LetBinding { - /// The binding to assign to. - pub fn binding(&self) -> Ident { - match self.0.cast_first_match() { - Some(Expr::Ident(binding)) => binding, - Some(Expr::Closure(closure)) => closure.name().unwrap_or_default(), - _ => Ident::default(), + /// The kind of the let binding. + pub fn kind(&self) -> LetBindingKind { + if let Some(pattern) = self.0.cast_first_match::() { + LetBindingKind::Normal(pattern) + } else { + LetBindingKind::Closure( + self.0 + .cast_first_match::() + .unwrap_or_default() + .name() + .unwrap_or_default(), + ) } } /// The expression the binding is initialized with. pub fn init(&self) -> Option { - if self.0.cast_first_match::().is_some() { - // This is a normal binding like `let x = 1`. - self.0.children().filter_map(SyntaxNode::cast).nth(1) - } else { - // This is a closure binding like `let f(x) = 1`. - self.0.cast_first_match() + match self.kind() { + LetBindingKind::Normal(_) => self.0.cast_last_match(), + LetBindingKind::Closure(_) => self.0.cast_first_match(), } } } @@ -1712,7 +1808,7 @@ node! { impl ForLoop { /// The pattern to assign to. - pub fn pattern(&self) -> ForPattern { + pub fn pattern(&self) -> Pattern { self.0.cast_first_match().unwrap_or_default() } @@ -1727,29 +1823,6 @@ impl ForLoop { } } -node! { - /// A for loop's destructuring pattern: `x` or `x, y`. - ForPattern -} - -impl ForPattern { - /// The key part of the pattern: index for arrays, name for dictionaries. - pub fn key(&self) -> Option { - let mut children = self.0.children().filter_map(SyntaxNode::cast); - let key = children.next(); - if children.next().is_some() { - key - } else { - Option::None - } - } - - /// The value part of the pattern. - pub fn value(&self) -> Ident { - self.0.cast_last_match().unwrap_or_default() - } -} - node! { /// A module import: `import "utils.typ": a, b, c`. ModuleImport diff --git a/src/syntax/kind.rs b/src/syntax/kind.rs index b0d934d1a..fcde2bb47 100644 --- a/src/syntax/kind.rs +++ b/src/syntax/kind.rs @@ -244,6 +244,8 @@ pub enum SyntaxKind { LoopContinue, /// A return from a function: `return`, `return x + 1`. FuncReturn, + /// A destructuring pattern: `x`, `(x, _, ..y)`. + Pattern, /// A line comment: `// ...`. LineComment, @@ -430,6 +432,7 @@ impl SyntaxKind { Self::LoopBreak => "`break` expression", Self::LoopContinue => "`continue` expression", Self::FuncReturn => "`return` expression", + Self::Pattern => "destructuring pattern", Self::LineComment => "line comment", Self::BlockComment => "block comment", Self::Error => "syntax error", diff --git a/src/syntax/parser.rs b/src/syntax/parser.rs index e95da4af7..83d9ae466 100644 --- a/src/syntax/parser.rs +++ b/src/syntax/parser.rs @@ -777,6 +777,11 @@ fn item(p: &mut Parser, keyed: bool) -> SyntaxKind { let m = p.marker(); if p.eat_if(SyntaxKind::Dots) { + if p.at(SyntaxKind::Comma) || p.at(SyntaxKind::RightParen) { + p.wrap(m, SyntaxKind::Spread); + return SyntaxKind::Spread; + } + code_expr(p); p.wrap(m, SyntaxKind::Spread); return SyntaxKind::Spread; @@ -833,22 +838,56 @@ fn args(p: &mut Parser) { p.wrap(m, SyntaxKind::Args); } +enum PatternKind { + Normal, + Destructuring, +} + +fn pattern(p: &mut Parser) -> PatternKind { + let m = p.marker(); + + if p.at(SyntaxKind::LeftParen) { + collection(p, false); + validate_destruct_pattern(p, m); + p.wrap(m, SyntaxKind::Pattern); + + PatternKind::Destructuring + } else { + let success = p.expect(SyntaxKind::Ident); + if p.at(SyntaxKind::Comma) { + // TODO: this should only be a warning instead + p.expected("keyword `in`. did you mean to use a destructuring pattern?"); + } + + if success { + p.wrap(m, SyntaxKind::Pattern); + } + + PatternKind::Normal + } +} + fn let_binding(p: &mut Parser) { let m = p.marker(); p.assert(SyntaxKind::Let); let m2 = p.marker(); - p.expect(SyntaxKind::Ident); - - let closure = p.directly_at(SyntaxKind::LeftParen); - if closure { - let m3 = p.marker(); - collection(p, false); - validate_params(p, m3); - p.wrap(m3, SyntaxKind::Params); + let mut closure = false; + let mut destructuring = false; + match pattern(p) { + PatternKind::Normal => { + closure = p.directly_at(SyntaxKind::LeftParen); + if closure { + let m3 = p.marker(); + collection(p, false); + validate_params(p, m3); + p.wrap(m3, SyntaxKind::Params); + } + } + PatternKind::Destructuring => destructuring = true, } - let f = if closure { Parser::expect } else { Parser::eat_if }; + let f = if closure || destructuring { Parser::expect } else { Parser::eat_if }; if f(p, SyntaxKind::Eq) { code_expr(p); } @@ -924,23 +963,13 @@ fn while_loop(p: &mut Parser) { fn for_loop(p: &mut Parser) { let m = p.marker(); p.assert(SyntaxKind::For); - for_pattern(p); + pattern(p); p.expect(SyntaxKind::In); code_expr(p); block(p); p.wrap(m, SyntaxKind::ForLoop); } -fn for_pattern(p: &mut Parser) { - let m = p.marker(); - if p.expect(SyntaxKind::Ident) { - if p.eat_if(SyntaxKind::Comma) { - p.expect(SyntaxKind::Ident); - } - p.wrap(m, SyntaxKind::ForPattern); - } -} - fn module_import(p: &mut Parser) { let m = p.marker(); p.assert(SyntaxKind::Import); @@ -1086,6 +1115,73 @@ fn validate_args(p: &mut Parser, m: Marker) { } } +fn validate_destruct_pattern(p: &mut Parser, m: Marker) { + let mut used_spread = false; + let mut used = HashSet::new(); + for child in p.post_process(m) { + match child.kind() { + SyntaxKind::Ident => { + if child.text() != "_" && !used.insert(child.text().clone()) { + child.convert_to_error( + "at most one binding per identifier is allowed", + ); + } + } + SyntaxKind::Spread => { + let Some(within) = child.children_mut().last_mut() else { continue }; + if used_spread { + child.convert_to_error("at most one destructuring sink is allowed"); + continue; + } + used_spread = true; + + if within.kind() == SyntaxKind::Dots { + continue; + } else if within.kind() != SyntaxKind::Ident { + within.convert_to_error(eco_format!( + "expected identifier, found {}", + within.kind().name(), + )); + child.make_erroneous(); + continue; + } + + if within.text() != "_" && !used.insert(within.text().clone()) { + within.convert_to_error( + "at most one binding per identifier is allowed", + ); + child.make_erroneous(); + } + } + SyntaxKind::Named => { + let Some(within) = child.children_mut().first_mut() else { return }; + if !used.insert(within.text().clone()) { + within.convert_to_error( + "at most one binding per identifier is allowed", + ); + child.make_erroneous(); + } + + let Some(within) = child.children_mut().last_mut() else { return }; + if within.kind() != SyntaxKind::Ident { + within.convert_to_error(eco_format!( + "expected identifier, found {}", + within.kind().name(), + )); + child.make_erroneous(); + } + } + SyntaxKind::LeftParen | SyntaxKind::RightParen | SyntaxKind::Comma => {} + kind => { + child.convert_to_error(eco_format!( + "expected identifier or destructuring sink, found {}", + kind.name() + )); + } + } + } +} + /// Manages parsing of a stream of tokens. struct Parser<'s> { text: &'s str, diff --git a/tests/typ/compiler/for.typ b/tests/typ/compiler/for.typ index 7833aad70..1c7807102 100644 --- a/tests/typ/compiler/for.typ +++ b/tests/typ/compiler/for.typ @@ -9,7 +9,7 @@ // Dictionary is not traversed in insertion order. // Should output `Age: 2. Name: Typst.`. -#for k, v in (Name: "Typst", Age: 2) [ +#for (k, v) in (Name: "Typst", Age: 2) [ #k: #v. ] @@ -47,22 +47,22 @@ } // Indices and values of array. -#for i, v in ("1", "2", "3") { +#for (i, v) in ("1", "2", "3").enumerate() { test(repr(i + 1), v) } -// Values of dictionary. +// Pairs of dictionary. #for v in (a: 4, b: 5) { out += (v,) } // Keys and values of dictionary. -#for k, v in (a: 6, b: 7) { +#for (k, v) in (a: 6, b: 7) { out += (k,) out += (v,) } -#test(out, (1, 2, 3, 4, 5, "a", 6, "b", 7)) +#test(out, (1, 2, 3, ("a", 4), ("b", 5), "a", 6, "b", 7)) // Grapheme clusters of string. #let first = true @@ -85,12 +85,19 @@ --- // Keys and values of strings. -// Error: 6-10 mismatched pattern -#for k, v in "hi" { +// Error: 6-12 cannot destructure values of string +#for (k, v) in "hi" { dont-care } --- +// Destructuring without parentheses. +// Error: 7 expected keyword `in`. did you mean to use a destructuring pattern? +// Error: 7 expected keyword `in` +#for k, v in (a: 4, b: 5) { + dont-care +} + // Error: 5 expected identifier #for diff --git a/tests/typ/compiler/let.typ b/tests/typ/compiler/let.typ index aa1132b91..4518f3d45 100644 --- a/tests/typ/compiler/let.typ +++ b/tests/typ/compiler/let.typ @@ -32,6 +32,156 @@ Three #test(v2, 2) #test(v3, 3) +--- +// Ref: false +// Simple destructuring. +#let (a, b) = (1, 2) +#test(a, 1) +#test(b, 2) + +--- +// Ref: false +// Destructuring with multiple placeholders. +#let (a, _, c, _) = (1, 2, 3, 4) +#test(a, 1) +#test(c, 3) + +--- +// Ref: false +// Destructuring with a sink. +#let (a, b, ..c) = (1, 2, 3, 4, 5, 6) +#test(a, 1) +#test(b, 2) +#test(c, (3, 4, 5, 6)) + +--- +// Ref: false +// Destructuring with a sink in the middle. +#let (a, ..b, c) = (1, 2, 3, 4, 5, 6) +#test(a, 1) +#test(b, (2, 3, 4, 5)) +#test(c, 6) + +--- +// Ref: false +// Destructuring with an empty sink. +#let (..a, b, c) = (1, 2) +#test(a, ()) +#test(b, 1) +#test(c, 2) + +--- +// Ref: false +// Destructuring with an empty sink. +#let (a, ..b, c) = (1, 2) +#test(a, 1) +#test(b, ()) +#test(c, 2) + +--- +// Ref: false +// Destructuring with an empty sink. +#let (a, b, ..c) = (1, 2) +#test(a, 1) +#test(b, 2) +#test(c, ()) + +--- +// Ref: false +// Destructuring with an empty sink and empty array. +#let (..a) = () +#test(a, ()) + +--- +// Ref: false +// Destructuring with unnamed sink. +#let (a, .., b) = (1, 2, 3, 4) +#test(a, 1) +#test(b, 4) + +// Error: 10-11 at most one binding per identifier is allowed +#let (a, a) = (1, 2) + +// Error: 12-15 at most one destructuring sink is allowed +#let (..a, ..a) = (1, 2) + +// Error: 12-13 at most one binding per identifier is allowed +#let (a, ..a) = (1, 2) + +// Error: 13-14 at most one binding per identifier is allowed +#let (a: a, a) = (a: 1, b: 2) + +--- +// Error: 13-14 not enough elements to destructure +#let (a, b, c) = (1, 2) + +--- +// Error: 6-9 too many elements to destructure +#let (a) = (1, 2) + +--- +// Error: 6-20 not enough elements to destructure +#let (..a, b, c, d) = (1, 2) + +--- +// Error: 6-12 cannot destructure boolean +#let (a, b) = true + +--- +// Ref: false +// Simple destructuring. +#let (a: a, b, x: c) = (a: 1, b: 2, x: 3) +#test(a, 1) +#test(b, 2) +#test(c, 3) + +--- +// Ref: false +// Destructuring with a sink. +#let (a: _, ..b) = (a: 1, b: 2, c: 3) +#test(b, (b: 2, c: 3)) + +--- +// Ref: false +// Destructuring with a sink in the middle. +#let (a: _, ..b, c: _) = (a: 1, b: 2, c: 3) +#test(b, (b: 2)) + +--- +// Ref: false +// Destructuring with an empty sink. +#let (a: _, ..b) = (a: 1) +#test(b, (:)) + +--- +// Ref: false +// Destructuring with an empty sink and empty dict. +#let (..a) = (:) +#test(a, (:)) + +--- +// Ref: false +// Destructuring with unnamed sink. +#let (a, ..) = (a: 1, b: 2) +#test(a, 1) + +--- +// Error: 10-13 expected identifier, found string +// Error: 18-19 expected identifier, found integer +#let (a: "a", b: 2) = (a: 1, b: 2) + +--- +// Error: 10-11 destructuring key not found in dictionary +#let (a, b) = (a: 1) + +--- +// Error: 13-14 destructuring key not found in dictionary +#let (a, b: b) = (a: 1) + +--- +// Error: 7-8 cannot destructure named elements from an array +#let (a: a, b) = (1, 2, 3) + --- // Error: 5 expected identifier #let @@ -62,6 +212,9 @@ Three // Error: 18 expected closing paren #let v5 = (1, 2 + ; Five +// Error: 9-13 expected identifier, found boolean +#let (..true) = false + --- // Error: 13 expected equals sign #let func(x) diff --git a/tests/typ/layout/stack-1.typ b/tests/typ/layout/stack-1.typ index 864a06dfb..8b5472e87 100644 --- a/tests/typ/layout/stack-1.typ +++ b/tests/typ/layout/stack-1.typ @@ -12,7 +12,7 @@ rect(width: w, height: 10pt, fill: rgb(v, v, v)) } -#let items = for i, w in widths { +#let items = for (i, w) in widths.enumerate() { (align(right, shaded(i, w)),) } diff --git a/tests/typ/visualize/shape-fill-stroke.typ b/tests/typ/visualize/shape-fill-stroke.typ index 24ace85b2..0d1331713 100644 --- a/tests/typ/visualize/shape-fill-stroke.typ +++ b/tests/typ/visualize/shape-fill-stroke.typ @@ -2,7 +2,7 @@ --- #let variant = rect.with(width: 20pt, height: 10pt) -#let items = for i, item in ( +#let items = for (i, item) in ( variant(stroke: none), variant(), variant(fill: none), @@ -15,7 +15,7 @@ variant(fill: forest, stroke: conifer), variant(fill: forest, stroke: black + 2pt), variant(fill: forest, stroke: conifer + 2pt), -) { +).enumerate() { (align(horizon)[#(i + 1).], item, []) }