From beca01c826ee51c9ee6d5eadd7e5ef10f7fb9f58 Mon Sep 17 00:00:00 2001 From: Laurenz Date: Fri, 18 Mar 2022 23:36:18 +0100 Subject: [PATCH] Methods --- Cargo.lock | 82 ++++++++ Cargo.toml | 1 + src/eval/args.rs | 19 +- src/eval/array.rs | 145 ++++++++++++-- src/eval/dict.rs | 43 ++++- src/eval/mod.rs | 150 +++++++++------ src/eval/ops.rs | 19 +- src/eval/scope.rs | 8 +- src/eval/str.rs | 37 ++++ src/eval/value.rs | 121 +++++++++++- src/geom/angle.rs | 2 +- src/library/mod.rs | 9 +- src/library/utility/color.rs | 58 ++++++ src/library/utility/math.rs | 29 +++ src/library/utility/mod.rs | 180 +----------------- .../utility/{numbering.rs => string.rs} | 37 ++++ src/parse/incremental.rs | 8 +- src/parse/mod.rs | 94 +++++---- src/parse/parser.rs | 11 +- src/parse/tokens.rs | 6 +- src/syntax/ast.rs | 75 ++++---- src/syntax/highlight.rs | 27 ++- src/syntax/mod.rs | 24 +-- tests/ref/code/for.png | Bin 4003 -> 3521 bytes tests/ref/utility/basics.png | Bin 1170 -> 0 bytes tests/ref/utility/collection.png | Bin 0 -> 1384 bytes tests/ref/utility/string.png | Bin 0 -> 10532 bytes tests/typ/code/for.typ | 8 +- tests/typ/code/if.typ | 8 +- tests/typ/code/methods.typ | 50 +++++ tests/typ/code/ops-invalid.typ | 12 +- tests/typ/code/ops-prec.typ | 2 +- tests/typ/code/ops.typ | 18 +- tests/typ/code/target.typ | 2 +- tests/typ/graphics/line.typ | 4 +- tests/typ/graphics/shape-fill-stroke.typ | 30 +-- tests/typ/text/deco.typ | 4 +- tests/typ/utility/basics.typ | 60 ------ tests/typ/utility/collection.typ | 99 +++++++--- tests/typ/utility/math.typ | 27 +++ tests/typ/utility/numbering.typ | 19 -- tests/typ/utility/string.typ | 52 +++++ tools/support/typst.tmLanguage.json | 24 +-- 43 files changed, 1041 insertions(+), 563 deletions(-) create mode 100644 src/eval/str.rs create mode 100644 src/library/utility/color.rs rename src/library/utility/{numbering.rs => string.rs} (69%) delete mode 100644 tests/ref/utility/basics.png create mode 100644 tests/ref/utility/collection.png create mode 100644 tests/ref/utility/string.png create mode 100644 tests/typ/code/methods.typ delete mode 100644 tests/typ/utility/numbering.typ create mode 100644 tests/typ/utility/string.typ 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 f9d42124379567d17a461d83f2ab0cdb52f75677..60c505ec0ed67daa22d13324075f4e15a25dd469 100644 GIT binary patch delta 3402 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 diff --git a/tests/ref/utility/collection.png b/tests/ref/utility/collection.png new file mode 100644 index 0000000000000000000000000000000000000000..e93e2beb116ccdfa85655850978bc9c0d797b149 GIT binary patch literal 1384 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 n1?ozt?EXW!@_)@6YW=@}Q6tpg literal 0 HcmV?d00001 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": "\\+|\\*|/|(?