diff --git a/crates/typst-syntax/src/ast.rs b/crates/typst-syntax/src/ast.rs index f411326f6..8da045e65 100644 --- a/crates/typst-syntax/src/ast.rs +++ b/crates/typst-syntax/src/ast.rs @@ -1270,11 +1270,8 @@ node! { impl<'a> Keyed<'a> { /// The key: `"spacy key"`. - pub fn key(self) -> Str<'a> { - self.0 - .children() - .find_map(|node| node.cast::()) - .unwrap_or_default() + pub fn key(self) -> Expr<'a> { + self.0.cast_first_match().unwrap_or_default() } /// The right-hand side of the pair: `true`. diff --git a/crates/typst-syntax/src/parser.rs b/crates/typst-syntax/src/parser.rs index 0cc733e6c..56fe3c9b7 100644 --- a/crates/typst-syntax/src/parser.rs +++ b/crates/typst-syntax/src/parser.rs @@ -943,19 +943,18 @@ fn item(p: &mut Parser, keyed: bool) -> SyntaxKind { let kind = match p.node(m).map(SyntaxNode::kind) { Some(SyntaxKind::Ident) => SyntaxKind::Named, - Some(SyntaxKind::Str) if keyed => SyntaxKind::Keyed, + Some(_) if keyed => SyntaxKind::Keyed, _ => { for child in p.post_process(m) { if child.kind() == SyntaxKind::Colon { break; } - let mut message = EcoString::from("expected identifier"); - if keyed { - message.push_str(" or string"); - } - message.push_str(", found "); - message.push_str(child.kind().name()); + let expected = if keyed { "expression" } else { "identifier" }; + let message = eco_format!( + "expected {expected}, found {found}", + found = child.kind().name(), + ); child.convert_to_error(message); } SyntaxKind::Named @@ -1235,9 +1234,12 @@ fn validate_dict<'a>(children: impl Iterator) { match child.kind() { SyntaxKind::Named | SyntaxKind::Keyed => { let Some(first) = child.children_mut().first_mut() else { continue }; - let key = match first.cast::() { - Some(str) => str.get(), - None => first.text().clone(), + let key = if let Some(str) = first.cast::() { + str.get() + } else if let Some(ident) = first.cast::() { + ident.get().clone() + } else { + continue; }; if !used.insert(key.clone()) { diff --git a/crates/typst/src/eval/mod.rs b/crates/typst/src/eval/mod.rs index 3dea512e1..ede21c528 100644 --- a/crates/typst/src/eval/mod.rs +++ b/crates/typst/src/eval/mod.rs @@ -1012,13 +1012,22 @@ impl Eval for ast::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) => { - map.insert(keyed.key().get().into(), keyed.expr().eval(vm)?); + let raw_key = keyed.key(); + let key = raw_key.eval(vm)?; + let key = key.cast::().unwrap_or_else(|error| { + let error = SourceDiagnostic::error(raw_key.span(), error); + invalid_keys.push(error); + Str::default() + }); + map.insert(key, keyed.expr().eval(vm)?); } ast::DictItem::Spread(expr) => match expr.eval(vm)? { Value::None => {} @@ -1028,6 +1037,10 @@ impl Eval for ast::Dict<'_> { } } + if !invalid_keys.is_empty() { + return Err(invalid_keys); + } + Ok(map.into()) } } diff --git a/tests/typ/compiler/dict.typ b/tests/typ/compiler/dict.typ index f3a70c2b6..138aa7d42 100644 --- a/tests/typ/compiler/dict.typ +++ b/tests/typ/compiler/dict.typ @@ -92,15 +92,12 @@ #(a: 1, b) // Identified as dictionary due to initial colon. +// The boolean key is allowed for now since it will only cause an error at the evaluation stage. // Error: 4-5 expected named or keyed pair, found integer // Error: 5 expected comma -// Error: 12-16 expected identifier or string, found boolean // Error: 17 expected expression #(:1 b:"", true:) -// Error: 3-8 expected identifier or string, found binary expression -#(a + b: "hey") - --- // Error: 3-15 cannot mutate a temporary value #((key: "val").other = "some") @@ -124,3 +121,24 @@ // Error: 8-15 type dictionary has no method `nonfunc` dict.nonfunc() } + +--- +#let a = "hello" +#let b = "world" +#let c = "value" +#let d = "conflict" + +#assert.eq(((a): b), ("hello": "world")) +#assert.eq(((a): 1, (a): 2), ("hello": 2)) +#assert.eq((hello: 1, (a): 2), ("hello": 2)) +#assert.eq((a + b: c, (a + b): d, (a): "value2", a: "value3"), ("helloworld": "conflict", "hello": "value2", "a": "value3")) + +--- +// Error: 7-10 expected identifier, found group +// Error: 12-14 expected identifier, found integer +#let ((a): 10) = "world" + +--- +// Error: 3-7 expected string, found boolean +// Error: 16-18 expected string, found integer +#(true: false, 42: 3)