summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorMichael Lohr <michael@lohr.dev>2023-05-03 12:34:35 +0200
committerGitHub <noreply@github.com>2023-05-03 12:34:35 +0200
commitffad8516af0b91121dc0761c8026e0a12939a7d4 (patch)
treeada5c6b5510b5f509997ccf5308a9cafc8618990
parentca8462642a96ec282afeb0774b6d5daf546ac236 (diff)
Implement default values for at() (#995)
-rw-r--r--docs/src/reference/types.md30
-rw-r--r--src/eval/array.rs22
-rw-r--r--src/eval/dict.rs22
-rw-r--r--src/eval/methods.rs14
-rw-r--r--src/eval/mod.rs14
-rw-r--r--src/eval/str.rs11
-rw-r--r--src/eval/value.rs4
-rw-r--r--src/model/content.rs16
-rw-r--r--tests/typ/compiler/array.typ11
-rw-r--r--tests/typ/compiler/dict.typ9
-rw-r--r--tests/typ/compiler/field.typ4
-rw-r--r--tests/typ/compiler/show-node.typ2
-rw-r--r--tests/typ/compiler/string.typ2
13 files changed, 115 insertions, 46 deletions
diff --git a/docs/src/reference/types.md b/docs/src/reference/types.md
index d6218029..cc30ef1f 100644
--- a/docs/src/reference/types.md
+++ b/docs/src/reference/types.md
@@ -259,11 +259,14 @@ Fails with an error if the string is empty.
- returns: any
### at()
-Extract the first grapheme cluster after the specified index. Fails with an
-error if the index is out of bounds.
+Extract the first grapheme cluster after the specified index. Returns the
+default value if the index is out of bounds or fails with an error if no default
+value was specified.
- index: integer (positional, required)
The byte index.
+- default: any (named)
+ A default value to return if the index is out of bounds.
- returns: string
### slice()
@@ -450,10 +453,13 @@ Whether the content has the specified field.
- returns: boolean
### at()
-Access the specified field on the content.
+Access the specified field on the content. Returns the default value if the
+field does not exist or fails with an error if no default value was specified.
- field: string (positional, required)
The field to access.
+- default: any (named)
+ A default value to return if the field does not exist.
- returns: any
### location()
@@ -518,12 +524,14 @@ Fails with an error if the array is empty.
- returns: any
### at()
-Returns the item at the specified index in the array.
-May be used on the left-hand side of an assignment.
-Fails with an error if the index is out of bounds.
+Returns the item at the specified index in the array. May be used on the
+left-hand side of an assignment. Returns the default value if the index is out
+of bounds or fails with an error if no default value was specified.
- index: integer (positional, required)
The index at which to retrieve the item.
+- default: any (named)
+ A default value to return if the index is out of bounds.
- returns: any
### push()
@@ -738,13 +746,15 @@ The number of pairs in the dictionary.
- returns: integer
### at()
-Returns the value associated with the specified key in the dictionary.
-May be used on the left-hand side of an assignment if the key is already
-present in the dictionary.
-Fails with an error if the key is not part of the dictionary.
+Returns the value associated with the specified key in the dictionary. May be
+used on the left-hand side of an assignment if the key is already present in the
+dictionary. Returns the default value if the key is not part of the dictionary
+or fails with an error if no default value was specified.
- key: string (positional, required)
The key at which to retrieve the item.
+- default: any (named)
+ A default value to return if the key is not part of the dictionary.
- returns: any
### insert()
diff --git a/src/eval/array.rs b/src/eval/array.rs
index f6e2f2d4..6ae5d7cf 100644
--- a/src/eval/array.rs
+++ b/src/eval/array.rs
@@ -74,10 +74,15 @@ impl Array {
}
/// Borrow the value at the given index.
- pub fn at(&self, index: i64) -> StrResult<&Value> {
+ pub fn at<'a>(
+ &'a self,
+ index: i64,
+ default: Option<&'a Value>,
+ ) -> StrResult<&'a Value> {
self.locate(index)
.and_then(|i| self.0.get(i))
- .ok_or_else(|| out_of_bounds(index, self.len()))
+ .or(default)
+ .ok_or_else(|| out_of_bounds_no_default(index, self.len()))
}
/// Mutably borrow the value at the given index.
@@ -85,7 +90,7 @@ impl Array {
let len = self.len();
self.locate(index)
.and_then(move |i| self.0.make_mut().get_mut(i))
- .ok_or_else(|| out_of_bounds(index, len))
+ .ok_or_else(|| out_of_bounds_no_default(index, len))
}
/// Push a value to the end of the array.
@@ -462,3 +467,14 @@ fn array_is_empty() -> EcoString {
fn out_of_bounds(index: i64, len: i64) -> EcoString {
eco_format!("array index out of bounds (index: {}, len: {})", index, len)
}
+
+/// The out of bounds access error message when no default value was given.
+#[cold]
+fn out_of_bounds_no_default(index: i64, len: i64) -> EcoString {
+ eco_format!(
+ "array index out of bounds (index: {}, len: {}) \
+ and no default value was specified",
+ index,
+ len
+ )
+}
diff --git a/src/eval/dict.rs b/src/eval/dict.rs
index b137f03c..1b28a6ba 100644
--- a/src/eval/dict.rs
+++ b/src/eval/dict.rs
@@ -53,16 +53,20 @@ impl Dict {
self.0.len() as i64
}
- /// Borrow the value the given `key` maps to.
- pub fn at(&self, key: &str) -> StrResult<&Value> {
- self.0.get(key).ok_or_else(|| missing_key(key))
+ /// Borrow the value the given `key` maps to,
+ pub fn at<'a>(
+ &'a self,
+ key: &str,
+ default: Option<&'a Value>,
+ ) -> StrResult<&'a Value> {
+ self.0.get(key).or(default).ok_or_else(|| missing_key_no_default(key))
}
/// Mutably borrow the value the given `key` maps to.
pub fn at_mut(&mut self, key: &str) -> StrResult<&mut Value> {
Arc::make_mut(&mut self.0)
.get_mut(key)
- .ok_or_else(|| missing_key(key))
+ .ok_or_else(|| missing_key_no_default(key))
}
/// Remove the value if the dictionary contains the given key.
@@ -218,3 +222,13 @@ impl<'a> IntoIterator for &'a Dict {
fn missing_key(key: &str) -> EcoString {
eco_format!("dictionary does not contain key {:?}", Str::from(key))
}
+
+/// The missing key access error message when no default was fiven.
+#[cold]
+fn missing_key_no_default(key: &str) -> EcoString {
+ eco_format!(
+ "dictionary does not contain key {:?} \
+ and no default value was specified",
+ Str::from(key)
+ )
+}
diff --git a/src/eval/methods.rs b/src/eval/methods.rs
index 6cbb846b..8d042a5c 100644
--- a/src/eval/methods.rs
+++ b/src/eval/methods.rs
@@ -30,7 +30,7 @@ pub fn call(
"len" => Value::Int(string.len()),
"first" => Value::Str(string.first().at(span)?),
"last" => Value::Str(string.last().at(span)?),
- "at" => Value::Str(string.at(args.expect("index")?).at(span)?),
+ "at" => Value::Str(string.at(args.expect("index")?, None).at(span)?),
"slice" => {
let start = args.expect("start")?;
let mut end = args.eat()?;
@@ -73,7 +73,7 @@ pub fn call(
Value::Content(content) => match method {
"func" => content.func().into(),
"has" => Value::Bool(content.has(&args.expect::<EcoString>("field")?)),
- "at" => content.at(&args.expect::<EcoString>("field")?).at(span)?,
+ "at" => content.at(&args.expect::<EcoString>("field")?, None).at(span)?,
"location" => content
.location()
.ok_or("this method can only be called on content returned by query(..)")
@@ -86,7 +86,10 @@ pub fn call(
"len" => Value::Int(array.len()),
"first" => array.first().at(span)?.clone(),
"last" => array.last().at(span)?.clone(),
- "at" => array.at(args.expect("index")?).at(span)?.clone(),
+ "at" => array
+ .at(args.expect("index")?, args.named("default")?.as_ref())
+ .at(span)?
+ .clone(),
"slice" => {
let start = args.expect("start")?;
let mut end = args.eat()?;
@@ -125,7 +128,10 @@ pub fn call(
Value::Dict(dict) => match method {
"len" => Value::Int(dict.len()),
- "at" => dict.at(&args.expect::<Str>("key")?).at(span)?.clone(),
+ "at" => dict
+ .at(&args.expect::<Str>("key")?, args.named("default")?.as_ref())
+ .at(span)?
+ .clone(),
"keys" => Value::Array(dict.keys()),
"values" => Value::Array(dict.values()),
"pairs" => Value::Array(dict.pairs()),
diff --git a/src/eval/mod.rs b/src/eval/mod.rs
index fe1fac3b..b430b400 100644
--- a/src/eval/mod.rs
+++ b/src/eval/mod.rs
@@ -1255,7 +1255,7 @@ impl ast::Pattern {
for p in destruct.bindings() {
match p {
ast::DestructuringKind::Normal(expr) => {
- let Ok(v) = value.at(i) else {
+ let Ok(v) = value.at(i, None) else {
bail!(expr.span(), "not enough elements to destructure");
};
f(vm, expr, v.clone())?;
@@ -1310,17 +1310,17 @@ impl ast::Pattern {
for p in destruct.bindings() {
match p {
ast::DestructuringKind::Normal(ast::Expr::Ident(ident)) => {
- let Ok(v) = value.at(&ident) else {
- bail!(ident.span(), "destructuring key not found in dictionary");
- };
+ let Ok(v) = value.at(&ident, None) else {
+ bail!(ident.span(), "destructuring key not found in dictionary");
+ };
f(vm, ast::Expr::Ident(ident.clone()), v.clone())?;
used.insert(ident.take());
}
ast::DestructuringKind::Sink(spread) => sink = spread.expr(),
ast::DestructuringKind::Named(named) => {
- let Ok(v) = value.at(named.name().as_str()) else {
- bail!(named.name().span(), "destructuring key not found in dictionary");
- };
+ let Ok(v) = value.at(named.name().as_str(), None) else {
+ bail!(named.name().span(), "destructuring key not found in dictionary");
+ };
f(vm, named.expr(), v.clone())?;
used.insert(named.name().take());
}
diff --git a/src/eval/str.rs b/src/eval/str.rs
index 89be3699..d7e00bf6 100644
--- a/src/eval/str.rs
+++ b/src/eval/str.rs
@@ -69,12 +69,13 @@ impl Str {
}
/// Extract the grapheme cluster at the given index.
- pub fn at(&self, index: i64) -> StrResult<Self> {
+ pub fn at<'a>(&'a self, index: i64, default: Option<&'a str>) -> StrResult<Self> {
let len = self.len();
let grapheme = self.0[self.locate(index)?..]
.graphemes(true)
.next()
- .ok_or_else(|| out_of_bounds(index, len))?;
+ .or(default)
+ .ok_or_else(|| no_default_and_out_of_bounds(index, len))?;
Ok(grapheme.into())
}
@@ -348,6 +349,12 @@ fn out_of_bounds(index: i64, len: i64) -> EcoString {
eco_format!("string index out of bounds (index: {}, len: {})", index, len)
}
+/// The out of bounds access error message when no default value was given.
+#[cold]
+fn no_default_and_out_of_bounds(index: i64, len: i64) -> EcoString {
+ eco_format!("no default value was specified and string index out of bounds (index: {}, len: {})", index, len)
+}
+
/// The char boundary access error message.
#[cold]
fn not_a_char_boundary(index: i64) -> EcoString {
diff --git a/src/eval/value.rs b/src/eval/value.rs
index 0548c01f..1bfad9c8 100644
--- a/src/eval/value.rs
+++ b/src/eval/value.rs
@@ -124,8 +124,8 @@ impl Value {
pub fn field(&self, field: &str) -> StrResult<Value> {
match self {
Self::Symbol(symbol) => symbol.clone().modified(field).map(Self::Symbol),
- Self::Dict(dict) => dict.at(field).cloned(),
- Self::Content(content) => content.at(field),
+ Self::Dict(dict) => dict.at(field, None).cloned(),
+ Self::Content(content) => content.at(field, None),
Self::Module(module) => module.get(field).cloned(),
v => Err(eco_format!("cannot access fields on type {}", v.type_name())),
}
diff --git a/src/model/content.rs b/src/model/content.rs
index 4af4e655..1bd19f14 100644
--- a/src/model/content.rs
+++ b/src/model/content.rs
@@ -252,8 +252,10 @@ impl Content {
}
/// Borrow the value of the given field.
- pub fn at(&self, field: &str) -> StrResult<Value> {
- self.field(field).ok_or_else(|| missing_field(field))
+ pub fn at(&self, field: &str, default: Option<Value>) -> StrResult<Value> {
+ self.field(field)
+ .or(default)
+ .ok_or_else(|| missing_field_no_default(field))
}
/// The content's label.
@@ -582,8 +584,12 @@ impl Fold for Vec<Meta> {
}
}
-/// The missing key access error message.
+/// The missing key access error message when no default value was given.
#[cold]
-fn missing_field(key: &str) -> EcoString {
- eco_format!("content does not contain field {:?}", Str::from(key))
+fn missing_field_no_default(key: &str) -> EcoString {
+ eco_format!(
+ "content does not contain field {:?} and \
+ no default value was specified",
+ Str::from(key)
+ )
}
diff --git a/tests/typ/compiler/array.typ b/tests/typ/compiler/array.typ
index 5d7e8b63..ef6e4b6b 100644
--- a/tests/typ/compiler/array.typ
+++ b/tests/typ/compiler/array.typ
@@ -47,18 +47,23 @@
---
// Test rvalue out of bounds.
-// Error: 2-17 array index out of bounds (index: 5, len: 3)
+// Error: 2-17 array index out of bounds (index: 5, len: 3) and no default value was specified
#(1, 2, 3).at(5)
---
// Test lvalue out of bounds.
#{
let array = (1, 2, 3)
- // Error: 3-14 array index out of bounds (index: 3, len: 3)
+ // Error: 3-14 array index out of bounds (index: 3, len: 3) and no default value was specified
array.at(3) = 5
}
---
+// Test default value.
+#test((1, 2, 3).at(2, default: 5), 3)
+#test((1, 2, 3).at(3, default: 5), 5)
+
+---
// Test bad lvalue.
// Error: 2:3-2:14 cannot mutate a temporary value
#let array = (1, 2, 3)
@@ -243,7 +248,7 @@
#([Hi], [There]).sorted()
---
-// Error: 2-18 array index out of bounds (index: -4, len: 3)
+// Error: 2-18 array index out of bounds (index: -4, len: 3) and no default value was specified
#(1, 2, 3).at(-4)
---
diff --git a/tests/typ/compiler/dict.typ b/tests/typ/compiler/dict.typ
index 6c982ed4..fd95920b 100644
--- a/tests/typ/compiler/dict.typ
+++ b/tests/typ/compiler/dict.typ
@@ -31,15 +31,20 @@
// Test rvalue missing key.
#{
let dict = (a: 1, b: 2)
- // Error: 11-23 dictionary does not contain key "c"
+ // Error: 11-23 dictionary does not contain key "c" and no default value was specified
let x = dict.at("c")
}
---
+// Test default value.
+#test((a: 1, b: 2).at("b", default: 3), 2)
+#test((a: 1, b: 2).at("c", default: 3), 3)
+
+---
// Missing lvalue is not automatically none-initialized.
#{
let dict = (:)
- // Error: 3-9 dictionary does not contain key "b"
+ // Error: 3-9 dictionary does not contain key "b" and no default value was specified
dict.b += 1
}
diff --git a/tests/typ/compiler/field.typ b/tests/typ/compiler/field.typ
index 7529cd85..d1e4a31a 100644
--- a/tests/typ/compiler/field.typ
+++ b/tests/typ/compiler/field.typ
@@ -23,7 +23,7 @@
- C
---
-// Error: 6-13 dictionary does not contain key "invalid"
+// Error: 6-13 dictionary does not contain key "invalid" and no default value was specified
#(:).invalid
---
@@ -31,7 +31,7 @@
#false.ok
---
-// Error: 25-28 content does not contain field "fun"
+// Error: 25-28 content does not contain field "fun" and no default value was specified
#show heading: it => it.fun
= A
diff --git a/tests/typ/compiler/show-node.typ b/tests/typ/compiler/show-node.typ
index 99a4364e..c69f46bd 100644
--- a/tests/typ/compiler/show-node.typ
+++ b/tests/typ/compiler/show-node.typ
@@ -78,7 +78,7 @@ Another text.
= Heading
---
-// Error: 25-29 content does not contain field "page"
+// Error: 25-29 content does not contain field "page" and no default value was specified
#show heading: it => it.page
= Heading
diff --git a/tests/typ/compiler/string.typ b/tests/typ/compiler/string.typ
index cba478f7..0bc3a9be 100644
--- a/tests/typ/compiler/string.typ
+++ b/tests/typ/compiler/string.typ
@@ -31,7 +31,7 @@
#"🏳️‍🌈".at(2)
---
-// Error: 2-15 string index out of bounds (index: 5, len: 5)
+// Error: 2-15 no default value was specified and string index out of bounds (index: 5, len: 5)
#"Hello".at(5)
---