diff --git a/src/model/args.rs b/src/model/args.rs index fe5f82540..4aaaded4f 100644 --- a/src/model/args.rs +++ b/src/model/args.rs @@ -165,36 +165,6 @@ impl Args { .filter_map(|item| item.name.clone().map(|name| (name, item.value.v.clone()))) .collect() } - - /// Reinterpret these arguments as actually being an array index. - pub fn into_index(self) -> SourceResult { - self.into_castable("index") - } - - /// Reinterpret these arguments as actually being a dictionary key. - pub fn into_key(self) -> SourceResult { - self.into_castable("key") - } - - /// Reinterpret these arguments as actually being a single castable thing. - fn into_castable(self, what: &str) -> SourceResult { - let mut iter = self.items.into_iter(); - let value = match iter.next() { - Some(Arg { name: None, value, .. }) => value.v.cast().at(value.span)?, - None => { - bail!(self.span, "missing {}", what); - } - Some(Arg { name: Some(_), span, .. }) => { - bail!(span, "named pair is not allowed here"); - } - }; - - if let Some(arg) = iter.next() { - bail!(arg.span, "only one {} is allowed", what); - } - - Ok(value) - } } impl Debug for Args { diff --git a/src/model/array.rs b/src/model/array.rs index 02607547a..fb740a13c 100644 --- a/src/model/array.rs +++ b/src/model/array.rs @@ -4,7 +4,7 @@ use std::ops::{Add, AddAssign}; use std::sync::Arc; use super::{ops, Args, Func, Value, Vm}; -use crate::diag::{At, SourceResult, StrResult}; +use crate::diag::{bail, At, SourceResult, StrResult}; use crate::syntax::Spanned; use crate::util::{format_eco, ArcExt, EcoString}; @@ -45,24 +45,34 @@ impl Array { } /// The first value in the array. - pub fn first(&self) -> Option<&Value> { - self.0.first() + pub fn first(&self) -> StrResult<&Value> { + self.0.first().ok_or_else(array_is_empty) + } + + /// Mutably borrow the first value in the array. + pub fn first_mut(&mut self) -> StrResult<&mut Value> { + Arc::make_mut(&mut self.0).first_mut().ok_or_else(array_is_empty) } /// The last value in the array. - pub fn last(&self) -> Option<&Value> { - self.0.last() + pub fn last(&self) -> StrResult<&Value> { + self.0.last().ok_or_else(array_is_empty) + } + + /// Mutably borrow the last value in the array. + pub fn last_mut(&mut self) -> StrResult<&mut Value> { + Arc::make_mut(&mut self.0).last_mut().ok_or_else(array_is_empty) } /// Borrow the value at the given index. - pub fn get(&self, index: i64) -> StrResult<&Value> { + pub fn at(&self, index: i64) -> StrResult<&Value> { self.locate(index) .and_then(|i| self.0.get(i)) .ok_or_else(|| out_of_bounds(index, self.len())) } /// Mutably borrow the value at the given index. - pub fn get_mut(&mut self, index: i64) -> StrResult<&mut Value> { + pub fn at_mut(&mut self, index: i64) -> StrResult<&mut Value> { let len = self.len(); self.locate(index) .and_then(move |i| Arc::make_mut(&mut self.0).get_mut(i)) @@ -128,6 +138,9 @@ impl Array { /// Return the first matching element. pub fn find(&self, vm: &Vm, f: Spanned) -> SourceResult> { + if f.v.argc().map_or(false, |count| count != 1) { + bail!(f.span, "function must have exactly one parameter"); + } for item in self.iter() { let args = Args::new(f.span, [item.clone()]); if f.v.call(vm, args)?.cast::().at(f.span)? { @@ -140,6 +153,9 @@ impl Array { /// Return the index of the first matching element. pub fn position(&self, vm: &Vm, f: Spanned) -> SourceResult> { + if f.v.argc().map_or(false, |count| count != 1) { + bail!(f.span, "function must have exactly one parameter"); + } for (i, item) in self.iter().enumerate() { let args = Args::new(f.span, [item.clone()]); if f.v.call(vm, args)?.cast::().at(f.span)? { @@ -153,6 +169,9 @@ impl Array { /// Return a new array with only those elements for which the function /// returns true. pub fn filter(&self, vm: &Vm, f: Spanned) -> SourceResult { + if f.v.argc().map_or(false, |count| count != 1) { + bail!(f.span, "function must have exactly one parameter"); + } let mut kept = vec![]; for item in self.iter() { let args = Args::new(f.span, [item.clone()]); @@ -165,6 +184,9 @@ impl Array { /// Transform each item in the array with a function. pub fn map(&self, vm: &Vm, f: Spanned) -> SourceResult { + if f.v.argc().map_or(false, |count| count < 1 || count > 2) { + bail!(f.span, "function must have one or two parameters"); + } let enumerate = f.v.argc() == Some(2); self.iter() .enumerate() @@ -179,8 +201,24 @@ impl Array { .collect() } + /// Fold all of the array's elements into one with a function. + pub fn fold(&self, vm: &Vm, init: Value, f: Spanned) -> SourceResult { + if f.v.argc().map_or(false, |count| count != 2) { + bail!(f.span, "function must have exactly two parameters"); + } + let mut acc = init; + for item in self.iter() { + let args = Args::new(f.span, [acc, item.clone()]); + acc = f.v.call(vm, args)?; + } + Ok(acc) + } + /// Whether any element matches. pub fn any(&self, vm: &Vm, f: Spanned) -> SourceResult { + if f.v.argc().map_or(false, |count| count != 1) { + bail!(f.span, "function must have exactly one parameter"); + } for item in self.iter() { let args = Args::new(f.span, [item.clone()]); if f.v.call(vm, args)?.cast::().at(f.span)? { @@ -193,6 +231,9 @@ impl Array { /// Whether all elements match. pub fn all(&self, vm: &Vm, f: Spanned) -> SourceResult { + if f.v.argc().map_or(false, |count| count != 1) { + bail!(f.span, "function must have exactly one parameter"); + } for item in self.iter() { let args = Args::new(f.span, [item.clone()]); if !f.v.call(vm, args)?.cast::().at(f.span)? { diff --git a/src/model/dict.rs b/src/model/dict.rs index e3c5454e6..83c16824f 100644 --- a/src/model/dict.rs +++ b/src/model/dict.rs @@ -4,7 +4,7 @@ use std::ops::{Add, AddAssign}; use std::sync::Arc; use super::{Args, Array, Func, Str, Value, Vm}; -use crate::diag::{SourceResult, StrResult}; +use crate::diag::{bail, SourceResult, StrResult}; use crate::syntax::is_ident; use crate::syntax::Spanned; use crate::util::{format_eco, ArcExt, EcoString}; @@ -50,7 +50,7 @@ impl Dict { } /// Borrow the value the given `key` maps to. - pub fn get(&self, key: &str) -> StrResult<&Value> { + pub fn at(&self, key: &str) -> StrResult<&Value> { self.0.get(key).ok_or_else(|| missing_key(key)) } @@ -58,7 +58,7 @@ impl Dict { /// /// This inserts the key with [`None`](Value::None) as the value if not /// present so far. - pub fn get_mut(&mut self, key: Str) -> &mut Value { + pub fn at_mut(&mut self, key: Str) -> &mut Value { Arc::make_mut(&mut self.0).entry(key).or_default() } @@ -108,6 +108,9 @@ impl Dict { /// Transform each pair in the dictionary with a function. pub fn map(&self, vm: &Vm, f: Spanned) -> SourceResult { + if f.v.argc().map_or(false, |count| count != 1) { + bail!(f.span, "function must have exactly two parameters"); + } self.iter() .map(|(key, value)| { let args = Args::new(f.span, [Value::Str(key.clone()), value.clone()]); diff --git a/src/model/eval.rs b/src/model/eval.rs index 54007e76c..ab89f9c23 100644 --- a/src/model/eval.rs +++ b/src/model/eval.rs @@ -780,7 +780,7 @@ impl Eval for ast::FieldAccess { let field = self.field().take(); Ok(match object { - Value::Dict(dict) => dict.get(&field).at(span)?.clone(), + Value::Dict(dict) => dict.at(&field).at(span)?.clone(), Value::Content(content) => content .field(&field) .ok_or_else(|| format!("unknown field {field:?}")) @@ -798,22 +798,11 @@ impl Eval for ast::FuncCall { bail!(self.span(), "maximum function call depth exceeded"); } - let callee = self.callee().eval(vm)?; + let callee = self.callee(); + let callee = callee.eval(vm)?.cast::().at(callee.span())?; let args = self.args().eval(vm)?; - - Ok(match callee { - Value::Array(array) => array.get(args.into_index()?).at(self.span())?.clone(), - Value::Dict(dict) => dict.get(&args.into_key()?).at(self.span())?.clone(), - Value::Func(func) => { - let point = || Tracepoint::Call(func.name().map(Into::into)); - func.call(vm, args).trace(vm.world, point, self.span())? - } - v => bail!( - self.callee().span(), - "expected callable or collection, found {}", - v.type_name(), - ), - }) + let point = || Tracepoint::Call(callee.name().map(Into::into)); + callee.call(vm, args).trace(vm.world, point, self.span()) } } @@ -1246,9 +1235,13 @@ impl Access for ast::Expr { fn access<'a>(&self, vm: &'a mut Vm) -> SourceResult<&'a mut Value> { match self { Self::Ident(v) => v.access(vm), + Self::Parenthesized(v) => v.access(vm), Self::FieldAccess(v) => v.access(vm), - Self::FuncCall(v) => v.access(vm), - _ => bail!(self.span(), "cannot mutate a temporary value"), + Self::MethodCall(v) => v.access(vm), + _ => { + let _ = self.eval(vm)?; + bail!(self.span(), "cannot mutate a temporary value"); + } } } } @@ -1259,10 +1252,16 @@ impl Access for ast::Ident { } } +impl Access for ast::Parenthesized { + fn access<'a>(&self, vm: &'a mut Vm) -> SourceResult<&'a mut Value> { + self.expr().access(vm) + } +} + impl Access for ast::FieldAccess { fn access<'a>(&self, vm: &'a mut Vm) -> SourceResult<&'a mut Value> { Ok(match self.target().access(vm)? { - Value::Dict(dict) => dict.get_mut(self.field().take().into()), + Value::Dict(dict) => dict.at_mut(self.field().take().into()), v => bail!( self.target().span(), "expected dictionary, found {}", @@ -1272,17 +1271,17 @@ impl Access for ast::FieldAccess { } } -impl Access for ast::FuncCall { +impl Access for ast::MethodCall { fn access<'a>(&self, vm: &'a mut Vm) -> SourceResult<&'a mut Value> { + let span = self.span(); + let method = self.method(); let args = self.args().eval(vm)?; - Ok(match self.callee().access(vm)? { - Value::Array(array) => array.get_mut(args.into_index()?).at(self.span())?, - Value::Dict(dict) => dict.get_mut(args.into_key()?), - v => bail!( - self.callee().span(), - "expected collection, found {}", - v.type_name(), - ), - }) + if methods::is_accessor(&method) { + let value = self.target().access(vm)?; + methods::call_access(value, &method, args, span) + } else { + let _ = self.eval(vm)?; + bail!(span, "cannot mutate a temporary value"); + } } } diff --git a/src/model/methods.rs b/src/model/methods.rs index dac36be25..8155685c6 100644 --- a/src/model/methods.rs +++ b/src/model/methods.rs @@ -1,6 +1,6 @@ //! Methods on values. -use super::{Args, Value, Vm}; +use super::{Args, Str, Value, Vm}; use crate::diag::{At, SourceResult}; use crate::syntax::Span; use crate::util::EcoString; @@ -26,6 +26,9 @@ pub fn call( Value::Str(string) => match method { "len" => Value::Int(string.len() as i64), + "first" => Value::Str(string.first().at(span)?), + "last" => Value::Str(string.last().at(span)?), + "at" => Value::Str(string.at(args.expect("index")?).at(span)?), "slice" => { let start = args.expect("start")?; let mut end = args.eat()?; @@ -65,8 +68,9 @@ pub fn call( Value::Array(array) => match method { "len" => Value::Int(array.len()), - "first" => array.first().cloned().unwrap_or(Value::None), - "last" => array.last().cloned().unwrap_or(Value::None), + "first" => array.first().at(span)?.clone(), + "last" => array.last().at(span)?.clone(), + "at" => array.at(args.expect("index")?).at(span)?.clone(), "slice" => { let start = args.expect("start")?; let mut end = args.eat()?; @@ -82,6 +86,9 @@ pub fn call( .map_or(Value::None, Value::Int), "filter" => Value::Array(array.filter(vm, args.expect("function")?)?), "map" => Value::Array(array.map(vm, args.expect("function")?)?), + "fold" => { + array.fold(vm, args.expect("initial value")?, args.expect("function")?)? + } "any" => Value::Bool(array.any(vm, args.expect("function")?)?), "all" => Value::Bool(array.all(vm, args.expect("function")?)?), "flatten" => Value::Array(array.flatten()), @@ -97,6 +104,7 @@ pub fn call( Value::Dict(dict) => match method { "len" => Value::Int(dict.len()), + "at" => dict.at(&args.expect::("key")?).cloned().at(span)?, "keys" => Value::Array(dict.keys()), "values" => Value::Array(dict.values()), "pairs" => Value::Array(dict.map(vm, args.expect("function")?)?), @@ -158,11 +166,44 @@ pub fn call_mut( Ok(output) } +/// Call an accessor method on a value. +pub fn call_access<'a>( + value: &'a mut Value, + method: &str, + mut args: Args, + span: Span, +) -> SourceResult<&'a mut Value> { + let name = value.type_name(); + let missing = || Err(missing_method(name, method)).at(span); + + let slot = match value { + Value::Array(array) => match method { + "first" => array.first_mut().at(span)?, + "last" => array.last_mut().at(span)?, + "at" => array.at_mut(args.expect("index")?).at(span)?, + _ => return missing(), + }, + Value::Dict(dict) => match method { + "at" => dict.at_mut(args.expect("index")?), + _ => return missing(), + }, + _ => return missing(), + }; + + args.finish()?; + Ok(slot) +} + /// Whether a specific method is mutating. pub fn is_mutating(method: &str) -> bool { matches!(method, "push" | "pop" | "insert" | "remove") } +/// Whether a specific method is an accessor. +pub fn is_accessor(method: &str) -> bool { + matches!(method, "first" | "last" | "at") +} + /// The missing method error message. #[cold] fn missing_method(type_name: &str, method: &str) -> String { diff --git a/src/model/str.rs b/src/model/str.rs index d1bf9d232..9196a35a1 100644 --- a/src/model/str.rs +++ b/src/model/str.rs @@ -42,16 +42,40 @@ impl Str { self } - /// The codepoints the string consists of. - pub fn codepoints(&self) -> Array { - self.as_str().chars().map(|c| Value::Str(c.into())).collect() - } - /// The grapheme clusters the string consists of. pub fn graphemes(&self) -> Array { self.as_str().graphemes(true).map(|s| Value::Str(s.into())).collect() } + /// Extract the first grapheme cluster. + pub fn first(&self) -> StrResult { + self.0 + .graphemes(true) + .next() + .map(Into::into) + .ok_or_else(string_is_empty) + } + + /// Extract the last grapheme cluster. + pub fn last(&self) -> StrResult { + self.0 + .graphemes(true) + .next_back() + .map(Into::into) + .ok_or_else(string_is_empty) + } + + /// Extract the grapheme cluster at the given index. + pub fn at(&self, index: i64) -> StrResult { + let len = self.len(); + let grapheme = self + .locate(index) + .filter(|&index| index <= self.0.len()) + .and_then(|index| self.0[index..].graphemes(true).next()) + .ok_or_else(|| out_of_bounds(index, len))?; + Ok(grapheme.into()) + } + /// Extract a contigous substring. pub fn slice(&self, start: i64, end: Option) -> StrResult { let len = self.len(); @@ -270,6 +294,12 @@ fn out_of_bounds(index: i64, len: i64) -> String { format!("string index out of bounds (index: {}, len: {})", index, len) } +/// The error message when the string is empty. +#[cold] +fn string_is_empty() -> EcoString { + "string is empty".into() +} + /// Convert an item of std's `match_indices` to a dictionary. fn match_to_dict((start, text): (usize, &str)) -> Dict { dict! { diff --git a/tests/ref/compiler/array.png b/tests/ref/compiler/array.png index cbda8aee7..d41e2cbbe 100644 Binary files a/tests/ref/compiler/array.png and b/tests/ref/compiler/array.png differ diff --git a/tests/ref/compiler/methods-color.png b/tests/ref/compiler/color.png similarity index 100% rename from tests/ref/compiler/methods-color.png rename to tests/ref/compiler/color.png diff --git a/tests/ref/compiler/methods-collection.png b/tests/ref/compiler/methods-collection.png deleted file mode 100644 index e93e2beb1..000000000 Binary files a/tests/ref/compiler/methods-collection.png and /dev/null differ diff --git a/tests/typ/basics/enum.typ b/tests/typ/basics/enum.typ index e651ca31b..c1ce17b72 100644 --- a/tests/typ/basics/enum.typ +++ b/tests/typ/basics/enum.typ @@ -39,11 +39,14 @@ --- // Test numbering with closure. #enum( - start: 4, - spacing: 0.65em - 3pt, - tight: false, - numbering: n => text(fill: (red, green, blue)(mod(n, 3)), numbering("A", n)), - [Red], [Green], [Blue], + start: 4, + spacing: 0.65em - 3pt, + tight: false, + numbering: n => text( + fill: (red, green, blue).at(mod(n, 3)), + numbering("A", n), + ), + [Red], [Green], [Blue], ) --- diff --git a/tests/typ/basics/terms.typ b/tests/typ/basics/terms.typ index ba23f9095..204defbff 100644 --- a/tests/typ/basics/terms.typ +++ b/tests/typ/basics/terms.typ @@ -40,7 +40,7 @@ No: list \ #show terms: it => table( columns: 2, inset: 3pt, - ..it.items.map(item => (emph(item(0)), item(1))).flatten(), + ..it.items.map(item => (emph(item.at(0)), item.at(1))).flatten(), ) / A: One letter diff --git a/tests/typ/compiler/array.typ b/tests/typ/compiler/array.typ index cb8433cba..ccde8598e 100644 --- a/tests/typ/compiler/array.typ +++ b/tests/typ/compiler/array.typ @@ -23,51 +23,187 @@ , rgb("002") ,)} +--- +// Test the `len` method. +#test(().len(), 0) +#test(("A", "B", "C").len(), 3) + --- // Test lvalue and rvalue access. { let array = (1, 2) - array(1) += 5 + array(0) + array.at(1) += 5 + array.at(0) test(array, (1, 8)) } +--- +// Test different lvalue method. +{ + let array = (1, 2, 3) + array.first() = 7 + array.at(1) *= 8 + test(array, (7, 16, 3)) +} + --- // Test rvalue out of bounds. -// Error: 2-14 array index out of bounds (index: 5, len: 3) -{(1, 2, 3)(5)} +// Error: 2-17 array index out of bounds (index: 5, len: 3) +{(1, 2, 3).at(5)} --- // Test lvalue out of bounds. { let array = (1, 2, 3) - // Error: 3-11 array index out of bounds (index: 3, len: 3) - array(3) = 5 + // Error: 3-14 array index out of bounds (index: 3, len: 3) + array.at(3) = 5 } +--- +// Test bad lvalue. +// Error: 2:3-2:14 cannot mutate a temporary value +#let array = (1, 2, 3) +{ array.len() = 4 } + +--- +// Test bad lvalue. +// Error: 2:3-2:15 type array has no method `yolo` +#let array = (1, 2, 3) +{ array.yolo() = 4 } + --- // Test negative indices. { let array = (1, 2, 3, 4) - test(array(0), 1) - test(array(-1), 4) - test(array(-2), 3) - test(array(-3), 2) - test(array(-4), 1) + test(array.at(0), 1) + test(array.at(-1), 4) + test(array.at(-2), 3) + test(array.at(-3), 2) + test(array.at(-4), 1) } --- -// Error: 2-15 array index out of bounds (index: -4, len: 3) -{(1, 2, 3)(-4)} +// The the `first` and `last` methods. +#test((1,).first(), 1) +#test((2,).last(), 2) +#test((1, 2, 3).first(), 1) +#test((1, 2, 3).last(), 3) --- -// Test non-collection indexing. +// Error: 3-13 array is empty +{ ().first() } +--- +// Error: 3-12 array is empty +{ ().last() } + +--- +// Test the `push` and `pop` methods. { - let x = 10pt - // Error: 3-4 expected collection, found length - x() = 1 + let tasks = (a: (1, 2, 3), b: (4, 5, 6)) + tasks.at("a").pop() + tasks.b.push(7) + test(tasks.a, (1, 2)) + test(tasks.at("b"), (4, 5, 6, 7)) } +--- +// Test the `insert` and `remove` methods. +{ + let array = (0, 1, 2, 4, 5) + array.insert(3, 3) + test(array, range(6)) + array.remove(1) + test(array, (0, 2, 3, 4, 5)) +} + +--- +// Error: 2:17-2:19 missing argument: index +#let numbers = () +{ numbers.insert() } +--- +// Test the `slice` method. +#test((1, 2, 3, 4).slice(2), (3, 4)) +#test(range(10).slice(2, 6), (2, 3, 4, 5)) +#test(range(10).slice(4, count: 3), (4, 5, 6)) +#test(range(10).slice(-5, count: 2), (5, 6)) +#test((1, 2, 3).slice(2, -2), ()) +#test((1, 2, 3).slice(-2, 2), (2,)) +#test((1, 2, 3).slice(-3, 2), (1, 2)) +#test("ABCD".split("").slice(1, -1).join("-"), "A-B-C-D") + +--- +// Error: 3-31 array index out of bounds (index: 12, len: 10) +{ range(10).slice(9, count: 3) } + +--- +// Error: 3-25 array index out of bounds (index: -4, len: 3) +{ (1, 2, 3).slice(0, -4) } + +--- +// Test the `position` method. +#test(("Hi", "❤️", "Love").position(s => s == "❤️"), 1) +#test(("Bye", "💘", "Apart").position(s => s == "❤️"), none) +#test(("A", "B", "CDEF", "G").position(v => v.len() > 2), 2) + +--- +// Test the `filter` method. +#test(().filter(even), ()) +#test((1, 2, 3, 4).filter(even), (2, 4)) +#test((7, 3, 2, 5, 1).filter(x => x < 5), (3, 2, 1)) + +--- +// Test the `map` method. +#test(().map(x => x * 2), ()) +#test((2, 3).map(x => x * 2), (4, 6)) + +--- +// Test the `fold` method. +#test(().fold("hi", grid), "hi") +#test((1, 2, 3, 4).fold(0, (s, x) => s + x), 10) + +--- +// Error: 21-31 function must have exactly two parameters +{ (1, 2, 3).fold(0, () => none) } + +--- +// Test the `rev` method. +#test(range(3).rev(), (2, 1, 0)) + +--- +// Test the `join` method. +#test(().join(), none) +#test((1,).join(), 1) +#test(("a", "b", "c").join(), "abc") +#test("(" + ("a", "b", "c").join(", ") + ")", "(a, b, c)") + +--- +// Error: 2-22 cannot join boolean with boolean +{(true, false).join()} + +--- +// Error: 2-20 cannot join string with integer +{("a", "b").join(1)} + +--- +// Test joining content. +// Ref: true +{([One], [Two], [Three]).join([, ], last: [ and ])}. + +--- +// Test the `sorted` method. +#test(().sorted(), ()) +#test(((true, false) * 10).sorted(), (false,) * 10 + (true,) * 10) +#test(("it", "the", "hi", "text").sorted(), ("hi", "it", "text", "the")) +#test((2, 1, 3, 10, 5, 8, 6, -7, 2).sorted(), (-7, 1, 2, 2, 3, 5, 6, 8, 10)) + +--- +// Error: 2-26 cannot order content and content +{([Hi], [There]).sorted()} + +--- +// Error: 2-18 array index out of bounds (index: -4, len: 3) +{(1, 2, 3).at(-4)} + --- // Error: 3 expected closing paren {(} diff --git a/tests/typ/compiler/call.typ b/tests/typ/compiler/call.typ index dc582c9c2..7ea0a998f 100644 --- a/tests/typ/compiler/call.typ +++ b/tests/typ/compiler/call.typ @@ -48,25 +48,25 @@ #set text(family: "Arial", family: "Helvetica") --- -// Error: 2-6 expected callable or collection, found boolean +// Error: 2-6 expected function, found boolean {true()} --- #let x = "x" -// Error: 1-3 expected callable or collection, found string +// Error: 1-3 expected function, found string #x() --- #let f(x) = x -// Error: 1-6 expected callable or collection, found integer +// Error: 1-6 expected function, found integer #f(1)(2) --- #let f(x) = x -// Error: 1-6 expected callable or collection, found content +// Error: 1-6 expected function, found content #f[1](2) --- diff --git a/tests/typ/compiler/methods-color.typ b/tests/typ/compiler/color.typ similarity index 100% rename from tests/typ/compiler/methods-color.typ rename to tests/typ/compiler/color.typ diff --git a/tests/typ/compiler/dict.typ b/tests/typ/compiler/dict.typ index d791f77be..0170cb8b3 100644 --- a/tests/typ/compiler/dict.typ +++ b/tests/typ/compiler/dict.typ @@ -12,17 +12,17 @@ #dict #test(dict.normal, 1) -#test(dict("spacy key"), 2) +#test(dict.at("spacy key"), 2) --- // Test lvalue and rvalue access. { let dict = (a: 1, "b b": 1) - dict("b b") += 1 + dict.at("b b") += 1 dict.state = (ok: true, err: false) test(dict, (a: 1, "b b": 2, state: (ok: true, err: false))) test(dict.state.ok, true) - dict("state").ok = false + dict.at("state").ok = false test(dict.state.ok, false) test(dict.state.err, false) } @@ -31,18 +31,30 @@ // Test rvalue missing key. { let dict = (a: 1, b: 2) - // Error: 11-20 dictionary does not contain key "c" - let x = dict("c") + // Error: 11-23 dictionary does not contain key "c" + let x = dict.at("c") } --- // Missing lvalue is automatically none-initialized. { let dict = (:) - dict("b") += 1 + dict.at("b") += 1 test(dict, (b: 1)) } +--- +// Test dictionary methods. +#let dict = (a: 3, c: 2, b: 1) +#test("c" in dict, true) +#test(dict.len(), 3) +#test(dict.values(), (3, 1, 2)) +#test(dict.pairs((k, v) => k + str(v)).join(), "a3b1c2") + +{ dict.remove("c") } +#test("c" in dict, false) +#test(dict, (a: 3, b: 1)) + --- // Error: 24-32 pair has duplicate key {(first: 1, second: 2, first: 3)} @@ -65,7 +77,7 @@ --- // Error: 3-15 cannot mutate a temporary value -{ (key: value).other = "some" } +{ (key: "val").other = "some" } --- { diff --git a/tests/typ/compiler/methods-collection.typ b/tests/typ/compiler/methods-collection.typ deleted file mode 100644 index fcebf6400..000000000 --- a/tests/typ/compiler/methods-collection.typ +++ /dev/null @@ -1,115 +0,0 @@ -// Test the collection methods. -// Ref: false - ---- -// Test the `len` method. -#test(().len(), 0) -#test(("A", "B", "C").len(), 3) -#test("Hello World!".len(), 12) -#test((a: 1, b: 2).len(), 2) - ---- -// The the `first` and `last` methods. -#test(().first(), none) -#test(().last(), none) -#test((1,).first(), 1) -#test((2,).last(), 2) -#test((1, 2, 3).first(), 1) -#test((1, 2, 3).last(), 3) - ---- -// Test the `push` and `pop` methods. -{ - let tasks = (a: (1, 2, 3), b: (4, 5, 6)) - tasks("a").pop() - tasks("b").push(7) - test(tasks("a"), (1, 2)) - test(tasks("b"), (4, 5, 6, 7)) -} - ---- -// Test the `insert` and `remove` methods. -{ - let array = (0, 1, 2, 4, 5) - array.insert(3, 3) - test(array, range(6)) - array.remove(1) - test(array, (0, 2, 3, 4, 5)) -} - ---- -// Error: 2:17-2:19 missing argument: index -#let numbers = () -{ numbers.insert() } - ---- -// Test the `slice` method. -#test((1, 2, 3, 4).slice(2), (3, 4)) -#test(range(10).slice(2, 6), (2, 3, 4, 5)) -#test(range(10).slice(4, count: 3), (4, 5, 6)) -#test(range(10).slice(-5, count: 2), (5, 6)) -#test((1, 2, 3).slice(2, -2), ()) -#test((1, 2, 3).slice(-2, 2), (2,)) -#test((1, 2, 3).slice(-3, 2), (1, 2)) -#test("ABCD".split("").slice(1, -1).join("-"), "A-B-C-D") - ---- -// Error: 3-31 array index out of bounds (index: 12, len: 10) -{ range(10).slice(9, count: 3) } - ---- -// Error: 3-25 array index out of bounds (index: -4, len: 3) -{ (1, 2, 3).slice(0, -4) } - ---- -// Test the `position` method. -#test(("Hi", "❤️", "Love").position(s => s == "❤️"), 1) -#test(("Bye", "💘", "Apart").position(s => s == "❤️"), none) -#test(("A", "B", "CDEF", "G").position(v => v.len() > 2), 2) - ---- -// Test the `rev` method. -#test(range(3).rev(), (2, 1, 0)) - ---- -// Test the `join` method. -#test(().join(), none) -#test((1,).join(), 1) -#test(("a", "b", "c").join(), "abc") -#test("(" + ("a", "b", "c").join(", ") + ")", "(a, b, c)") - ---- -// Error: 2-22 cannot join boolean with boolean -{(true, false).join()} - ---- -// Error: 2-20 cannot join string with integer -{("a", "b").join(1)} - ---- -// Test joining content. -// Ref: true -{([One], [Two], [Three]).join([, ], last: [ and ])}. - ---- -// Test the `sorted` method. -#test(().sorted(), ()) -#test(((true, false) * 10).sorted(), (false,) * 10 + (true,) * 10) -#test(("it", "the", "hi", "text").sorted(), ("hi", "it", "text", "the")) -#test((2, 1, 3, 10, 5, 8, 6, -7, 2).sorted(), (-7, 1, 2, 2, 3, 5, 6, 8, 10)) - ---- -// Error: 2-26 cannot order content and content -{([Hi], [There]).sorted()} - ---- -// Test dictionary methods. -#let dict = (a: 3, c: 2, b: 1) -#test("c" in dict, true) -#test(dict.len(), 3) -#test(dict.values(), (3, 1, 2)) -#test(dict.pairs((k, v) => k + str(v)).join(), "a3b1c2") - -{ dict.remove("c") } -#test("c" in dict, false) -#test(dict, (a: 3, b: 1)) diff --git a/tests/typ/compiler/methods.typ b/tests/typ/compiler/methods.typ index 07f6e4106..f468320b9 100644 --- a/tests/typ/compiler/methods.typ +++ b/tests/typ/compiler/methods.typ @@ -9,7 +9,7 @@ // Test mutating indexed value. { let matrix = (((1,), (2,)), ((3,), (4,))) - matrix(1)(0).push(5) + matrix.at(1).at(0).push(5) test(matrix, (((1,), (2,)), ((3, 5), (4,)))) } diff --git a/tests/typ/compiler/ops-invalid.typ b/tests/typ/compiler/ops-invalid.typ index 3d41a3d1f..d51e42fb5 100644 --- a/tests/typ/compiler/ops-invalid.typ +++ b/tests/typ/compiler/ops-invalid.typ @@ -90,20 +90,29 @@ { let x = 2 for _ in range(61) { - x *= 2 + (x) *= 2 } // Error: 4-18 cannot repeat this string 4611686018427387904 times {x * "abcdefgh"} } --- -// Error: 3-6 cannot mutate a temporary value +// Error: 4-5 unknown variable { (x) = "" } --- // Error: 3-8 cannot mutate a temporary value { 1 + 2 += 3 } +--- +// Error: 2:2-2:7 cannot apply 'not' to string +#let x = "Hey" +{not x = "a"} + +--- +// Error: 7-8 unknown variable +{ 1 + x += 3 } + --- // Error: 3-4 unknown variable { z = 1 } diff --git a/tests/typ/compiler/ops-prec.typ b/tests/typ/compiler/ops-prec.typ index 23afcc5f8..eba0c8a97 100644 --- a/tests/typ/compiler/ops-prec.typ +++ b/tests/typ/compiler/ops-prec.typ @@ -12,8 +12,10 @@ #test("a" == "a" and 2 < 3, true) #test(not "b" == "b", false) +--- // Assignment binds stronger than boolean operations. -// Error: 2-7 cannot mutate a temporary value +// Error: 2:2-2:7 cannot mutate a temporary value +#let x = false {not x = "a"} --- diff --git a/tests/typ/compiler/set.typ b/tests/typ/compiler/set.typ index 7414ad5e1..034bfab8a 100644 --- a/tests/typ/compiler/set.typ +++ b/tests/typ/compiler/set.typ @@ -40,7 +40,7 @@ Hello *{x}* // Test relative path resolving in layout phase. #let choice = ("monkey.svg", "rhino.png", "tiger.jpg") #set enum(numbering: n => { - let path = "../../res/" + choice(n - 1) + let path = "../../res/" + choice.at(n - 1) move(dy: -0.15em, image(path, width: 1em, height: 1em)) }) diff --git a/tests/typ/compiler/methods-str.typ b/tests/typ/compiler/string.typ similarity index 84% rename from tests/typ/compiler/methods-str.typ rename to tests/typ/compiler/string.typ index aead4aa4b..8ac515a51 100644 --- a/tests/typ/compiler/methods-str.typ +++ b/tests/typ/compiler/string.typ @@ -1,6 +1,35 @@ // Test the string methods. // Ref: false +--- +// Test the `len` method. +#test("Hello World!".len(), 12) + +--- +// Test the `first` and `last` methods. +#test("Hello".first(), "H") +#test("Hello".last(), "o") +#test("🏳️‍🌈A🏳️‍⚧️".first(), "🏳️‍🌈") +#test("🏳️‍🌈A🏳️‍⚧️".last(), "🏳️‍⚧️") + +--- +// Error: 3-13 string is empty +{ "".first() } + +--- +// Error: 3-12 string is empty +{ "".last() } + +--- +// Test the `at` method. +#test("Hello".at(1), "e") +#test("Hello".at(4), "o") +#test("Hey: 🏳️‍🌈 there!".at(5), "🏳️‍🌈") + +--- +// Error: 3-16 string index out of bounds (index: 5, len: 5) +{ "Hello".at(5) } + --- // Test the `slice` method. #test("abc".slice(1, 2), "b") @@ -57,7 +86,7 @@ let time = 0 for match in text.matches(regex("(\d+):(\d+)")) { let caps = match.captures - time += 60 * int(caps(0)) + int(caps(1)) + time += 60 * int(caps.at(0)) + int(caps.at(1)) } str(int(time / 60)) + ":" + str(mod(time, 60)) } diff --git a/tests/typ/compute/data.typ b/tests/typ/compute/data.typ index 5a0f76c6b..dc5630220 100644 --- a/tests/typ/compute/data.typ +++ b/tests/typ/compute/data.typ @@ -19,8 +19,8 @@ // Ref: true #set page(width: auto) #let data = csv("/res/zoo.csv") -#let cells = data(0).map(strong) + data.slice(1).flatten() -#table(columns: data(0).len(), ..cells) +#let cells = data.at(0).map(strong) + data.slice(1).flatten() +#table(columns: data.at(0).len(), ..cells) --- // Error: 6-16 file not found (searched at typ/compute/nope.csv) @@ -34,8 +34,8 @@ // Test reading JSON data. #let data = json("/res/zoo.json") #test(data.len(), 3) -#test(data(0).name, "Debby") -#test(data(2).weight, 150) +#test(data.at(0).name, "Debby") +#test(data.at(2).weight, 150) --- // Error: 7-22 failed to parse json file: syntax error in line 3 diff --git a/tests/typ/layout/repeat.typ b/tests/typ/layout/repeat.typ index 3b5459c94..82d64b941 100644 --- a/tests/typ/layout/repeat.typ +++ b/tests/typ/layout/repeat.typ @@ -12,7 +12,7 @@ ) #for section in sections [ - #section(0) #repeat[.] #section(1) \ + {section.at(0)} #repeat[.] {section.at(1)} \ ] ---