diff --git a/Cargo.toml b/Cargo.toml index 0c39f4876..d13c5d6f5 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -11,7 +11,6 @@ members = ["main"] opt-level = 2 [dependencies] -async-trait = "0.1" fontdock = { path = "../fontdock" } tide = { path = "../tide" } ttf-parser = "0.8.2" diff --git a/benches/bench_parsing.rs b/benches/bench_parsing.rs index d6b63cafc..a3a17a84e 100644 --- a/benches/bench_parsing.rs +++ b/benches/bench_parsing.rs @@ -1,22 +1,18 @@ use criterion::{criterion_group, criterion_main, Criterion}; -use typstc::library::_std; -use typstc::syntax::parsing::{parse, ParseState}; +use typstc::syntax::parsing::parse; use typstc::syntax::span::Pos; // 28 not too dense lines. const COMA: &str = include_str!("../tests/coma.typ"); fn parsing_benchmark(c: &mut Criterion) { - let state = ParseState { scope: _std() }; - c.bench_function("parse-coma-28-lines", |b| { - b.iter(|| parse(COMA, Pos::ZERO, &state)) + b.iter(|| parse(COMA, Pos::ZERO)) }); - // 2800 lines of Typst code. let long = COMA.repeat(100); c.bench_function("parse-coma-2800-lines", |b| { - b.iter(|| parse(&long, Pos::ZERO, &state)) + b.iter(|| parse(&long, Pos::ZERO)) }); } diff --git a/src/color.rs b/src/color.rs new file mode 100644 index 000000000..1feca8ef2 --- /dev/null +++ b/src/color.rs @@ -0,0 +1,131 @@ +//! Color handling. + +use std::fmt::{self, Debug, Formatter}; +use std::str::FromStr; + +/// An 8-bit RGBA color. +/// +/// # Example +/// ```typst +/// [page: background=#423abaff] +/// ^^^^^^^^ +/// ``` +#[derive(Copy, Clone, Eq, PartialEq, Hash)] +pub struct RgbaColor { + /// Red channel. + pub r: u8, + /// Green channel. + pub g: u8, + /// Blue channel. + pub b: u8, + /// Alpha channel. + pub a: u8, + /// This is true if this value was provided as a fail-over by the parser + /// because the user-defined value was invalid. This color may be + /// overwritten if this property is true. + pub healed: bool, +} + +impl RgbaColor { + /// Constructs a new color. + pub fn new(r: u8, g: u8, b: u8, a: u8) -> Self { + Self { r, g, b, a, healed: false } + } + + /// Constructs a new color with the healed property set to true. + pub fn new_healed(r: u8, g: u8, b: u8, a: u8) -> Self { + Self { r, g, b, a, healed: true } + } +} + +impl FromStr for RgbaColor { + type Err = ParseColorError; + + /// Constructs a new color from a hex string like `7a03c2`. Do not specify a + /// leading `#`. + fn from_str(hex_str: &str) -> Result { + if !hex_str.is_ascii() { + return Err(ParseColorError); + } + + let len = hex_str.len(); + let long = len == 6 || len == 8; + let short = len == 3 || len == 4; + let alpha = len == 4 || len == 8; + + if !long && !short { + return Err(ParseColorError); + } + + let mut values: [u8; 4] = [255; 4]; + + for elem in if alpha { 0..4 } else { 0..3 } { + let item_len = if long { 2 } else { 1 }; + let pos = elem * item_len; + + let item = &hex_str[pos..(pos+item_len)]; + values[elem] = u8::from_str_radix(item, 16) + .map_err(|_| ParseColorError)?; + + if short { + // Duplicate number for shorthand notation, i.e. `a` -> `aa` + values[elem] += values[elem] * 16; + } + } + + Ok(Self::new(values[0], values[1], values[2], values[3])) + } +} + +impl Debug for RgbaColor { + fn fmt(&self, f: &mut Formatter) -> fmt::Result { + if f.alternate() { + write!( + f, "rgba({:02}, {:02}, {:02}, {:02})", + self.r, self.g, self.b, self.a, + )?; + } else { + write!( + f, "#{:02x}{:02x}{:02x}{:02x}", + self.r, self.g, self.b, self.a, + )?; + } + if self.healed { + f.write_str(" [healed]")?; + } + Ok(()) + } +} + +/// The error when parsing an `RgbaColor` fails. +#[derive(Debug, Copy, Clone, Eq, PartialEq)] +pub struct ParseColorError; + +impl std::error::Error for ParseColorError {} + +impl fmt::Display for ParseColorError { + fn fmt(&self, f: &mut Formatter) -> fmt::Result { + f.pad("invalid color") + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn parse_color_strings() { + fn test(hex: &str, r: u8, g: u8, b: u8, a: u8) { + assert_eq!( + RgbaColor::from_str(hex), + Ok(RgbaColor::new(r, g, b, a)), + ); + } + + test("f61243ff", 0xf6, 0x12, 0x43, 0xff); + test("b3d8b3", 0xb3, 0xd8, 0xb3, 0xff); + test("fCd2a9AD", 0xfc, 0xd2, 0xa9, 0xad); + test("233", 0x22, 0x33, 0x33, 0xff); + test("111b", 0x11, 0x11, 0x11, 0xbb); + } +} diff --git a/src/compute/mod.rs b/src/compute/mod.rs new file mode 100644 index 000000000..ac278243e --- /dev/null +++ b/src/compute/mod.rs @@ -0,0 +1,5 @@ +//! Building blocks for the computational part. + +pub mod scope; +pub mod table; +pub mod value; diff --git a/src/compute/scope.rs b/src/compute/scope.rs new file mode 100644 index 000000000..1fd4db0b2 --- /dev/null +++ b/src/compute/scope.rs @@ -0,0 +1,50 @@ +//! Mapping from identifiers to functions. + +use std::collections::HashMap; +use std::fmt::{self, Debug, Formatter}; + +use super::value::FuncValue; + +/// A map from identifiers to functions. +pub struct Scope { + functions: HashMap, + fallback: FuncValue, +} + +impl Scope { + // Create a new empty scope with a fallback function that is invoked when no + // match is found. + pub fn new(fallback: FuncValue) -> Self { + Self { + functions: HashMap::new(), + fallback, + } + } + + /// Associate the given name with the function. + pub fn insert(&mut self, name: impl Into, function: FuncValue) { + self.functions.insert(name.into(), function); + } + + /// Return the function with the given name if there is one. + pub fn func(&self, name: &str) -> Option<&FuncValue> { + self.functions.get(name) + } + + /// Return the function with the given name or the fallback if there is no + /// such function. + pub fn func_or_fallback(&self, name: &str) -> &FuncValue { + self.func(name).unwrap_or_else(|| self.fallback()) + } + + /// Return the fallback function. + pub fn fallback(&self) -> &FuncValue { + &self.fallback + } +} + +impl Debug for Scope { + fn fmt(&self, f: &mut Formatter) -> fmt::Result { + f.debug_set().entries(self.functions.keys()).finish() + } +} diff --git a/src/table.rs b/src/compute/table.rs similarity index 84% rename from src/table.rs rename to src/compute/table.rs index 2d07ee5e1..f11eacfce 100644 --- a/src/table.rs +++ b/src/compute/table.rs @@ -1,17 +1,17 @@ -//! A table data structure. +//! A key-value map that can also model array-like structures. use std::collections::BTreeMap; use std::fmt::{self, Debug, Formatter}; use std::ops::Index; -/// A table is a key-value map that can also model array-like structures. +use crate::syntax::span::{Span, Spanned}; + +/// A table data structure, which maps from integers (`u64`) or strings to a +/// generic value type. /// -/// An array-like table assigns value to successive indices from `0..n`. The -/// table type offers special support for this pattern through the `push` -/// method. -/// -/// The keys of a table may be strings or integers (`u64`). The table is generic -/// over the value type. +/// The table can be used to model arrays by assigns values to successive +/// indices from `0..n`. The table type offers special support for this pattern +/// through the `push` method. #[derive(Clone)] pub struct Table { nums: BTreeMap, @@ -224,8 +224,8 @@ impl From for OwnedKey { } } -impl From<&str> for OwnedKey { - fn from(string: &str) -> Self { +impl From<&'static str> for OwnedKey { + fn from(string: &'static str) -> Self { Self::Str(string.to_string()) } } @@ -255,6 +255,46 @@ impl<'a> From<&'a str> for BorrowedKey<'a> { } } +/// An table entry which tracks key and value span. +#[derive(Clone, PartialEq)] +pub struct SpannedEntry { + pub key: Span, + pub val: Spanned, +} + +impl SpannedEntry { + /// Create a new entry. + pub fn new(key: Span, val: Spanned) -> Self { + Self { key, val } + } + + /// Create an entry with the same span for key and value. + pub fn val(val: Spanned) -> Self { + Self { key: Span::ZERO, val } + } + + /// Convert from `&SpannedEntry` to `SpannedEntry<&T>` + pub fn as_ref(&self) -> SpannedEntry<&V> { + SpannedEntry { key: self.key, val: self.val.as_ref() } + } + + /// Map the entry to a different value type. + pub fn map(self, f: impl FnOnce(V) -> U) -> SpannedEntry { + SpannedEntry { key: self.key, val: self.val.map(f) } + } +} + +impl Debug for SpannedEntry { + fn fmt(&self, f: &mut Formatter) -> fmt::Result { + if f.alternate() { + f.write_str("key")?; + self.key.fmt(f)?; + f.write_str(" ")?; + } + self.val.fmt(f) + } +} + #[cfg(test)] mod tests { use super::Table; diff --git a/src/compute/value.rs b/src/compute/value.rs new file mode 100644 index 000000000..daa3b17be --- /dev/null +++ b/src/compute/value.rs @@ -0,0 +1,474 @@ +//! Computational values: Syntactical expressions can be evaluated into these. + +use std::fmt::{self, Debug, Formatter}; +use std::rc::Rc; + +use fontdock::{FontStyle, FontWeight, FontWidth}; + +use crate::color::RgbaColor; +use crate::layout::{Commands, Dir, LayoutContext, SpecAlign}; +use crate::length::{Length, ScaleLength}; +use crate::paper::Paper; +use crate::syntax::span::{Span, Spanned}; +use crate::syntax::tree::SyntaxTree; +use crate::syntax::Ident; +use crate::{DynFuture, Feedback, Pass}; +use super::table::{BorrowedKey, SpannedEntry, Table}; + +/// A computational value. +#[derive(Clone)] +pub enum Value { + /// An identifier: `ident`. + Ident(Ident), + /// A string: `"string"`. + Str(String), + /// A boolean: `true, false`. + Bool(bool), + /// A number: `1.2, 200%`. + Number(f64), + /// A length: `2cm, 5.2in`. + Length(Length), + /// A color value with alpha channel: `#f79143ff`. + Color(RgbaColor), + /// A table value: `(false, 12cm, greeting="hi")`. + Table(TableValue), + /// A syntax tree containing typesetting content. + Tree(SyntaxTree), + /// An executable function. + Func(FuncValue), + /// Layouting commands. + Commands(Commands), +} + +impl Value { + /// A natural-language name of the type of this expression, e.g. + /// "identifier". + pub fn name(&self) -> &'static str { + use Value::*; + match self { + Ident(_) => "identifier", + Str(_) => "string", + Bool(_) => "bool", + Number(_) => "number", + Length(_) => "length", + Color(_) => "color", + Table(_) => "table", + Tree(_) => "syntax tree", + Func(_) => "function", + Commands(_) => "commands", + } + } +} + +impl Debug for Value { + fn fmt(&self, f: &mut Formatter) -> fmt::Result { + use Value::*; + match self { + Ident(i) => i.fmt(f), + Str(s) => s.fmt(f), + Bool(b) => b.fmt(f), + Number(n) => n.fmt(f), + Length(s) => s.fmt(f), + Color(c) => c.fmt(f), + Table(t) => t.fmt(f), + Tree(t) => t.fmt(f), + Func(_) => f.pad(""), + Commands(c) => c.fmt(f), + } + } +} + +impl PartialEq for Value { + fn eq(&self, other: &Self) -> bool { + use Value::*; + match (self, other) { + (Ident(a), Ident(b)) => a == b, + (Str(a), Str(b)) => a == b, + (Bool(a), Bool(b)) => a == b, + (Number(a), Number(b)) => a == b, + (Length(a), Length(b)) => a == b, + (Color(a), Color(b)) => a == b, + (Table(a), Table(b)) => a == b, + (Tree(a), Tree(b)) => a == b, + (Func(a), Func(b)) => { + a.as_ref() as *const _ == b.as_ref() as *const _ + } + (Commands(a), Commands(b)) => a == b, + _ => false, + } + } +} + +/// An executable function value. +/// +/// The first argument is a table containing the arguments passed to the +/// function. The function may be asynchronous (as such it returns a dynamic +/// future) and it may emit diagnostics, which are contained in the returned +/// `Pass`. In the end, the function must evaluate to `Value`. Your typical +/// typesetting function will return a `Commands` value which will instruct the +/// layouting engine to do what the function pleases. +/// +/// The dynamic function object is wrapped in an `Rc` to keep `Value` clonable. +pub type FuncValue = Rc< + dyn Fn(TableValue, LayoutContext<'_>) -> DynFuture> +>; + +/// A table of values. +/// +/// # Example +/// ```typst +/// (false, 12cm, greeting="hi") +/// ``` +pub type TableValue = Table>; + +impl TableValue { + /// Retrieve and remove the matching value with the lowest number key, + /// skipping and ignoring all non-matching entries with lower keys. + pub fn take(&mut self) -> Option { + for (&key, entry) in self.nums() { + let expr = entry.val.as_ref(); + if let Some(val) = T::try_from_value(expr, &mut Feedback::new()) { + self.remove(key); + return Some(val); + } + } + None + } + + /// Retrieve and remove the matching value with the lowest number key, + /// removing and generating errors for all non-matching entries with lower + /// keys. + pub fn expect(&mut self, f: &mut Feedback) -> Option { + while let Some((num, _)) = self.first() { + let entry = self.remove(num).unwrap(); + if let Some(val) = T::try_from_value(entry.val.as_ref(), f) { + return Some(val); + } + } + None + } + + /// Retrieve and remove a matching value associated with the given key if + /// there is any. + /// + /// Generates an error if the key exists but the value does not match. + pub fn take_with_key<'a, K, T>(&mut self, key: K, f: &mut Feedback) -> Option + where + K: Into>, + T: TryFromValue, + { + self.remove(key).and_then(|entry| { + let expr = entry.val.as_ref(); + T::try_from_value(expr, f) + }) + } + + /// Retrieve and remove all matching pairs with number keys, skipping and + /// ignoring non-matching entries. + /// + /// The pairs are returned in order of increasing keys. + pub fn take_all_num<'a, T>(&'a mut self) -> impl Iterator + 'a + where + T: TryFromValue, + { + let mut skip = 0; + std::iter::from_fn(move || { + for (&key, entry) in self.nums().skip(skip) { + let expr = entry.val.as_ref(); + if let Some(val) = T::try_from_value(expr, &mut Feedback::new()) { + self.remove(key); + return Some((key, val)); + } + skip += 1; + } + + None + }) + } + + + /// Retrieve and remove all matching values with number keys, skipping and + /// ignoring non-matching entries. + /// + /// The values are returned in order of increasing keys. + pub fn take_all_num_vals<'a, T: 'a>(&'a mut self) -> impl Iterator + 'a + where + T: TryFromValue, + { + self.take_all_num::().map(|(_, v)| v) + } + + /// Retrieve and remove all matching pairs with string keys, skipping and + /// ignoring non-matching entries. + /// + /// The pairs are returned in order of increasing keys. + pub fn take_all_str<'a, T>(&'a mut self) -> impl Iterator + 'a + where + T: TryFromValue, + { + let mut skip = 0; + std::iter::from_fn(move || { + for (key, entry) in self.strs().skip(skip) { + let expr = entry.val.as_ref(); + if let Some(val) = T::try_from_value(expr, &mut Feedback::new()) { + let key = key.clone(); + self.remove(&key); + return Some((key, val)); + } + skip += 1; + } + + None + }) + } + + /// Generated `"unexpected argument"` errors for all remaining entries. + pub fn unexpected(&self, f: &mut Feedback) { + for entry in self.values() { + let span = Span::merge(entry.key, entry.val.span); + error!(@f, span, "unexpected argument"); + } + } +} + +/// A trait for converting values into more specific types. +pub trait TryFromValue: Sized { + // This trait takes references because we don't want to move the value + // out of its origin in case this returns `None`. This solution is not + // perfect because we need to do some cloning in the impls for this trait, + // but we haven't got a better solution, for now. + + /// Try to convert a value to this type. + /// + /// Returns `None` and generates an appropriate error if the value is not + /// valid for this type. + fn try_from_value(value: Spanned<&Value>, f: &mut Feedback) -> Option; +} + +macro_rules! impl_match { + ($type:ty, $name:expr, $($p:pat => $r:expr),* $(,)?) => { + impl TryFromValue for $type { + fn try_from_value(value: Spanned<&Value>, f: &mut Feedback) -> Option { + #[allow(unreachable_patterns)] + match value.v { + $($p => Some($r)),*, + other => { + error!( + @f, value.span, + "expected {}, found {}", $name, other.name() + ); + None + } + } + } + } + }; +} + +macro_rules! impl_ident { + ($type:ty, $name:expr, $parse:expr) => { + impl TryFromValue for $type { + fn try_from_value(value: Spanned<&Value>, f: &mut Feedback) -> Option { + if let Value::Ident(ident) = value.v { + let val = $parse(ident.as_str()); + if val.is_none() { + error!(@f, value.span, "invalid {}", $name); + } + val + } else { + error!( + @f, value.span, + "expected {}, found {}", $name, value.v.name() + ); + None + } + } + } + }; +} + +impl TryFromValue for Spanned { + fn try_from_value(value: Spanned<&Value>, f: &mut Feedback) -> Option { + let span = value.span; + T::try_from_value(value, f).map(|v| Spanned { v, span }) + } +} + +impl_match!(Value, "value", v => v.clone()); +impl_match!(Ident, "identifier", Value::Ident(i) => i.clone()); +impl_match!(String, "string", Value::Str(s) => s.clone()); +impl_match!(bool, "bool", &Value::Bool(b) => b); +impl_match!(f64, "number", &Value::Number(n) => n); +impl_match!(Length, "length", &Value::Length(l) => l); +impl_match!(SyntaxTree, "tree", Value::Tree(t) => t.clone()); +impl_match!(TableValue, "table", Value::Table(t) => t.clone()); +impl_match!(FuncValue, "function", Value::Func(f) => f.clone()); +impl_match!(ScaleLength, "number or length", + &Value::Length(length) => ScaleLength::Absolute(length), + &Value::Number(scale) => ScaleLength::Scaled(scale), +); + +/// A value type that matches identifiers and strings and implements +/// `Into`. +pub struct StringLike(pub String); + +impl From for String { + fn from(like: StringLike) -> String { + like.0 + } +} + +impl_match!(StringLike, "identifier or string", + Value::Ident(Ident(s)) => StringLike(s.clone()), + Value::Str(s) => StringLike(s.clone()), +); + +impl_ident!(Dir, "direction", |s| match s { + "ltr" => Some(Self::LTR), + "rtl" => Some(Self::RTL), + "ttb" => Some(Self::TTB), + "btt" => Some(Self::BTT), + _ => None, +}); + +impl_ident!(SpecAlign, "alignment", |s| match s { + "left" => Some(Self::Left), + "right" => Some(Self::Right), + "top" => Some(Self::Top), + "bottom" => Some(Self::Bottom), + "center" => Some(Self::Center), + _ => None, +}); + +impl_ident!(FontStyle, "font style", FontStyle::from_name); +impl_ident!(Paper, "paper", Paper::from_name); + +impl TryFromValue for FontWeight { + fn try_from_value(value: Spanned<&Value>, f: &mut Feedback) -> Option { + match value.v { + &Value::Number(weight) => { + const MIN: u16 = 100; + const MAX: u16 = 900; + + Some(Self(if weight < MIN as f64 { + error!(@f, value.span, "the minimum font weight is {}", MIN); + MIN + } else if weight > MAX as f64 { + error!(@f, value.span, "the maximum font weight is {}", MAX); + MAX + } else { + weight.round() as u16 + })) + } + Value::Ident(ident) => { + let weight = Self::from_name(ident.as_str()); + if weight.is_none() { + error!(@f, value.span, "invalid font weight"); + } + weight + } + other => { + error!( + @f, value.span, + "expected font weight (name or number), found {}", + other.name(), + ); + None + } + } + } +} + +impl TryFromValue for FontWidth { + fn try_from_value(value: Spanned<&Value>, f: &mut Feedback) -> Option { + match value.v { + &Value::Number(width) => { + const MIN: u16 = 1; + const MAX: u16 = 9; + + Self::new(if width < MIN as f64 { + error!(@f, value.span, "the minimum font width is {}", MIN); + MIN + } else if width > MAX as f64 { + error!(@f, value.span, "the maximum font width is {}", MAX); + MAX + } else { + width.round() as u16 + }) + } + Value::Ident(ident) => { + let width = Self::from_name(ident.as_str()); + if width.is_none() { + error!(@f, value.span, "invalid font width"); + } + width + } + other => { + error!( + @f, value.span, + "expected font width (name or number), found {}", + other.name(), + ); + None + } + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + + fn entry(value: Value) -> SpannedEntry { + SpannedEntry::val(Spanned::zero(value)) + } + + #[test] + fn test_table_take_removes_correct_entry() { + let mut table = Table::new(); + table.insert(1, entry(Value::Bool(false))); + table.insert(2, entry(Value::Str("hi".to_string()))); + assert_eq!(table.take::(), Some("hi".to_string())); + assert_eq!(table.len(), 1); + assert_eq!(table.take::(), Some(false)); + assert!(table.is_empty()); + } + + #[test] + fn test_table_expect_errors_about_previous_entries() { + let mut f = Feedback::new(); + let mut table = Table::new(); + table.insert(1, entry(Value::Bool(false))); + table.insert(3, entry(Value::Str("hi".to_string()))); + table.insert(5, entry(Value::Bool(true))); + assert_eq!(table.expect::(&mut f), Some("hi".to_string())); + assert_eq!(f.diagnostics, [error!(Span::ZERO, "expected string, found bool")]); + assert_eq!(table.len(), 1); + } + + #[test] + fn test_table_take_with_key_removes_the_entry() { + let mut f = Feedback::new(); + let mut table = Table::new(); + table.insert(1, entry(Value::Bool(false))); + table.insert("hi", entry(Value::Bool(true))); + assert_eq!(table.take_with_key::<_, bool>(1, &mut f), Some(false)); + assert_eq!(table.take_with_key::<_, f64>("hi", &mut f), None); + assert_eq!(f.diagnostics, [error!(Span::ZERO, "expected number, found bool")]); + assert!(table.is_empty()); + } + + #[test] + fn test_table_take_all_removes_the_correct_entries() { + let mut table = Table::new(); + table.insert(1, entry(Value::Bool(false))); + table.insert(3, entry(Value::Number(0.0))); + table.insert(7, entry(Value::Bool(true))); + assert_eq!( + table.take_all_num::().collect::>(), + [(1, false), (7, true)], + ); + assert_eq!(table.len(), 1); + assert_eq!(table[3].val.v, Value::Number(0.0)); + } +} diff --git a/src/diagnostic.rs b/src/diagnostic.rs index 2c649dac5..834cd928e 100644 --- a/src/diagnostic.rs +++ b/src/diagnostic.rs @@ -13,7 +13,7 @@ use crate::syntax::span::SpanVec; pub type Diagnostics = SpanVec; /// A diagnostic that arose in parsing or layouting. -#[derive(Debug, Clone, Eq, PartialEq)] +#[derive(Debug, Clone, Eq, PartialEq, Ord, PartialOrd)] #[cfg_attr(feature = "serialize", derive(Serialize))] pub struct Diagnostic { /// How severe / important the diagnostic is. diff --git a/src/layout/mod.rs b/src/layout/mod.rs index 5f5a48590..837c19ec6 100644 --- a/src/layout/mod.rs +++ b/src/layout/mod.rs @@ -10,9 +10,7 @@ mod tree; /// Basic types used across the layouting engine. pub mod prelude { pub use super::primitive::*; - pub use super::{ - BoxLayout, layout, Layout, LayoutContext, LayoutSpace, MultiLayout, - }; + pub use super::{BoxLayout, layout, LayoutContext, LayoutSpace, MultiLayout}; pub use Dir::*; pub use GenAlign::*; pub use GenAxis::*; @@ -23,13 +21,11 @@ pub mod prelude { pub use primitive::*; pub use tree::layout_tree as layout; -use async_trait::async_trait; - +use crate::compute::scope::Scope; use crate::font::SharedFontLoader; use crate::geom::{Margins, Size}; use crate::style::{LayoutStyle, PageStyle, TextStyle}; use crate::syntax::tree::SyntaxTree; -use crate::Pass; use elements::LayoutElements; use prelude::*; @@ -48,18 +44,13 @@ pub struct BoxLayout { pub elements: LayoutElements, } -/// Command-based layouting. -#[async_trait(?Send)] -pub trait Layout { - /// Create a sequence of layouting commands to execute. - async fn layout<'a>(&'a self, ctx: LayoutContext<'_>) -> Pass>; -} - /// The context for layouting. -#[derive(Debug, Clone)] +#[derive(Debug)] pub struct LayoutContext<'a> { /// The font loader to query fonts from when typesetting text. pub loader: &'a SharedFontLoader, + /// The function scope. + pub scope: &'a Scope, /// The style for pages and text. pub style: &'a LayoutStyle, /// The unpadded size of this container (the base 100% for relative sizes). @@ -118,11 +109,11 @@ impl LayoutSpace { } /// A sequence of layouting commands. -pub type Commands<'a> = Vec>; +pub type Commands = Vec; /// Commands executable by the layouting engine. -#[derive(Debug, Clone)] -pub enum Command<'a> { +#[derive(Debug, Clone, PartialEq)] +pub enum Command { /// Layout the given tree in the current context (i.e. not nested). The /// content of the tree is not laid out into a separate box and then added, /// but simply laid out flatly in the active layouting process. @@ -130,7 +121,7 @@ pub enum Command<'a> { /// This has the effect that the content fits nicely into the active line /// layouting, enabling functions to e.g. change the style of some piece of /// text while keeping it part of the current paragraph. - LayoutSyntaxTree(&'a SyntaxTree), + LayoutSyntaxTree(SyntaxTree), /// Add a finished layout. Add(BoxLayout), diff --git a/src/layout/tree.rs b/src/layout/tree.rs index 3abdc9342..39e111bdf 100644 --- a/src/layout/tree.rs +++ b/src/layout/tree.rs @@ -1,9 +1,10 @@ //! Layouting of syntax trees. +use crate::compute::value::Value; use crate::style::LayoutStyle; use crate::syntax::decoration::Decoration; -use crate::syntax::span::{Span, Spanned}; -use crate::syntax::tree::{DynamicNode, SyntaxNode, SyntaxTree}; +use crate::syntax::span::{Offset, Span, Spanned}; +use crate::syntax::tree::{CallExpr, SyntaxNode, SyntaxTree}; use crate::{DynFuture, Feedback, Pass}; use super::line::{LineContext, LineLayouter}; use super::text::{layout_text, TextContext}; @@ -66,60 +67,28 @@ impl<'a> TreeLayouter<'a> { self.style.text.word_spacing(), SpacingKind::WORD, ); - }, - + } SyntaxNode::Linebreak => self.layouter.finish_line(), SyntaxNode::ToggleItalic => { self.style.text.italic = !self.style.text.italic; decorate(self, Decoration::Italic); } - SyntaxNode::ToggleBolder => { self.style.text.bolder = !self.style.text.bolder; decorate(self, Decoration::Bold); } SyntaxNode::Text(text) => { - if self.style.text.italic { - decorate(self, Decoration::Italic); - } - - if self.style.text.bolder { - decorate(self, Decoration::Bold); - } - + if self.style.text.italic { decorate(self, Decoration::Italic); } + if self.style.text.bolder { decorate(self, Decoration::Bold); } self.layout_text(text).await; } - SyntaxNode::Raw(lines) => { - // TODO: Make this more efficient. - let fallback = self.style.text.fallback.clone(); - self.style.text.fallback - .list_mut() - .insert(0, "monospace".to_string()); - - self.style.text.fallback.flatten(); - - // Layout the first line. - let mut iter = lines.iter(); - if let Some(line) = iter.next() { - self.layout_text(line).await; - } - - // Put a newline before each following line. - for line in iter { - self.layouter.finish_line(); - self.layout_text(line).await; - } - - self.style.text.fallback = fallback; - } - + SyntaxNode::Raw(lines) => self.layout_raw(lines).await, SyntaxNode::Par(par) => self.layout_par(par).await, - - SyntaxNode::Dyn(dynamic) => { - self.layout_dyn(Spanned::new(dynamic.as_ref(), node.span)).await; + SyntaxNode::Call(call) => { + self.layout_call(Spanned::new(call, node.span)).await; } } } @@ -133,19 +102,35 @@ impl<'a> TreeLayouter<'a> { self.layout_tree(par).await; } - async fn layout_dyn(&mut self, dynamic: Spanned<&dyn DynamicNode>) { - // Execute the dynamic node's command-generating layout function. - let layouted = dynamic.v.layout(LayoutContext { + async fn layout_call(&mut self, call: Spanned<&CallExpr>) { + let name = call.v.name.v.as_str(); + let span = call.v.name.span.offset(call.span.start); + + let (func, deco) = if let Some(func) = self.ctx.scope.func(name) { + (func, Decoration::Resolved) + } else { + error!(@self.feedback, span, "unknown function"); + (self.ctx.scope.fallback(), Decoration::Unresolved) + }; + + self.feedback.decorations.push(Spanned::new(deco, span)); + + let args = call.v.args.eval(); + let pass = func(args, LayoutContext { style: &self.style, spaces: self.layouter.remaining(), root: true, ..self.ctx }).await; - self.feedback.extend_offset(layouted.feedback, dynamic.span.start); + self.feedback.extend_offset(pass.feedback, call.span.start); - for command in layouted.output { - self.execute_command(command, dynamic.span).await; + if let Value::Commands(commands) = pass.output { + for command in commands { + self.execute_command(command, call.span).await; + } + } else { + self.layout_raw(&[format!("{:?}", pass.output)]).await; } } @@ -163,11 +148,35 @@ impl<'a> TreeLayouter<'a> { ); } - async fn execute_command(&mut self, command: Command<'_>, span: Span) { + async fn layout_raw(&mut self, lines: &[String]) { + // TODO: Make this more efficient. + let fallback = self.style.text.fallback.clone(); + self.style.text.fallback + .list_mut() + .insert(0, "monospace".to_string()); + + self.style.text.fallback.flatten(); + + // Layout the first line. + let mut iter = lines.iter(); + if let Some(line) = iter.next() { + self.layout_text(line).await; + } + + // Put a newline before each following line. + for line in iter { + self.layouter.finish_line(); + self.layout_text(line).await; + } + + self.style.text.fallback = fallback; + } + + async fn execute_command(&mut self, command: Command, span: Span) { use Command::*; match command { - LayoutSyntaxTree(tree) => self.layout_tree(tree).await, + LayoutSyntaxTree(tree) => self.layout_tree(&tree).await, Add(layout) => self.layouter.add(layout), AddMultiple(layouts) => self.layouter.add_multiple(layouts), diff --git a/src/lib.rs b/src/lib.rs index 21adad9e9..e30e41b2e 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -24,9 +24,9 @@ mod macros; #[macro_use] pub mod diagnostic; -#[macro_use] -pub mod func; +pub mod color; +pub mod compute; pub mod export; pub mod font; pub mod geom; @@ -34,22 +34,24 @@ pub mod layout; pub mod length; pub mod library; pub mod paper; +pub mod prelude; pub mod style; pub mod syntax; -pub mod table; use std::fmt::Debug; use std::future::Future; use std::pin::Pin; +use crate::compute::scope::Scope; +use crate::compute::value::Value; use crate::diagnostic::Diagnostics; use crate::font::SharedFontLoader; -use crate::layout::MultiLayout; +use crate::layout::{Commands, MultiLayout}; use crate::style::{LayoutStyle, PageStyle, TextStyle}; use crate::syntax::decoration::Decorations; -use crate::syntax::parsing::{parse, ParseState}; +use crate::syntax::parsing::parse; use crate::syntax::span::{Offset, Pos}; -use crate::syntax::tree::{DynamicNode, SyntaxNode, SyntaxTree}; +use crate::syntax::tree::SyntaxTree; /// Transforms source code into typesetted layouts. /// @@ -57,10 +59,10 @@ use crate::syntax::tree::{DynamicNode, SyntaxNode, SyntaxTree}; pub struct Typesetter { /// The font loader shared by all typesetting processes. loader: SharedFontLoader, + /// A scope that contains the standard library function definitions. + std: Scope, /// The base layouting style. style: LayoutStyle, - /// The base parser state. - parse_state: ParseState, } impl Typesetter { @@ -68,8 +70,8 @@ impl Typesetter { pub fn new(loader: SharedFontLoader) -> Self { Self { loader, + std: crate::library::_std(), style: LayoutStyle::default(), - parse_state: ParseState { scope: crate::library::_std() }, } } @@ -85,7 +87,7 @@ impl Typesetter { /// Parse source code into a syntax tree. pub fn parse(&self, src: &str) -> Pass { - parse(src, Pos::ZERO, &self.parse_state) + parse(src, Pos::ZERO) } /// Layout a syntax tree and return the produced layout. @@ -97,6 +99,7 @@ impl Typesetter { &tree, LayoutContext { loader: &self.loader, + scope: &self.std, style: &self.style, base: self.style.page.size.unpadded(margins), spaces: vec![LayoutSpace { @@ -155,10 +158,10 @@ impl Pass { } } -impl Pass { - /// Create a new pass from an unboxed dynamic node and feedback data.. - pub fn node(node: T, feedback: Feedback) -> Self { - Pass::new(SyntaxNode::boxed(node), feedback) +impl Pass { + /// Create a new pass with a list of layouting commands. + pub fn commands(commands: Commands, feedback: Feedback) -> Self { + Pass::new(Value::Commands(commands), feedback) } } diff --git a/src/library/align.rs b/src/library/align.rs index b4cfd2e27..c716faefb 100644 --- a/src/library/align.rs +++ b/src/library/align.rs @@ -10,67 +10,49 @@ use super::*; /// - `vertical`: Any of `top`, `bottom` or `center`. /// /// There may not be two alignment specifications for the same axis. -pub fn align(call: FuncCall, _: &ParseState) -> Pass { +pub async fn align(mut args: TableValue, mut ctx: LayoutContext<'_>) -> Pass { let mut f = Feedback::new(); - let mut args = call.args; - let node = AlignNode { - content: args.take::(), - aligns: args.take_all_num_vals::>().collect(), - h: args.take_with_key::<_, Spanned>("horizontal", &mut f), - v: args.take_with_key::<_, Spanned>("vertical", &mut f), - }; + + let content = args.take::(); + let aligns: Vec<_> = args.take_all_num_vals::>().collect(); + let h = args.take_with_key::<_, Spanned>("horizontal", &mut f); + let v = args.take_with_key::<_, Spanned>("vertical", &mut f); args.unexpected(&mut f); - Pass::node(node, f) -} -#[derive(Debug, Clone, PartialEq)] -struct AlignNode { - content: Option, - aligns: SpanVec, - h: Option>, - v: Option>, -} + ctx.base = ctx.spaces[0].size; -#[async_trait(?Send)] -impl Layout for AlignNode { - async fn layout<'a>(&'a self, mut ctx: LayoutContext<'_>) -> Pass> { - let mut f = Feedback::new(); + let axes = ctx.axes; + let all = aligns.iter() + .map(|align| { + let spec = align.v.axis().unwrap_or(axes.primary.axis()); + (spec, align) + }) + .chain(h.iter().map(|align| (Horizontal, align))) + .chain(v.iter().map(|align| (Vertical, align))); - ctx.base = ctx.spaces[0].size; - - let axes = ctx.axes; - let all = self.aligns.iter() - .map(|align| { - let spec = align.v.axis().unwrap_or(axes.primary.axis()); - (spec, align) - }) - .chain(self.h.iter().map(|align| (Horizontal, align))) - .chain(self.v.iter().map(|align| (Vertical, align))); - - let mut had = [false; 2]; - for (axis, align) in all { - if align.v.axis().map(|a| a != axis).unwrap_or(false) { - error!( - @f, align.span, - "invalid alignment {} for {} axis", align.v, axis, - ); - } else if had[axis as usize] { - error!(@f, align.span, "duplicate alignment for {} axis", axis); - } else { - had[axis as usize] = true; - let gen_axis = axis.to_generic(ctx.axes); - let gen_align = align.v.to_generic(ctx.axes); - *ctx.align.get_mut(gen_axis) = gen_align; - } + let mut had = [false; 2]; + for (axis, align) in all { + if align.v.axis().map(|a| a != axis).unwrap_or(false) { + error!( + @f, align.span, + "invalid alignment {} for {} axis", align.v, axis, + ); + } else if had[axis as usize] { + error!(@f, align.span, "duplicate alignment for {} axis", axis); + } else { + had[axis as usize] = true; + let gen_axis = axis.to_generic(ctx.axes); + let gen_align = align.v.to_generic(ctx.axes); + *ctx.align.get_mut(gen_axis) = gen_align; } - - Pass::new(match &self.content { - Some(tree) => { - let layouted = layout(tree, ctx).await; - f.extend(layouted.feedback); - vec![AddMultiple(layouted.output)] - } - None => vec![SetAlignment(ctx.align)], - }, f) } + + Pass::commands(match content { + Some(tree) => { + let layouted = layout(&tree, ctx).await; + f.extend(layouted.feedback); + vec![AddMultiple(layouted.output)] + } + None => vec![SetAlignment(ctx.align)], + }, f) } diff --git a/src/library/boxed.rs b/src/library/boxed.rs index 3637f0724..c03043aee 100644 --- a/src/library/boxed.rs +++ b/src/library/boxed.rs @@ -6,48 +6,32 @@ use super::*; /// # Keyword arguments /// - `width`: The width of the box (length of relative to parent's width). /// - `height`: The height of the box (length of relative to parent's height). -pub fn boxed(call: FuncCall, _: &ParseState) -> Pass { +pub async fn boxed(mut args: TableValue, mut ctx: LayoutContext<'_>) -> Pass { let mut f = Feedback::new(); - let mut args = call.args; - let node = BoxNode { - content: args.take::().unwrap_or(SyntaxTree::new()), - width: args.take_with_key::<_, ScaleLength>("width", &mut f), - height: args.take_with_key::<_, ScaleLength>("height", &mut f), - }; + let content = args.take::().unwrap_or(SyntaxTree::new()); + let width = args.take_with_key::<_, ScaleLength>("width", &mut f); + let height = args.take_with_key::<_, ScaleLength>("height", &mut f); args.unexpected(&mut f); - Pass::node(node, f) -} - -#[derive(Debug, Clone, PartialEq)] -struct BoxNode { - content: SyntaxTree, - width: Option, - height: Option, -} - -#[async_trait(?Send)] -impl Layout for BoxNode { - async fn layout<'a>(&'a self, mut ctx: LayoutContext<'_>) -> Pass> { - ctx.spaces.truncate(1); - ctx.repeat = false; - - self.width.with(|v| { - let length = v.raw_scaled(ctx.base.x); - ctx.base.x = length; - ctx.spaces[0].size.x = length; - ctx.spaces[0].expansion.horizontal = true; - }); - - self.height.with(|v| { - let length = v.raw_scaled(ctx.base.y); - ctx.base.y = length; - ctx.spaces[0].size.y = length; - ctx.spaces[0].expansion.vertical = true; - }); - - layout(&self.content, ctx).await.map(|out| { - let layout = out.into_iter().next().unwrap(); - vec![Add(layout)] - }) - } + + ctx.spaces.truncate(1); + ctx.repeat = false; + + width.with(|v| { + let length = v.raw_scaled(ctx.base.x); + ctx.base.x = length; + ctx.spaces[0].size.x = length; + ctx.spaces[0].expansion.horizontal = true; + }); + + height.with(|v| { + let length = v.raw_scaled(ctx.base.y); + ctx.base.y = length; + ctx.spaces[0].size.y = length; + ctx.spaces[0].expansion.vertical = true; + }); + + let layouted = layout(&content, ctx).await; + let layout = layouted.output.into_iter().next().unwrap(); + f.extend(layouted.feedback); + Pass::commands(vec![Add(layout)], f) } diff --git a/src/library/font.rs b/src/library/font.rs index 8787cc91a..71e9552f3 100644 --- a/src/library/font.rs +++ b/src/library/font.rs @@ -18,80 +18,60 @@ use super::*; /// ```typst /// serif = ("Source Serif Pro", "Noto Serif") /// ``` -pub fn font(call: FuncCall, _: &ParseState) -> Pass { +pub async fn font(mut args: TableValue, ctx: LayoutContext<'_>) -> Pass { let mut f = Feedback::new(); - let mut args = call.args; - let node = FontNode { - content: args.take::(), - size: args.take::(), - style: args.take_with_key::<_, FontStyle>("style", &mut f), - weight: args.take_with_key::<_, FontWeight>("weight", &mut f), - width: args.take_with_key::<_, FontWidth>("width", &mut f), - list: args.take_all_num_vals::() - .map(|s| s.0.to_lowercase()) - .collect(), - classes: args.take_all_str::() - .map(|(class, mut table)| { - let fallback = table.take_all_num_vals::() - .map(|s| s.0.to_lowercase()) - .collect(); - (class, fallback) - }) - .collect() - }; + let content = args.take::(); + let size = args.take::(); + let style = args.take_with_key::<_, FontStyle>("style", &mut f); + let weight = args.take_with_key::<_, FontWeight>("weight", &mut f); + let width = args.take_with_key::<_, FontWidth>("width", &mut f); + let list: Vec<_> = args.take_all_num_vals::() + .map(|s| s.0.to_lowercase()) + .collect(); + let classes: Vec<(_, Vec<_>)> = args.take_all_str::() + .map(|(class, mut table)| { + let fallback = table.take_all_num_vals::() + .map(|s| s.0.to_lowercase()) + .collect(); + (class, fallback) + }) + .collect(); args.unexpected(&mut f); - Pass::node(node, f) -} -#[derive(Debug, Clone, PartialEq)] -struct FontNode { - content: Option, - size: Option, - style: Option, - weight: Option, - width: Option, - list: Vec, - classes: Vec<(String, Vec)>, -} + let mut text = ctx.style.text.clone(); -#[async_trait(?Send)] -impl Layout for FontNode { - async fn layout<'a>(&'a self, ctx: LayoutContext<'_>) -> Pass> { - let mut text = ctx.style.text.clone(); - - self.size.with(|s| match s { - ScaleLength::Absolute(length) => { - text.base_font_size = length.as_raw(); - text.font_scale = 1.0; - } - ScaleLength::Scaled(scale) => text.font_scale = scale, - }); - - self.style.with(|s| text.variant.style = s); - self.weight.with(|w| text.variant.weight = w); - self.width.with(|w| text.variant.width = w); - - if !self.list.is_empty() { - *text.fallback.list_mut() = self.list.iter() - .map(|s| s.to_lowercase()) - .collect(); + size.with(|s| match s { + ScaleLength::Absolute(length) => { + text.base_font_size = length.as_raw(); + text.font_scale = 1.0; } + ScaleLength::Scaled(scale) => text.font_scale = scale, + }); - for (class, fallback) in &self.classes { - text.fallback.set_class_list(class.clone(), fallback.clone()); - } + style.with(|s| text.variant.style = s); + weight.with(|w| text.variant.weight = w); + width.with(|w| text.variant.width = w); - text.fallback.flatten(); - - Pass::okay(match &self.content { - Some(tree) => vec![ - SetTextStyle(text), - LayoutSyntaxTree(tree), - SetTextStyle(ctx.style.text.clone()), - ], - None => vec![SetTextStyle(text)], - }) + if !list.is_empty() { + *text.fallback.list_mut() = list.iter() + .map(|s| s.to_lowercase()) + .collect(); } + + for (class, fallback) in classes { + text.fallback.set_class_list(class.clone(), fallback.clone()); + } + + text.fallback.flatten(); + + Pass::commands(match content { + Some(tree) => vec![ + SetTextStyle(text), + LayoutSyntaxTree(tree), + SetTextStyle(ctx.style.text.clone()), + ], + None => vec![SetTextStyle(text)], + }, f) } diff --git a/src/library/mod.rs b/src/library/mod.rs index ef24d74f0..1999ba311 100644 --- a/src/library/mod.rs +++ b/src/library/mod.rs @@ -5,30 +5,60 @@ mod boxed; mod font; mod page; mod spacing; -mod val; pub use align::*; pub use boxed::*; pub use font::*; pub use page::*; pub use spacing::*; -pub use val::*; -use crate::func::prelude::*; -use crate::syntax::scope::Scope; +use std::rc::Rc; -/// Create a scope with all standard library functions. -pub fn _std() -> Scope { - let mut std = Scope::new(Box::new(val)); +use crate::compute::scope::Scope; +use crate::prelude::*; - std.insert("val", Box::new(val)); - std.insert("font", Box::new(font)); - std.insert("page", Box::new(page)); - std.insert("align", Box::new(align)); - std.insert("box", Box::new(boxed)); - std.insert("pagebreak", Box::new(pagebreak)); - std.insert("h", Box::new(h)); - std.insert("v", Box::new(v)); - - std +macro_rules! std { + (fallback: $fallback:expr $(, $name:literal => $func:expr)* $(,)?) => { + /// Create a scope with all standard library functions. + pub fn _std() -> Scope { + let mut std = Scope::new(wrap!(val)); + $(std.insert($name, wrap!($func));)* + std + } + }; +} + +macro_rules! wrap { + ($func:expr) => { + Rc::new(|args, ctx| Box::pin($func(args, ctx))) + }; +} + +std! { + fallback: val, + "align" => align, + "box" => boxed, + "dump" => dump, + "font" => font, + "h" => h, + "page" => page, + "pagebreak" => pagebreak, + "v" => v, + "val" => val, +} + +/// `val`: Layouts its body flatly, ignoring other arguments. +/// +/// This is also the fallback function, which is used when a function name +/// cannot be resolved. +pub async fn val(mut args: TableValue, _: LayoutContext<'_>) -> Pass { + Pass::commands(match args.take::() { + Some(tree) => vec![LayoutSyntaxTree(tree)], + None => vec![], + }, Feedback::new()) +} + +/// `dump`: Dumps its arguments. +pub async fn dump(args: TableValue, _: LayoutContext<'_>) -> Pass { + Pass::okay(Value::Table(args)) } diff --git a/src/library/page.rs b/src/library/page.rs index 7e4e6e546..42f29dbb7 100644 --- a/src/library/page.rs +++ b/src/library/page.rs @@ -16,78 +16,46 @@ use super::*; /// - `top`: The top margin (length or relative to height). /// - `bottom`: The bottom margin (length or relative to height). /// - `flip`: Flips custom or paper-defined width and height (boolean). -pub fn page(call: FuncCall, _: &ParseState) -> Pass { +pub async fn page(mut args: TableValue, ctx: LayoutContext<'_>) -> Pass { let mut f = Feedback::new(); - let mut args = call.args; - let node = PageNode { - paper: args.take::(), - width: args.take_with_key::<_, Length>("width", &mut f), - height: args.take_with_key::<_, Length>("height", &mut f), - margins: args.take_with_key::<_, ScaleLength>("margins", &mut f), - left: args.take_with_key::<_, ScaleLength>("left", &mut f), - right: args.take_with_key::<_, ScaleLength>("right", &mut f), - top: args.take_with_key::<_, ScaleLength>("top", &mut f), - bottom: args.take_with_key::<_, ScaleLength>("bottom", &mut f), - flip: args.take_with_key::<_, bool>("flip", &mut f).unwrap_or(false), - }; + let paper = args.take::(); + let width = args.take_with_key::<_, Length>("width", &mut f); + let height = args.take_with_key::<_, Length>("height", &mut f); + let margins = args.take_with_key::<_, ScaleLength>("margins", &mut f); + let left = args.take_with_key::<_, ScaleLength>("left", &mut f); + let right = args.take_with_key::<_, ScaleLength>("right", &mut f); + let top = args.take_with_key::<_, ScaleLength>("top", &mut f); + let bottom = args.take_with_key::<_, ScaleLength>("bottom", &mut f); + let flip = args.take_with_key::<_, bool>("flip", &mut f).unwrap_or(false); args.unexpected(&mut f); - Pass::node(node, f) -} -#[derive(Debug, Clone, PartialEq)] -struct PageNode { - paper: Option, - width: Option, - height: Option, - margins: Option, - left: Option, - right: Option, - top: Option, - bottom: Option, - flip: bool, -} + let mut style = ctx.style.page; -#[async_trait(?Send)] -impl Layout for PageNode { - async fn layout<'a>(&'a self, ctx: LayoutContext<'_>) -> Pass> { - let mut style = ctx.style.page; - - if let Some(paper) = self.paper { - style.class = paper.class; - style.size = paper.size(); - } else if self.width.is_some() || self.height.is_some() { - style.class = PaperClass::Custom; - } - - self.width.with(|v| style.size.x = v.as_raw()); - self.height.with(|v| style.size.y = v.as_raw()); - self.margins.with(|v| style.margins.set_all(Some(v))); - self.left.with(|v| style.margins.left = Some(v)); - self.right.with(|v| style.margins.right = Some(v)); - self.top.with(|v| style.margins.top = Some(v)); - self.bottom.with(|v| style.margins.bottom = Some(v)); - - if self.flip { - style.size.swap(); - } - - Pass::okay(vec![SetPageStyle(style)]) + if let Some(paper) = paper { + style.class = paper.class; + style.size = paper.size(); + } else if width.is_some() || height.is_some() { + style.class = PaperClass::Custom; } + + width.with(|v| style.size.x = v.as_raw()); + height.with(|v| style.size.y = v.as_raw()); + margins.with(|v| style.margins.set_all(Some(v))); + left.with(|v| style.margins.left = Some(v)); + right.with(|v| style.margins.right = Some(v)); + top.with(|v| style.margins.top = Some(v)); + bottom.with(|v| style.margins.bottom = Some(v)); + + if flip { + style.size.swap(); + } + + Pass::commands(vec![SetPageStyle(style)], f) } /// `pagebreak`: Ends the current page. -pub fn pagebreak(call: FuncCall, _: &ParseState) -> Pass { +pub async fn pagebreak(args: TableValue, _: LayoutContext<'_>) -> Pass { let mut f = Feedback::new(); - call.args.unexpected(&mut f); - Pass::node(PageBreakNode, f) -} - -#[derive(Debug, Default, Clone, PartialEq)] -struct PageBreakNode; - -#[async_trait(?Send)] -impl Layout for PageBreakNode { - async fn layout<'a>(&'a self, _: LayoutContext<'_>) -> Pass> { - Pass::okay(vec![BreakPage]) - } + args.unexpected(&mut f); + Pass::commands(vec![BreakPage], f) } diff --git a/src/library/spacing.rs b/src/library/spacing.rs index 81112cbda..3cd775c9f 100644 --- a/src/library/spacing.rs +++ b/src/library/spacing.rs @@ -6,44 +6,32 @@ use super::*; /// /// # Positional arguments /// - The spacing (length or relative to font size). -pub fn h(call: FuncCall, _: &ParseState) -> Pass { - spacing(call, Horizontal) +pub async fn h(args: TableValue, ctx: LayoutContext<'_>) -> Pass { + spacing(args, ctx, Horizontal).await } /// `v`: Add vertical spacing. /// /// # Positional arguments /// - The spacing (length or relative to font size). -pub fn v(call: FuncCall, _: &ParseState) -> Pass { - spacing(call, Vertical) +pub async fn v(args: TableValue, ctx: LayoutContext<'_>) -> Pass { + spacing(args, ctx, Vertical).await } -fn spacing(call: FuncCall, axis: SpecAxis) -> Pass { +async fn spacing( + mut args: TableValue, + ctx: LayoutContext<'_>, + axis: SpecAxis, +) -> Pass { let mut f = Feedback::new(); - let mut args = call.args; - let node = SpacingNode { - spacing: args.expect::(&mut f) - .map(|s| (axis, s)) - .or_missing(call.name.span, "spacing", &mut f), - }; + let spacing = args.expect::(&mut f).map(|s| (axis, s)); args.unexpected(&mut f); - Pass::node(node, f) -} -#[derive(Debug, Clone, PartialEq)] -struct SpacingNode { - spacing: Option<(SpecAxis, ScaleLength)>, -} - -#[async_trait(?Send)] -impl Layout for SpacingNode { - async fn layout<'a>(&'a self, ctx: LayoutContext<'_>) -> Pass> { - Pass::okay(if let Some((axis, spacing)) = self.spacing { - let axis = axis.to_generic(ctx.axes); - let spacing = spacing.raw_scaled(ctx.style.text.font_size()); - vec![AddSpacing(spacing, SpacingKind::Hard, axis)] - } else { - vec![] - }) - } + Pass::commands(if let Some((axis, spacing)) = spacing { + let axis = axis.to_generic(ctx.axes); + let spacing = spacing.raw_scaled(ctx.style.text.font_size()); + vec![AddSpacing(spacing, SpacingKind::Hard, axis)] + } else { + vec![] + }, f) } diff --git a/src/library/val.rs b/src/library/val.rs deleted file mode 100644 index 9df554012..000000000 --- a/src/library/val.rs +++ /dev/null @@ -1,28 +0,0 @@ -use super::*; - -/// `val`: Ignores all arguments and layouts its body flatly. -/// -/// This is also the fallback function, which is used when a function name -/// cannot be resolved. -pub fn val(call: FuncCall, _: &ParseState) -> Pass { - let mut args = call.args; - let node = ValNode { - content: args.take::(), - }; - Pass::node(node, Feedback::new()) -} - -#[derive(Debug, Clone, PartialEq)] -struct ValNode { - content: Option, -} - -#[async_trait(?Send)] -impl Layout for ValNode { - async fn layout<'a>(&'a self, _: LayoutContext<'_>) -> Pass> { - Pass::okay(match &self.content { - Some(tree) => vec![LayoutSyntaxTree(tree)], - None => vec![], - }) - } -} diff --git a/src/func.rs b/src/prelude.rs similarity index 54% rename from src/func.rs rename to src/prelude.rs index bff4fdab3..214b00c8e 100644 --- a/src/func.rs +++ b/src/prelude.rs @@ -1,21 +1,15 @@ -//! Tools for building custom functions. +//! A prelude for building custom functions. -/// Useful things for creating functions. -pub mod prelude { - pub use async_trait::async_trait; - pub use crate::layout::prelude::*; - pub use crate::layout::Commands; - pub use crate::layout::Command::{self, *}; - pub use crate::style::*; - pub use crate::syntax::expr::*; - pub use crate::syntax::parsing::{parse, FuncCall, ParseState}; - pub use crate::syntax::span::{Pos, Span, SpanVec, Spanned}; - pub use crate::syntax::tree::{DynamicNode, SyntaxNode, SyntaxTree}; - pub use crate::{Pass, Feedback}; - pub use super::*; -} - -use prelude::*; +pub use crate::compute::value::*; +pub use crate::layout::prelude::*; +pub use crate::layout::Commands; +pub use crate::layout::Command::{self, *}; +pub use crate::style::*; +pub use crate::syntax::parsing::parse; +pub use crate::syntax::span::{Pos, Span, SpanVec, Spanned}; +pub use crate::syntax::tree::*; +pub use crate::{Pass, Feedback}; +pub use super::*; /// Extra methods on `Option`s used for function argument parsing. pub trait OptionExt: Sized { diff --git a/src/syntax/decoration.rs b/src/syntax/decoration.rs index a686596cc..fe961be39 100644 --- a/src/syntax/decoration.rs +++ b/src/syntax/decoration.rs @@ -9,18 +9,18 @@ use super::span::SpanVec; pub type Decorations = SpanVec; /// Decorations for semantic syntax highlighting. -#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)] +#[derive(Debug, Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash)] #[cfg_attr(feature = "serialize", derive(Serialize))] #[cfg_attr(feature = "serialize", serde(rename_all = "camelCase"))] pub enum Decoration { - /// A valid, successfully resolved function name. - ResolvedFunc, - /// An invalid, unresolved function name. - UnresolvedFunc, - /// The key part of a key-value entry in a table. - TableKey, /// Text in italics. Italic, /// Text in bold. Bold, + /// A valid, successfully resolved name. + Resolved, + /// An invalid, unresolved name. + Unresolved, + /// The key part of a key-value entry in a table. + TableKey, } diff --git a/src/syntax/expr.rs b/src/syntax/expr.rs deleted file mode 100644 index e92a75b05..000000000 --- a/src/syntax/expr.rs +++ /dev/null @@ -1,639 +0,0 @@ -//! Expressions in function headers. - -use std::fmt::{self, Debug, Formatter}; -use std::str::FromStr; -use std::u8; - -use fontdock::{FontStyle, FontWeight, FontWidth}; - -use crate::layout::{Dir, SpecAlign}; -use crate::length::{Length, ScaleLength}; -use crate::paper::Paper; -use crate::table::{BorrowedKey, Table}; -use crate::Feedback; -use super::parsing::FuncCall; -use super::span::{Span, Spanned}; -use super::tokens::is_identifier; -use super::tree::SyntaxTree; - -/// An expression. -#[derive(Clone, PartialEq)] -pub enum Expr { - /// An identifier: `ident`. - Ident(Ident), - /// A string: `"string"`. - Str(String), - /// A boolean: `true, false`. - Bool(bool), - /// A number: `1.2, 200%`. - Number(f64), - /// A length: `2cm, 5.2in`. - Length(Length), - /// A color value with alpha channel: `#f79143ff`. - Color(RgbaColor), - /// A syntax tree containing typesetting content. - Tree(SyntaxTree), - /// A table: `(false, 12cm, greeting="hi")`. - Table(TableExpr), - /// An operation that negates the contained expression. - Neg(Box>), - /// An operation that adds the contained expressions. - Add(Box>, Box>), - /// An operation that subtracts the contained expressions. - Sub(Box>, Box>), - /// An operation that multiplies the contained expressions. - Mul(Box>, Box>), - /// An operation that divides the contained expressions. - Div(Box>, Box>), - /// A function call: `cmyk(37.7, 0, 3.9, 1.1)`. - Call(FuncCall), -} - -impl Expr { - /// A natural-language name of the type of this expression, e.g. - /// "identifier". - pub fn name(&self) -> &'static str { - use Expr::*; - match self { - Ident(_) => "identifier", - Str(_) => "string", - Bool(_) => "bool", - Number(_) => "number", - Length(_) => "length", - Color(_) => "color", - Tree(_) => "syntax tree", - Table(_) => "table", - Neg(_) => "negation", - Add(_, _) => "addition", - Sub(_, _) => "subtraction", - Mul(_, _) => "multiplication", - Div(_, _) => "division", - Call(_) => "function call", - } - } -} - -impl Debug for Expr { - fn fmt(&self, f: &mut Formatter) -> fmt::Result { - use Expr::*; - match self { - Ident(i) => i.fmt(f), - Str(s) => s.fmt(f), - Bool(b) => b.fmt(f), - Number(n) => n.fmt(f), - Length(s) => s.fmt(f), - Color(c) => c.fmt(f), - Tree(t) => t.fmt(f), - Table(t) => t.fmt(f), - Neg(e) => write!(f, "-{:?}", e), - Add(a, b) => write!(f, "({:?} + {:?})", a, b), - Sub(a, b) => write!(f, "({:?} - {:?})", a, b), - Mul(a, b) => write!(f, "({:?} * {:?})", a, b), - Div(a, b) => write!(f, "({:?} / {:?})", a, b), - Call(c) => c.fmt(f), - } - } -} - -/// An identifier as defined by unicode with a few extra permissible characters. -#[derive(Clone, Eq, PartialEq, Ord, PartialOrd, Hash)] -pub struct Ident(pub String); - -impl Ident { - /// Create a new identifier from a string checking that it is a valid. - pub fn new(ident: impl AsRef + Into) -> Option { - if is_identifier(ident.as_ref()) { - Some(Self(ident.into())) - } else { - None - } - } - - /// Return a reference to the underlying string. - pub fn as_str(&self) -> &str { - self.0.as_str() - } -} - -impl Debug for Ident { - fn fmt(&self, f: &mut Formatter) -> fmt::Result { - write!(f, "`{}`", self.0) - } -} - -/// An 8-bit RGBA color. -/// -/// # Example -/// ```typst -/// [page: background=#423abaff] -/// ^^^^^^^^ -/// ``` -#[derive(Copy, Clone, Eq, PartialEq, Hash)] -pub struct RgbaColor { - /// Red channel. - pub r: u8, - /// Green channel. - pub g: u8, - /// Blue channel. - pub b: u8, - /// Alpha channel. - pub a: u8, - /// This is true if this value was provided as a fail-over by the parser - /// because the user-defined value was invalid. This color may be - /// overwritten if this property is true. - pub healed: bool, -} - -impl RgbaColor { - /// Constructs a new color. - pub fn new(r: u8, g: u8, b: u8, a: u8) -> Self { - Self { r, g, b, a, healed: false } - } - - /// Constructs a new color with the healed property set to true. - pub fn new_healed(r: u8, g: u8, b: u8, a: u8) -> Self { - Self { r, g, b, a, healed: true } - } -} - -impl FromStr for RgbaColor { - type Err = ParseColorError; - - /// Constructs a new color from a hex string like `7a03c2`. Do not specify a - /// leading `#`. - fn from_str(hex_str: &str) -> Result { - if !hex_str.is_ascii() { - return Err(ParseColorError); - } - - let len = hex_str.len(); - let long = len == 6 || len == 8; - let short = len == 3 || len == 4; - let alpha = len == 4 || len == 8; - - if !long && !short { - return Err(ParseColorError); - } - - let mut values: [u8; 4] = [255; 4]; - - for elem in if alpha { 0..4 } else { 0..3 } { - let item_len = if long { 2 } else { 1 }; - let pos = elem * item_len; - - let item = &hex_str[pos..(pos+item_len)]; - values[elem] = u8::from_str_radix(item, 16) - .map_err(|_| ParseColorError)?; - - if short { - // Duplicate number for shorthand notation, i.e. `a` -> `aa` - values[elem] += values[elem] * 16; - } - } - - Ok(Self::new(values[0], values[1], values[2], values[3])) - } -} - -impl Debug for RgbaColor { - fn fmt(&self, f: &mut Formatter) -> fmt::Result { - if f.alternate() { - write!( - f, "rgba({:02}, {:02}, {:02}, {:02})", - self.r, self.g, self.b, self.a, - )?; - } else { - write!( - f, "#{:02x}{:02x}{:02x}{:02x}", - self.r, self.g, self.b, self.a, - )?; - } - if self.healed { - f.write_str(" [healed]")?; - } - Ok(()) - } -} - -/// The error when parsing an `RgbaColor` fails. -#[derive(Debug, Copy, Clone, Eq, PartialEq)] -pub struct ParseColorError; - -impl std::error::Error for ParseColorError {} - -impl fmt::Display for ParseColorError { - fn fmt(&self, f: &mut Formatter) -> fmt::Result { - f.pad("invalid color") - } -} - -/// A table expression. -/// -/// # Example -/// ```typst -/// (false, 12cm, greeting="hi") -/// ``` -pub type TableExpr = Table; - -impl TableExpr { - /// Retrieve and remove the matching value with the lowest number key, - /// skipping and ignoring all non-matching entries with lower keys. - pub fn take(&mut self) -> Option { - for (&key, entry) in self.nums() { - let expr = entry.val.as_ref(); - if let Some(val) = T::try_from_expr(expr, &mut Feedback::new()) { - self.remove(key); - return Some(val); - } - } - None - } - - /// Retrieve and remove the matching value with the lowest number key, - /// removing and generating errors for all non-matching entries with lower - /// keys. - pub fn expect(&mut self, f: &mut Feedback) -> Option { - while let Some((num, _)) = self.first() { - let entry = self.remove(num).unwrap(); - if let Some(val) = T::try_from_expr(entry.val.as_ref(), f) { - return Some(val); - } - } - None - } - - /// Retrieve and remove a matching value associated with the given key if - /// there is any. - /// - /// Generates an error if the key exists but the value does not match. - pub fn take_with_key<'a, K, T>(&mut self, key: K, f: &mut Feedback) -> Option - where - K: Into>, - T: TryFromExpr, - { - self.remove(key).and_then(|entry| { - let expr = entry.val.as_ref(); - T::try_from_expr(expr, f) - }) - } - - /// Retrieve and remove all matching pairs with number keys, skipping and - /// ignoring non-matching entries. - /// - /// The pairs are returned in order of increasing keys. - pub fn take_all_num<'a, T>(&'a mut self) -> impl Iterator + 'a - where - T: TryFromExpr, - { - let mut skip = 0; - std::iter::from_fn(move || { - for (&key, entry) in self.nums().skip(skip) { - let expr = entry.val.as_ref(); - if let Some(val) = T::try_from_expr(expr, &mut Feedback::new()) { - self.remove(key); - return Some((key, val)); - } - skip += 1; - } - - None - }) - } - - - /// Retrieve and remove all matching values with number keys, skipping and - /// ignoring non-matching entries. - /// - /// The values are returned in order of increasing keys. - pub fn take_all_num_vals<'a, T: 'a>(&'a mut self) -> impl Iterator + 'a - where - T: TryFromExpr, - { - self.take_all_num::().map(|(_, v)| v) - } - - /// Retrieve and remove all matching pairs with string keys, skipping and - /// ignoring non-matching entries. - /// - /// The pairs are returned in order of increasing keys. - pub fn take_all_str<'a, T>(&'a mut self) -> impl Iterator + 'a - where - T: TryFromExpr, - { - let mut skip = 0; - std::iter::from_fn(move || { - for (key, entry) in self.strs().skip(skip) { - let expr = entry.val.as_ref(); - if let Some(val) = T::try_from_expr(expr, &mut Feedback::new()) { - let key = key.clone(); - self.remove(&key); - return Some((key, val)); - } - skip += 1; - } - - None - }) - } - - /// Generated `"unexpected argument"` errors for all remaining entries. - pub fn unexpected(&self, f: &mut Feedback) { - for entry in self.values() { - let span = Span::merge(entry.key, entry.val.span); - error!(@f, span, "unexpected argument"); - } - } -} - -/// An entry in a table expression. -/// -/// Contains the key's span and the value. -#[derive(Clone, PartialEq)] -pub struct TableExprEntry { - pub key: Span, - pub val: Spanned, -} - -impl TableExprEntry { - /// Create a new entry. - pub fn new(key: Span, val: Spanned) -> Self { - Self { key, val } - } - - /// Create an entry for a positional argument with the same span for key and - /// value. - pub fn val(val: Spanned) -> Self { - Self { key: Span::ZERO, val } - } -} - -impl Debug for TableExprEntry { - fn fmt(&self, f: &mut Formatter) -> fmt::Result { - if f.alternate() { - f.write_str("key")?; - self.key.fmt(f)?; - f.write_str(" ")?; - } - self.val.fmt(f) - } -} - -/// A trait for converting expressions into specific types. -pub trait TryFromExpr: Sized { - // This trait takes references because we don't want to move the expression - // out of its origin in case this returns `None`. This solution is not - // perfect because we need to do some cloning in the impls for this trait, - // but I haven't got a better solution, for now. - - /// Try to convert an expression into this type. - /// - /// Returns `None` and generates an appropriate error if the expression is - /// not valid for this type. - fn try_from_expr(expr: Spanned<&Expr>, f: &mut Feedback) -> Option; -} - -macro_rules! impl_match { - ($type:ty, $name:expr, $($p:pat => $r:expr),* $(,)?) => { - impl TryFromExpr for $type { - fn try_from_expr(expr: Spanned<&Expr>, f: &mut Feedback) -> Option { - #[allow(unreachable_patterns)] - match expr.v { - $($p => Some($r)),*, - other => { - error!( - @f, expr.span, - "expected {}, found {}", $name, other.name() - ); - None - } - } - } - } - }; -} - -macro_rules! impl_ident { - ($type:ty, $name:expr, $parse:expr) => { - impl TryFromExpr for $type { - fn try_from_expr(expr: Spanned<&Expr>, f: &mut Feedback) -> Option { - if let Expr::Ident(ident) = expr.v { - let val = $parse(ident.as_str()); - if val.is_none() { - error!(@f, expr.span, "invalid {}", $name); - } - val - } else { - error!( - @f, expr.span, - "expected {}, found {}", $name, expr.v.name() - ); - None - } - } - } - }; -} - -impl TryFromExpr for Spanned { - fn try_from_expr(expr: Spanned<&Expr>, f: &mut Feedback) -> Option { - let span = expr.span; - T::try_from_expr(expr, f).map(|v| Spanned { v, span }) - } -} - -impl_match!(Expr, "expression", e => e.clone()); -impl_match!(Ident, "identifier", Expr::Ident(i) => i.clone()); -impl_match!(String, "string", Expr::Str(s) => s.clone()); -impl_match!(bool, "bool", Expr::Bool(b) => b.clone()); -impl_match!(f64, "number", Expr::Number(n) => n.clone()); -impl_match!(Length, "length", Expr::Length(l) => l.clone()); -impl_match!(SyntaxTree, "tree", Expr::Tree(t) => t.clone()); -impl_match!(TableExpr, "table", Expr::Table(t) => t.clone()); -impl_match!(ScaleLength, "number or length", - &Expr::Length(length) => ScaleLength::Absolute(length), - &Expr::Number(scale) => ScaleLength::Scaled(scale), -); - -/// A value type that matches identifiers and strings and implements -/// `Into`. -pub struct StringLike(pub String); - -impl From for String { - fn from(like: StringLike) -> String { - like.0 - } -} - -impl_match!(StringLike, "identifier or string", - Expr::Ident(Ident(s)) => StringLike(s.clone()), - Expr::Str(s) => StringLike(s.clone()), -); - -impl_ident!(Dir, "direction", |s| match s { - "ltr" => Some(Self::LTR), - "rtl" => Some(Self::RTL), - "ttb" => Some(Self::TTB), - "btt" => Some(Self::BTT), - _ => None, -}); - -impl_ident!(SpecAlign, "alignment", |s| match s { - "left" => Some(Self::Left), - "right" => Some(Self::Right), - "top" => Some(Self::Top), - "bottom" => Some(Self::Bottom), - "center" => Some(Self::Center), - _ => None, -}); - -impl_ident!(FontStyle, "font style", FontStyle::from_name); -impl_ident!(Paper, "paper", Paper::from_name); - -impl TryFromExpr for FontWeight { - fn try_from_expr(expr: Spanned<&Expr>, f: &mut Feedback) -> Option { - match expr.v { - &Expr::Number(weight) => { - const MIN: u16 = 100; - const MAX: u16 = 900; - - Some(Self(if weight < MIN as f64 { - error!(@f, expr.span, "the minimum font weight is {}", MIN); - MIN - } else if weight > MAX as f64 { - error!(@f, expr.span, "the maximum font weight is {}", MAX); - MAX - } else { - weight.round() as u16 - })) - } - Expr::Ident(ident) => { - let weight = Self::from_name(ident.as_str()); - if weight.is_none() { - error!(@f, expr.span, "invalid font weight"); - } - weight - } - other => { - error!( - @f, expr.span, - "expected font weight (name or number), found {}", - other.name(), - ); - None - } - } - } -} - -impl TryFromExpr for FontWidth { - fn try_from_expr(expr: Spanned<&Expr>, f: &mut Feedback) -> Option { - match expr.v { - &Expr::Number(width) => { - const MIN: u16 = 1; - const MAX: u16 = 9; - - Self::new(if width < MIN as f64 { - error!(@f, expr.span, "the minimum font width is {}", MIN); - MIN - } else if width > MAX as f64 { - error!(@f, expr.span, "the maximum font width is {}", MAX); - MAX - } else { - width.round() as u16 - }) - } - Expr::Ident(ident) => { - let width = Self::from_name(ident.as_str()); - if width.is_none() { - error!(@f, expr.span, "invalid font width"); - } - width - } - other => { - error!( - @f, expr.span, - "expected font width (name or number), found {}", - other.name(), - ); - None - } - } - } -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn parse_color_strings() { - fn test(hex: &str, r: u8, g: u8, b: u8, a: u8) { - assert_eq!( - RgbaColor::from_str(hex), - Ok(RgbaColor::new(r, g, b, a)), - ); - } - - test("f61243ff", 0xf6, 0x12, 0x43, 0xff); - test("b3d8b3", 0xb3, 0xd8, 0xb3, 0xff); - test("fCd2a9AD", 0xfc, 0xd2, 0xa9, 0xad); - test("233", 0x22, 0x33, 0x33, 0xff); - test("111b", 0x11, 0x11, 0x11, 0xbb); - } - - fn entry(expr: Expr) -> TableExprEntry { - TableExprEntry { - key: Span::ZERO, - val: Spanned::zero(expr), - } - } - - #[test] - fn test_table_take_removes_correct_entry() { - let mut table = TableExpr::new(); - table.insert(1, entry(Expr::Bool(false))); - table.insert(2, entry(Expr::Str("hi".to_string()))); - assert_eq!(table.take::(), Some("hi".to_string())); - assert_eq!(table.len(), 1); - assert_eq!(table.take::(), Some(false)); - assert!(table.is_empty()); - } - - #[test] - fn test_table_expect_errors_about_previous_entries() { - let mut f = Feedback::new(); - let mut table = TableExpr::new(); - table.insert(1, entry(Expr::Bool(false))); - table.insert(3, entry(Expr::Str("hi".to_string()))); - table.insert(5, entry(Expr::Bool(true))); - assert_eq!(table.expect::(&mut f), Some("hi".to_string())); - assert_eq!(f.diagnostics, [error!(Span::ZERO, "expected string, found bool")]); - assert_eq!(table.len(), 1); - } - - #[test] - fn test_table_take_with_key_removes_the_entry() { - let mut f = Feedback::new(); - let mut table = TableExpr::new(); - table.insert(1, entry(Expr::Bool(false))); - table.insert("hi", entry(Expr::Bool(true))); - assert_eq!(table.take_with_key::<_, bool>(1, &mut f), Some(false)); - assert_eq!(table.take_with_key::<_, f64>("hi", &mut f), None); - assert_eq!(f.diagnostics, [error!(Span::ZERO, "expected number, found bool")]); - assert!(table.is_empty()); - } - - #[test] - fn test_table_take_all_removes_the_correct_entries() { - let mut table = TableExpr::new(); - table.insert(1, entry(Expr::Bool(false))); - table.insert(3, entry(Expr::Number(0.0))); - table.insert(7, entry(Expr::Bool(true))); - assert_eq!( - table.take_all_num::().collect::>(), - [(1, false), (7, true)], - ); - assert_eq!(table.len(), 1); - assert_eq!(table[3].val.v, Expr::Number(0.0)); - } -} diff --git a/src/syntax/mod.rs b/src/syntax/mod.rs index fdc105a35..596291a54 100644 --- a/src/syntax/mod.rs +++ b/src/syntax/mod.rs @@ -1,17 +1,44 @@ //! Syntax trees, parsing and tokenization. pub mod decoration; -pub mod expr; pub mod parsing; -pub mod scope; pub mod span; pub mod tokens; pub mod tree; +use std::fmt::{self, Debug, Formatter}; +use tokens::is_identifier; + +/// An identifier as defined by unicode with a few extra permissible characters. +#[derive(Clone, Eq, PartialEq, Ord, PartialOrd, Hash)] +pub struct Ident(pub String); + +impl Ident { + /// Create a new identifier from a string checking that it is a valid. + pub fn new(ident: impl AsRef + Into) -> Option { + if is_identifier(ident.as_ref()) { + Some(Self(ident.into())) + } else { + None + } + } + + /// Return a reference to the underlying string. + pub fn as_str(&self) -> &str { + self.0.as_str() + } +} + +impl Debug for Ident { + fn fmt(&self, f: &mut Formatter) -> fmt::Result { + write!(f, "`{}`", self.0) + } +} + #[cfg(test)] mod tests { use std::fmt::Debug; - use crate::func::prelude::*; + use crate::prelude::*; use super::span; /// Assert that expected and found are equal, printing both and panicking @@ -49,18 +76,4 @@ mod tests { Spanned::zero(t) } } - - pub fn debug_func(call: FuncCall, _: &ParseState) -> Pass { - Pass::node(DebugNode(call), Feedback::new()) - } - - #[derive(Debug, Clone, PartialEq)] - pub struct DebugNode(pub FuncCall); - - #[async_trait(?Send)] - impl Layout for DebugNode { - async fn layout<'a>(&'a self, _: LayoutContext<'_>) -> Pass> { - unimplemented!() - } - } } diff --git a/src/syntax/parsing.rs b/src/syntax/parsing.rs index 88c930cb7..8ed778e15 100644 --- a/src/syntax/parsing.rs +++ b/src/syntax/parsing.rs @@ -3,31 +3,13 @@ use std::str::FromStr; use crate::{Feedback, Pass}; +use crate::color::RgbaColor; +use crate::compute::table::SpannedEntry; use super::decoration::Decoration; -use super::expr::*; -use super::scope::Scope; use super::span::{Pos, Span, Spanned}; use super::tokens::{is_newline_char, Token, TokenMode, Tokens}; -use super::tree::{SyntaxNode, SyntaxTree}; - -/// A function which parses a function call into a dynamic node. -pub type CallParser = dyn Fn(FuncCall, &ParseState) -> Pass; - -/// An invocation of a function. -#[derive(Debug, Clone, PartialEq)] -pub struct FuncCall { - pub name: Spanned, - pub args: TableExpr, -} - -/// The state which can influence how a string of source code is parsed. -/// -/// Parsing is pure - when passed in the same state and source code, the output -/// must be the same. -pub struct ParseState { - /// The scope containing all function definitions. - pub scope: Scope, -} +use super::tree::{CallExpr, Expr, SyntaxNode, SyntaxTree, TableExpr}; +use super::Ident; /// Parse a string of source code. /// @@ -35,7 +17,7 @@ pub struct ParseState { /// `offset` position. This is used to make spans of a function body relative to /// the start of the function as a whole as opposed to the start of the /// function's body. -pub fn parse(src: &str, offset: Pos, state: &ParseState) -> Pass { +pub fn parse(src: &str, offset: Pos) -> Pass { let mut tree = SyntaxTree::new(); let mut par = SyntaxTree::new(); let mut feedback = Feedback::new(); @@ -58,12 +40,12 @@ pub fn parse(src: &str, offset: Pos, state: &ParseState) -> Pass { } Token::Function { header, body, terminated } => { - let parsed = FuncParser::new(header, body, state).parse(); + let parsed = FuncParser::new(header, body).parse(); feedback.extend_offset(parsed.feedback, span.start); if !terminated { error!(@feedback, Span::at(span.end), "expected closing bracket"); } - parsed.output + SyntaxNode::Call(parsed.output) } Token::Star => SyntaxNode::ToggleBolder, @@ -97,8 +79,6 @@ pub fn parse(src: &str, offset: Pos, state: &ParseState) -> Pass { } struct FuncParser<'s> { - state: &'s ParseState, - /// The tokens inside the header. tokens: Tokens<'s>, peeked: Option>>>, body: Option>, @@ -106,13 +86,8 @@ struct FuncParser<'s> { } impl<'s> FuncParser<'s> { - fn new( - header: &'s str, - body: Option>, - state: &'s ParseState, - ) -> Self { + fn new(header: &'s str, body: Option>) -> Self { Self { - state, // Start at column 1 because the opening bracket is also part of // the function, but not part of the `header` string. tokens: Tokens::new(header, Pos::new(0, 1), TokenMode::Header), @@ -122,65 +97,17 @@ impl<'s> FuncParser<'s> { } } - fn parse(mut self) -> Pass { - let (parser, mut call) = if let Some(call) = self.parse_func_header() { - let name = call.name.v.as_str(); - let (parser, deco) = match self.state.scope.get_parser(name) { - // The function exists in the scope. - Some(parser) => (parser, Decoration::ResolvedFunc), - - // The function does not exist in the scope. The parser that is - // returned here is a fallback parser which exists to make sure - // the content of the function is not totally dropped (on a best - // effort basis). - None => { - error!(@self.feedback, call.name.span, "unknown function"); - let parser = self.state.scope.get_fallback_parser(); - (parser, Decoration::UnresolvedFunc) - } - }; - - self.feedback.decorations.push(Spanned::new(deco, call.name.span)); - (parser, call) - } else { - // Parse the call with the fallback parser even when the header is - // completely unparsable. - let parser = self.state.scope.get_fallback_parser(); - let call = FuncCall { - name: Spanned::new(Ident(String::new()), Span::ZERO), - args: TableExpr::new(), - }; - (parser, call) - }; - - if let Some(body) = self.body { - call.args.push(TableExprEntry { - key: Span::ZERO, - val: body.map(|src| { - let parsed = parse(src, body.span.start, &self.state); - self.feedback.extend(parsed.feedback); - Expr::Tree(parsed.output) - }), - }); - } - - let parsed = parser(call, self.state); - self.feedback.extend(parsed.feedback); - - Pass::new(parsed.output, self.feedback) - } - - fn parse_func_header(&mut self) -> Option { + fn parse(mut self) -> Pass { let after_bracket = self.pos(); self.skip_white(); - let name = try_opt_or!(self.parse_ident(), { + let name = self.parse_ident().unwrap_or_else(|| { self.expected_found_or_at("function name", after_bracket); - return None; + Spanned::zero(Ident(String::new())) }); self.skip_white(); - let args = match self.eat().map(Spanned::value) { + let mut args = match self.eat().map(Spanned::value) { Some(Token::Colon) => self.parse_table(false).0.v, Some(_) => { self.expected_at("colon", name.span.end); @@ -189,7 +116,15 @@ impl<'s> FuncParser<'s> { None => TableExpr::new(), }; - Some(FuncCall { name, args }) + if let Some(body) = self.body { + args.push(SpannedEntry::val(body.map(|src| { + let parsed = parse(src, body.span.start); + self.feedback.extend(parsed.feedback); + Expr::Tree(parsed.output) + }))); + } + + Pass::new(CallExpr { name, args }, self.feedback) } } @@ -325,14 +260,20 @@ impl FuncParser<'_> { } } - fn parse_func_call(&mut self, name: Spanned) -> Spanned { + fn parse_func_call(&mut self, name: Spanned) -> Spanned { let args = self.parse_table(true).0; let span = Span::merge(name.span, args.span); - Spanned::new(FuncCall { name, args: args.v }, span) + Spanned::new(CallExpr { name, args: args.v }, span) } - /// The boolean tells you whether the table can be coerced into an expression - /// (this is the case when it's length 1 and has no trailing comma). + /// Set `parens` to true, when this should expect an opening paren and stop + /// at the balanced closing paren (this is the case for normal tables and + /// round-paren function calls). Set it to false, when this is used to parse + /// the top-level function arguments. + /// + /// The returned boolean tells you whether the table can be coerced into an + /// expression (this is the case when it's length 1 and has no trailing + /// comma). fn parse_table(&mut self, parens: bool) -> (Spanned, bool) { let start = self.pos(); if parens { @@ -369,19 +310,19 @@ impl FuncParser<'_> { coercable = false; behind_arg = val.span.end; - table.insert(key.v.0, TableExprEntry::new(key.span, val)); + table.insert(key.v.0, SpannedEntry::new(key.span, val)); } else if self.check(Token::LeftParen) { let call = self.parse_func_call(ident); let expr = call.map(|call| Expr::Call(call)); behind_arg = expr.span.end; - table.push(TableExprEntry::val(expr)); + table.push(SpannedEntry::val(expr)); } else { let expr = ident.map(|id| Expr::Ident(id)); behind_arg = expr.span.end; - table.push(TableExprEntry::val(expr)); + table.push(SpannedEntry::val(expr)); } } else { // It's a positional argument. @@ -390,7 +331,7 @@ impl FuncParser<'_> { continue; }); behind_arg = expr.span.end; - table.push(TableExprEntry::val(expr)); + table.push(SpannedEntry::val(expr)); } self.skip_white(); @@ -593,7 +534,7 @@ mod tests { } macro_rules! F { - ($($tts:tt)*) => { SyntaxNode::boxed(DebugNode(Call!(@$($tts)*))) } + ($($tts:tt)*) => { SyntaxNode::Call(Call!(@$($tts)*)) } } // ------------------------ Construct Expressions ----------------------- // @@ -603,24 +544,17 @@ mod tests { fn Id(ident: &str) -> Expr { Expr::Ident(Ident(ident.to_string())) } fn Str(string: &str) -> Expr { Expr::Str(string.to_string()) } - macro_rules! Tree { - (@$($node:expr),* $(,)?) => { - vec![$(Into::>::into($node)),*] - }; - ($($tts:tt)*) => { Expr::Tree(Tree![@$($tts)*]) }; - } - macro_rules! Table { (@table=$table:expr,) => {}; (@table=$table:expr, $key:expr => $value:expr $(, $($tts:tt)*)?) => {{ let key = Into::>::into($key); let val = Into::>::into($value); - $table.insert(key.v, TableExprEntry::new(key.span, val)); + $table.insert(key.v, SpannedEntry::new(key.span, val)); Table![@table=$table, $($($tts)*)?]; }}; (@table=$table:expr, $value:expr $(, $($tts:tt)*)?) => { let val = Into::>::into($value); - $table.push(TableExprEntry::val(val)); + $table.push(SpannedEntry::val(val)); Table![@table=$table, $($($tts)*)?]; }; (@$($tts:tt)*) => {{ @@ -632,6 +566,24 @@ mod tests { ($($tts:tt)*) => { Expr::Table(Table![@$($tts)*]) }; } + macro_rules! Tree { + (@$($node:expr),* $(,)?) => { + vec![$(Into::>::into($node)),*] + }; + ($($tts:tt)*) => { Expr::Tree(Tree![@$($tts)*]) }; + } + + macro_rules! Call { + (@$name:expr $(; $($tts:tt)*)?) => {{ + let name = Into::>::into($name); + CallExpr { + name: name.map(|n| Ident(n.to_string())), + args: Table![@$($($tts)*)?], + } + }}; + ($($tts:tt)*) => { Expr::Call(Call![@$($tts)*]) }; + } + fn Neg>>(e1: T) -> Expr { Expr::Neg(Box::new(e1.into())) } @@ -648,17 +600,6 @@ mod tests { Expr::Div(Box::new(e1.into()), Box::new(e2.into())) } - macro_rules! Call { - (@$name:expr $(; $($tts:tt)*)?) => {{ - let name = Into::>::into($name); - FuncCall { - name: name.map(|n| Ident(n.to_string())), - args: Table![@$($($tts)*)?], - } - }}; - ($($tts:tt)*) => { Expr::Call(Call![@$($tts)*]) }; - } - // ----------------------------- Test Macros ---------------------------- // // Test syntax trees with or without spans. @@ -667,7 +608,7 @@ mod tests { macro_rules! test { (@spans=$spans:expr, $src:expr => $($tts:tt)*) => { let exp = Tree![@$($tts)*]; - let pass = parse_default($src); + let pass = parse($src, Pos::ZERO); check($src, exp, pass.output, $spans); }; } @@ -683,7 +624,7 @@ mod tests { macro_rules! e { ($src:expr => $($tts:tt)*) => { let exp = vec![$($tts)*]; - let pass = parse_default($src); + let pass = parse($src, Pos::ZERO); let found = pass.feedback.diagnostics.iter() .map(|s| s.as_ref().map(|e| e.message.as_str())) .collect::>(); @@ -695,20 +636,11 @@ mod tests { macro_rules! d { ($src:expr => $($tts:tt)*) => { let exp = vec![$($tts)*]; - let pass = parse_default($src); + let pass = parse($src, Pos::ZERO); check($src, exp, pass.feedback.decorations, true); }; } - fn parse_default(src: &str) -> Pass { - let mut scope = Scope::new(Box::new(debug_func)); - scope.insert("box", Box::new(debug_func)); - scope.insert("val", Box::new(debug_func)); - scope.insert("f", Box::new(debug_func)); - let state = ParseState { scope }; - parse(src, Pos::ZERO, &state) - } - // -------------------------------- Tests ------------------------------- // #[test] @@ -797,15 +729,9 @@ mod tests { e!("[\"]" => s(0,1, 0,3, "expected function name, found string"), s(0,3, 0,3, "expected closing bracket")); - // An unknown name. - t!("[hi]" => P![F!("hi")]); - e!("[hi]" => s(0,1, 0,3, "unknown function")); - d!("[hi]" => s(0,1, 0,3, UnresolvedFunc)); - // A valid name. - t!("[f]" => P![F!("f")]); + t!("[hi]" => P![F!("hi")]); t!("[ f]" => P![F!("f")]); - d!("[ f]" => s(0,3, 0,4, ResolvedFunc)); // An invalid name. e!("[12]" => s(0,1, 0,3, "expected function name, found number")); @@ -821,7 +747,6 @@ mod tests { t!("[val=]" => P![F!("val")]); e!("[val=]" => s(0,4, 0,4, "expected colon")); e!("[val/🌎:$]" => s(0,4, 0,4, "expected colon")); - d!("[val=]" => s(0,1, 0,4, ResolvedFunc)); // String in invalid header without colon still parsed as string // Note: No "expected quote" error because not even the string was @@ -939,9 +864,9 @@ mod tests { v!("(1, key=\"value\")" => Table![Num(1.0), "key" => Str("value")]); // Decorations. - d!("[val: key=hi]" => s(0,6, 0,9, TableKey), s(0,1, 0,4, ResolvedFunc)); - d!("[val: (key=hi)]" => s(0,7, 0,10, TableKey), s(0,1, 0,4, ResolvedFunc)); - d!("[val: f(key=hi)]" => s(0,8, 0,11, TableKey), s(0,1, 0,4, ResolvedFunc)); + d!("[val: key=hi]" => s(0,6, 0,9, TableKey)); + d!("[val: (key=hi)]" => s(0,7, 0,10, TableKey)); + d!("[val: f(key=hi)]" => s(0,8, 0,11, TableKey)); // Spanned with spacing around keyword arguments. ts!("[val: \n hi \n = /* //\n */ \"s\n\"]" => s(0,0, 4,2, P![ diff --git a/src/syntax/scope.rs b/src/syntax/scope.rs deleted file mode 100644 index c17ff64d0..000000000 --- a/src/syntax/scope.rs +++ /dev/null @@ -1,44 +0,0 @@ -//! Mapping of function names to function parsers. - -use std::collections::HashMap; -use std::fmt::{self, Debug, Formatter}; - -use super::parsing::CallParser; - -/// A map from identifiers to function parsers. -pub struct Scope { - parsers: HashMap>, - fallback: Box, -} - -impl Scope { - /// Create a new empty scope with a fallback parser that is invoked when no - /// match is found. - pub fn new(fallback: Box) -> Self { - Self { - parsers: HashMap::new(), - fallback, - } - } - - /// Associate the given function name with a dynamic node type. - pub fn insert(&mut self, name: impl Into, parser: Box) { - self.parsers.insert(name.into(), parser); - } - - /// Return the parser with the given name if there is one. - pub fn get_parser(&self, name: &str) -> Option<&CallParser> { - self.parsers.get(name).map(AsRef::as_ref) - } - - /// Return the fallback parser. - pub fn get_fallback_parser(&self) -> &CallParser { - &*self.fallback - } -} - -impl Debug for Scope { - fn fmt(&self, f: &mut Formatter) -> fmt::Result { - f.debug_set().entries(self.parsers.keys()).finish() - } -} diff --git a/src/syntax/span.rs b/src/syntax/span.rs index 923d13c29..89f773c75 100644 --- a/src/syntax/span.rs +++ b/src/syntax/span.rs @@ -39,13 +39,11 @@ impl Offset for SpanVec { } /// A value with the span it corresponds to in the source code. -#[derive(Copy, Clone, Eq, PartialEq, Hash)] +#[derive(Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash)] #[cfg_attr(feature = "serialize", derive(Serialize))] pub struct Spanned { - /// The value. - pub v: T, - /// The corresponding span. pub span: Span, + pub v: T, } impl Spanned { @@ -99,7 +97,7 @@ impl Debug for Spanned { } /// Locates a slice of source code. -#[derive(Copy, Clone, Hash)] +#[derive(Copy, Clone, Ord, PartialOrd, Hash)] #[cfg_attr(feature = "serialize", derive(Serialize))] pub struct Span { /// The inclusive start position. diff --git a/src/syntax/tree.rs b/src/syntax/tree.rs index 4ce39cd4b..e7a1eaf14 100644 --- a/src/syntax/tree.rs +++ b/src/syntax/tree.rs @@ -1,17 +1,20 @@ //! The syntax tree. -use std::any::Any; -use std::fmt::Debug; +use std::fmt::{self, Debug, Formatter}; -use crate::layout::Layout; -use super::span::SpanVec; +use crate::color::RgbaColor; +use crate::compute::table::{SpannedEntry, Table}; +use crate::compute::value::{TableValue, Value}; +use crate::length::Length; +use super::span::{Spanned, SpanVec}; +use super::Ident; /// A collection of nodes which form a tree together with the nodes' children. pub type SyntaxTree = SpanVec; /// A syntax node, which encompasses a single logical entity of parsed source /// code. -#[derive(Debug, Clone)] +#[derive(Debug, Clone, PartialEq)] pub enum SyntaxNode { /// Whitespace containing less than two newlines. Spacing, @@ -27,84 +30,138 @@ pub enum SyntaxNode { Raw(Vec), /// A paragraph of child nodes. Par(SyntaxTree), - /// A dynamic node, created through function invocations in source code. - Dyn(Box), + /// A function call. + Call(CallExpr), } -impl SyntaxNode { - /// Create a `Dyn` variant from an unboxed dynamic node. - pub fn boxed(node: T) -> SyntaxNode { - SyntaxNode::Dyn(Box::new(node)) +/// An expression. +#[derive(Clone, PartialEq)] +pub enum Expr { + /// An identifier: `ident`. + Ident(Ident), + /// A string: `"string"`. + Str(String), + /// A boolean: `true, false`. + Bool(bool), + /// A number: `1.2, 200%`. + Number(f64), + /// A length: `2cm, 5.2in`. + Length(Length), + /// A color value with alpha channel: `#f79143ff`. + Color(RgbaColor), + /// A table expression: `(false, 12cm, greeting="hi")`. + Table(TableExpr), + /// A syntax tree containing typesetting content. + Tree(SyntaxTree), + /// A function call expression: `cmyk(37.7, 0, 3.9, 1.1)`. + Call(CallExpr), + /// An operation that negates the contained expression. + Neg(Box>), + /// An operation that adds the contained expressions. + Add(Box>, Box>), + /// An operation that subtracts the contained expressions. + Sub(Box>, Box>), + /// An operation that multiplies the contained expressions. + Mul(Box>, Box>), + /// An operation that divides the contained expressions. + Div(Box>, Box>), +} + +impl Expr { + /// A natural-language name of the type of this expression, e.g. + /// "identifier". + pub fn name(&self) -> &'static str { + use Expr::*; + match self { + Ident(_) => "identifier", + Str(_) => "string", + Bool(_) => "bool", + Number(_) => "number", + Length(_) => "length", + Color(_) => "color", + Table(_) => "table", + Tree(_) => "syntax tree", + Call(_) => "function call", + Neg(_) => "negation", + Add(_, _) => "addition", + Sub(_, _) => "subtraction", + Mul(_, _) => "multiplication", + Div(_, _) => "division", + } } -} -impl PartialEq for SyntaxNode { - fn eq(&self, other: &SyntaxNode) -> bool { - use SyntaxNode::*; - match (self, other) { - (Spacing, Spacing) => true, - (Linebreak, Linebreak) => true, - (ToggleItalic, ToggleItalic) => true, - (ToggleBolder, ToggleBolder) => true, - (Text(a), Text(b)) => a == b, - (Raw(a), Raw(b)) => a == b, - (Par(a), Par(b)) => a == b, - (Dyn(a), Dyn(b)) => a == b, - _ => false, + /// Evaluate the expression to a value. + pub fn eval(&self) -> Value { + use Expr::*; + match self { + Ident(i) => Value::Ident(i.clone()), + Str(s) => Value::Str(s.clone()), + &Bool(b) => Value::Bool(b), + &Number(n) => Value::Number(n), + &Length(s) => Value::Length(s), + &Color(c) => Value::Color(c), + Table(t) => Value::Table(t.eval()), + Tree(t) => Value::Tree(t.clone()), + Call(_) => todo!("eval call"), + Neg(_) => todo!("eval neg"), + Add(_, _) => todo!("eval add"), + Sub(_, _) => todo!("eval sub"), + Mul(_, _) => todo!("eval mul"), + Div(_, _) => todo!("eval div"), } } } -/// Dynamic syntax nodes. +impl Debug for Expr { + fn fmt(&self, f: &mut Formatter) -> fmt::Result { + use Expr::*; + match self { + Ident(i) => i.fmt(f), + Str(s) => s.fmt(f), + Bool(b) => b.fmt(f), + Number(n) => n.fmt(f), + Length(s) => s.fmt(f), + Color(c) => c.fmt(f), + Table(t) => t.fmt(f), + Tree(t) => t.fmt(f), + Call(c) => c.fmt(f), + Neg(e) => write!(f, "-{:?}", e), + Add(a, b) => write!(f, "({:?} + {:?})", a, b), + Sub(a, b) => write!(f, "({:?} - {:?})", a, b), + Mul(a, b) => write!(f, "({:?} * {:?})", a, b), + Div(a, b) => write!(f, "({:?} / {:?})", a, b), + } + } +} + +/// A table of expressions. /// -/// *Note*: This is automatically implemented for all types which are -/// `Debug + Clone + PartialEq`, `Layout` and `'static`. -pub trait DynamicNode: Debug + Layout { - /// Convert into a `dyn Any`. - fn as_any(&self) -> &dyn Any; +/// # Example +/// ```typst +/// (false, 12cm, greeting="hi") +/// ``` +pub type TableExpr = Table>; - /// Check for equality with another dynamic node. - fn dyn_eq(&self, other: &dyn DynamicNode) -> bool; +impl TableExpr { + /// Evaluate the table expression to a table value. + pub fn eval(&self) -> TableValue { + let mut table = TableValue::new(); - /// Clone into a boxed node trait object. - fn box_clone(&self) -> Box; -} - -impl dyn DynamicNode { - /// Downcast this dynamic node to a concrete node. - pub fn downcast(&self) -> Option<&T> { - self.as_any().downcast_ref::() - } -} - -impl PartialEq for dyn DynamicNode { - fn eq(&self, other: &Self) -> bool { - self.dyn_eq(other) - } -} - -impl Clone for Box { - fn clone(&self) -> Self { - self.box_clone() - } -} - -impl DynamicNode for T -where - T: Debug + PartialEq + Clone + Layout + 'static, -{ - fn as_any(&self) -> &dyn Any { - self - } - - fn dyn_eq(&self, other: &dyn DynamicNode) -> bool { - match other.as_any().downcast_ref::() { - Some(other) => self == other, - None => false, + for (&key, entry) in self.nums() { + table.insert(key, entry.as_ref().map(|val| val.eval())); } - } - fn box_clone(&self) -> Box { - Box::new(self.clone()) + for (key, entry) in self.strs() { + table.insert(key.clone(), entry.as_ref().map(|val| val.eval())); + } + + table } } + +/// An invocation of a function. +#[derive(Debug, Clone, PartialEq)] +pub struct CallExpr { + pub name: Spanned, + pub args: TableExpr, +} diff --git a/tests/test_typeset.rs b/tests/test_typeset.rs index 880818daa..4d8533935 100644 --- a/tests/test_typeset.rs +++ b/tests/test_typeset.rs @@ -87,7 +87,10 @@ fn test( let typeset = block_on(typesetter.typeset(src)); let layouts = typeset.output; - for diagnostic in typeset.feedback.diagnostics { + let mut feedback = typeset.feedback; + + feedback.diagnostics.sort(); + for diagnostic in feedback.diagnostics { println!( " {:?} {:?}: {}", diagnostic.v.level, diagnostic.span, diagnostic.v.message,