summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--src/eval/dict.rs7
-rw-r--r--src/eval/mod.rs3
-rw-r--r--src/parse/mod.rs61
-rw-r--r--src/syntax/ast.rs37
-rw-r--r--src/syntax/highlight.rs1
-rw-r--r--src/syntax/mod.rs6
-rw-r--r--tests/ref/code/dict.pngbin1973 -> 4764 bytes
-rw-r--r--tests/typ/code/array.typ4
-rw-r--r--tests/typ/code/call.typ3
-rw-r--r--tests/typ/code/closure.typ12
-rw-r--r--tests/typ/code/dict.typ18
-rw-r--r--tests/typ/code/field.typ16
-rw-r--r--tests/typ/code/import.typ4
-rw-r--r--tests/typ/code/spread.typ2
14 files changed, 136 insertions, 38 deletions
diff --git a/src/eval/dict.rs b/src/eval/dict.rs
index 6b4dbbd7..22b73e76 100644
--- a/src/eval/dict.rs
+++ b/src/eval/dict.rs
@@ -5,6 +5,7 @@ use std::sync::Arc;
use super::{Args, Array, Func, Value};
use crate::diag::{StrResult, TypResult};
+use crate::parse::is_ident;
use crate::syntax::Spanned;
use crate::util::{ArcExt, EcoString};
use crate::Context;
@@ -127,7 +128,11 @@ impl Debug for Dict {
f.write_char(':')?;
}
for (i, (key, value)) in self.iter().enumerate() {
- f.write_str(key)?;
+ if is_ident(key) {
+ f.write_str(key)?;
+ } else {
+ write!(f, "{key:?}")?;
+ }
f.write_str(": ")?;
value.fmt(f)?;
if i + 1 < self.0.len() {
diff --git a/src/eval/mod.rs b/src/eval/mod.rs
index e9d62a69..677fa4aa 100644
--- a/src/eval/mod.rs
+++ b/src/eval/mod.rs
@@ -335,6 +335,9 @@ impl Eval for DictExpr {
DictItem::Named(named) => {
map.insert(named.name().take(), named.expr().eval(ctx, scp)?);
}
+ DictItem::Keyed(keyed) => {
+ map.insert(keyed.key(), keyed.expr().eval(ctx, scp)?);
+ }
DictItem::Spread(expr) => match expr.eval(ctx, scp)? {
Value::None => {}
Value::Dict(dict) => map.extend(dict.into_iter()),
diff --git a/src/parse/mod.rs b/src/parse/mod.rs
index 7811482b..8bf7b1c5 100644
--- a/src/parse/mod.rs
+++ b/src/parse/mod.rs
@@ -526,7 +526,7 @@ fn parenthesized(p: &mut Parser, atomic: bool) -> ParseResult {
p.start_group(Group::Paren);
let colon = p.eat_if(NodeKind::Colon);
- let kind = collection(p).0;
+ let kind = collection(p, true).0;
p.end_group();
// Leading colon makes this a dictionary.
@@ -568,14 +568,14 @@ enum CollectionKind {
///
/// Returns the length of the collection and whether the literal contained any
/// commas.
-fn collection(p: &mut Parser) -> (CollectionKind, usize) {
+fn collection(p: &mut Parser, keyed: bool) -> (CollectionKind, usize) {
let mut kind = None;
let mut items = 0;
let mut can_group = true;
let mut missing_coma: Option<Marker> = None;
while !p.eof() {
- if let Ok(item_kind) = item(p) {
+ if let Ok(item_kind) = item(p, keyed) {
match item_kind {
NodeKind::Spread => can_group = false,
NodeKind::Named if kind.is_none() => {
@@ -619,7 +619,7 @@ fn collection(p: &mut Parser) -> (CollectionKind, usize) {
/// Parse an expression or a named pair, returning whether it's a spread or a
/// named pair.
-fn item(p: &mut Parser) -> ParseResult<NodeKind> {
+fn item(p: &mut Parser, keyed: bool) -> ParseResult<NodeKind> {
let marker = p.marker();
if p.eat_if(NodeKind::Dots) {
marker.perform(p, NodeKind::Spread, expr)?;
@@ -629,18 +629,27 @@ fn item(p: &mut Parser) -> ParseResult<NodeKind> {
expr(p)?;
if p.at(NodeKind::Colon) {
- marker.perform(p, NodeKind::Named, |p| {
- if let Some(NodeKind::Ident(_)) = marker.after(p).map(|c| c.kind()) {
+ match marker.after(p).map(|c| c.kind()) {
+ Some(NodeKind::Ident(_)) => {
p.eat();
- expr(p)
- } else {
- let error = NodeKind::Error(ErrorPos::Full, "expected identifier".into());
+ marker.perform(p, NodeKind::Named, expr)?;
+ }
+ Some(NodeKind::Str(_)) if keyed => {
+ p.eat();
+ marker.perform(p, NodeKind::Keyed, expr)?;
+ }
+ _ => {
+ let mut msg = EcoString::from("expected identifier");
+ if keyed {
+ msg.push_str(" or string");
+ }
+ let error = NodeKind::Error(ErrorPos::Full, msg);
marker.end(p, error);
p.eat();
- expr(p).ok();
- Err(ParseError)
+ marker.perform(p, NodeKind::Named, expr).ok();
+ return Err(ParseError);
}
- })?;
+ }
Ok(NodeKind::Named)
} else {
@@ -653,6 +662,7 @@ fn item(p: &mut Parser) -> ParseResult<NodeKind> {
fn array(p: &mut Parser, marker: Marker) {
marker.filter_children(p, |x| match x.kind() {
NodeKind::Named => Err("expected expression, found named pair"),
+ NodeKind::Keyed => Err("expected expression, found keyed pair"),
_ => Ok(()),
});
marker.end(p, NodeKind::ArrayExpr);
@@ -664,18 +674,18 @@ fn dict(p: &mut Parser, marker: Marker) {
let mut used = HashSet::new();
marker.filter_children(p, |x| match x.kind() {
kind if kind.is_paren() => Ok(()),
- NodeKind::Named => {
- if let Some(NodeKind::Ident(ident)) =
+ NodeKind::Named | NodeKind::Keyed => {
+ if let Some(NodeKind::Ident(key) | NodeKind::Str(key)) =
x.children().first().map(|child| child.kind())
{
- if !used.insert(ident.clone()) {
+ if !used.insert(key.clone()) {
return Err("pair has duplicate key");
}
}
Ok(())
}
- NodeKind::Comma | NodeKind::Colon | NodeKind::Spread => Ok(()),
- _ => Err("expected named pair, found expression"),
+ NodeKind::Spread | NodeKind::Comma | NodeKind::Colon => Ok(()),
+ _ => Err("expected named or keyed pair, found expression"),
});
marker.end(p, NodeKind::DictExpr);
}
@@ -685,7 +695,7 @@ fn dict(p: &mut Parser, marker: Marker) {
fn params(p: &mut Parser, marker: Marker) {
marker.filter_children(p, |x| match x.kind() {
kind if kind.is_paren() => Ok(()),
- NodeKind::Named | NodeKind::Comma | NodeKind::Ident(_) => Ok(()),
+ NodeKind::Named | NodeKind::Ident(_) | NodeKind::Comma => Ok(()),
NodeKind::Spread
if matches!(
x.children().last().map(|child| child.kind()),
@@ -694,7 +704,7 @@ fn params(p: &mut Parser, marker: Marker) {
{
Ok(())
}
- _ => Err("expected identifier"),
+ _ => Err("expected identifier, named pair or argument sink"),
});
marker.end(p, NodeKind::ClosureParams);
}
@@ -746,12 +756,12 @@ fn args(p: &mut Parser, direct: bool, brackets: bool) -> ParseResult {
if p.at(NodeKind::LeftParen) {
let marker = p.marker();
p.start_group(Group::Paren);
- collection(p);
+ collection(p, false);
p.end_group();
let mut used = HashSet::new();
- marker.filter_children(p, |x| {
- if x.kind() == &NodeKind::Named {
+ marker.filter_children(p, |x| match x.kind() {
+ NodeKind::Named => {
if let Some(NodeKind::Ident(ident)) =
x.children().first().map(|child| child.kind())
{
@@ -759,8 +769,9 @@ fn args(p: &mut Parser, direct: bool, brackets: bool) -> ParseResult {
return Err("duplicate argument");
}
}
+ Ok(())
}
- Ok(())
+ _ => Ok(()),
});
}
@@ -785,7 +796,7 @@ fn let_expr(p: &mut Parser) -> ParseResult {
if has_params {
let marker = p.marker();
p.start_group(Group::Paren);
- collection(p);
+ collection(p, false);
p.end_group();
params(p, marker);
}
@@ -904,7 +915,7 @@ fn import_expr(p: &mut Parser) -> ParseResult {
p.perform(NodeKind::ImportItems, |p| {
p.start_group(Group::Imports);
let marker = p.marker();
- let items = collection(p).1;
+ let items = collection(p, false).1;
if items == 0 {
p.expected("import items");
}
diff --git a/src/syntax/ast.rs b/src/syntax/ast.rs
index fa00fe4b..1add2fd6 100644
--- a/src/syntax/ast.rs
+++ b/src/syntax/ast.rs
@@ -472,7 +472,7 @@ impl ArrayExpr {
/// An item in an array expresssion.
#[derive(Debug, Clone, PartialEq, Hash)]
pub enum ArrayItem {
- /// A simple value: `12`.
+ /// A bare value: `12`.
Pos(Expr),
/// A spreaded value: `..things`.
Spread(Expr),
@@ -509,8 +509,10 @@ impl DictExpr {
/// An item in an dictionary expresssion.
#[derive(Debug, Clone, PartialEq, Hash)]
pub enum DictItem {
- /// A simple named pair: `12`.
+ /// A named pair: `thickness: 3pt`.
Named(Named),
+ /// A keyed pair: `"spaced key": true`.
+ Keyed(Keyed),
/// A spreaded value: `..things`.
Spread(Expr),
}
@@ -519,6 +521,7 @@ impl TypedNode for DictItem {
fn from_red(node: RedRef) -> Option<Self> {
match node.kind() {
NodeKind::Named => node.cast().map(Self::Named),
+ NodeKind::Keyed => node.cast().map(Self::Keyed),
NodeKind::Spread => node.cast_first_child().map(Self::Spread),
_ => None,
}
@@ -527,29 +530,53 @@ impl TypedNode for DictItem {
fn as_red(&self) -> RedRef<'_> {
match self {
Self::Named(v) => v.as_red(),
+ Self::Keyed(v) => v.as_red(),
Self::Spread(v) => v.as_red(),
}
}
}
node! {
- /// A pair of a name and an expression: `pattern: dashed`.
+ /// A pair of a name and an expression: `thickness: 3pt`.
Named
}
impl Named {
- /// The name: `pattern`.
+ /// The name: `thickness`.
pub fn name(&self) -> Ident {
self.0.cast_first_child().expect("named pair is missing name")
}
- /// The right-hand side of the pair: `dashed`.
+ /// The right-hand side of the pair: `3pt`.
pub fn expr(&self) -> Expr {
self.0.cast_last_child().expect("named pair is missing expression")
}
}
node! {
+ /// A pair of a string key and an expression: `"spaced key": true`.
+ Keyed
+}
+
+impl Keyed {
+ /// The key: `"spaced key"`.
+ pub fn key(&self) -> EcoString {
+ self.0
+ .children()
+ .find_map(|node| match node.kind() {
+ NodeKind::Str(key) => Some(key.clone()),
+ _ => None,
+ })
+ .expect("keyed pair is missing key")
+ }
+
+ /// The right-hand side of the pair: `true`.
+ pub fn expr(&self) -> Expr {
+ self.0.cast_last_child().expect("keyed pair is missing expression")
+ }
+}
+
+node! {
/// A unary operation: `-x`.
UnaryExpr: UnaryExpr
}
diff --git a/src/syntax/highlight.rs b/src/syntax/highlight.rs
index dae379ac..06e88691 100644
--- a/src/syntax/highlight.rs
+++ b/src/syntax/highlight.rs
@@ -215,6 +215,7 @@ impl Category {
NodeKind::ArrayExpr => None,
NodeKind::DictExpr => None,
NodeKind::Named => None,
+ NodeKind::Keyed => None,
NodeKind::UnaryExpr => None,
NodeKind::BinaryExpr => None,
NodeKind::FieldAccess => None,
diff --git a/src/syntax/mod.rs b/src/syntax/mod.rs
index d21597ff..09d7265d 100644
--- a/src/syntax/mod.rs
+++ b/src/syntax/mod.rs
@@ -648,6 +648,8 @@ pub enum NodeKind {
DictExpr,
/// A named pair: `thickness: 3pt`.
Named,
+ /// A keyed pair: `"spaced key": true`.
+ Keyed,
/// A unary operation: `-x`.
UnaryExpr,
/// A binary operation: `a + b`.
@@ -896,7 +898,8 @@ impl NodeKind {
Self::GroupExpr => "group",
Self::ArrayExpr => "array",
Self::DictExpr => "dictionary",
- Self::Named => "named argument",
+ Self::Named => "named pair",
+ Self::Keyed => "keyed pair",
Self::UnaryExpr => "unary expression",
Self::BinaryExpr => "binary expression",
Self::FieldAccess => "field access",
@@ -1021,6 +1024,7 @@ impl Hash for NodeKind {
Self::ArrayExpr => {}
Self::DictExpr => {}
Self::Named => {}
+ Self::Keyed => {}
Self::UnaryExpr => {}
Self::BinaryExpr => {}
Self::FieldAccess => {}
diff --git a/tests/ref/code/dict.png b/tests/ref/code/dict.png
index 28e44999..d7ade71d 100644
--- a/tests/ref/code/dict.png
+++ b/tests/ref/code/dict.png
Binary files differ
diff --git a/tests/typ/code/array.typ b/tests/typ/code/array.typ
index 58b43ebf..1fa4d13c 100644
--- a/tests/typ/code/array.typ
+++ b/tests/typ/code/array.typ
@@ -92,3 +92,7 @@
// Named pair after this is already identified as an array.
// Error: 6-10 expected expression, found named pair
{(1, b: 2)}
+
+// Keyed pair after this is already identified as an array.
+// Error: 6-14 expected expression, found keyed pair
+{(1, "key": 2)}
diff --git a/tests/typ/code/call.typ b/tests/typ/code/call.typ
index 5b8b5b05..0285c8a2 100644
--- a/tests/typ/code/call.typ
+++ b/tests/typ/code/call.typ
@@ -86,6 +86,9 @@
// Error: 7-8 expected identifier
#func(1:2)
+// Error: 7-12 expected identifier
+#func("abc":2)
+
// Error: 7-10 expected identifier
{func((x):1)}
diff --git a/tests/typ/code/closure.typ b/tests/typ/code/closure.typ
index 29fca404..45f232ca 100644
--- a/tests/typ/code/closure.typ
+++ b/tests/typ/code/closure.typ
@@ -143,3 +143,15 @@
// Error: 23-35 unexpected argument
test(greet("Typst", whatever: 10))
}
+
+---
+// Error: 6-16 expected identifier, named pair or argument sink
+{(a, "named": b) => none}
+
+---
+// Error: 10-15 expected identifier
+#let foo("key": b) = key
+
+---
+// Error: 10-14 expected identifier
+#let foo(none: b) = key
diff --git a/tests/typ/code/dict.typ b/tests/typ/code/dict.typ
index 23af145f..00c78c17 100644
--- a/tests/typ/code/dict.typ
+++ b/tests/typ/code/dict.typ
@@ -7,8 +7,12 @@
// Empty
{(:)}
-// Two pairs.
-{(a1: 1, a2: 2)}
+// Two pairs and string key.
+#let dict = (normal: 1, "spaced key": 2)
+#dict
+
+#test(dict.normal, 1)
+#test(dict("spaced key"), 2)
---
// Test lvalue and rvalue access.
@@ -40,13 +44,17 @@
{(first: 1, second: 2, first: 3)}
---
+// Error: 17-23 pair has duplicate key
+{(a: 1, "b": 2, "a": 3)}
+
+---
// Simple expression after already being identified as a dictionary.
-// Error: 9-10 expected named pair, found expression
+// Error: 9-10 expected named or keyed pair, found expression
{(a: 1, b)}
// Identified as dictionary due to initial colon.
-// Error: 4-5 expected named pair, found expression
+// Error: 4-5 expected named or keyed pair, found expression
// Error: 5 expected comma
-// Error: 12-16 expected identifier
+// Error: 12-16 expected identifier or string
// Error: 17-18 expected expression, found colon
{(:1 b:"", true::)}
diff --git a/tests/typ/code/field.typ b/tests/typ/code/field.typ
index abccaede..b63a8768 100644
--- a/tests/typ/code/field.typ
+++ b/tests/typ/code/field.typ
@@ -2,6 +2,7 @@
// Ref: false
---
+// Test field on dictionary.
#let dict = (nothing: "ness", hello: "world")
#test(dict.nothing, "ness")
{
@@ -12,6 +13,16 @@
}
---
+// Test field on node.
+#show node: list as {
+ test(node.items.len(), 3)
+}
+
+- A
+- B
+- C
+
+---
// Error: 6-13 dictionary does not contain key "invalid"
{(:).invalid}
@@ -20,5 +31,10 @@
{false.ok}
---
+// Error: 29-32 unknown field "fun"
+#show node: heading as node.fun
+= A
+
+---
// Error: 8-12 expected identifier, found boolean
{false.true}
diff --git a/tests/typ/code/import.typ b/tests/typ/code/import.typ
index 683bb52a..312ee676 100644
--- a/tests/typ/code/import.typ
+++ b/tests/typ/code/import.typ
@@ -115,3 +115,7 @@ This is never reached.
// Should output `, a from "target.typ"`.
// Error: 10 expected keyword `from`
#import *, a from "target.typ"
+
+---
+// Error: 9-13 expected identifier
+#import a: 1 from ""
diff --git a/tests/typ/code/spread.typ b/tests/typ/code/spread.typ
index c5415c42..a41e04b9 100644
--- a/tests/typ/code/spread.typ
+++ b/tests/typ/code/spread.typ
@@ -62,7 +62,7 @@
#min(.."nope")
---
-// Error: 8-14 expected identifier
+// Error: 8-14 expected identifier, named pair or argument sink
#let f(..true) = none
---