From f70cea508cd30fa40770ea989fe2a19e715a357b Mon Sep 17 00:00:00 2001 From: Laurenz Date: Fri, 30 Dec 2022 15:13:28 +0100 Subject: [PATCH] Remove index syntax in favor of accessor methods --- src/model/args.rs | 30 ---- src/model/array.rs | 55 +++++- src/model/dict.rs | 9 +- src/model/eval.rs | 57 +++--- src/model/methods.rs | 47 ++++- src/model/str.rs | 40 ++++- tests/ref/compiler/array.png | Bin 6191 -> 9140 bytes .../compiler/{methods-color.png => color.png} | Bin tests/ref/compiler/methods-collection.png | Bin 1384 -> 0 bytes tests/typ/basics/enum.typ | 13 +- tests/typ/basics/terms.typ | 2 +- tests/typ/compiler/array.typ | 168 ++++++++++++++++-- tests/typ/compiler/call.typ | 8 +- .../compiler/{methods-color.typ => color.typ} | 0 tests/typ/compiler/dict.typ | 26 ++- tests/typ/compiler/methods-collection.typ | 115 ------------ tests/typ/compiler/methods.typ | 2 +- tests/typ/compiler/ops-invalid.typ | 13 +- tests/typ/compiler/ops-prec.typ | 4 +- tests/typ/compiler/set.typ | 2 +- .../compiler/{methods-str.typ => string.typ} | 31 +++- tests/typ/compute/data.typ | 8 +- tests/typ/layout/repeat.typ | 2 +- 23 files changed, 396 insertions(+), 236 deletions(-) rename tests/ref/compiler/{methods-color.png => color.png} (100%) delete mode 100644 tests/ref/compiler/methods-collection.png rename tests/typ/compiler/{methods-color.typ => color.typ} (100%) delete mode 100644 tests/typ/compiler/methods-collection.typ rename tests/typ/compiler/{methods-str.typ => string.typ} (84%) 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 cbda8aee7f8d34e6e8153264f06b677998c14356..d41e2cbbe6b8c5c0128287ef50235b6c275acf21 100644 GIT binary patch literal 9140 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 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)} \ ] ---