From ffad8516af0b91121dc0761c8026e0a12939a7d4 Mon Sep 17 00:00:00 2001 From: Michael Lohr Date: Wed, 3 May 2023 12:34:35 +0200 Subject: [PATCH] Implement default values for at() (#995) --- docs/src/reference/types.md | 30 ++++++++++++++++++++---------- src/eval/array.rs | 22 +++++++++++++++++++--- src/eval/dict.rs | 22 ++++++++++++++++++---- src/eval/methods.rs | 14 ++++++++++---- src/eval/mod.rs | 14 +++++++------- src/eval/str.rs | 11 +++++++++-- src/eval/value.rs | 4 ++-- src/model/content.rs | 16 +++++++++++----- tests/typ/compiler/array.typ | 11 ++++++++--- tests/typ/compiler/dict.typ | 9 +++++++-- tests/typ/compiler/field.typ | 4 ++-- tests/typ/compiler/show-node.typ | 2 +- tests/typ/compiler/string.typ | 2 +- 13 files changed, 115 insertions(+), 46 deletions(-) diff --git a/docs/src/reference/types.md b/docs/src/reference/types.md index d62180291..cc30ef1f4 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 f6e2f2d4e..6ae5d7cfe 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 b137f03c7..1b28a6baa 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 6cbb846ba..8d042a5ca 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::("field")?)), - "at" => content.at(&args.expect::("field")?).at(span)?, + "at" => content.at(&args.expect::("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::("key")?).at(span)?.clone(), + "at" => dict + .at(&args.expect::("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 fe1fac3b8..b430b4003 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 89be36998..d7e00bf6a 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 { + pub fn at<'a>(&'a self, index: i64, default: Option<&'a str>) -> StrResult { 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 0548c01fd..1bfad9c87 100644 --- a/src/eval/value.rs +++ b/src/eval/value.rs @@ -124,8 +124,8 @@ impl Value { pub fn field(&self, field: &str) -> StrResult { 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 4af4e6550..1bd19f141 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 { - self.field(field).ok_or_else(|| missing_field(field)) + pub fn at(&self, field: &str, default: Option) -> StrResult { + 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 { } } -/// 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 5d7e8b63c..ef6e4b6ba 100644 --- a/tests/typ/compiler/array.typ +++ b/tests/typ/compiler/array.typ @@ -47,17 +47,22 @@ --- // 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 @@ -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 6c982ed44..fd95920b4 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 7529cd850..d1e4a31a0 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 99a4364eb..c69f46bd1 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 cba478f7b..0bc3a9be5 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) ---