Implement default values for at() (#995)

This commit is contained in:
Michael Lohr 2023-05-03 12:34:35 +02:00 committed by GitHub
parent ca8462642a
commit ffad8516af
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
13 changed files with 115 additions and 46 deletions

View File

@ -259,11 +259,14 @@ Fails with an error if the string is empty.
- returns: any - returns: any
### at() ### at()
Extract the first grapheme cluster after the specified index. Fails with an Extract the first grapheme cluster after the specified index. Returns the
error if the index is out of bounds. default value if the index is out of bounds or fails with an error if no default
value was specified.
- index: integer (positional, required) - index: integer (positional, required)
The byte index. The byte index.
- default: any (named)
A default value to return if the index is out of bounds.
- returns: string - returns: string
### slice() ### slice()
@ -450,10 +453,13 @@ Whether the content has the specified field.
- returns: boolean - returns: boolean
### at() ### 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) - field: string (positional, required)
The field to access. The field to access.
- default: any (named)
A default value to return if the field does not exist.
- returns: any - returns: any
### location() ### location()
@ -518,12 +524,14 @@ Fails with an error if the array is empty.
- returns: any - returns: any
### at() ### at()
Returns the item at the specified index in the array. Returns the item at the specified index in the array. May be used on the
May be used on the left-hand side of an assignment. left-hand side of an assignment. Returns the default value if the index is out
Fails with an error if the index is out of bounds. of bounds or fails with an error if no default value was specified.
- index: integer (positional, required) - index: integer (positional, required)
The index at which to retrieve the item. 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 - returns: any
### push() ### push()
@ -738,13 +746,15 @@ The number of pairs in the dictionary.
- returns: integer - returns: integer
### at() ### at()
Returns the value associated with the specified key in the dictionary. Returns the value associated with the specified key in the dictionary. May be
May be used on the left-hand side of an assignment if the key is already used on the left-hand side of an assignment if the key is already present in the
present in the dictionary. dictionary. Returns the default value if the key is not part of the dictionary
Fails with an error if the key is not part of the dictionary. or fails with an error if no default value was specified.
- key: string (positional, required) - key: string (positional, required)
The key at which to retrieve the item. 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 - returns: any
### insert() ### insert()

View File

@ -74,10 +74,15 @@ impl Array {
} }
/// Borrow the value at the given index. /// 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) self.locate(index)
.and_then(|i| self.0.get(i)) .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. /// Mutably borrow the value at the given index.
@ -85,7 +90,7 @@ impl Array {
let len = self.len(); let len = self.len();
self.locate(index) self.locate(index)
.and_then(move |i| self.0.make_mut().get_mut(i)) .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. /// 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 { fn out_of_bounds(index: i64, len: i64) -> EcoString {
eco_format!("array index out of bounds (index: {}, len: {})", index, len) 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
)
}

View File

@ -53,16 +53,20 @@ impl Dict {
self.0.len() as i64 self.0.len() as i64
} }
/// Borrow the value the given `key` maps to. /// Borrow the value the given `key` maps to,
pub fn at(&self, key: &str) -> StrResult<&Value> { pub fn at<'a>(
self.0.get(key).ok_or_else(|| missing_key(key)) &'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. /// Mutably borrow the value the given `key` maps to.
pub fn at_mut(&mut self, key: &str) -> StrResult<&mut Value> { pub fn at_mut(&mut self, key: &str) -> StrResult<&mut Value> {
Arc::make_mut(&mut self.0) Arc::make_mut(&mut self.0)
.get_mut(key) .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. /// 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 { fn missing_key(key: &str) -> EcoString {
eco_format!("dictionary does not contain key {:?}", Str::from(key)) 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)
)
}

View File

