summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorMatt Fellenz <matt@felle.nz>2023-11-02 09:08:08 -0700
committerGitHub <noreply@github.com>2023-11-02 17:08:08 +0100
commit8fd546760c7c425398f0114997c8085a481d8d2a (patch)
tree9d0a58cd50494145af9d4b322f6477fddd917959
parentb716700b61b229e2b703b892d3e047d84633d982 (diff)
Implement dict key interpolation (#2559)
-rw-r--r--crates/typst-syntax/src/ast.rs7
-rw-r--r--crates/typst-syntax/src/parser.rs22
-rw-r--r--crates/typst/src/eval/mod.rs15
-rw-r--r--tests/typ/compiler/dict.typ26
4 files changed, 50 insertions, 20 deletions
diff --git a/crates/typst-syntax/src/ast.rs b/crates/typst-syntax/src/ast.rs
index f411326f..8da045e6 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::<Str>())
- .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 0cc733e6..56fe3c9b 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<Item = &'a mut SyntaxNode>) {
match child.kind() {
SyntaxKind::Named | SyntaxKind::Keyed => {
let Some(first) = child.children_mut().first_mut() else { continue };
- let key = match first.cast::<ast::Str>() {
- Some(str) => str.get(),
- None => first.text().clone(),
+ let key = if let Some(str) = first.cast::<ast::Str>() {
+ str.get()
+ } else if let Some(ident) = first.cast::<ast::Ident>() {
+ 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 3dea512e..ede21c52 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<Self::Output> {
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::<Str>().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 f3a70c2b..138aa7d4 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)