summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--docs/src/reference/scripting.md55
-rw-r--r--library/src/compute/data.rs2
-rw-r--r--library/src/layout/terms.rs2
-rw-r--r--src/eval/array.rs11
-rw-r--r--src/eval/func.rs14
-rw-r--r--src/eval/methods.rs2
-rw-r--r--src/eval/mod.rs151
-rw-r--r--src/ide/complete.rs9
-rw-r--r--src/ide/highlight.rs1
-rw-r--r--src/syntax/ast.rs147
-rw-r--r--src/syntax/kind.rs3
-rw-r--r--src/syntax/parser.rs136
-rw-r--r--tests/typ/compiler/for.typ21
-rw-r--r--tests/typ/compiler/let.typ153
-rw-r--r--tests/typ/layout/stack-1.typ2
-rw-r--r--tests/typ/visualize/shape-fill-stroke.typ4
16 files changed, 587 insertions, 126 deletions
diff --git a/docs/src/reference/scripting.md b/docs/src/reference/scripting.md
index 8f72c219..ca56103c 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.
-
-- `{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}`.
+ Iterates over the items in the [array]($type/array). The destructuring syntax
+ described in [Let binding]($scripting/bindings) can also be used here.
+
+- `{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 1e0edebb..3b30efac 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 e51280f9..45ba82b5 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 8dcbd5a9..394191ea 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 e6402e87..93da6d7b 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 b88bca50..8b364fcb 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 e278d787..ca69b2d8 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<Value> {
+ 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 da9e0725..0c80c8df 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::<ast::LetBinding>() {
- 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::<ast::ForLoop>() {
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 e948975b..cf8fdd10 100644
--- a/src/ide/highlight.rs
+++ b/src/ide/highlight.rs
@@ -246,6 +246,7 @@ pub fn highlight(node: &LinkedNode) -> Option<Tag> {
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 780c6164..94114958 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<Ident> {
- self.0.children().next()?.cast()
+ match self.0.cast_first_match::<Pattern>()?.kind() {
+ PatternKind::Ident(ident) => Some(ident),
+ _ => Option::None,
+ }
}
/// The parameter bindings.
@@ -1590,28 +1593,121 @@ 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<DestructuringKind>),
+}
+
+/// 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<Ident>),
+ /// 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<Ident> {
+ 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<Ident> {
+ 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::<Pattern>() {
+ LetBindingKind::Normal(pattern)
+ } else {
+ LetBindingKind::Closure(
+ self.0
+ .cast_first_match::<Closure>()
+ .unwrap_or_default()
+ .name()
+ .unwrap_or_default(),
+ )
}
}
/// The expression the binding is initialized with.
pub fn init(&self) -> Option<Expr> {
- if self.0.cast_first_match::<Ident>().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()
}
@@ -1728,29 +1824,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<Ident> {
- 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 b0d934d1..fcde2bb4 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 e95da4af..83d9ae46 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 7833aad7..1c780710 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 aa1132b9..4518f3d4 100644
--- a/tests/typ/compiler/let.typ
+++ b/tests/typ/compiler/let.typ
@@ -33,6 +33,156 @@ Three
#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 864a06df..8b5472e8 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 24ace85b..0d133171 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, [])
}