diff --git a/Cargo.lock b/Cargo.lock index b4fd7b72d..dcf7bfdf5 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -334,6 +334,15 @@ dependencies = [ "safemem", ] +[[package]] +name = "lock_api" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "88943dd7ef4a2e5a4bfa2753aaab3013e34ce2533d1996fb18ef591e315e2b3b" +dependencies = [ + "scopeguard", +] + [[package]] name = "log" version = "0.4.14" @@ -448,6 +457,29 @@ version = "1.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "da32515d9f6e6e489d7bc9d84c71b060db7247dc035bbe44eac88cf87486d8d5" +[[package]] +name = "parking_lot" +version = "0.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87f5ec2493a61ac0506c0f4199f99070cbe83857b0337006a30f3e6719b8ef58" +dependencies = [ + "lock_api", + "parking_lot_core", +] + +[[package]] +name = "parking_lot_core" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "28141e0cc4143da2443301914478dc976a61ffdb3f043058310c70df2fed8954" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall", + "smallvec", + "windows-sys", +] + [[package]] name = "pdf-writer" version = "0.4.1" @@ -651,6 +683,12 @@ dependencies = [ "winapi-util", ] +[[package]] +name = "scopeguard" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d29ab0c6d3fc0ee92fe66e2d99f700eab17a8d57d1c1d3b748380fb20baa78cd" + [[package]] name = "serde" version = "1.0.136" @@ -819,6 +857,7 @@ dependencies = [ "memmap2", "miniz_oxide 0.4.4", "once_cell", + "parking_lot", "pdf-writer", "pico-args", "pixglyph", @@ -966,6 +1005,49 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" +[[package]] +name = "windows-sys" +version = "0.32.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3df6e476185f92a12c072be4a189a0210dcdcf512a1891d6dff9edb874deadc6" +dependencies = [ + "windows_aarch64_msvc", + "windows_i686_gnu", + "windows_i686_msvc", + "windows_x86_64_gnu", + "windows_x86_64_msvc", +] + +[[package]] +name = "windows_aarch64_msvc" +version = "0.32.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d8e92753b1c443191654ec532f14c199742964a061be25d77d7a96f09db20bf5" + +[[package]] +name = "windows_i686_gnu" +version = "0.32.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a711c68811799e017b6038e0922cb27a5e2f43a2ddb609fe0b6f3eeda9de615" + +[[package]] +name = "windows_i686_msvc" +version = "0.32.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "146c11bb1a02615db74680b32a68e2d61f553cc24c4eb5b4ca10311740e44172" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.32.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c912b12f7454c6620635bbff3450962753834be2a594819bd5e945af18ec64bc" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.32.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "504a2476202769977a040c6364301a3f65d0cc9e3fb08600b2bda150a0488316" + [[package]] name = "xi-unicode" version = "0.3.0" diff --git a/Cargo.toml b/Cargo.toml index ed265b1a7..e30971bdf 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -20,6 +20,7 @@ fxhash = "0.2" once_cell = "1" serde = { version = "1", features = ["derive"] } typed-arena = "2" +parking_lot = "0.12" # Text and font handling hypher = "0.1" diff --git a/src/eval/args.rs b/src/eval/args.rs index 67da9865f..40454ff58 100644 --- a/src/eval/args.rs +++ b/src/eval/args.rs @@ -1,6 +1,6 @@ use std::fmt::{self, Debug, Formatter, Write}; -use super::{Cast, Value}; +use super::{Array, Cast, Dict, Value}; use crate::diag::{At, TypResult}; use crate::syntax::{Span, Spanned}; use crate::util::EcoString; @@ -147,6 +147,23 @@ impl Args { Ok(()) } + /// Extract the positional arguments as an array. + pub fn to_positional(&self) -> Array { + self.items + .iter() + .filter(|item| item.name.is_none()) + .map(|item| item.value.v.clone()) + .collect() + } + + /// Extract the named arguments as a dictionary. + pub fn to_named(&self) -> Dict { + self.items + .iter() + .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) -> TypResult { self.into_castable("index") diff --git a/src/eval/array.rs b/src/eval/array.rs index 2da1a5f47..6fb278e39 100644 --- a/src/eval/array.rs +++ b/src/eval/array.rs @@ -3,9 +3,11 @@ use std::fmt::{self, Debug, Formatter, Write}; use std::ops::{Add, AddAssign}; use std::sync::Arc; -use super::Value; -use crate::diag::StrResult; +use super::{Args, Func, Value}; +use crate::diag::{At, StrResult, TypResult}; +use crate::syntax::Spanned; use crate::util::ArcExt; +use crate::Context; /// Create a new [`Array`] from values. #[allow(unused_macros)] @@ -66,36 +68,134 @@ impl Array { Arc::make_mut(&mut self.0).push(value); } + /// Remove the last value in the array. + pub fn pop(&mut self) -> StrResult<()> { + Arc::make_mut(&mut self.0).pop().ok_or_else(|| "array is empty")?; + Ok(()) + } + + /// Insert a value at the specified index. + pub fn insert(&mut self, index: i64, value: Value) -> StrResult<()> { + let len = self.len(); + let i = usize::try_from(index) + .ok() + .filter(|&i| i <= self.0.len()) + .ok_or_else(|| out_of_bounds(index, len))?; + + Arc::make_mut(&mut self.0).insert(i, value); + Ok(()) + } + + /// Remove and return the value at the specified index. + pub fn remove(&mut self, index: i64) -> StrResult<()> { + let len = self.len(); + let i = usize::try_from(index) + .ok() + .filter(|&i| i < self.0.len()) + .ok_or_else(|| out_of_bounds(index, len))?; + + Arc::make_mut(&mut self.0).remove(i); + return Ok(()); + } + /// Whether the array contains a specific value. pub fn contains(&self, value: &Value) -> bool { self.0.contains(value) } - /// Clear the array. - pub fn clear(&mut self) { - if Arc::strong_count(&self.0) == 1 { - Arc::make_mut(&mut self.0).clear(); - } else { - *self = Self::new(); + /// Extract a contigous subregion of the array. + pub fn slice(&self, start: i64, end: Option) -> StrResult { + let len = self.len(); + let start = usize::try_from(start) + .ok() + .filter(|&start| start <= self.0.len()) + .ok_or_else(|| out_of_bounds(start, len))?; + + let end = end.unwrap_or(self.len()); + let end = usize::try_from(end) + .ok() + .filter(|&end| end <= self.0.len()) + .ok_or_else(|| out_of_bounds(end, len))?; + + Ok(Self::from_vec(self.0[start .. end].to_vec())) + } + + /// Transform each item in the array with a function. + pub fn map(&self, ctx: &mut Context, f: Spanned) -> TypResult { + Ok(self + .iter() + .cloned() + .map(|item| f.v.call(ctx, Args::from_values(f.span, [item]))) + .collect::>()?) + } + + /// Return a new array with only those elements for which the function + /// return true. + pub fn filter(&self, ctx: &mut Context, f: Spanned) -> TypResult { + let mut kept = vec![]; + for item in self.iter() { + if f.v + .call(ctx, Args::from_values(f.span, [item.clone()]))? + .cast::() + .at(f.span)? + { + kept.push(item.clone()) + } } + Ok(Self::from_vec(kept)) } - /// Iterate over references to the contained values. - pub fn iter(&self) -> std::slice::Iter { - self.0.iter() + /// Return a new array with all items from this and nested arrays. + pub fn flatten(&self) -> Self { + let mut flat = vec![]; + for item in self.iter() { + if let Value::Array(nested) = item { + flat.extend(nested.flatten().into_iter()); + } else { + flat.push(item.clone()); + } + } + Self::from_vec(flat) } - /// Extracts a slice of the whole array. - pub fn as_slice(&self) -> &[Value] { - self.0.as_slice() + /// Return the index of the element if it is part of the array. + pub fn find(&self, value: Value) -> Option { + self.0.iter().position(|x| *x == value).map(|i| i as i64) + } + + /// Join all values in the array, optionally with separator and last + /// separator (between the final two items). + pub fn join(&self, sep: Option, mut last: Option) -> StrResult { + let len = self.0.len(); + let sep = sep.unwrap_or(Value::None); + + let mut result = Value::None; + for (i, value) in self.iter().cloned().enumerate() { + if i > 0 { + if i + 1 == len { + if let Some(last) = last.take() { + result = result.join(last)?; + } else { + result = result.join(sep.clone())?; + } + } else { + result = result.join(sep.clone())?; + } + } + + result = result.join(value)?; + } + + Ok(result) } /// Return a sorted version of this array. /// /// Returns an error if two values could not be compared. - pub fn sorted(mut self) -> StrResult { + pub fn sorted(&self) -> StrResult { let mut result = Ok(()); - Arc::make_mut(&mut self.0).sort_by(|a, b| { + let mut vec = (*self.0).clone(); + vec.sort_by(|a, b| { a.partial_cmp(b).unwrap_or_else(|| { if result.is_ok() { result = Err(format!( @@ -107,7 +207,7 @@ impl Array { Ordering::Equal }) }); - result.map(|_| self) + result.map(|_| Self::from_vec(vec)) } /// Repeat this array `n` times. @@ -119,6 +219,17 @@ impl Array { Ok(self.iter().cloned().cycle().take(count).collect()) } + + /// Extract a slice of the whole array. + pub fn as_slice(&self) -> &[Value] { + self.0.as_slice() + } + + + /// Iterate over references to the contained values. + pub fn iter(&self) -> std::slice::Iter { + self.0.iter() + } } /// The out of bounds access error message. diff --git a/src/eval/dict.rs b/src/eval/dict.rs index 9127b2eb9..b630fc63c 100644 --- a/src/eval/dict.rs +++ b/src/eval/dict.rs @@ -3,9 +3,11 @@ use std::fmt::{self, Debug, Formatter, Write}; use std::ops::{Add, AddAssign}; use std::sync::Arc; -use super::Value; -use crate::diag::StrResult; +use super::{Args, Array, Func, Value}; +use crate::diag::{StrResult, TypResult}; +use crate::syntax::Spanned; use crate::util::{ArcExt, EcoString}; +use crate::Context; /// Create a new [`Dict`] from key-value pairs. #[allow(unused_macros)] @@ -56,14 +58,22 @@ impl Dict { Arc::make_mut(&mut self.0).entry(key).or_default() } + /// Whether the dictionary contains a specific key. + pub fn contains(&self, key: &str) -> bool { + self.0.contains_key(key) + } + /// Insert a mapping from the given `key` to the given `value`. pub fn insert(&mut self, key: EcoString, value: Value) { Arc::make_mut(&mut self.0).insert(key, value); } - /// Whether the dictionary contains a specific key. - pub fn contains_key(&self, key: &str) -> bool { - self.0.contains_key(key) + /// Remove a mapping by `key`. + pub fn remove(&mut self, key: EcoString) -> StrResult<()> { + match Arc::make_mut(&mut self.0).remove(&key) { + Some(_) => Ok(()), + None => Err(missing_key(&key)), + } } /// Clear the dictionary. @@ -75,6 +85,29 @@ impl Dict { } } + /// Return the keys of the dictionary as an array. + pub fn keys(&self) -> Array { + self.iter().map(|(key, _)| Value::Str(key.clone())).collect() + } + + /// Return the values of the dictionary as an array. + pub fn values(&self) -> Array { + self.iter().map(|(_, value)| value.clone()).collect() + } + + /// Transform each pair in the array with a function. + pub fn map(&self, ctx: &mut Context, f: Spanned) -> TypResult { + Ok(self + .iter() + .map(|(key, value)| { + f.v.call( + ctx, + Args::from_values(f.span, [Value::Str(key.clone()), value.clone()]), + ) + }) + .collect::>()?) + } + /// Iterate over pairs of references to the contained keys and values. pub fn iter(&self) -> std::collections::btree_map::Iter { self.0.iter() diff --git a/src/eval/mod.rs b/src/eval/mod.rs index 2c864036d..564dca20c 100644 --- a/src/eval/mod.rs +++ b/src/eval/mod.rs @@ -19,7 +19,9 @@ mod module; mod ops; mod scope; mod show; +mod str; +pub use self::str::*; pub use args::*; pub use array::*; pub use capture::*; @@ -35,6 +37,7 @@ pub use show::*; pub use styles::*; pub use value::*; +use parking_lot::{MappedRwLockWriteGuard, RwLockWriteGuard}; use unicode_segmentation::UnicodeSegmentation; use crate::diag::{At, StrResult, Trace, Tracepoint, TypResult}; @@ -207,9 +210,9 @@ impl Eval for Expr { Self::Array(v) => v.eval(ctx, scp).map(Value::Array), Self::Dict(v) => v.eval(ctx, scp).map(Value::Dict), Self::Group(v) => v.eval(ctx, scp), - Self::Call(v) => v.eval(ctx, scp), + Self::FuncCall(v) => v.eval(ctx, scp), + Self::MethodCall(v) => v.eval(ctx, scp), Self::Closure(v) => v.eval(ctx, scp), - Self::With(v) => v.eval(ctx, scp), Self::Unary(v) => v.eval(ctx, scp), Self::Binary(v) => v.eval(ctx, scp), Self::Let(v) => v.eval(ctx, scp), @@ -254,7 +257,7 @@ impl Eval for Ident { fn eval(&self, _: &mut Context, scp: &mut Scopes) -> EvalResult { match scp.get(self) { - Some(slot) => Ok(slot.read().unwrap().clone()), + Some(slot) => Ok(slot.read().clone()), None => bail!(self.span(), "unknown variable"), } } @@ -384,47 +387,62 @@ impl BinaryExpr { op: fn(Value, Value) -> StrResult, ) -> EvalResult { let rhs = self.rhs().eval(ctx, scp)?; - self.lhs().access( - ctx, - scp, - Box::new(|target| { - let lhs = std::mem::take(&mut *target); - *target = op(lhs, rhs).at(self.span())?; - Ok(()) - }), - )?; + let lhs = self.lhs(); + let mut location = lhs.access(ctx, scp)?; + let lhs = std::mem::take(&mut *location); + *location = op(lhs, rhs).at(self.span())?; Ok(Value::None) } } -impl Eval for CallExpr { +impl Eval for FuncCall { type Output = Value; fn eval(&self, ctx: &mut Context, scp: &mut Scopes) -> EvalResult { - let span = self.callee().span(); let callee = self.callee().eval(ctx, scp)?; let args = self.args().eval(ctx, scp)?; Ok(match callee { Value::Array(array) => { - array.get(args.into_index()?).map(Value::clone).at(self.span()) + array.get(args.into_index()?).map(Value::clone).at(self.span())? } Value::Dict(dict) => { - dict.get(args.into_key()?).map(Value::clone).at(self.span()) + dict.get(args.into_key()?).map(Value::clone).at(self.span())? } Value::Func(func) => { let point = || Tracepoint::Call(func.name().map(ToString::to_string)); - func.call(ctx, args).trace(point, self.span()) + func.call(ctx, args).trace(point, self.span())? } v => bail!( - span, + self.callee().span(), "expected callable or collection, found {}", v.type_name(), ), - }?) + }) + } +} + +impl Eval for MethodCall { + type Output = Value; + + fn eval(&self, ctx: &mut Context, scp: &mut Scopes) -> EvalResult { + let span = self.span(); + let method = self.method(); + let point = || Tracepoint::Call(Some(method.to_string())); + + Ok(if Value::is_mutable_method(&method) { + let args = self.args().eval(ctx, scp)?; + let mut receiver = self.receiver().access(ctx, scp)?; + receiver.call_mut(ctx, &method, span, args).trace(point, span)?; + Value::None + } else { + let receiver = self.receiver().eval(ctx, scp)?; + let args = self.args().eval(ctx, scp)?; + receiver.call(ctx, &method, span, args).trace(point, span)? + }) } } @@ -527,17 +545,6 @@ impl Eval for ClosureExpr { } } -impl Eval for WithExpr { - type Output = Value; - - fn eval(&self, ctx: &mut Context, scp: &mut Scopes) -> EvalResult { - let callee = self.callee(); - let func = callee.eval(ctx, scp)?.cast::().at(callee.span())?; - let args = self.args().eval(ctx, scp)?; - Ok(Value::Func(func.with(args))) - } -} - impl Eval for LetExpr { type Output = Value; @@ -694,13 +701,13 @@ impl Eval for ImportExpr { match self.imports() { Imports::Wildcard => { for (var, slot) in module.scope.iter() { - scp.top.def_mut(var, slot.read().unwrap().clone()); + scp.top.def_mut(var, slot.read().clone()); } } Imports::Items(idents) => { for ident in idents { if let Some(slot) = module.scope.get(&ident) { - scp.top.def_mut(ident.take(), slot.read().unwrap().clone()); + scp.top.def_mut(ident.take(), slot.read().clone()); } else { bail!(ident.span(), "unresolved import"); } @@ -773,56 +780,85 @@ impl Eval for ReturnExpr { } } -/// Try to mutably access the value an expression points to. -/// -/// This only works if the expression is a valid lvalue. +/// Access an expression mutably. pub trait Access { - /// Try to access the value. - fn access(&self, ctx: &mut Context, scp: &mut Scopes, f: Handler) -> TypResult<()>; + /// Access the value. + fn access<'a>( + &self, + ctx: &mut Context, + scp: &'a mut Scopes, + ) -> EvalResult>; } -/// Process an accessed value. -type Handler<'a> = Box TypResult<()> + 'a>; - impl Access for Expr { - fn access(&self, ctx: &mut Context, scp: &mut Scopes, f: Handler) -> TypResult<()> { + fn access<'a>( + &self, + ctx: &mut Context, + scp: &'a mut Scopes, + ) -> EvalResult> { match self { - Expr::Ident(ident) => ident.access(ctx, scp, f), - Expr::Call(call) => call.access(ctx, scp, f), - _ => bail!(self.span(), "cannot access this expression mutably"), + Expr::Ident(ident) => ident.access(ctx, scp), + Expr::FuncCall(call) => call.access(ctx, scp), + _ => bail!(self.span(), "cannot mutate a temporary value"), } } } impl Access for Ident { - fn access(&self, _: &mut Context, scp: &mut Scopes, f: Handler) -> TypResult<()> { + fn access<'a>( + &self, + _: &mut Context, + scp: &'a mut Scopes, + ) -> EvalResult> { match scp.get(self) { Some(slot) => match slot.try_write() { - Ok(mut guard) => f(&mut guard), - Err(_) => bail!(self.span(), "cannot mutate a constant"), + Some(guard) => Ok(RwLockWriteGuard::map(guard, |v| v)), + None => bail!(self.span(), "cannot mutate a constant"), }, None => bail!(self.span(), "unknown variable"), } } } -impl Access for CallExpr { - fn access(&self, ctx: &mut Context, scp: &mut Scopes, f: Handler) -> TypResult<()> { +impl Access for FuncCall { + fn access<'a>( + &self, + ctx: &mut Context, + scp: &'a mut Scopes, + ) -> EvalResult> { let args = self.args().eval(ctx, scp)?; - self.callee().access( - ctx, - scp, - Box::new(|value| match value { + let guard = self.callee().access(ctx, scp)?; + try_map(guard, |value| { + Ok(match value { Value::Array(array) => { - f(array.get_mut(args.into_index()?).at(self.span())?) + array.get_mut(args.into_index()?).at(self.span())? } - Value::Dict(dict) => f(dict.get_mut(args.into_key()?)), + Value::Dict(dict) => dict.get_mut(args.into_key()?), v => bail!( self.callee().span(), "expected collection, found {}", v.type_name(), ), - }), - ) + }) + }) } } + +/// A mutable location. +type Location<'a> = MappedRwLockWriteGuard<'a, Value>; + +/// Map a reader-writer lock with a function. +fn try_map(location: Location, f: F) -> EvalResult +where + F: FnOnce(&mut Value) -> EvalResult<&mut Value>, +{ + let mut error = None; + MappedRwLockWriteGuard::try_map(location, |value| match f(value) { + Ok(value) => Some(value), + Err(err) => { + error = Some(err); + None + } + }) + .map_err(|_| error.unwrap()) +} diff --git a/src/eval/ops.rs b/src/eval/ops.rs index 6a8f5284b..9b46e8f60 100644 --- a/src/eval/ops.rs +++ b/src/eval/ops.rs @@ -1,9 +1,8 @@ use std::cmp::Ordering; -use super::{Dynamic, Value}; +use super::{Dynamic, StrExt, Value}; use crate::diag::StrResult; use crate::geom::{Align, Spec, SpecAxis}; -use crate::util::EcoString; use Value::*; /// Bail with a type mismatch error. @@ -174,8 +173,8 @@ pub fn mul(lhs: Value, rhs: Value) -> StrResult { (Fractional(a), Float(b)) => Fractional(a * b), (Int(a), Fractional(b)) => Fractional(a as f64 * b), - (Str(a), Int(b)) => Str(repeat_str(a, b)?), - (Int(a), Str(b)) => Str(repeat_str(b, a)?), + (Str(a), Int(b)) => Str(StrExt::repeat(&a, b)?), + (Int(a), Str(b)) => Str(StrExt::repeat(&b, a)?), (Array(a), Int(b)) => Array(a.repeat(b)?), (Int(a), Array(b)) => Array(b.repeat(a)?), (Content(a), Int(b)) => Content(a.repeat(b)?), @@ -185,16 +184,6 @@ pub fn mul(lhs: Value, rhs: Value) -> StrResult { }) } -/// Repeat a string a number of times. -fn repeat_str(string: EcoString, n: i64) -> StrResult { - let n = usize::try_from(n) - .ok() - .and_then(|n| string.len().checked_mul(n).map(|_| n)) - .ok_or_else(|| format!("cannot repeat this string {} times", n))?; - - Ok(string.repeat(n)) -} - /// Compute the quotient of two values. pub fn div(lhs: Value, rhs: Value) -> StrResult { Ok(match (lhs, rhs) { @@ -358,7 +347,7 @@ pub fn not_in(lhs: Value, rhs: Value) -> StrResult { pub fn contains(lhs: &Value, rhs: &Value) -> Option { Some(match (lhs, rhs) { (Value::Str(a), Value::Str(b)) => b.contains(a.as_str()), - (Value::Str(a), Value::Dict(b)) => b.contains_key(a), + (Value::Str(a), Value::Dict(b)) => b.contains(a), (a, Value::Array(b)) => b.contains(a), _ => return Option::None, }) diff --git a/src/eval/scope.rs b/src/eval/scope.rs index 19899cae9..8acaa4314 100644 --- a/src/eval/scope.rs +++ b/src/eval/scope.rs @@ -2,7 +2,9 @@ use std::collections::BTreeMap; use std::fmt::{self, Debug, Formatter}; use std::hash::{Hash, Hasher}; use std::iter; -use std::sync::{Arc, RwLock}; +use std::sync::Arc; + +use parking_lot::RwLock; use super::{Args, Func, Node, Value}; use crate::diag::TypResult; @@ -113,7 +115,7 @@ impl Hash for Scope { self.values.len().hash(state); for (name, value) in self.values.iter() { name.hash(state); - value.read().unwrap().hash(state); + value.read().hash(state); } } } @@ -122,7 +124,7 @@ impl Debug for Scope { fn fmt(&self, f: &mut Formatter) -> fmt::Result { f.write_str("Scope ")?; f.debug_map() - .entries(self.values.iter().map(|(k, v)| (k, v.read().unwrap()))) + .entries(self.values.iter().map(|(k, v)| (k, v.read()))) .finish() } } diff --git a/src/eval/str.rs b/src/eval/str.rs new file mode 100644 index 000000000..3b4349a16 --- /dev/null +++ b/src/eval/str.rs @@ -0,0 +1,37 @@ +use super::{Array, Value}; +use crate::diag::StrResult; +use crate::util::EcoString; + +/// Extra methods on strings. +pub trait StrExt { + /// Repeat a string a number of times. + fn repeat(&self, n: i64) -> StrResult; + + /// Split this string at whitespace or a specific pattern. + fn split(&self, at: Option) -> Array; +} + +impl StrExt for EcoString { + fn repeat(&self, n: i64) -> StrResult { + let n = usize::try_from(n) + .ok() + .and_then(|n| self.len().checked_mul(n).map(|_| n)) + .ok_or_else(|| format!("cannot repeat this string {} times", n))?; + + Ok(self.repeat(n)) + } + + fn split(&self, at: Option) -> Array { + if let Some(pat) = at { + self.as_str() + .split(pat.as_str()) + .map(|s| Value::Str(s.into())) + .collect() + } else { + self.as_str() + .split_whitespace() + .map(|s| Value::Str(s.into())) + .collect() + } + } +} diff --git a/src/eval/value.rs b/src/eval/value.rs index 0e0d08a8d..a76b377de 100644 --- a/src/eval/value.rs +++ b/src/eval/value.rs @@ -4,10 +4,10 @@ use std::fmt::{self, Debug, Formatter}; use std::hash::{Hash, Hasher}; use std::sync::Arc; -use super::{ops, Args, Array, Content, Dict, Func, Layout}; -use crate::diag::{with_alternative, StrResult}; +use super::{ops, Args, Array, Content, Context, Dict, Func, Layout, StrExt}; +use crate::diag::{with_alternative, At, StrResult, TypResult}; use crate::geom::{Angle, Color, Fractional, Length, Linear, Relative, RgbaColor}; -use crate::syntax::Spanned; +use crate::syntax::{Span, Spanned}; use crate::util::EcoString; /// A computational value. @@ -120,6 +120,121 @@ impl Value { v => Content::Text(v.repr()).monospaced(), } } + + /// Call a method on the value. + pub fn call( + &self, + ctx: &mut Context, + method: &str, + span: Span, + mut args: Args, + ) -> TypResult { + let name = self.type_name(); + let missing = || Err(missing_method(name, method)).at(span); + + let output = match self { + Value::Str(string) => match method { + "len" => Value::Int(string.len() as i64), + "trim" => Value::Str(string.trim().into()), + "split" => Value::Array(string.split(args.eat()?)), + _ => missing()?, + }, + + Value::Array(array) => match method { + "len" => Value::Int(array.len()), + "slice" => { + let start = args.expect("start")?; + let mut end = args.eat()?; + if end.is_none() { + end = args.named("count")?.map(|c: i64| start + c); + } + Value::Array(array.slice(start, end).at(span)?) + } + "map" => Value::Array(array.map(ctx, args.expect("function")?)?), + "filter" => Value::Array(array.filter(ctx, args.expect("function")?)?), + "flatten" => Value::Array(array.flatten()), + "find" => { + array.find(args.expect("value")?).map_or(Value::None, Value::Int) + } + "join" => { + let sep = args.eat()?; + let last = args.named("last")?; + array.join(sep, last).at(span)? + } + "sorted" => Value::Array(array.sorted().at(span)?), + _ => missing()?, + }, + + Value::Dict(dict) => match method { + "len" => Value::Int(dict.len()), + "keys" => Value::Array(dict.keys()), + "values" => Value::Array(dict.values()), + "pairs" => Value::Array(dict.map(ctx, args.expect("function")?)?), + _ => missing()?, + }, + + Value::Func(func) => match method { + "with" => Value::Func(func.clone().with(args.take())), + _ => missing()?, + }, + + Value::Args(args) => match method { + "positional" => Value::Array(args.to_positional()), + "named" => Value::Dict(args.to_named()), + _ => missing()?, + }, + + _ => missing()?, + }; + + args.finish()?; + Ok(output) + } + + /// Call a mutating method on the value. + pub fn call_mut( + &mut self, + _: &mut Context, + method: &str, + span: Span, + mut args: Args, + ) -> TypResult<()> { + let name = self.type_name(); + let missing = || Err(missing_method(name, method)).at(span); + + match self { + Value::Array(array) => match method { + "push" => array.push(args.expect("value")?), + "pop" => array.pop().at(span)?, + "insert" => { + array.insert(args.expect("index")?, args.expect("value")?).at(span)? + } + "remove" => array.remove(args.expect("index")?).at(span)?, + _ => missing()?, + }, + + Value::Dict(dict) => match method { + "remove" => dict.remove(args.expect("key")?).at(span)?, + _ => missing()?, + }, + + _ => missing()?, + } + + args.finish()?; + Ok(()) + } + + /// Whether a specific method is mutable. + pub fn is_mutable_method(method: &str) -> bool { + matches!(method, "push" | "pop" | "insert" | "remove") + } +} + +/// The missing method error message. +#[cold] +fn missing_method(type_name: &str, method: &str) -> String { + format!("type {type_name} has no method `{method}`") } impl Default for Value { diff --git a/src/geom/angle.rs b/src/geom/angle.rs index b4d6f79ab..b64ec77e0 100644 --- a/src/geom/angle.rs +++ b/src/geom/angle.rs @@ -129,7 +129,7 @@ assign_impl!(Angle /= f64); impl Sum for Angle { fn sum>(iter: I) -> Self { - iter.fold(Angle::zero(), Add::add) + Self(iter.map(|s| s.0).sum()) } } /// Different units of angular measurement. diff --git a/src/library/mod.rs b/src/library/mod.rs index 087ff7eaa..528a2ce7f 100644 --- a/src/library/mod.rs +++ b/src/library/mod.rs @@ -67,13 +67,10 @@ pub fn new() -> Scope { std.def_node::("math"); // Utility functions. - std.def_fn("assert", utility::assert); std.def_fn("type", utility::type_); - std.def_fn("repr", utility::repr); - std.def_fn("join", utility::join); + std.def_fn("assert", utility::assert); std.def_fn("int", utility::int); std.def_fn("float", utility::float); - std.def_fn("str", utility::str); std.def_fn("abs", utility::abs); std.def_fn("min", utility::min); std.def_fn("max", utility::max); @@ -83,13 +80,13 @@ pub fn new() -> Scope { std.def_fn("range", utility::range); std.def_fn("rgb", utility::rgb); std.def_fn("cmyk", utility::cmyk); + std.def_fn("repr", utility::repr); + std.def_fn("str", utility::str); std.def_fn("lower", utility::lower); std.def_fn("upper", utility::upper); std.def_fn("letter", utility::letter); std.def_fn("roman", utility::roman); std.def_fn("symbol", utility::symbol); - std.def_fn("len", utility::len); - std.def_fn("sorted", utility::sorted); // Predefined colors. std.def_const("black", Color::BLACK); diff --git a/src/library/utility/color.rs b/src/library/utility/color.rs new file mode 100644 index 000000000..df24f6154 --- /dev/null +++ b/src/library/utility/color.rs @@ -0,0 +1,58 @@ +use std::str::FromStr; + +use crate::library::prelude::*; + +/// Create an RGB(A) color. +pub fn rgb(_: &mut Context, args: &mut Args) -> TypResult { + Ok(Value::from( + if let Some(string) = args.find::>()? { + match RgbaColor::from_str(&string.v) { + Ok(color) => color, + Err(_) => bail!(string.span, "invalid hex string"), + } + } else { + struct Component(u8); + + castable! { + Component, + Expected: "integer or relative", + Value::Int(v) => match v { + 0 ..= 255 => Self(v as u8), + _ => Err("must be between 0 and 255")?, + }, + Value::Relative(v) => if (0.0 ..= 1.0).contains(&v.get()) { + Self((v.get() * 255.0).round() as u8) + } else { + Err("must be between 0% and 100%")? + }, + } + + let Component(r) = args.expect("red component")?; + let Component(g) = args.expect("green component")?; + let Component(b) = args.expect("blue component")?; + let Component(a) = args.eat()?.unwrap_or(Component(255)); + RgbaColor::new(r, g, b, a) + }, + )) +} + +/// Create a CMYK color. +pub fn cmyk(_: &mut Context, args: &mut Args) -> TypResult { + struct Component(u8); + + castable! { + Component, + Expected: "relative", + Value::Relative(v) => if (0.0 ..= 1.0).contains(&v.get()) { + Self((v.get() * 255.0).round() as u8) + } else { + Err("must be between 0% and 100%")? + }, + } + + let Component(c) = args.expect("cyan component")?; + let Component(m) = args.expect("magenta component")?; + let Component(y) = args.expect("yellow component")?; + let Component(k) = args.expect("key component")?; + Ok(Value::Color(CmykColor::new(c, m, y, k).into())) +} diff --git a/src/library/utility/math.rs b/src/library/utility/math.rs index e48af4268..0aebc5732 100644 --- a/src/library/utility/math.rs +++ b/src/library/utility/math.rs @@ -2,6 +2,35 @@ use std::cmp::Ordering; use crate::library::prelude::*; +/// Convert a value to a integer. +pub fn int(_: &mut Context, args: &mut Args) -> TypResult { + let Spanned { v, span } = args.expect("value")?; + Ok(Value::Int(match v { + Value::Bool(v) => v as i64, + Value::Int(v) => v, + Value::Float(v) => v as i64, + Value::Str(v) => match v.parse() { + Ok(v) => v, + Err(_) => bail!(span, "invalid integer"), + }, + v => bail!(span, "cannot convert {} to integer", v.type_name()), + })) +} + +/// Convert a value to a float. +pub fn float(_: &mut Context, args: &mut Args) -> TypResult { + let Spanned { v, span } = args.expect("value")?; + Ok(Value::Float(match v { + Value::Int(v) => v as f64, + Value::Float(v) => v, + Value::Str(v) => match v.parse() { + Ok(v) => v, + Err(_) => bail!(span, "invalid float"), + }, + v => bail!(span, "cannot convert {} to float", v.type_name()), + })) +} + /// The absolute value of a numeric value. pub fn abs(_: &mut Context, args: &mut Args) -> TypResult { let Spanned { v, span } = args.expect("numeric value")?; diff --git a/src/library/utility/mod.rs b/src/library/utility/mod.rs index d85c3f125..13220242b 100644 --- a/src/library/utility/mod.rs +++ b/src/library/utility/mod.rs @@ -1,15 +1,19 @@ //! Computational utility functions. +mod color; mod math; -mod numbering; +mod string; +pub use color::*; pub use math::*; -pub use numbering::*; - -use std::str::FromStr; +pub use string::*; use crate::library::prelude::*; -use crate::library::text::{Case, TextNode}; + +/// The name of a value's type. +pub fn type_(_: &mut Context, args: &mut Args) -> TypResult { + Ok(args.expect::("value")?.type_name().into()) +} /// Ensure that a condition is fulfilled. pub fn assert(_: &mut Context, args: &mut Args) -> TypResult { @@ -19,169 +23,3 @@ pub fn assert(_: &mut Context, args: &mut Args) -> TypResult { } Ok(Value::None) } - -/// The name of a value's type. -pub fn type_(_: &mut Context, args: &mut Args) -> TypResult { - Ok(args.expect::("value")?.type_name().into()) -} - -/// The string representation of a value. -pub fn repr(_: &mut Context, args: &mut Args) -> TypResult { - Ok(args.expect::("value")?.repr().into()) -} - -/// Join a sequence of values, optionally interspersing it with another value. -pub fn join(_: &mut Context, args: &mut Args) -> TypResult { - let span = args.span; - let sep = args.named::("sep")?.unwrap_or(Value::None); - - let mut result = Value::None; - let mut iter = args.all::()?.into_iter(); - - if let Some(first) = iter.next() { - result = first; - } - - for value in iter { - result = result.join(sep.clone()).at(span)?; - result = result.join(value).at(span)?; - } - - Ok(result) -} - -/// Convert a value to a integer. -pub fn int(_: &mut Context, args: &mut Args) -> TypResult { - let Spanned { v, span } = args.expect("value")?; - Ok(Value::Int(match v { - Value::Bool(v) => v as i64, - Value::Int(v) => v, - Value::Float(v) => v as i64, - Value::Str(v) => match v.parse() { - Ok(v) => v, - Err(_) => bail!(span, "invalid integer"), - }, - v => bail!(span, "cannot convert {} to integer", v.type_name()), - })) -} - -/// Convert a value to a float. -pub fn float(_: &mut Context, args: &mut Args) -> TypResult { - let Spanned { v, span } = args.expect("value")?; - Ok(Value::Float(match v { - Value::Int(v) => v as f64, - Value::Float(v) => v, - Value::Str(v) => match v.parse() { - Ok(v) => v, - Err(_) => bail!(span, "invalid float"), - }, - v => bail!(span, "cannot convert {} to float", v.type_name()), - })) -} - -/// Cconvert a value to a string. -pub fn str(_: &mut Context, args: &mut Args) -> TypResult { - let Spanned { v, span } = args.expect("value")?; - Ok(Value::Str(match v { - Value::Int(v) => format_eco!("{}", v), - Value::Float(v) => format_eco!("{}", v), - Value::Str(v) => v, - v => bail!(span, "cannot convert {} to string", v.type_name()), - })) -} - -/// Create an RGB(A) color. -pub fn rgb(_: &mut Context, args: &mut Args) -> TypResult { - Ok(Value::from( - if let Some(string) = args.find::>()? { - match RgbaColor::from_str(&string.v) { - Ok(color) => color, - Err(_) => bail!(string.span, "invalid hex string"), - } - } else { - struct Component(u8); - - castable! { - Component, - Expected: "integer or relative", - Value::Int(v) => match v { - 0 ..= 255 => Self(v as u8), - _ => Err("must be between 0 and 255")?, - }, - Value::Relative(v) => if (0.0 ..= 1.0).contains(&v.get()) { - Self((v.get() * 255.0).round() as u8) - } else { - Err("must be between 0% and 100%")? - }, - } - - let Component(r) = args.expect("red component")?; - let Component(g) = args.expect("green component")?; - let Component(b) = args.expect("blue component")?; - let Component(a) = args.eat()?.unwrap_or(Component(255)); - RgbaColor::new(r, g, b, a) - }, - )) -} - -/// Create a CMYK color. -pub fn cmyk(_: &mut Context, args: &mut Args) -> TypResult { - struct Component(u8); - - castable! { - Component, - Expected: "relative", - Value::Relative(v) => if (0.0 ..= 1.0).contains(&v.get()) { - Self((v.get() * 255.0).round() as u8) - } else { - Err("must be between 0% and 100%")? - }, - } - - let Component(c) = args.expect("cyan component")?; - let Component(m) = args.expect("magenta component")?; - let Component(y) = args.expect("yellow component")?; - let Component(k) = args.expect("key component")?; - Ok(Value::Color(CmykColor::new(c, m, y, k).into())) -} - -/// The length of a string, an array or a dictionary. -pub fn len(_: &mut Context, args: &mut Args) -> TypResult { - let Spanned { v, span } = args.expect("collection")?; - Ok(Value::Int(match v { - Value::Str(v) => v.len() as i64, - Value::Array(v) => v.len(), - Value::Dict(v) => v.len(), - v => bail!( - span, - "expected string, array or dictionary, found {}", - v.type_name(), - ), - })) -} - -/// Convert a string to lowercase. -pub fn lower(_: &mut Context, args: &mut Args) -> TypResult { - case(Case::Lower, args) -} - -/// Convert a string to uppercase. -pub fn upper(_: &mut Context, args: &mut Args) -> TypResult { - case(Case::Upper, args) -} - -/// Change the case of a string or content. -fn case(case: Case, args: &mut Args) -> TypResult { - let Spanned { v, span } = args.expect("string or content")?; - Ok(match v { - Value::Str(v) => Value::Str(case.apply(&v).into()), - Value::Content(v) => Value::Content(v.styled(TextNode::CASE, Some(case))), - v => bail!(span, "expected string or content, found {}", v.type_name()), - }) -} - -/// The sorted version of an array. -pub fn sorted(_: &mut Context, args: &mut Args) -> TypResult { - let Spanned { v, span } = args.expect::>("array")?; - Ok(Value::Array(v.sorted().at(span)?)) -} diff --git a/src/library/utility/numbering.rs b/src/library/utility/string.rs similarity index 69% rename from src/library/utility/numbering.rs rename to src/library/utility/string.rs index 0070873fe..92d80be2c 100644 --- a/src/library/utility/numbering.rs +++ b/src/library/utility/string.rs @@ -1,4 +1,41 @@ use crate::library::prelude::*; +use crate::library::text::{Case, TextNode}; + +/// The string representation of a value. +pub fn repr(_: &mut Context, args: &mut Args) -> TypResult { + Ok(args.expect::("value")?.repr().into()) +} + +/// Cconvert a value to a string. +pub fn str(_: &mut Context, args: &mut Args) -> TypResult { + let Spanned { v, span } = args.expect("value")?; + Ok(Value::Str(match v { + Value::Int(v) => format_eco!("{}", v), + Value::Float(v) => format_eco!("{}", v), + Value::Str(v) => v, + v => bail!(span, "cannot convert {} to string", v.type_name()), + })) +} + +/// Convert a string to lowercase. +pub fn lower(_: &mut Context, args: &mut Args) -> TypResult { + case(Case::Lower, args) +} + +/// Convert a string to uppercase. +pub fn upper(_: &mut Context, args: &mut Args) -> TypResult { + case(Case::Upper, args) +} + +/// Change the case of a string or content. +fn case(case: Case, args: &mut Args) -> TypResult { + let Spanned { v, span } = args.expect("string or content")?; + Ok(match v { + Value::Str(v) => Value::Str(case.apply(&v).into()), + Value::Content(v) => Value::Content(v.styled(TextNode::CASE, Some(case))), + v => bail!(span, "expected string or content, found {}", v.type_name()), + }) +} /// Converts an integer into one or multiple letters. pub fn letter(_: &mut Context, args: &mut Args) -> TypResult { diff --git a/src/parse/incremental.rs b/src/parse/incremental.rs index 468f344e7..a2ba502b8 100644 --- a/src/parse/incremental.rs +++ b/src/parse/incremental.rs @@ -4,7 +4,8 @@ use std::sync::Arc; use crate::syntax::{Green, GreenNode, NodeKind}; use super::{ - is_newline, parse, reparse_block, reparse_content, reparse_markup_elements, TokenMode, + is_newline, parse, reparse_code_block, reparse_content_block, + reparse_markup_elements, TokenMode, }; /// Allows partial refreshs of the [`Green`] node tree. @@ -210,12 +211,12 @@ impl Reparser<'_> { } let (newborns, terminated, amount) = match mode { - ReparseMode::Code => reparse_block( + ReparseMode::Code => reparse_code_block( &prefix, &self.src[newborn_span.start ..], newborn_span.len(), ), - ReparseMode::Content => reparse_content( + ReparseMode::Content => reparse_content_block( &prefix, &self.src[newborn_span.start ..], newborn_span.len(), @@ -344,7 +345,6 @@ mod tests { test("this~is -- in my opinion -- spectacular", 8 .. 10, "---", 5 .. 25); test("understanding `code` is complicated", 15 .. 15, "C ", 14 .. 22); test("{ let x = g() }", 10 .. 12, "f(54", 0 .. 17); - test("a #let rect with (fill: eastern)\nb", 16 .. 31, " (stroke: conifer", 2 .. 34); test(r#"a ```typst hello``` b"#, 16 .. 17, "", 2 .. 18); test(r#"a ```typst hello```"#, 16 .. 17, "", 2 .. 18); test("#for", 4 .. 4, "//", 0 .. 6); diff --git a/src/parse/mod.rs b/src/parse/mod.rs index 5eaba8b04..58b81521e 100644 --- a/src/parse/mod.rs +++ b/src/parse/mod.rs @@ -31,7 +31,7 @@ pub fn parse(src: &str) -> Arc { /// Reparse a code block. /// /// Returns `Some` if all of the input was consumed. -pub fn reparse_block( +pub fn reparse_code_block( prefix: &str, src: &str, end_pos: usize, @@ -41,7 +41,7 @@ pub fn reparse_block( return None; } - block(&mut p); + code_block(&mut p); let (mut green, terminated) = p.consume()?; let first = green.remove(0); @@ -55,7 +55,7 @@ pub fn reparse_block( /// Reparse a content block. /// /// Returns `Some` if all of the input was consumed. -pub fn reparse_content( +pub fn reparse_content_block( prefix: &str, src: &str, end_pos: usize, @@ -65,7 +65,7 @@ pub fn reparse_content( return None; } - content(&mut p); + content_block(&mut p); let (mut green, terminated) = p.consume()?; let first = green.remove(0); @@ -236,8 +236,8 @@ fn markup_node(p: &mut Parser, at_start: &mut bool) { | NodeKind::Include => markup_expr(p), // Code and content block. - NodeKind::LeftBrace => block(p), - NodeKind::LeftBracket => content(p), + NodeKind::LeftBrace => code_block(p), + NodeKind::LeftBracket => content_block(p), NodeKind::Error(_, _) => p.eat(), _ => p.unexpected(), @@ -364,7 +364,7 @@ fn expr_prec(p: &mut Parser, atomic: bool, min_prec: usize) -> ParseResult { // Exclamation mark, parenthesis or bracket means this is a function // call. if let Some(NodeKind::LeftParen | NodeKind::LeftBracket) = p.peek_direct() { - call(p, marker)?; + func_call(p, marker)?; continue; } @@ -372,8 +372,9 @@ fn expr_prec(p: &mut Parser, atomic: bool, min_prec: usize) -> ParseResult { break; } - if p.at(&NodeKind::With) { - with_expr(p, marker)?; + if p.at(&NodeKind::Dot) { + method_call(p, marker)?; + continue; } let op = if p.eat_if(&NodeKind::Not) { @@ -432,8 +433,8 @@ fn primary(p: &mut Parser, atomic: bool) -> ParseResult { // Structures. Some(NodeKind::LeftParen) => parenthesized(p, atomic), - Some(NodeKind::LeftBrace) => Ok(block(p)), - Some(NodeKind::LeftBracket) => Ok(content(p)), + Some(NodeKind::LeftBrace) => Ok(code_block(p)), + Some(NodeKind::LeftBracket) => Ok(content_block(p)), // Keywords. Some(NodeKind::Let) => let_expr(p), @@ -671,7 +672,7 @@ fn params(p: &mut Parser, marker: Marker) { } /// Parse a code block: `{...}`. -fn block(p: &mut Parser) { +fn code_block(p: &mut Parser) { p.perform(NodeKind::CodeBlock, |p| { p.start_group(Group::Brace); while !p.eof() { @@ -689,7 +690,7 @@ fn block(p: &mut Parser) { } // Parse a content block: `[...]`. -fn content(p: &mut Parser) { +fn content_block(p: &mut Parser) { p.perform(NodeKind::ContentBlock, |p| { p.start_group(Group::Bracket); markup(p, true); @@ -698,8 +699,17 @@ fn content(p: &mut Parser) { } /// Parse a function call. -fn call(p: &mut Parser, callee: Marker) -> ParseResult { - callee.perform(p, NodeKind::CallExpr, |p| args(p, true, true)) +fn func_call(p: &mut Parser, callee: Marker) -> ParseResult { + callee.perform(p, NodeKind::FuncCall, |p| args(p, true, true)) +} + +/// Parse a method call. +fn method_call(p: &mut Parser, marker: Marker) -> ParseResult { + marker.perform(p, NodeKind::MethodCall, |p| { + p.eat_assert(&NodeKind::Dot); + ident(p)?; + args(p, true, true) + }) } /// Parse the arguments to a function call. @@ -721,21 +731,13 @@ fn args(p: &mut Parser, direct: bool, brackets: bool) -> ParseResult { } while brackets && p.peek_direct() == Some(&NodeKind::LeftBracket) { - content(p); + content_block(p); } }); Ok(()) } -/// Parse a with expression. -fn with_expr(p: &mut Parser, marker: Marker) -> ParseResult { - marker.perform(p, NodeKind::WithExpr, |p| { - p.eat_assert(&NodeKind::With); - args(p, false, false) - }) -} - /// Parse a let expression. fn let_expr(p: &mut Parser) -> ParseResult { p.perform(NodeKind::LetExpr, |p| { @@ -744,30 +746,26 @@ fn let_expr(p: &mut Parser) -> ParseResult { let marker = p.marker(); ident(p)?; - if p.at(&NodeKind::With) { - with_expr(p, marker)?; - } else { - // If a parenthesis follows, this is a function definition. - let has_params = p.peek_direct() == Some(&NodeKind::LeftParen); - if has_params { - let marker = p.marker(); - p.start_group(Group::Paren); - collection(p); - p.end_group(); - params(p, marker); - } + // If a parenthesis follows, this is a function definition. + let has_params = p.peek_direct() == Some(&NodeKind::LeftParen); + if has_params { + let marker = p.marker(); + p.start_group(Group::Paren); + collection(p); + p.end_group(); + params(p, marker); + } - if p.eat_if(&NodeKind::Eq) { - expr(p)?; - } else if has_params { - // Function definitions must have a body. - p.expected("body"); - } + if p.eat_if(&NodeKind::Eq) { + expr(p)?; + } else if has_params { + // Function definitions must have a body. + p.expected("body"); + } - // Rewrite into a closure expression if it's a function definition. - if has_params { - marker.end(p, NodeKind::ClosureExpr); - } + // Rewrite into a closure expression if it's a function definition. + if has_params { + marker.end(p, NodeKind::ClosureExpr); } Ok(()) @@ -931,8 +929,8 @@ fn return_expr(p: &mut Parser) -> ParseResult { /// Parse a control flow body. fn body(p: &mut Parser) -> ParseResult { match p.peek() { - Some(NodeKind::LeftBracket) => Ok(content(p)), - Some(NodeKind::LeftBrace) => Ok(block(p)), + Some(NodeKind::LeftBracket) => Ok(content_block(p)), + Some(NodeKind::LeftBrace) => Ok(code_block(p)), _ => { p.expected("body"); Err(ParseError) diff --git a/src/parse/parser.rs b/src/parse/parser.rs index 33cf489c1..63ba49187 100644 --- a/src/parse/parser.rs +++ b/src/parse/parser.rs @@ -352,7 +352,16 @@ impl<'s> Parser<'s> { match self.groups.last().map(|group| group.kind) { Some(Group::Strong | Group::Emph) => n >= 2, - Some(Group::Expr | Group::Imports) => n >= 1, + Some(Group::Imports) => n >= 1, + Some(Group::Expr) if n >= 1 => { + // Allow else and method call to continue on next line. + self.groups.iter().nth_back(1).map(|group| group.kind) + != Some(Group::Brace) + || !matches!( + self.tokens.clone().next(), + Some(NodeKind::Else | NodeKind::Dot) + ) + } _ => false, } } diff --git a/src/parse/tokens.rs b/src/parse/tokens.rs index 752714fd3..0c05d7707 100644 --- a/src/parse/tokens.rs +++ b/src/parse/tokens.rs @@ -10,6 +10,7 @@ use crate::syntax::{ErrorPos, NodeKind}; use crate::util::EcoString; /// An iterator over the tokens of a string of source code. +#[derive(Clone)] pub struct Tokens<'s> { /// The underlying scanner. s: Scanner<'s>, @@ -184,6 +185,7 @@ impl<'s> Tokens<'s> { '=' => NodeKind::Eq, '<' => NodeKind::Lt, '>' => NodeKind::Gt, + '.' if self.s.check_or(true, |n| !n.is_ascii_digit()) => NodeKind::Dot, // Identifiers. c if is_id_start(c) => self.ident(start), @@ -572,7 +574,6 @@ fn keyword(ident: &str) -> Option { "not" => NodeKind::Not, "and" => NodeKind::And, "or" => NodeKind::Or, - "with" => NodeKind::With, "let" => NodeKind::Let, "set" => NodeKind::Set, "show" => NodeKind::Show, @@ -859,6 +860,7 @@ mod tests { t!(Code: "-" => Minus); t!(Code[" a1"]: "*" => Star); t!(Code[" a1"]: "/" => Slash); + t!(Code[" a/"]: "." => Dot); t!(Code: "=" => Eq); t!(Code: "==" => EqEq); t!(Code: "!=" => ExclEq); @@ -875,7 +877,7 @@ mod tests { // Test combinations. t!(Code: "<=>" => LtEq, Gt); - t!(Code[" a/"]: "..." => Dots, Invalid(".")); + t!(Code[" a/"]: "..." => Dots, Dot); // Test hyphen as symbol vs part of identifier. t!(Code[" /"]: "-1" => Minus, Int(1)); diff --git a/src/syntax/ast.rs b/src/syntax/ast.rs index b87805908..cb0a99b9d 100644 --- a/src/syntax/ast.rs +++ b/src/syntax/ast.rs @@ -234,11 +234,11 @@ pub enum Expr { /// A binary operation: `a + b`. Binary(BinaryExpr), /// An invocation of a function: `f(x, y)`. - Call(CallExpr), + FuncCall(FuncCall), + /// An invocation of a method: `array.push(v)`. + MethodCall(MethodCall), /// A closure expression: `(x, y) => z`. Closure(ClosureExpr), - /// A with expression: `f with (x, y: 1)`. - With(WithExpr), /// A let expression: `let x = 1`. Let(LetExpr), /// A set expression: `set text(...)`. @@ -276,9 +276,9 @@ impl TypedNode for Expr { NodeKind::DictExpr => node.cast().map(Self::Dict), NodeKind::UnaryExpr => node.cast().map(Self::Unary), NodeKind::BinaryExpr => node.cast().map(Self::Binary), - NodeKind::CallExpr => node.cast().map(Self::Call), + NodeKind::FuncCall => node.cast().map(Self::FuncCall), + NodeKind::MethodCall => node.cast().map(Self::MethodCall), NodeKind::ClosureExpr => node.cast().map(Self::Closure), - NodeKind::WithExpr => node.cast().map(Self::With), NodeKind::LetExpr => node.cast().map(Self::Let), NodeKind::SetExpr => node.cast().map(Self::Set), NodeKind::ShowExpr => node.cast().map(Self::Show), @@ -306,9 +306,9 @@ impl TypedNode for Expr { Self::Group(v) => v.as_red(), Self::Unary(v) => v.as_red(), Self::Binary(v) => v.as_red(), - Self::Call(v) => v.as_red(), + Self::FuncCall(v) => v.as_red(), + Self::MethodCall(v) => v.as_red(), Self::Closure(v) => v.as_red(), - Self::With(v) => v.as_red(), Self::Let(v) => v.as_red(), Self::Set(v) => v.as_red(), Self::Show(v) => v.as_red(), @@ -331,7 +331,7 @@ impl Expr { matches!( self, Self::Ident(_) - | Self::Call(_) + | Self::FuncCall(_) | Self::Let(_) | Self::Set(_) | Self::Show(_) @@ -735,19 +735,45 @@ pub enum Associativity { } node! { - /// An invocation of a function: `foo(...)`. - CallExpr: CallExpr + /// An invocation of a function: `f(x, y)`. + FuncCall: FuncCall } -impl CallExpr { +impl FuncCall { /// The function to call. pub fn callee(&self) -> Expr { - self.0.cast_first_child().expect("call is missing callee") + self.0.cast_first_child().expect("function call is missing callee") } /// The arguments to the function. pub fn args(&self) -> CallArgs { - self.0.cast_last_child().expect("call is missing argument list") + self.0 + .cast_last_child() + .expect("function call is missing argument list") + } +} + +node! { + /// An invocation of a method: `array.push(v)`. + MethodCall: MethodCall +} + +impl MethodCall { + /// The value to call the method on. + pub fn receiver(&self) -> Expr { + self.0.cast_first_child().expect("method call is missing callee") + } + + /// The name of the method. + pub fn method(&self) -> Ident { + self.0.cast_last_child().expect("method call is missing name") + } + + /// The arguments to the method. + pub fn args(&self) -> CallArgs { + self.0 + .cast_last_child() + .expect("method call is missing argument list") } } @@ -862,25 +888,6 @@ impl TypedNode for ClosureParam { } } -node! { - /// A with expression: `f with (x, y: 1)`. - WithExpr -} - -impl WithExpr { - /// The function to apply the arguments to. - pub fn callee(&self) -> Expr { - self.0.cast_first_child().expect("with expression is missing callee") - } - - /// The arguments to apply to the function. - pub fn args(&self) -> CallArgs { - self.0 - .cast_first_child() - .expect("with expression is missing argument list") - } -} - node! { /// A let expression: `let x = 1`. LetExpr @@ -891,10 +898,6 @@ impl LetExpr { pub fn binding(&self) -> Ident { match self.0.cast_first_child() { Some(Expr::Ident(binding)) => binding, - Some(Expr::With(with)) => match with.callee() { - Expr::Ident(binding) => binding, - _ => panic!("let .. with callee must be identifier"), - }, Some(Expr::Closure(closure)) => { closure.name().expect("let-bound closure is missing name") } diff --git a/src/syntax/highlight.rs b/src/syntax/highlight.rs index c0e3376e4..bad434b98 100644 --- a/src/syntax/highlight.rs +++ b/src/syntax/highlight.rs @@ -11,10 +11,10 @@ pub fn highlight(node: RedRef, range: Range, f: &mut F) where F: FnMut(Range, Category), { - for child in node.children() { + for (i, child) in node.children().enumerate() { let span = child.span(); if range.start <= span.end && range.end >= span.start { - if let Some(category) = Category::determine(child, node) { + if let Some(category) = Category::determine(child, node, i) { f(span.to_range(), category); } highlight(child, range.clone(), f); @@ -44,9 +44,9 @@ fn highlight_syntect_impl( return; } - for child in node.children() { + for (i, child) in node.children().enumerate() { let mut scopes = scopes.clone(); - if let Some(category) = Category::determine(child, node) { + if let Some(category) = Category::determine(child, node, i) { scopes.push(Scope::new(category.tm_scope()).unwrap()) } highlight_syntect_impl(child, scopes, highlighter, f); @@ -101,8 +101,9 @@ pub enum Category { } impl Category { - /// Determine the highlighting category of a node given its parent. - pub fn determine(child: RedRef, parent: RedRef) -> Option { + /// Determine the highlighting category of a node given its parent and its + /// index in its siblings. + pub fn determine(child: RedRef, parent: RedRef, i: usize) -> Option { match child.kind() { NodeKind::LeftBrace => Some(Category::Bracket), NodeKind::RightBrace => Some(Category::Bracket), @@ -133,7 +134,6 @@ impl Category { NodeKind::Not => Some(Category::Keyword), NodeKind::And => Some(Category::Keyword), NodeKind::Or => Some(Category::Keyword), - NodeKind::With => Some(Category::Keyword), NodeKind::Let => Some(Category::Keyword), NodeKind::Set => Some(Category::Keyword), NodeKind::Show => Some(Category::Keyword), @@ -156,6 +156,7 @@ impl Category { _ => Some(Category::Operator), }, NodeKind::Slash => Some(Category::Operator), + NodeKind::Dot => Some(Category::Operator), NodeKind::PlusEq => Some(Category::Operator), NodeKind::HyphEq => Some(Category::Operator), NodeKind::StarEq => Some(Category::Operator), @@ -176,13 +177,11 @@ impl Category { NodeKind::Auto => Some(Category::Auto), NodeKind::Ident(_) => match parent.kind() { NodeKind::Named => None, - NodeKind::ClosureExpr if child.span().start == parent.span().start => { - Some(Category::Function) - } - NodeKind::WithExpr => Some(Category::Function), + NodeKind::ClosureExpr if i == 0 => Some(Category::Function), NodeKind::SetExpr => Some(Category::Function), NodeKind::ShowExpr => Some(Category::Function), - NodeKind::CallExpr => Some(Category::Function), + NodeKind::FuncCall => Some(Category::Function), + NodeKind::MethodCall if i > 0 => Some(Category::Function), _ => Some(Category::Variable), }, NodeKind::Bool(_) => Some(Category::Bool), @@ -210,12 +209,12 @@ impl Category { NodeKind::Named => None, NodeKind::UnaryExpr => None, NodeKind::BinaryExpr => None, - NodeKind::CallExpr => None, + NodeKind::FuncCall => None, + NodeKind::MethodCall => None, NodeKind::CallArgs => None, NodeKind::Spread => None, NodeKind::ClosureExpr => None, NodeKind::ClosureParams => None, - NodeKind::WithExpr => None, NodeKind::LetExpr => None, NodeKind::SetExpr => None, NodeKind::ShowExpr => None, diff --git a/src/syntax/mod.rs b/src/syntax/mod.rs index e15cfabc2..d0920d203 100644 --- a/src/syntax/mod.rs +++ b/src/syntax/mod.rs @@ -509,6 +509,8 @@ pub enum NodeKind { Minus, /// A slash: `/`. Slash, + /// A dot: `.`. + Dot, /// A single equals sign: `=`. Eq, /// Two equals signs: `==`. @@ -537,8 +539,6 @@ pub enum NodeKind { And, /// The `or` operator. Or, - /// The `with` operator. - With, /// Two dots: `..`. Dots, /// An equals sign followed by a greater-than sign: `=>`. @@ -659,7 +659,9 @@ pub enum NodeKind { /// A binary operation: `a + b`. BinaryExpr, /// An invocation of a function: `f(x, y)`. - CallExpr, + FuncCall, + /// An invocation of a method: `array.push(v)`. + MethodCall, /// A function call's argument list: `(x, y)`. CallArgs, /// Spreaded arguments or a parameter sink: `..x`. @@ -668,8 +670,6 @@ pub enum NodeKind { ClosureExpr, /// A closure's parameters: `(x, y)`. ClosureParams, - /// A with expression: `f with (x, y: 1)`. - WithExpr, /// A let expression: `let x = 1`. LetExpr, /// A set expression: `set text(...)`. @@ -802,7 +802,7 @@ impl NodeKind { | Self::WhileExpr | Self::ForExpr | Self::ImportExpr - | Self::CallExpr + | Self::FuncCall | Self::IncludeExpr | Self::LineComment | Self::BlockComment @@ -830,6 +830,7 @@ impl NodeKind { Self::Plus => "plus", Self::Minus => "minus", Self::Slash => "slash", + Self::Dot => "dot", Self::Eq => "assignment operator", Self::EqEq => "equality operator", Self::ExclEq => "inequality operator", @@ -844,7 +845,6 @@ impl NodeKind { Self::Not => "operator `not`", Self::And => "operator `and`", Self::Or => "operator `or`", - Self::With => "operator `with`", Self::Dots => "dots", Self::Arrow => "arrow", Self::None => "`none`", @@ -899,12 +899,12 @@ impl NodeKind { Self::Named => "named argument", Self::UnaryExpr => "unary expression", Self::BinaryExpr => "binary expression", - Self::CallExpr => "call", + Self::FuncCall => "function call", + Self::MethodCall => "method call", Self::CallArgs => "call arguments", Self::Spread => "parameter sink", Self::ClosureExpr => "closure", Self::ClosureParams => "closure parameters", - Self::WithExpr => "`with` expression", Self::LetExpr => "`let` expression", Self::SetExpr => "`set` expression", Self::ShowExpr => "`show` expression", @@ -954,6 +954,7 @@ impl Hash for NodeKind { Self::Plus => {} Self::Minus => {} Self::Slash => {} + Self::Dot => {} Self::Eq => {} Self::EqEq => {} Self::ExclEq => {} @@ -968,7 +969,6 @@ impl Hash for NodeKind { Self::Not => {} Self::And => {} Self::Or => {} - Self::With => {} Self::Dots => {} Self::Arrow => {} Self::None => {} @@ -1023,12 +1023,12 @@ impl Hash for NodeKind { Self::Named => {} Self::UnaryExpr => {} Self::BinaryExpr => {} - Self::CallExpr => {} + Self::FuncCall => {} + Self::MethodCall => {} Self::CallArgs => {} Self::Spread => {} Self::ClosureExpr => {} Self::ClosureParams => {} - Self::WithExpr => {} Self::LetExpr => {} Self::SetExpr => {} Self::ShowExpr => {} diff --git a/tests/ref/code/for.png b/tests/ref/code/for.png index f9d421243..60c505ec0 100644 Binary files a/tests/ref/code/for.png and b/tests/ref/code/for.png differ diff --git a/tests/ref/utility/basics.png b/tests/ref/utility/basics.png deleted file mode 100644 index a80afe5f0..000000000 Binary files a/tests/ref/utility/basics.png and /dev/null differ diff --git a/tests/ref/utility/collection.png b/tests/ref/utility/collection.png new file mode 100644 index 000000000..e93e2beb1 Binary files /dev/null and b/tests/ref/utility/collection.png differ diff --git a/tests/ref/utility/string.png b/tests/ref/utility/string.png new file mode 100644 index 000000000..67563668c Binary files /dev/null and b/tests/ref/utility/string.png differ diff --git a/tests/typ/code/for.typ b/tests/typ/code/for.typ index e161ba84f..822f7423c 100644 --- a/tests/typ/code/for.typ +++ b/tests/typ/code/for.typ @@ -32,10 +32,10 @@ // Should output `2345`. #for v in (1, 2, 3, 4, 5, 6, 7) [#if v >= 2 and v <= 5 { repr(v) }] -// Loop over captured arguments. -#let f1(..args) = for v in args { (repr(v),) } -#let f2(..args) = for k, v in args { (repr(k) + ": " + repr(v),) } -#let f(..args) = join(sep: ", ", ..f1(..args), ..f2(..args)) +// Map captured arguments. +#let f1(..args) = args.positional().map(repr) +#let f2(..args) = args.named().pairs((k, v) => repr(k) + ": " + repr(v)) +#let f(..args) = (f1(..args) + f2(..args)).join(", ") #f(1, a: 2) --- diff --git a/tests/typ/code/if.typ b/tests/typ/code/if.typ index 0ab5c495f..0d87c689b 100644 --- a/tests/typ/code/if.typ +++ b/tests/typ/code/if.typ @@ -60,10 +60,10 @@ #let nth(n) = { str(n) - (if n == 1 { "st" } - else if n == 2 { "nd" } - else if n == 3 { "rd" } - else { "th" }) + if n == 1 { "st" } + else if n == 2 { "nd" } + else if n == 3 { "rd" } + else { "th" } } #test(nth(1), "1st") diff --git a/tests/typ/code/methods.typ b/tests/typ/code/methods.typ new file mode 100644 index 000000000..b5eff78d7 --- /dev/null +++ b/tests/typ/code/methods.typ @@ -0,0 +1,50 @@ +// Test method calls. +// Ref: false + +--- +// Test whitespace around dot. +#test( "Hi there" . split() , ("Hi", "there")) + +--- +// Test mutating indexed value. +{ + let matrix = (((1,), (2,)), ((3,), (4,))) + matrix(1)(0).push(5) + test(matrix, (((1,), (2,)), ((3, 5), (4,)))) +} + +--- +// Test multiline chain in code block. +{ + let rewritten = "Hello. This is a sentence. And one more." + .split(".") + .map(s => s.trim()) + .filter(s => s != "") + .map(s => s + "!") + .join([\ ]) + + test(rewritten, [Hello!\ This is a sentence!\ And one more!]) +} + +--- +// Error: 2:3-2:16 type array has no method `fun` +#let numbers = () +{ numbers.fun() } + +--- +// Error: 2:3-2:44 cannot mutate a temporary value +#let numbers = (1, 2, 3) +{ numbers.map(v => v / 2).sorted().map(str).remove(4) } + +--- +// Error: 2:3-2:19 cannot mutate a temporary value +#let numbers = (1, 2, 3) +{ numbers.sorted() = 1 } + +--- +// Error: 3-6 cannot mutate a constant +{ box = 1 } + +--- +// Error: 3-6 cannot mutate a constant +{ box.push(1) } diff --git a/tests/typ/code/ops-invalid.typ b/tests/typ/code/ops-invalid.typ index 184e20cfa..68bce4afc 100644 --- a/tests/typ/code/ops-invalid.typ +++ b/tests/typ/code/ops-invalid.typ @@ -65,19 +65,11 @@ { let x = 1; x += "2" } --- -// Error: 13-14 expected argument list, found integer -{ test with 2 } - ---- -// Error: 3-4 expected function, found integer -{ 1 with () } - ---- -// Error: 3-6 cannot access this expression mutably +// Error: 3-6 cannot mutate a temporary value { (x) = "" } --- -// Error: 3-8 cannot access this expression mutably +// Error: 3-8 cannot mutate a temporary value { 1 + 2 += 3 } --- diff --git a/tests/typ/code/ops-prec.typ b/tests/typ/code/ops-prec.typ index 2cec0d046..23afcc5f8 100644 --- a/tests/typ/code/ops-prec.typ +++ b/tests/typ/code/ops-prec.typ @@ -13,7 +13,7 @@ #test(not "b" == "b", false) // Assignment binds stronger than boolean operations. -// Error: 2-7 cannot access this expression mutably +// Error: 2-7 cannot mutate a temporary value {not x = "a"} --- diff --git a/tests/typ/code/ops.typ b/tests/typ/code/ops.typ index 899ee71c1..79743f5db 100644 --- a/tests/typ/code/ops.typ +++ b/tests/typ/code/ops.typ @@ -184,21 +184,19 @@ {"a" not} --- -// Test `with` operator. +// Test `with` method. // Apply positional arguments. #let add(x, y) = x + y -#test((add with (2))(4), 6) - -// Let .. with .. syntax. -#let f = add -#let f with (2) -#test(f(4), 6) +#test(add.with(2)(3), 5) +#test(add.with(2).with(3)(), 5) +#test((add.with(2))(4), 6) +#test((add.with(2).with(3))(), 5) // Make sure that named arguments are overridable. #let inc(x, y: 1) = x + y #test(inc(1), 2) -#let inc with (y: 2) -#test(inc(2), 4) -#test(inc(2, y: 4), 6) +#let inc2 = inc.with(y: 2) +#test(inc2(2), 4) +#test(inc2(2, y: 4), 6) diff --git a/tests/typ/code/target.typ b/tests/typ/code/target.typ index 735168173..6c3215920 100644 --- a/tests/typ/code/target.typ +++ b/tests/typ/code/target.typ @@ -7,6 +7,6 @@ #let d = 3 #let value = [hi] #let item(a, b) = a + b -#let fn = rect with (fill: conifer, padding: 5pt) +#let fn = rect.with(fill: conifer, padding: 5pt) Some _includable_ text. diff --git a/tests/typ/graphics/line.typ b/tests/typ/graphics/line.typ index 452e52f3c..050ce05c9 100644 --- a/tests/typ/graphics/line.typ +++ b/tests/typ/graphics/line.typ @@ -34,10 +34,10 @@ #line(length: +30%, origin: (25.4%, 48%), angle: -36deg) #line(length: +30%, origin: (25.6%, 48%), angle: -72deg) #line(length: +32%, origin: (8.50%, 02%), angle: 34deg) - ] + ] ] -#align(center, grid(columns: (1fr, ) * 3, ..((star(20pt, thickness: .5pt), ) * 9))) +#align(center, grid(columns: (1fr,) * 3, ..((star(20pt, thickness: .5pt),) * 9))) --- // Test errors. diff --git a/tests/typ/graphics/shape-fill-stroke.typ b/tests/typ/graphics/shape-fill-stroke.typ index 935f3bc7e..dd5b9ee8b 100644 --- a/tests/typ/graphics/shape-fill-stroke.typ +++ b/tests/typ/graphics/shape-fill-stroke.typ @@ -1,22 +1,22 @@ // Test shape fill & stroke. --- -#let rect with (width: 20pt, height: 10pt) -#let items = for i, rect in ( - rect(stroke: none), - rect(), - rect(fill: none), - rect(thickness: 2pt), - rect(stroke: eastern), - rect(stroke: eastern, thickness: 2pt), - rect(fill: eastern), - rect(fill: eastern, stroke: none), - rect(fill: forest, stroke: none, thickness: 2pt), - rect(fill: forest, stroke: conifer), - rect(fill: forest, stroke: black, thickness: 2pt), - rect(fill: forest, stroke: conifer, thickness: 2pt), +#let variant = rect.with(width: 20pt, height: 10pt) +#let items = for i, item in ( + variant(stroke: none), + variant(), + variant(fill: none), + variant(thickness: 2pt), + variant(stroke: eastern), + variant(stroke: eastern, thickness: 2pt), + variant(fill: eastern), + variant(fill: eastern, stroke: none), + variant(fill: forest, stroke: none, thickness: 2pt), + variant(fill: forest, stroke: conifer), + variant(fill: forest, stroke: black, thickness: 2pt), + variant(fill: forest, stroke: conifer, thickness: 2pt), ) { - (align(horizon)[{i + 1}.], rect, []) + (align(horizon)[{i + 1}.], item, []) } #grid( diff --git a/tests/typ/text/deco.typ b/tests/typ/text/deco.typ index e0693ca37..a9f380b9c 100644 --- a/tests/typ/text/deco.typ +++ b/tests/typ/text/deco.typ @@ -19,8 +19,8 @@ #overline(underline[Running amongst the wolves.]) --- -#let redact = strike with (10pt, extent: 5%) -#let highlight = strike with ( +#let redact = strike.with(10pt, extent: 5%) +#let highlight = strike.with( stroke: rgb("abcdef88"), thickness: 10pt, extent: 5%, diff --git a/tests/typ/utility/basics.typ b/tests/typ/utility/basics.typ index 7fccc781b..83d192c4e 100644 --- a/tests/typ/utility/basics.typ +++ b/tests/typ/utility/basics.typ @@ -21,64 +21,4 @@ // Test the `type` function. #test(type(1), "integer") #test(type(ltr), "direction") - ---- -// Test the `repr` function. -#test(repr(ltr), "ltr") -#test(repr((1, 2, false, )), "(1, 2, false)") - ---- -// Test the `join` function. -#test(join(), none) -#test(join(sep: false), none) -#test(join(1), 1) -#test(join("a", "b", "c"), "abc") -#test("(" + join("a", "b", "c", sep: ", ") + ")", "(a, b, c)") - ---- -// Test content joining. -// Ref: true -#join([One], [Two], [Three], sep: [, ]). - ---- -// Error: 11-24 cannot join boolean with boolean -#test(join(true, false)) - ---- -// Error: 11-29 cannot join string with integer -#test(join("a", "b", sep: 1)) - ---- -// Test conversion functions. -#test(int(false), 0) -#test(int(true), 1) -#test(int(10), 10) -#test(int("150"), 150) #test(type(10 / 3), "float") -#test(int(10 / 3), 3) -#test(float(10), 10.0) -#test(float("31.4e-1"), 3.14) -#test(type(float(10)), "float") -#test(str(123), "123") -#test(str(50.14), "50.14") -#test(len(str(10 / 3)) > 10, true) - ---- -// Error: 6-10 cannot convert length to integer -#int(10pt) - ---- -// Error: 8-13 cannot convert function to float -#float(float) - ---- -// Error: 6-8 cannot convert content to string -#str([]) - ---- -// Error: 6-12 invalid integer -#int("nope") - ---- -// Error: 8-15 invalid float -#float("1.2.3") diff --git a/tests/typ/utility/collection.typ b/tests/typ/utility/collection.typ index e8be07b57..924200cb0 100644 --- a/tests/typ/utility/collection.typ +++ b/tests/typ/utility/collection.typ @@ -2,42 +2,91 @@ // Ref: false --- -// Test the `len` function. -#test(len(()), 0) -#test(len(("A", "B", "C")), 3) -#test(len("Hello World!"), 12) -#test(len((a: 1, b: 2)), 2) +// 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) --- -// Error: 5-7 missing argument: collection -#len() +// 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)) +} --- -// Error: 6-10 expected string, array or dictionary, found length -#len(12pt) +// 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)) +} --- -// Test the `upper` and `lower` functions. -#let memes = "ArE mEmEs gReAt?"; -#test(lower(memes), "are memes great?") -#test(upper(memes), "ARE MEMES GREAT?") -#test(upper("Ελλάδα"), "ΕΛΛΆΔΑ") +// Test the `find` method. +#test(("Hi", "❤️", "Love").find("❤️"), 1) +#test(("Bye", "💘", "Apart").find("❤️"), none) --- -// Error: 8-9 expected string or content, found integer -#upper(1) +// 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 the `sorted` function. -#test(sorted(()), ()) -#test(sorted((true, false) * 10), (false,) * 10 + (true,) * 10) -#test(sorted(("it", "the", "hi", "text")), ("hi", "it", "text", "the")) -#test(sorted((2, 1, 3, 10, 5, 8, 6, -7, 2)), (-7, 1, 2, 2, 3, 5, 6, 8, 10)) +// Error: 3-31 array index out of bounds (index: 12, len: 10) +{ range(10).slice(9, count: 3) } --- -// Error: 9-21 cannot order string and integer -#sorted((1, 2, "ab")) +// Error: 2:17-2:19 missing argument: index +#let numbers = () +{ numbers.insert() } --- -// Error: 9-24 cannot order content and content -#sorted(([Hi], [There])) +// 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/utility/math.typ b/tests/typ/utility/math.typ index ec62dbd23..d4ac7aa27 100644 --- a/tests/typ/utility/math.typ +++ b/tests/typ/utility/math.typ @@ -1,6 +1,33 @@ // Test math functions. // Ref: false +--- +// Test conversion to numbers. +#test(int(false), 0) +#test(int(true), 1) +#test(int(10), 10) +#test(int("150"), 150) +#test(int(10 / 3), 3) +#test(float(10), 10.0) +#test(float("31.4e-1"), 3.14) +#test(type(float(10)), "float") + +--- +// Error: 6-10 cannot convert length to integer +#int(10pt) + +--- +// Error: 8-13 cannot convert function to float +#float(float) + +--- +// Error: 6-12 invalid integer +#int("nope") + +--- +// Error: 8-15 invalid float +#float("1.2.3") + --- // Test the `abs` function. #test(abs(-3), 3) diff --git a/tests/typ/utility/numbering.typ b/tests/typ/utility/numbering.typ deleted file mode 100644 index 65dc12d00..000000000 --- a/tests/typ/utility/numbering.typ +++ /dev/null @@ -1,19 +0,0 @@ -// Test numbering formatting functions. - ---- -#upper("Abc 8") -#upper[def] - -#lower("SCREAMING MUST BE SILENCED in " + roman(1672) + " years") - -#for i in range(9) { - symbol(i) - [ and ] - roman(i) - [ for #i] - parbreak() -} - ---- -// Error: 9-11 must be at least zero -#symbol(-1) diff --git a/tests/typ/utility/string.typ b/tests/typ/utility/string.typ new file mode 100644 index 000000000..9b57e833c --- /dev/null +++ b/tests/typ/utility/string.typ @@ -0,0 +1,52 @@ +// Test string related methods. +// Ref: false + +--- +// Test conversion to string. +#test(str(123), "123") +#test(str(50.14), "50.14") +#test(str(10 / 3).len() > 10, true) +#test(repr(ltr), "ltr") +#test(repr((1, 2, false, )), "(1, 2, false)") + +--- +// Error: 6-8 cannot convert content to string +#str([]) + +--- +// Test the `split` and `trim` methods. +#test( + "Typst, LaTeX, Word, InDesign".split(",").map(s => s.trim()), + ("Typst", "LaTeX", "Word", "InDesign"), +) + +--- +// Test the `upper` and `lower` functions. +#let memes = "ArE mEmEs gReAt?"; +#test(lower(memes), "are memes great?") +#test(upper(memes), "ARE MEMES GREAT?") +#test(upper("Ελλάδα"), "ΕΛΛΆΔΑ") + +--- +// Error: 8-9 expected string or content, found integer +#upper(1) + +--- +// Error: 9-11 must be at least zero +#symbol(-1) + +--- +// Test integrated lower, upper and symbols. +// Ref: true +#upper("Abc 8") +#upper[def] + +#lower("SCREAMING MUST BE SILENCED in " + roman(1672) + " years") + +#for i in range(9) { + symbol(i) + [ and ] + roman(i) + [ for #i] + parbreak() +} diff --git a/tools/support/typst.tmLanguage.json b/tools/support/typst.tmLanguage.json index ce683699a..ef7806c86 100644 --- a/tools/support/typst.tmLanguage.json +++ b/tools/support/typst.tmLanguage.json @@ -124,7 +124,7 @@ }, { "name": "keyword.other.typst", - "match": "(#)(as|in|with|from)\\b", + "match": "(#)(as|in|from)\\b", "captures": { "1": { "name": "punctuation.definition.keyword.typst" } } }, { @@ -168,19 +168,19 @@ { "comment": "Function name", "name": "entity.name.function.typst", - "match": "((#)[[:alpha:]_][[:alnum:]._-]*!?)(?=\\[|\\()", + "match": "((#)[[:alpha:]_][[:alnum:]_-]*!?)(?=\\[|\\()", "captures": { "2": { "name": "punctuation.definition.function.typst" } } }, { "comment": "Function arguments", - "begin": "(?<=#[[:alpha:]_][[:alnum:]._-]*!?)\\(", + "begin": "(?<=#[[:alpha:]_][[:alnum:]_-]*!?)\\(", "end": "\\)", "captures": { "0": { "name": "punctuation.definition.group.typst" } }, "patterns": [{ "include": "#arguments" }] }, { "name": "variable.interpolated.typst", - "match": "(#)[[:alpha:]_][[:alnum:]._-]*", + "match": "(#)[[:alpha:]_][[:alnum:]_-]*", "captures": { "1": { "name": "punctuation.definition.variable.typst" } } } ] @@ -216,7 +216,7 @@ }, { "name": "keyword.operator.arithmetic.typst", - "match": "\\+|\\*|/|(?