@ -30,7 +30,7 @@ pub fn call(
"len" => Value::Int(string.len()), "len" => Value::Int(string.len()),
"first" => Value::Str(string.first().at(span)?), "first" => Value::Str(string.first().at(span)?),
"last" => Value::Str(string.last().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" => { "slice" => {
let start = args.expect("start")?; let start = args.expect("start")?;
let mut end = args.eat()?; let mut end = args.eat()?;
@ -73,7 +73,7 @@ pub fn call(
Value::Content(content) => match method { Value::Content(content) => match method {
"func" => content.func().into(), "func" => content.func().into(),
"has" => Value::Bool(content.has(&args.expect::<EcoString>("field")?)), "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" => content
.location() .location()
.ok_or("this method can only be called on content returned by query(..)") .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()), "len" => Value::Int(array.len()),
"first" => array.first().at(span)?.clone(), "first" => array.first().at(span)?.clone(),
"last" => array.last().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" => { "slice" => {
let start = args.expect("start")?; let start = args.expect("start")?;
let mut end = args.eat()?; let mut end = args.eat()?;
@ -125,7 +128,10 @@ pub fn call(
Value::Dict(dict) => match method { Value::Dict(dict) => match method {
"len" => Value::Int(dict.len()), "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()), "keys" => Value::Array(dict.keys()),
"values" => Value::Array(dict.values()), "values" => Value::Array(dict.values()),
"pairs" => Value::Array(dict.pairs()), "pairs" => Value::Array(dict.pairs()),

View File

@ -1255,7 +1255,7 @@ impl ast::Pattern {
for p in destruct.bindings() { for p in destruct.bindings() {
match p { match p {
ast::DestructuringKind::Normal(expr) => { 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"); bail!(expr.span(), "not enough elements to destructure");
}; };
f(vm, expr, v.clone())?; f(vm, expr, v.clone())?;
@ -1310,7 +1310,7 @@ impl ast::Pattern {
for p in destruct.bindings() { for p in destruct.bindings() {
match p { match p {
ast::DestructuringKind::Normal(ast::Expr::Ident(ident)) => { ast::DestructuringKind::Normal(ast::Expr::Ident(ident)) => {
let Ok(v) = value.at(&ident) else { let Ok(v) = value.at(&ident, None) else {
bail!(ident.span(), "destructuring key not found in dictionary"); bail!(ident.span(), "destructuring key not found in dictionary");
}; };
f(vm, ast::Expr::Ident(ident.clone()), v.clone())?; f(vm, ast::Expr::Ident(ident.clone()), v.clone())?;
@ -1318,7 +1318,7 @@ impl ast::Pattern {
} }
ast::DestructuringKind::Sink(spread) => sink = spread.expr(), ast::DestructuringKind::Sink(spread) => sink = spread.expr(),
ast::DestructuringKind::Named(named) => { ast::DestructuringKind::Named(named) => {
let Ok(v) = value.at(named.name().as_str()) else { let Ok(v) = value.at(named.name().as_str(), None) else {
bail!(named.name().span(), "destructuring key not found in dictionary"); bail!(named.name().span(), "destructuring key not found in dictionary");
}; };
f(vm, named.expr(), v.clone())?; f(vm, named.expr(), v.clone())?;

View File

@ -69,12 +69,13 @@ impl Str {
} }
/// Extract the grapheme cluster at the given index. /// 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 len = self.len();
let grapheme = self.0[self.locate(index)?..] let grapheme = self.0[self.locate(index)?..]
.graphemes(true) .graphemes(true)
.next() .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()) 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) 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. /// The char boundary access error message.
#[cold] #[cold]
fn not_a_char_boundary(index: i64) -> EcoString { fn not_a_char_boundary(index: i64) -> EcoString {

View File

@ -124,8 +124,8 @@ impl Value {
pub fn field(&self, field: &str) -> StrResult<Value> { pub fn field(&self, field: &str) -> StrResult<Value> {
match self { match self {
Self::Symbol(symbol) => symbol.clone().modified(field).map(Self::Symbol), Self::Symbol(symbol) => symbol.clone().modified(field).map(Self::Symbol),
Self::Dict(dict) => dict.at(field).cloned(), Self::Dict(dict) => dict.at(field, None).cloned(),
Self::Content(content) => content.at(field), Self::Content(content) => content.at(field, None),
Self::Module(module) => module.get(field).cloned(), Self::Module(module) => module.get(field).cloned(),
v => Err(eco_format!("cannot access fields on type {}", v.type_name())), v => Err(eco_format!("cannot access fields on type {}", v.type_name())),
} }

View File

@ -252,8 +252,10 @@ impl Content {
} }
/// Borrow the value of the given field. /// Borrow the value of the given field.
pub fn at(&self, field: &str) -> StrResult<Value> { pub fn at(&self, field: &str, default: Option<Value>) -> StrResult<Value> {
self.field(field).ok_or_else(|| missing_field(field)) self.field(field)
.or(default)
.ok_or_else(|| missing_field_no_default(field))
} }
/// The content's label. /// 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] #[cold]
fn missing_field(key: &str) -> EcoString { fn missing_field_no_default(key: &str) -> EcoString {
eco_format!("content does not contain field {:?}", Str::from(key)) eco_format!(
"content does not contain field {:?} and \
no default value was specified",
Str::from(key)
)
} }

View File

@ -47,17 +47,22 @@
--- ---
// Test rvalue out of bounds. // 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) #(1, 2, 3).at(5)
--- ---
// Test lvalue out of bounds. // Test lvalue out of bounds.
#{ #{
let array = (1, 2, 3) 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 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. // Test bad lvalue.
// Error: 2:3-2:14 cannot mutate a temporary value // Error: 2:3-2:14 cannot mutate a temporary value
@ -243,7 +248,7 @@
#([Hi], [There]).sorted() #([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) #(1, 2, 3).at(-4)
--- ---

View File

@ -31,15 +31,20 @@
// Test rvalue missing key. // Test rvalue missing key.
#{ #{
let dict = (a: 1, b: 2) 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") 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. // Missing lvalue is not automatically none-initialized.
#{ #{
let dict = (:) 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 dict.b += 1
} }

View File

@ -23,7 +23,7 @@
- C - 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 #(:).invalid
--- ---
@ -31,7 +31,7 @@
#false.ok #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 #show heading: it => it.fun
= A = A

View File

@ -78,7 +78,7 @@ Another text.
= Heading = 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 #show heading: it => it.page
= Heading = Heading

View File

@ -31,7 +31,7 @@
#"🏳️‍🌈".at(2) #"🏳️‍🌈".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) #"Hello".at(5)
--- ---