Dynamic values, Types, Arrays, and Dictionaries 🚀

- Identifiers are now evaluated as variables instead of being plain values
- Constants like `left` or `bold` are stored as dynamic values containing the respective rust types
- We now distinguish between arrays and dictionaries to make things more intuitive (at the cost of a bit more complex parsing)
- Spans were removed from collections (arrays, dictionaries), function arguments still have spans for the top-level values to enable good diagnostics
This commit is contained in:
Laurenz 2021-01-02 19:37:10 +01:00
parent 8cad78481c
commit 1c40dc42e7
24 changed files with 1110 additions and 1332 deletions

View File

@ -111,7 +111,7 @@ mod tests {
use super::*;
#[test]
fn parse_color_strings() {
fn test_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)));
}
@ -124,7 +124,7 @@ mod tests {
}
#[test]
fn parse_invalid_colors() {
fn test_parse_invalid_colors() {
fn test(hex: &str) {
assert_eq!(RgbaColor::from_str(hex), Err(ParseRgbaError));
}

View File

@ -1,184 +1,138 @@
//! Simplifies argument parsing.
use super::*;
use std::mem;
/// Evaluated arguments to a function.
#[derive(Debug)]
pub struct Args {
span: Span,
pos: SpanVec<Value>,
named: Vec<(Spanned<String>, Spanned<Value>)>,
}
use super::{Conv, EvalContext, RefKey, TryFromValue, Value, ValueDict};
use crate::diag::Diag;
use crate::syntax::{Span, SpanVec, Spanned, WithSpan};
impl Eval for Spanned<&Arguments> {
type Output = Args;
/// A wrapper around a dictionary value that simplifies argument parsing in
/// functions.
pub struct Args(pub Spanned<ValueDict>);
fn eval(self, ctx: &mut EvalContext) -> Self::Output {
let mut pos = vec![];
let mut named = vec![];
for arg in self.v {
match arg {
Argument::Pos(expr) => {
pos.push(expr.as_ref().eval(ctx).with_span(expr.span));
}
Argument::Named(Named { name, expr }) => {
named.push((
name.as_ref().map(|id| id.0.clone()),
expr.as_ref().eval(ctx).with_span(expr.span),
));
}
}
}
Args { span: self.span, pos, named }
}
}
impl Args {
/// Retrieve and remove the argument associated with the given key if there
/// is any.
/// Find the first convertible positional argument.
pub fn find<T>(&mut self, ctx: &mut EvalContext) -> Option<T>
where
T: Cast<Spanned<Value>>,
{
self.pos.iter_mut().find_map(move |slot| try_cast(ctx, slot))
}
/// Find the first convertible positional argument, producing an error if
/// no match was found.
pub fn require<T>(&mut self, ctx: &mut EvalContext, what: &str) -> Option<T>
where
T: Cast<Spanned<Value>>,
{
let found = self.find(ctx);
if found.is_none() {
ctx.diag(error!(self.span, "missing argument: {}", what));
}
found
}
/// Filter out all convertible positional arguments.
pub fn filter<'a, T>(
&'a mut self,
ctx: &'a mut EvalContext,
) -> impl Iterator<Item = T> + 'a
where
T: Cast<Spanned<Value>>,
{
self.pos.iter_mut().filter_map(move |slot| try_cast(ctx, slot))
}
/// Convert the value for the given named argument.
///
/// Generates an error if the key exists, but the value can't be converted
/// into the type `T`.
pub fn get<'a, K, T>(&mut self, ctx: &mut EvalContext, key: K) -> Option<T>
/// Generates an error if the conversion fails.
pub fn get<'a, T>(&mut self, ctx: &mut EvalContext, name: &str) -> Option<T>
where
K: Into<RefKey<'a>>,
T: TryFromValue,
T: Cast<Spanned<Value>>,
{
self.0.v.remove(key).and_then(|entry| {
let span = entry.value.span;
conv_diag(
T::try_from_value(entry.value),
&mut ctx.feedback.diags,
span,
)
})
let index = self.named.iter().position(|(k, _)| k.v.as_str() == name)?;
let value = self.named.remove(index).1;
cast(ctx, value)
}
/// This is the same as [`get`](Self::get), except that it generates an error about a
/// missing argument with the given `name` if the key does not exist.
pub fn need<'a, K, T>(
&mut self,
ctx: &mut EvalContext,
key: K,
name: &str,
) -> Option<T>
where
K: Into<RefKey<'a>>,
T: TryFromValue,
{
if let Some(entry) = self.0.v.remove(key) {
let span = entry.value.span;
conv_diag(
T::try_from_value(entry.value),
&mut ctx.feedback.diags,
span,
)
} else {
ctx.diag(error!(self.0.span, "missing argument: {}", name));
None
}
}
/// Retrieve and remove the first matching positional argument.
pub fn find<T>(&mut self) -> Option<T>
where
T: TryFromValue,
{
for (&key, entry) in self.0.v.nums_mut() {
let span = entry.value.span;
let slot = &mut entry.value;
let conv = conv_put_back(T::try_from_value(mem::take(slot)), slot, span);
if let Some(t) = conv {
self.0.v.remove(key);
return Some(t);
/// Generate "unexpected argument" errors for all remaining arguments.
pub fn finish(self, ctx: &mut EvalContext) {
let a = self.pos.iter().map(|v| v.as_ref());
let b = self.named.iter().map(|(k, v)| (&v.v).with_span(k.span.join(v.span)));
for value in a.chain(b) {
if value.v != &Value::Error {
ctx.diag(error!(value.span, "unexpected argument"));
}
}
None
}
/// Retrieve and remove all matching positional arguments.
pub fn find_all<T>(&mut self) -> impl Iterator<Item = T> + '_
where
T: TryFromValue,
{
let mut skip = 0;
std::iter::from_fn(move || {
for (&key, entry) in self.0.v.nums_mut().skip(skip) {
let span = entry.value.span;
let slot = &mut entry.value;
let conv = conv_put_back(T::try_from_value(mem::take(slot)), slot, span);
if let Some(t) = conv {
self.0.v.remove(key);
return Some(t);
}
skip += 1;
}
None
})
}
/// Retrieve and remove all matching named arguments.
pub fn find_all_str<T>(&mut self) -> impl Iterator<Item = (String, T)> + '_
where
T: TryFromValue,
{
let mut skip = 0;
std::iter::from_fn(move || {
for (key, entry) in self.0.v.strs_mut().skip(skip) {
let span = entry.value.span;
let slot = &mut entry.value;
let conv = conv_put_back(T::try_from_value(mem::take(slot)), slot, span);
if let Some(t) = conv {
let key = key.clone();
self.0.v.remove(&key);
return Some((key, t));
}
skip += 1;
}
None
})
}
/// Generated _unexpected argument_ errors for all remaining entries.
pub fn done(&self, ctx: &mut EvalContext) {
for entry in self.0.v.values() {
let span = entry.key_span.join(entry.value.span);
ctx.diag(error!(span, "unexpected argument"));
}
}
}
fn conv_diag<T>(conv: Conv<T>, diags: &mut SpanVec<Diag>, span: Span) -> Option<T> {
match conv {
Conv::Ok(t) => Some(t),
Conv::Warn(t, warn) => {
diags.push(warn.with_span(span));
/// Cast the value into `T`, generating an error if the conversion fails.
fn cast<T>(ctx: &mut EvalContext, value: Spanned<Value>) -> Option<T>
where
T: Cast<Spanned<Value>>,
{
let span = value.span;
match T::cast(value) {
CastResult::Ok(t) => Some(t),
CastResult::Warn(t, m) => {
ctx.diag(warning!(span, "{}", m));
Some(t)
}
Conv::Err(_, err) => {
diags.push(err.with_span(span));
CastResult::Err(value) => {
ctx.diag(error!(
span,
"expected {}, found {}",
T::TYPE_NAME,
value.v.type_name()
));
None
}
}
}
fn conv_put_back<T>(conv: Conv<T>, slot: &mut Spanned<Value>, span: Span) -> Option<T> {
match conv {
Conv::Ok(t) => Some(t),
Conv::Warn(t, _) => Some(t),
Conv::Err(v, _) => {
*slot = v.with_span(span);
/// Try to cast the value in the slot into `T`, putting it back if the
/// conversion fails.
fn try_cast<T>(ctx: &mut EvalContext, slot: &mut Spanned<Value>) -> Option<T>
where
T: Cast<Spanned<Value>>,
{
// Replace with error placeholder when conversion works since error values
// are ignored when generating "unexpected argument" errors.
let value = std::mem::replace(slot, Spanned::zero(Value::Error));
let span = value.span;
match T::cast(value) {
CastResult::Ok(t) => Some(t),
CastResult::Warn(t, m) => {
ctx.diag(warning!(span, "{}", m));
Some(t)
}
CastResult::Err(value) => {
*slot = value;
None
}
}
}
#[cfg(test)]
mod tests {
use super::super::{Dict, SpannedEntry, Value};
use super::*;
fn entry(value: Value) -> SpannedEntry<Value> {
SpannedEntry::value(Spanned::zero(value))
}
#[test]
fn test_args_find() {
let mut args = Args(Spanned::zero(Dict::new()));
args.0.v.insert(1, entry(Value::Bool(false)));
args.0.v.insert(2, entry(Value::Str("hi".to_string())));
assert_eq!(args.find::<String>(), Some("hi".to_string()));
assert_eq!(args.0.v.len(), 1);
assert_eq!(args.find::<bool>(), Some(false));
assert!(args.0.v.is_empty());
}
#[test]
fn test_args_find_all() {
let mut args = Args(Spanned::zero(Dict::new()));
args.0.v.insert(1, entry(Value::Bool(false)));
args.0.v.insert(3, entry(Value::Float(0.0)));
args.0.v.insert(7, entry(Value::Bool(true)));
assert_eq!(args.find_all::<bool>().collect::<Vec<_>>(), [false, true]);
assert_eq!(args.0.v.len(), 1);
assert_eq!(args.0.v[3].value.v, Value::Float(0.0));
}
}

View File

@ -1,522 +0,0 @@
//! A key-value map that can also model array-like structures.
use std::collections::BTreeMap;
use std::fmt::{self, Debug, Display, Formatter};
use std::iter::{Extend, FromIterator};
use std::ops::Index;
use crate::syntax::{Span, Spanned};
/// A dictionary data structure, which maps from integers and strings to a
/// generic value type.
///
/// The dictionary can be used to model arrays by assigning values to successive
/// indices from `0..n`. The `push` method offers special support for this
/// pattern.
#[derive(Clone)]
pub struct Dict<V> {
nums: BTreeMap<u64, V>,
strs: BTreeMap<String, V>,
lowest_free: u64,
}
impl<V> Dict<V> {
/// Create a new empty dictionary.
pub fn new() -> Self {
Self {
nums: BTreeMap::new(),
strs: BTreeMap::new(),
lowest_free: 0,
}
}
/// The total number of entries in the dictionary.
pub fn len(&self) -> usize {
self.nums.len() + self.strs.len()
}
/// Whether the dictionary contains no entries.
pub fn is_empty(&self) -> bool {
self.len() == 0
}
/// The first number key-value pair (with lowest number).
pub fn first(&self) -> Option<(u64, &V)> {
self.nums.iter().next().map(|(&k, v)| (k, v))
}
/// The last number key-value pair (with highest number).
pub fn last(&self) -> Option<(u64, &V)> {
self.nums.iter().next_back().map(|(&k, v)| (k, v))
}
/// Get a reference to the value with the given key.
pub fn get<'a, K>(&self, key: K) -> Option<&V>
where
K: Into<RefKey<'a>>,
{
match key.into() {
RefKey::Num(num) => self.nums.get(&num),
RefKey::Str(string) => self.strs.get(string),
}
}
/// Borrow the value with the given key mutably.
pub fn get_mut<'a, K>(&mut self, key: K) -> Option<&mut V>
where
K: Into<RefKey<'a>>,
{
match key.into() {
RefKey::Num(num) => self.nums.get_mut(&num),
RefKey::Str(string) => self.strs.get_mut(string),
}
}
/// Insert a value into the dictionary.
pub fn insert<K>(&mut self, key: K, value: V)
where
K: Into<DictKey>,
{
match key.into() {
DictKey::Num(num) => {
self.nums.insert(num, value);
if self.lowest_free == num {
self.lowest_free += 1;
}
}
DictKey::Str(string) => {
self.strs.insert(string, value);
}
}
}
/// Remove the value with the given key from the dictionary.
pub fn remove<'a, K>(&mut self, key: K) -> Option<V>
where
K: Into<RefKey<'a>>,
{
match key.into() {
RefKey::Num(num) => {
self.lowest_free = self.lowest_free.min(num);
self.nums.remove(&num)
}
RefKey::Str(string) => self.strs.remove(string),
}
}
/// Append a value to the dictionary.
///
/// This will associate the `value` with the lowest free number key (zero if
/// there is no number key so far).
pub fn push(&mut self, value: V) {
while self.nums.contains_key(&self.lowest_free) {
self.lowest_free += 1;
}
self.nums.insert(self.lowest_free, value);
self.lowest_free += 1;
}
}
impl<'a, K, V> Index<K> for Dict<V>
where
K: Into<RefKey<'a>>,
{
type Output = V;
fn index(&self, index: K) -> &Self::Output {
self.get(index).expect("key not in dict")
}
}
impl<V: Eq> Eq for Dict<V> {}
impl<V: PartialEq> PartialEq for Dict<V> {
fn eq(&self, other: &Self) -> bool {
self.iter().eq(other.iter())
}
}
impl<V> Default for Dict<V> {
fn default() -> Self {
Self::new()
}
}
impl<V: Debug> Debug for Dict<V> {
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
if self.is_empty() {
return f.write_str("()");
}
let mut builder = f.debug_tuple("");
struct Entry<'a>(bool, &'a dyn Display, &'a dyn Debug);
impl<'a> Debug for Entry<'a> {
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
if self.0 {
f.write_str("\"")?;
}
self.1.fmt(f)?;
if self.0 {
f.write_str("\"")?;
}
f.write_str(": ")?;
self.2.fmt(f)
}
}
for (key, value) in self.nums() {
builder.field(&Entry(false, &key, &value));
}
for (key, value) in self.strs() {
builder.field(&Entry(key.contains(' '), &key, &value));
}
builder.finish()
}
}
/// Iteration.
impl<V> Dict<V> {
/// Iterator over all borrowed keys and values.
pub fn iter(&self) -> impl Iterator<Item = (RefKey, &V)> {
self.into_iter()
}
/// Iterator over all borrowed keys and values.
pub fn iter_mut(&mut self) -> impl Iterator<Item = (RefKey, &mut V)> {
self.into_iter()
}
/// Iterate over all values in the dictionary.
pub fn values(&self) -> impl Iterator<Item = &V> {
self.nums().map(|(_, v)| v).chain(self.strs().map(|(_, v)| v))
}
/// Iterate over all values in the dictionary.
pub fn values_mut(&mut self) -> impl Iterator<Item = &mut V> {
self.nums
.iter_mut()
.map(|(_, v)| v)
.chain(self.strs.iter_mut().map(|(_, v)| v))
}
/// Move into an owned iterator over all values in the dictionary.
pub fn into_values(self) -> impl Iterator<Item = V> {
self.nums
.into_iter()
.map(|(_, v)| v)
.chain(self.strs.into_iter().map(|(_, v)| v))
}
/// Iterate over the number key-value pairs.
pub fn nums(&self) -> std::collections::btree_map::Iter<u64, V> {
self.nums.iter()
}
/// Iterate mutably over the number key-value pairs.
pub fn nums_mut(&mut self) -> std::collections::btree_map::IterMut<u64, V> {
self.nums.iter_mut()
}
/// Iterate over the number key-value pairs.
pub fn into_nums(self) -> std::collections::btree_map::IntoIter<u64, V> {
self.nums.into_iter()
}
/// Iterate over the string key-value pairs.
pub fn strs(&self) -> std::collections::btree_map::Iter<String, V> {
self.strs.iter()
}
/// Iterate mutably over the string key-value pairs.
pub fn strs_mut(&mut self) -> std::collections::btree_map::IterMut<String, V> {
self.strs.iter_mut()
}
/// Iterate over the string key-value pairs.
pub fn into_strs(self) -> std::collections::btree_map::IntoIter<String, V> {
self.strs.into_iter()
}
}
impl<V> Extend<(DictKey, V)> for Dict<V> {
fn extend<T: IntoIterator<Item = (DictKey, V)>>(&mut self, iter: T) {
for (key, value) in iter.into_iter() {
self.insert(key, value);
}
}
}
impl<V> FromIterator<(DictKey, V)> for Dict<V> {
fn from_iter<T: IntoIterator<Item = (DictKey, V)>>(iter: T) -> Self {
let mut v = Self::new();
v.extend(iter);
v
}
}
impl<V> IntoIterator for Dict<V> {
type Item = (DictKey, V);
type IntoIter = std::iter::Chain<
std::iter::Map<
std::collections::btree_map::IntoIter<u64, V>,
fn((u64, V)) -> (DictKey, V),
>,
std::iter::Map<
std::collections::btree_map::IntoIter<String, V>,
fn((String, V)) -> (DictKey, V),
>,
>;
fn into_iter(self) -> Self::IntoIter {
let nums = self.nums.into_iter().map((|(k, v)| (DictKey::Num(k), v)) as _);
let strs = self.strs.into_iter().map((|(k, v)| (DictKey::Str(k), v)) as _);
nums.chain(strs)
}
}
impl<'a, V> IntoIterator for &'a Dict<V> {
type Item = (RefKey<'a>, &'a V);
type IntoIter = std::iter::Chain<
std::iter::Map<
std::collections::btree_map::Iter<'a, u64, V>,
fn((&'a u64, &'a V)) -> (RefKey<'a>, &'a V),
>,
std::iter::Map<
std::collections::btree_map::Iter<'a, String, V>,
fn((&'a String, &'a V)) -> (RefKey<'a>, &'a V),
>,
>;
fn into_iter(self) -> Self::IntoIter {
let nums = self.nums().map((|(k, v): (&u64, _)| (RefKey::Num(*k), v)) as _);
let strs = self.strs().map((|(k, v): (&'a String, _)| (RefKey::Str(k), v)) as _);
nums.chain(strs)
}
}
impl<'a, V> IntoIterator for &'a mut Dict<V> {
type Item = (RefKey<'a>, &'a mut V);
type IntoIter = std::iter::Chain<
std::iter::Map<
std::collections::btree_map::IterMut<'a, u64, V>,
fn((&'a u64, &'a mut V)) -> (RefKey<'a>, &'a mut V),
>,
std::iter::Map<
std::collections::btree_map::IterMut<'a, String, V>,
fn((&'a String, &'a mut V)) -> (RefKey<'a>, &'a mut V),
>,
>;
fn into_iter(self) -> Self::IntoIter {
let nums = self
.nums
.iter_mut()
.map((|(k, v): (&u64, _)| (RefKey::Num(*k), v)) as _);
let strs = self
.strs
.iter_mut()
.map((|(k, v): (&'a String, _)| (RefKey::Str(k), v)) as _);
nums.chain(strs)
}
}
/// The owned variant of a dictionary key.
#[derive(Debug, Clone, Eq, PartialEq, Ord, PartialOrd)]
pub enum DictKey {
Num(u64),
Str(String),
}
impl From<&Self> for DictKey {
fn from(key: &Self) -> Self {
key.clone()
}
}
impl From<RefKey<'_>> for DictKey {
fn from(key: RefKey<'_>) -> Self {
match key {
RefKey::Num(num) => Self::Num(num),
RefKey::Str(string) => Self::Str(string.to_string()),
}
}
}
impl From<u64> for DictKey {
fn from(num: u64) -> Self {
Self::Num(num)
}
}
impl From<String> for DictKey {
fn from(string: String) -> Self {
Self::Str(string)
}
}
impl From<&'static str> for DictKey {
fn from(string: &'static str) -> Self {
Self::Str(string.to_string())
}
}
/// The borrowed variant of a dictionary key.
#[derive(Debug, Copy, Clone, Eq, PartialEq, Ord, PartialOrd)]
pub enum RefKey<'a> {
Num(u64),
Str(&'a str),
}
impl From<u64> for RefKey<'static> {
fn from(num: u64) -> Self {
Self::Num(num)
}
}
impl<'a> From<&'a String> for RefKey<'a> {
fn from(string: &'a String) -> Self {
Self::Str(&string)
}
}
impl<'a> From<&'a str> for RefKey<'a> {
fn from(string: &'a str) -> Self {
Self::Str(string)
}
}
/// A dictionary entry which combines key span and value.
///
/// This exists because a key in a directory can't track its span by itself.
#[derive(Clone, PartialEq)]
pub struct SpannedEntry<V> {
pub key_span: Span,
pub value: Spanned<V>,
}
impl<V> SpannedEntry<V> {
/// Create a new entry.
pub fn new(key: Span, val: Spanned<V>) -> Self {
Self { key_span: key, value: val }
}
/// Create an entry with the same span for key and value.
pub fn value(val: Spanned<V>) -> Self {
Self { key_span: val.span, value: val }
}
/// Convert from `&SpannedEntry<T>` to `SpannedEntry<&T>`
pub fn as_ref(&self) -> SpannedEntry<&V> {
SpannedEntry {
key_span: self.key_span,
value: self.value.as_ref(),
}
}
/// Map the entry to a different value type.
pub fn map<U>(self, f: impl FnOnce(V) -> U) -> SpannedEntry<U> {
SpannedEntry {
key_span: self.key_span,
value: self.value.map(f),
}
}
}
impl<V: Debug> Debug for SpannedEntry<V> {
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
if f.alternate() {
f.write_str("key")?;
self.key_span.fmt(f)?;
f.write_str(" ")?;
}
self.value.fmt(f)
}
}
#[cfg(test)]
mod tests {
use super::Dict;
#[test]
fn test_dict_different_key_types_dont_interfere() {
let mut dict = Dict::new();
dict.insert(10, "hello");
dict.insert("twenty", "there");
assert_eq!(dict.len(), 2);
assert_eq!(dict[10], "hello");
assert_eq!(dict["twenty"], "there");
}
#[test]
fn test_dict_push_skips_already_inserted_keys() {
let mut dict = Dict::new();
dict.insert(2, "2");
dict.push("0");
dict.insert(3, "3");
dict.push("1");
dict.push("4");
assert_eq!(dict.len(), 5);
assert_eq!(dict[0], "0");
assert_eq!(dict[1], "1");
assert_eq!(dict[2], "2");
assert_eq!(dict[3], "3");
assert_eq!(dict[4], "4");
}
#[test]
fn test_dict_push_remove_push_reuses_index() {
let mut dict = Dict::new();
dict.push("0");
dict.push("1");
dict.push("2");
dict.remove(1);
dict.push("a");
dict.push("3");
assert_eq!(dict.len(), 4);
assert_eq!(dict[0], "0");
assert_eq!(dict[1], "a");
assert_eq!(dict[2], "2");
assert_eq!(dict[3], "3");
}
#[test]
fn test_dict_first_and_last_are_correct() {
let mut dict = Dict::new();
assert_eq!(dict.first(), None);
assert_eq!(dict.last(), None);
dict.insert(4, "hi");
dict.insert("string", "hi");
assert_eq!(dict.first(), Some((4, &"hi")));
assert_eq!(dict.last(), Some((4, &"hi")));
dict.insert(2, "bye");
assert_eq!(dict.first(), Some((2, &"bye")));
assert_eq!(dict.last(), Some((4, &"hi")));
}
#[test]
fn test_dict_format_debug() {
let mut dict = Dict::new();
assert_eq!(format!("{:?}", dict), "()");
assert_eq!(format!("{:#?}", dict), "()");
dict.insert(10, "hello");
dict.insert("twenty", "there");
dict.insert("sp ace", "quotes");
assert_eq!(
format!("{:?}", dict),
r#"(10: "hello", "sp ace": "quotes", twenty: "there")"#,
);
assert_eq!(format!("{:#?}", dict).lines().collect::<Vec<_>>(), [
"(",
r#" 10: "hello","#,
r#" "sp ace": "quotes","#,
r#" twenty: "there","#,
")",
]);
}
}

View File

@ -3,12 +3,10 @@
#[macro_use]
mod value;
mod args;
mod dict;
mod scope;
mod state;
pub use args::*;
pub use dict::*;
pub use scope::*;
pub use state::*;
pub use value::*;
@ -451,7 +449,13 @@ impl Eval for Spanned<&Lit> {
fn eval(self, ctx: &mut EvalContext) -> Self::Output {
match *self.v {
Lit::Ident(ref v) => Value::Ident(v.clone()),
Lit::Ident(ref v) => match ctx.state.scope.get(v.as_str()) {
Some(value) => value.clone(),
None => {
ctx.diag(error!(self.span, "unknown variable"));
Value::Error
}
},
Lit::Bool(v) => Value::Bool(v),
Lit::Int(v) => Value::Int(v),
Lit::Float(v) => Value::Float(v),
@ -459,28 +463,29 @@ impl Eval for Spanned<&Lit> {
Lit::Percent(v) => Value::Relative(Relative::new(v / 100.0)),
Lit::Color(v) => Value::Color(Color::Rgba(v)),
Lit::Str(ref v) => Value::Str(v.clone()),
Lit::Array(ref v) => Value::Array(v.with_span(self.span).eval(ctx)),
Lit::Dict(ref v) => Value::Dict(v.with_span(self.span).eval(ctx)),
Lit::Content(ref v) => Value::Content(v.clone()),
}
}
}
impl Eval for Spanned<&LitDict> {
impl Eval for Spanned<&Array> {
type Output = ValueArray;
fn eval(self, ctx: &mut EvalContext) -> Self::Output {
self.v.iter().map(|expr| expr.as_ref().eval(ctx)).collect()
}
}
impl Eval for Spanned<&Dict> {
type Output = ValueDict;
fn eval(self, ctx: &mut EvalContext) -> Self::Output {
let mut dict = ValueDict::new();
for entry in &self.v.0 {
let val = entry.expr.as_ref().eval(ctx);
let spanned = val.with_span(entry.expr.span);
if let Some(key) = &entry.key {
dict.insert(&key.v, SpannedEntry::new(key.span, spanned));
} else {
dict.push(SpannedEntry::value(spanned));
}
}
dict
self.v
.iter()
.map(|Named { name, expr }| (name.v.0.clone(), expr.as_ref().eval(ctx)))
.collect()
}
}
@ -490,19 +495,27 @@ impl Eval for Spanned<&ExprCall> {
fn eval(self, ctx: &mut EvalContext) -> Self::Output {
let name = &self.v.name.v;
let span = self.v.name.span;
let dict = self.v.args.as_ref().eval(ctx);
if let Some(func) = ctx.state.scope.get(name) {
let args = Args(dict.with_span(self.v.args.span));
ctx.feedback.decos.push(Deco::Resolved.with_span(span));
(func.clone())(args, ctx)
} else {
if !name.is_empty() {
ctx.diag(error!(span, "unknown function"));
ctx.feedback.decos.push(Deco::Unresolved.with_span(span));
if let Some(value) = ctx.state.scope.get(name) {
if let Value::Func(func) = value {
let func = func.clone();
ctx.feedback.decos.push(Deco::Resolved.with_span(span));
let mut args = self.v.args.as_ref().eval(ctx);
let returned = func(ctx, &mut args);
args.finish(ctx);
return returned;
} else {
let ty = value.type_name();
ctx.diag(error!(span, "a value of type {} is not callable", ty));
}
Value::Dict(dict)
} else if !name.is_empty() {
ctx.diag(error!(span, "unknown function"));
}
ctx.feedback.decos.push(Deco::Unresolved.with_span(span));
Value::Error
}
}
@ -554,7 +567,7 @@ fn neg(ctx: &mut EvalContext, span: Span, value: Value) -> Value {
Relative(v) => Relative(-v),
Linear(v) => Linear(-v),
v => {
ctx.diag(error!(span, "cannot negate {}", v.ty()));
ctx.diag(error!(span, "cannot negate {}", v.type_name()));
Value::Error
}
}
@ -589,7 +602,12 @@ fn add(ctx: &mut EvalContext, span: Span, lhs: Value, rhs: Value) -> Value {
(Content(a), Content(b)) => Content(concat(a, b)),
(a, b) => {
ctx.diag(error!(span, "cannot add {} and {}", a.ty(), b.ty()));
ctx.diag(error!(
span,
"cannot add {} and {}",
a.type_name(),
b.type_name()
));
Value::Error
}
}
@ -617,7 +635,12 @@ fn sub(ctx: &mut EvalContext, span: Span, lhs: Value, rhs: Value) -> Value {
(Linear(a), Linear(b)) => Linear(a - b),
(a, b) => {
ctx.diag(error!(span, "cannot subtract {1} from {0}", a.ty(), b.ty()));
ctx.diag(error!(
span,
"cannot subtract {1} from {0}",
a.type_name(),
b.type_name()
));
Value::Error
}
}
@ -652,7 +675,12 @@ fn mul(ctx: &mut EvalContext, span: Span, lhs: Value, rhs: Value) -> Value {
(Str(a), Int(b)) => Str(a.repeat(0.max(b) as usize)),
(a, b) => {
ctx.diag(error!(span, "cannot multiply {} with {}", a.ty(), b.ty()));
ctx.diag(error!(
span,
"cannot multiply {} with {}",
a.type_name(),
b.type_name()
));
Value::Error
}
}
@ -677,7 +705,12 @@ fn div(ctx: &mut EvalContext, span: Span, lhs: Value, rhs: Value) -> Value {
(Linear(a), Float(b)) => Linear(a / b),
(a, b) => {
ctx.diag(error!(span, "cannot divide {} by {}", a.ty(), b.ty()));
ctx.diag(error!(
span,
"cannot divide {} by {}",
a.type_name(),
b.type_name()
));
Value::Error
}
}

View File

@ -3,12 +3,12 @@
use std::collections::HashMap;
use std::fmt::{self, Debug, Formatter};
use super::value::ValueFunc;
use super::Value;
/// A map from identifiers to functions.
#[derive(Default, Clone, PartialEq)]
pub struct Scope {
functions: HashMap<String, ValueFunc>,
values: HashMap<String, Value>,
}
impl Scope {
@ -18,19 +18,19 @@ impl Scope {
Self::default()
}
/// Return the function with the given name if there is one.
pub fn get(&self, name: &str) -> Option<&ValueFunc> {
self.functions.get(name)
/// Return the value of the given variable.
pub fn get(&self, var: &str) -> Option<&Value> {
self.values.get(var)
}
/// Associate the given name with the function.
pub fn set(&mut self, name: impl Into<String>, function: ValueFunc) {
self.functions.insert(name.into(), function);
/// Store the value for the given variable.
pub fn set(&mut self, var: impl Into<String>, value: impl Into<Value>) {
self.values.insert(var.into(), value.into());
}
}
impl Debug for Scope {
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
f.debug_set().entries(self.functions.keys()).finish()
self.values.fmt(f)
}
}

View File

@ -1,25 +1,21 @@
//! Computational values.
use std::any::Any;
use std::collections::HashMap;
use std::fmt::{self, Debug, Formatter};
use std::ops::Deref;
use std::rc::Rc;
use fontdock::{FontStretch, FontStyle, FontWeight};
use super::{Args, Dict, Eval, EvalContext, SpannedEntry};
use super::{Args, Eval, EvalContext};
use crate::color::Color;
use crate::diag::Diag;
use crate::geom::{Dir, Length, Linear, Relative};
use crate::paper::Paper;
use crate::syntax::{Ident, Spanned, SynTree, WithSpan};
use crate::geom::{Length, Linear, Relative};
use crate::syntax::{Spanned, SynTree, WithSpan};
/// A computational value.
#[derive(Clone, PartialEq)]
pub enum Value {
/// The value that indicates the absence of a meaningful value.
None,
/// An identifier: `ident`.
Ident(Ident),
/// A boolean: `true, false`.
Bool(bool),
/// An integer: `120`.
@ -36,34 +32,46 @@ pub enum Value {
Color(Color),
/// A string: `"string"`.
Str(String),
/// A dictionary value: `(false, 12cm, greeting: "hi")`.
/// An array value: `(1, "hi", 12cm)`.
Array(ValueArray),
/// A dictionary value: `(color: #f79143, pattern: dashed)`.
Dict(ValueDict),
/// A content value: `{*Hi* there}`.
Content(SynTree),
Content(ValueContent),
/// An executable function.
Func(ValueFunc),
/// Any object.
Any(ValueAny),
/// The result of invalid operations.
Error,
}
impl Value {
/// The natural-language name of this value's type for use in error
/// messages.
pub fn ty(&self) -> &'static str {
/// Try to cast the value into a specific type.
pub fn cast<T>(self) -> CastResult<T, Self>
where
T: Cast<Value>,
{
T::cast(self)
}
/// The name of the stored value's type.
pub fn type_name(&self) -> &'static str {
match self {
Self::None => "none",
Self::Ident(_) => "identifier",
Self::Bool(_) => "bool",
Self::Int(_) => "integer",
Self::Float(_) => "float",
Self::Relative(_) => "relative",
Self::Length(_) => "length",
Self::Linear(_) => "linear",
Self::Color(_) => "color",
Self::Str(_) => "string",
Self::Dict(_) => "dict",
Self::Content(_) => "content",
Self::Func(_) => "function",
Self::Bool(_) => bool::TYPE_NAME,
Self::Int(_) => i64::TYPE_NAME,
Self::Float(_) => f64::TYPE_NAME,
Self::Relative(_) => Relative::TYPE_NAME,
Self::Length(_) => Length::TYPE_NAME,
Self::Linear(_) => Linear::TYPE_NAME,
Self::Color(_) => Color::TYPE_NAME,
Self::Str(_) => String::TYPE_NAME,
Self::Array(_) => ValueArray::TYPE_NAME,
Self::Dict(_) => ValueDict::TYPE_NAME,
Self::Content(_) => ValueContent::TYPE_NAME,
Self::Func(_) => ValueFunc::TYPE_NAME,
Self::Any(v) => v.type_name(),
Self::Error => "error",
}
}
@ -81,13 +89,6 @@ impl Eval for &Value {
// Pass through.
Value::Content(tree) => tree.eval(ctx),
// Forward to each dictionary entry.
Value::Dict(dict) => {
for entry in dict.values() {
entry.value.v.eval(ctx);
}
}
// Format with debug.
val => ctx.push(ctx.make_text_node(format!("{:?}", val))),
}
@ -104,7 +105,6 @@ impl Debug for Value {
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
match self {
Self::None => f.pad("none"),
Self::Ident(v) => v.fmt(f),
Self::Bool(v) => v.fmt(f),
Self::Int(v) => v.fmt(f),
Self::Float(v) => v.fmt(f),
@ -113,45 +113,36 @@ impl Debug for Value {
Self::Linear(v) => v.fmt(f),
Self::Color(v) => v.fmt(f),
Self::Str(v) => v.fmt(f),
Self::Array(v) => v.fmt(f),
Self::Dict(v) => v.fmt(f),
Self::Content(v) => v.fmt(f),
Self::Func(v) => v.fmt(f),
Self::Any(v) => v.fmt(f),
Self::Error => f.pad("<error>"),
}
}
}
/// A dictionary of values.
///
/// # Example
/// ```typst
/// (false, 12cm, greeting: "hi")
/// ```
pub type ValueDict = Dict<SpannedEntry<Value>>;
/// An array value: `(1, "hi", 12cm)`.
pub type ValueArray = Vec<Value>;
/// An wrapper around a reference-counted function trait object.
///
/// The dynamic function object is wrapped in an `Rc` to keep [`Value`]
/// cloneable.
///
/// _Note_: This is needed because the compiler can't `derive(PartialEq)` for
/// [`Value`] when directly putting the `Rc` in there, see the [Rust
/// Issue].
///
/// [Rust Issue]: https://github.com/rust-lang/rust/issues/31740
/// A dictionary value: `(color: #f79143, pattern: dashed)`.
pub type ValueDict = HashMap<String, Value>;
/// A content value: `{*Hi* there}`.
pub type ValueContent = SynTree;
/// A wrapper around a reference-counted executable function.
#[derive(Clone)]
pub struct ValueFunc(pub Rc<Func>);
/// The signature of executable functions.
type Func = dyn Fn(Args, &mut EvalContext) -> Value;
pub struct ValueFunc(Rc<dyn Fn(&mut EvalContext, &mut Args) -> Value>);
impl ValueFunc {
/// Create a new function value from a rust function or closure.
pub fn new<F>(f: F) -> Self
pub fn new<F>(func: F) -> Self
where
F: Fn(Args, &mut EvalContext) -> Value + 'static,
F: Fn(&mut EvalContext, &mut Args) -> Value + 'static,
{
Self(Rc::new(f))
Self(Rc::new(func))
}
}
@ -162,7 +153,7 @@ impl PartialEq for ValueFunc {
}
impl Deref for ValueFunc {
type Target = Func;
type Target = dyn Fn(&mut EvalContext, &mut Args) -> Value;
fn deref(&self) -> &Self::Target {
self.0.as_ref()
@ -175,160 +166,288 @@ impl Debug for ValueFunc {
}
}
/// Try to convert a value into a more specific type.
pub trait TryFromValue: Sized {
/// Try to convert the value into yourself.
fn try_from_value(value: Spanned<Value>) -> Conv<Self>;
/// A wrapper around a dynamic value.
pub struct ValueAny(Box<dyn Bounds>);
impl ValueAny {
/// Create a new instance from any value that satisifies the required bounds.
pub fn new<T>(any: T) -> Self
where
T: Type + Debug + Clone + PartialEq + 'static,
{
Self(Box::new(any))
}
/// Whether the wrapped type is `T`.
pub fn is<T: 'static>(&self) -> bool {
self.0.as_any().is::<T>()
}
/// Try to downcast to a specific type.
pub fn downcast<T: 'static>(self) -> Result<T, Self> {
if self.is::<T>() {
Ok(*self.0.into_any().downcast().unwrap())
} else {
Err(self)
}
}
/// Try to downcast to a reference to a specific type.
pub fn downcast_ref<T: 'static>(&self) -> Option<&T> {
self.0.as_any().downcast_ref()
}
/// The name of the stored object's type.
pub fn type_name(&self) -> &'static str {
self.0.dyn_type_name()
}
}
/// The result of a conversion.
#[derive(Debug, Clone, PartialEq)]
pub enum Conv<T> {
/// Success conversion.
impl Clone for ValueAny {
fn clone(&self) -> Self {
Self(self.0.dyn_clone())
}
}
impl PartialEq for ValueAny {
fn eq(&self, other: &Self) -> bool {
self.0.dyn_eq(other)
}
}
impl Debug for ValueAny {
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
self.0.fmt(f)
}
}
trait Bounds: Debug + 'static {
fn as_any(&self) -> &dyn Any;
fn into_any(self: Box<Self>) -> Box<dyn Any>;
fn dyn_eq(&self, other: &ValueAny) -> bool;
fn dyn_clone(&self) -> Box<dyn Bounds>;
fn dyn_type_name(&self) -> &'static str;
}
impl<T> Bounds for T
where
T: Type + Debug + Clone + PartialEq + 'static,
{
fn as_any(&self) -> &dyn Any {
self
}
fn into_any(self: Box<Self>) -> Box<dyn Any> {
self
}
fn dyn_eq(&self, other: &ValueAny) -> bool {
if let Some(other) = other.downcast_ref::<Self>() {
self == other
} else {
false
}
}
fn dyn_clone(&self) -> Box<dyn Bounds> {
Box::new(self.clone())
}
fn dyn_type_name(&self) -> &'static str {
T::TYPE_NAME
}
}
/// Types that can be stored in values.
pub trait Type {
/// The name of the type.
const TYPE_NAME: &'static str;
}
impl<T> Type for Spanned<T>
where
T: Type,
{
const TYPE_NAME: &'static str = T::TYPE_NAME;
}
/// Cast from a value to a specific type.
pub trait Cast<V>: Type + Sized {
/// Try to cast the value into an instance of `Self`.
fn cast(value: V) -> CastResult<Self, V>;
}
/// The result of casting a value to a specific type.
#[derive(Debug, Clone, Eq, PartialEq)]
pub enum CastResult<T, V> {
/// The value was cast successfully.
Ok(T),
/// Sucessful conversion with a warning.
Warn(T, Diag),
/// Unsucessful conversion, gives back the value alongside the error.
Err(Value, Diag),
/// The value was cast successfully, but with a warning message.
Warn(T, String),
/// The value could not be cast into the specified type.
Err(V),
}
impl<T> Conv<T> {
/// Map the conversion result.
pub fn map<U>(self, f: impl FnOnce(T) -> U) -> Conv<U> {
impl<T, V> CastResult<T, V> {
/// Access the conversion resulting, discarding a possibly existing warning.
pub fn ok(self) -> Option<T> {
match self {
Conv::Ok(t) => Conv::Ok(f(t)),
Conv::Warn(t, warn) => Conv::Warn(f(t), warn),
Conv::Err(v, err) => Conv::Err(v, err),
CastResult::Ok(t) | CastResult::Warn(t, _) => Some(t),
CastResult::Err(_) => None,
}
}
}
impl<T: TryFromValue> TryFromValue for Spanned<T> {
fn try_from_value(value: Spanned<Value>) -> Conv<Self> {
impl<T> Cast<Spanned<Value>> for T
where
T: Cast<Value>,
{
fn cast(value: Spanned<Value>) -> CastResult<Self, Spanned<Value>> {
let span = value.span;
T::try_from_value(value).map(|v| v.with_span(span))
match T::cast(value.v) {
CastResult::Ok(t) => CastResult::Ok(t),
CastResult::Warn(t, m) => CastResult::Warn(t, m),
CastResult::Err(v) => CastResult::Err(v.with_span(span)),
}
}
}
/// A value type that matches [identifier](Value::Ident) and [string](Value::Str) values.
pub struct StringLike(pub String);
impl From<StringLike> for String {
fn from(like: StringLike) -> String {
like.0
impl<T> Cast<Spanned<Value>> for Spanned<T>
where
T: Cast<Value>,
{
fn cast(value: Spanned<Value>) -> CastResult<Self, Spanned<Value>> {
let span = value.span;
match T::cast(value.v) {
CastResult::Ok(t) => CastResult::Ok(t.with_span(span)),
CastResult::Warn(t, m) => CastResult::Warn(t.with_span(span), m),
CastResult::Err(v) => CastResult::Err(v.with_span(span)),
}
}
}
impl Deref for StringLike {
type Target = str;
macro_rules! impl_primitive {
($type:ty:
$type_name:literal,
$variant:path
$(, $pattern:pat => $out:expr)* $(,)?
) => {
impl Type for $type {
const TYPE_NAME: &'static str = $type_name;
}
fn deref(&self) -> &str {
self.0.as_str()
}
}
impl From<$type> for Value {
fn from(v: $type) -> Self {
$variant(v)
}
}
/// Implement [`TryFromValue`] through a match.
macro_rules! try_from_match {
($type:ty[$name:literal] $(@ $span:ident)?: $($pattern:pat => $output:expr),* $(,)?) => {
impl $crate::eval::TryFromValue for $type {
fn try_from_value(value: Spanned<Value>) -> $crate::eval::Conv<Self> {
use $crate::eval::Conv;
#[allow(unused)]
$(let $span = value.span;)?
#[allow(unreachable_patterns)]
match value.v {
$($pattern => Conv::Ok($output)),*,
v => {
let e = error!("expected {}, found {}", $name, v.ty());
Conv::Err(v, e)
}
impl Cast<Value> for $type {
fn cast(value: Value) -> CastResult<Self, Value> {
match value {
$variant(v) => CastResult::Ok(v),
$($pattern => CastResult::Ok($out),)*
v => CastResult::Err(v),
}
}
}
};
}
/// Implement [`TryFromValue`] through a function parsing an identifier.
macro_rules! try_from_id {
($type:ty[$name:literal]: $from_str:expr) => {
impl $crate::eval::TryFromValue for $type {
fn try_from_value(value: Spanned<Value>) -> $crate::eval::Conv<Self> {
use $crate::eval::Conv;
let v = value.v;
if let Value::Ident(id) = v {
if let Some(v) = $from_str(&id) {
Conv::Ok(v)
} else {
Conv::Err(Value::Ident(id), error!("invalid {}", $name))
}
} else {
let e = error!("expected identifier, found {}", v.ty());
Conv::Err(v, e)
}
}
}
};
}
impl_primitive! { bool: "boolean", Value::Bool }
impl_primitive! { i64: "integer", Value::Int }
impl_primitive! { Length: "length", Value::Length }
impl_primitive! { Relative: "relative", Value::Relative }
impl_primitive! { Color: "color", Value::Color }
impl_primitive! { String: "string", Value::Str }
impl_primitive! { ValueArray: "array", Value::Array }
impl_primitive! { ValueDict: "dictionary", Value::Dict }
impl_primitive! { ValueContent: "content", Value::Content }
impl_primitive! { ValueFunc: "function", Value::Func }
try_from_match!(Value["value"]: v => v);
try_from_match!(Ident["identifier"]: Value::Ident(v) => v);
try_from_match!(bool["bool"]: Value::Bool(v) => v);
try_from_match!(i64["integer"]: Value::Int(v) => v);
try_from_match!(f64["float"]:
impl_primitive! {
f64: "float",
Value::Float,
Value::Int(v) => v as f64,
Value::Float(v) => v,
);
try_from_match!(Length["length"]: Value::Length(v) => v);
try_from_match!(Relative["relative"]: Value::Relative(v) => v);
try_from_match!(Linear["linear"]:
Value::Linear(v) => v,
}
impl_primitive! {
Linear: "linear",
Value::Linear,
Value::Length(v) => v.into(),
Value::Relative(v) => v.into(),
);
try_from_match!(Color["color"]: Value::Color(v) => v);
try_from_match!(String["string"]: Value::Str(v) => v);
try_from_match!(SynTree["tree"]: Value::Content(v) => v);
try_from_match!(ValueDict["dictionary"]: Value::Dict(v) => v);
try_from_match!(ValueFunc["function"]: Value::Func(v) => v);
try_from_match!(StringLike["identifier or string"]:
Value::Ident(Ident(v)) => Self(v),
Value::Str(v) => Self(v),
);
try_from_id!(Dir["direction"]: |v| match v {
"ltr" | "left-to-right" => Some(Self::LTR),
"rtl" | "right-to-left" => Some(Self::RTL),
"ttb" | "top-to-bottom" => Some(Self::TTB),
"btt" | "bottom-to-top" => Some(Self::BTT),
_ => None,
});
try_from_id!(FontStyle["font style"]: Self::from_str);
try_from_id!(FontStretch["font stretch"]: Self::from_str);
try_from_id!(Paper["paper"]: Self::from_name);
}
impl TryFromValue for FontWeight {
fn try_from_value(value: Spanned<Value>) -> Conv<Self> {
match value.v {
Value::Int(number) => {
let [min, max] = [Self::THIN, Self::BLACK];
if number < i64::from(min.to_number()) {
Conv::Warn(min, warning!("the minimum font weight is {:#?}", min))
} else if number > i64::from(max.to_number()) {
Conv::Warn(max, warning!("the maximum font weight is {:#?}", max))
} else {
Conv::Ok(Self::from_number(number as u16))
}
}
Value::Ident(id) => {
if let Some(weight) = Self::from_str(&id) {
Conv::Ok(weight)
} else {
Conv::Err(Value::Ident(id), error!("invalid font weight"))
}
}
v => {
let e = error!("expected font weight, found {}", v.ty());
Conv::Err(v, e)
}
}
impl From<&str> for Value {
fn from(v: &str) -> Self {
Self::Str(v.to_string())
}
}
impl<F> From<F> for Value
where
F: Fn(&mut EvalContext, &mut Args) -> Value + 'static,
{
fn from(func: F) -> Self {
Self::Func(ValueFunc::new(func))
}
}
impl From<ValueAny> for Value {
fn from(v: ValueAny) -> Self {
Self::Any(v)
}
}
/// Make a type usable with [`ValueAny`].
///
/// Given a type `T`, this implements the following traits:
/// - [`Type`] for `T`,
/// - [`From<T>`](From) for [`Value`],
/// - [`Cast<Value>`](Cast) for `T`.
#[macro_export]
macro_rules! impl_type {
($type:ty:
$type_name:literal
$(, $pattern:pat => $out:expr)*
$(, #($anyvar:ident: $anytype:ty) => $anyout:expr)*
$(,)?
) => {
impl $crate::eval::Type for $type {
const TYPE_NAME: &'static str = $type_name;
}
impl From<$type> for $crate::eval::Value {
fn from(any: $type) -> Self {
$crate::eval::Value::Any($crate::eval::ValueAny::new(any))
}
}
impl $crate::eval::Cast<$crate::eval::Value> for $type {
fn cast(
value: $crate::eval::Value,
) -> $crate::eval::CastResult<Self, $crate::eval::Value> {
use $crate::eval::*;
#[allow(unreachable_code)]
match value {
$($pattern => CastResult::Ok($out),)*
Value::Any(mut any) => {
any = match any.downcast::<Self>() {
Ok(t) => return CastResult::Ok(t),
Err(any) => any,
};
$(any = match any.downcast::<$anytype>() {
Ok($anyvar) => return CastResult::Ok($anyout),
Err(any) => any,
};)*
CastResult::Err(Value::Any(any))
},
v => CastResult::Err(v),
}
}
}
};
}

View File

@ -208,14 +208,14 @@ mod tests {
use super::*;
#[test]
fn test_length_formats_correctly() {
fn test_length_unit_conversion() {
assert!((Length::mm(150.0).to_cm() - 15.0) < 1e-4);
}
#[test]
fn test_length_formatting() {
assert_eq!(Length::pt(-28.34).to_string(), "-1.00cm".to_string());
assert_eq!(Length::pt(23.0).to_string(), "23.00pt".to_string());
assert_eq!(Length::cm(12.728).to_string(), "12.73cm".to_string());
}
#[test]
fn test_length_unit_conversion() {
assert!((Length::mm(150.0).to_cm() - 15.0) < 1e-4);
}
}

View File

@ -6,14 +6,14 @@ use crate::prelude::*;
/// `image`: Insert an image.
///
/// # Positional arguments
/// - Path (`string`): The path to the image file.
///
/// Supports PNG and JPEG files.
pub fn image(mut args: Args, ctx: &mut EvalContext) -> Value {
let path = args.need::<_, Spanned<String>>(ctx, 0, "path");
let width = args.get::<_, Linear>(ctx, "width");
let height = args.get::<_, Linear>(ctx, "height");
///
/// # Positional arguments
/// - Path to image file: of type `string`.
pub fn image(ctx: &mut EvalContext, args: &mut Args) -> Value {
let path = args.require::<Spanned<String>>(ctx, "path to image file");
let width = args.get(ctx, "width");
let height = args.get(ctx, "height");
if let Some(path) = path {
let mut env = ctx.env.borrow_mut();

View File

@ -1,51 +1,44 @@
use std::fmt::{self, Display, Formatter};
use crate::eval::Softness;
use crate::geom::{Length, Linear};
use crate::layout::{Expansion, Fixed, Spacing, Stack};
use crate::paper::{Paper, PaperClass};
use crate::prelude::*;
/// `align`: Align content along the layouting axes.
///
/// Which axis an alignment should apply to (main or cross) is inferred from
/// either the argument itself (for anything other than `center`) or from the
/// second argument if present, defaulting to the cross axis for a single
/// `center` alignment.
///
/// # Positional arguments
/// - first (optional, `Alignment`): An alignment for any of the two axes.
/// - second (optional, `Alignment`): An alignment for the other axis.
/// - Alignments: variadic, of type `alignment`.
///
/// # Named arguments
/// - `horizontal` (`Alignment`): An alignment for the horizontal axis.
/// - `vertical` (`Alignment`): An alignment for the vertical axis.
/// - Horizontal alignment: `horizontal`, of type `alignment`.
/// - Vertical alignment: `vertical`, of type `alignment`.
///
/// # Enumerations
/// - `Alignment`
/// # Relevant types and constants
/// - Type `alignment`
/// - `left`
/// - `right`
/// - `top`
/// - `bottom`
/// - `center`
///
/// # Notes
/// Which axis an alignment should apply to (main or cross) is inferred from
/// either the argument itself (for anything other than `center`) or from the
/// second argument if present, defaulting to the cross axis for a single
/// `center` alignment.
pub fn align(mut args: Args, ctx: &mut EvalContext) -> Value {
pub fn align(ctx: &mut EvalContext, args: &mut Args) -> Value {
let snapshot = ctx.state.clone();
let body = args.find::<SynTree>();
let first = args.get::<_, Spanned<SpecAlign>>(ctx, 0);
let second = args.get::<_, Spanned<SpecAlign>>(ctx, 1);
let hor = args.get::<_, Spanned<SpecAlign>>(ctx, "horizontal");
let ver = args.get::<_, Spanned<SpecAlign>>(ctx, "vertical");
args.done(ctx);
let prev_main = ctx.state.align.main;
let first = args.find(ctx);
let second = args.find(ctx);
let hor = args.get(ctx, "horizontal");
let ver = args.get(ctx, "vertical");
let mut had = Gen::uniform(false);
let mut had_center = false;
for (axis, Spanned { v: arg, span }) in first
.into_iter()
.chain(second.into_iter())
.map(|arg| (arg.v.axis(), arg))
.map(|arg: Spanned<Alignment>| (arg.v.axis(), arg))
.chain(hor.into_iter().map(|arg| (Some(SpecAxis::Horizontal), arg)))
.chain(ver.into_iter().map(|arg| (Some(SpecAxis::Vertical), arg)))
{
@ -56,10 +49,7 @@ pub fn align(mut args: Args, ctx: &mut EvalContext) -> Value {
let gen_align = arg.switch(ctx.state.flow);
if arg.axis().map_or(false, |a| a != axis) {
ctx.diag(error!(
span,
"invalid alignment `{}` for {} axis", arg, axis,
));
ctx.diag(error!(span, "invalid alignment for {} axis", axis));
} else if had.get(gen_axis) {
ctx.diag(error!(span, "duplicate alignment for {} axis", axis));
} else {
@ -69,7 +59,7 @@ pub fn align(mut args: Args, ctx: &mut EvalContext) -> Value {
} else {
// We don't know the axis: This has to be a `center` alignment for a
// positional argument.
debug_assert_eq!(arg, SpecAlign::Center);
debug_assert_eq!(arg, Alignment::Center);
if had.main && had.cross {
ctx.diag(error!(span, "duplicate alignment"));
@ -104,12 +94,12 @@ pub fn align(mut args: Args, ctx: &mut EvalContext) -> Value {
ctx.state.align.cross = Align::Center;
}
if ctx.state.align.main != prev_main {
if ctx.state.align.main != snapshot.align.main {
ctx.end_par_group();
ctx.start_par_group();
}
if let Some(body) = body {
if let Some(body) = args.find::<ValueContent>(ctx) {
body.eval(ctx);
ctx.state = snapshot;
}
@ -117,30 +107,18 @@ pub fn align(mut args: Args, ctx: &mut EvalContext) -> Value {
Value::None
}
/// An argument to `[align]`.
#[derive(Debug, Copy, Clone, Eq, PartialEq, Ord, PartialOrd)]
enum SpecAlign {
pub(crate) enum Alignment {
Left,
Center,
Right,
Top,
Bottom,
Center,
}
try_from_id!(SpecAlign["alignment"]: |v| match v {
"left" => Some(Self::Left),
"right" => Some(Self::Right),
"top" => Some(Self::Top),
"bottom" => Some(Self::Bottom),
"center" => Some(Self::Center),
_ => None,
});
impl SpecAlign {
impl Alignment {
/// The specific axis this alignment refers to.
///
/// Returns `None` if this is `Center` since the axis is unknown.
pub fn axis(self) -> Option<SpecAxis> {
fn axis(self) -> Option<SpecAxis> {
match self {
Self::Left => Some(SpecAxis::Horizontal),
Self::Right => Some(SpecAxis::Horizontal),
@ -151,7 +129,7 @@ impl SpecAlign {
}
}
impl Switch for SpecAlign {
impl Switch for Alignment {
type Other = Align;
fn switch(self, flow: Flow) -> Self::Other {
@ -174,38 +152,34 @@ impl Switch for SpecAlign {
}
}
impl Display for SpecAlign {
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
f.pad(match self {
Self::Left => "left",
Self::Right => "right",
Self::Top => "top",
Self::Bottom => "bottom",
Self::Center => "center",
})
}
impl_type! {
Alignment: "alignment"
}
/// `box`: Layout content into a box.
///
/// # Named arguments
/// - `width` (`linear` relative to parent width): The width of the box.
/// - `height` (`linear` relative to parent height): The height of the box.
pub fn boxed(mut args: Args, ctx: &mut EvalContext) -> Value {
/// - Width of the box: `width`, of type `linear` relative to parent width.
/// - Height of the box: `height`, of type `linear` relative to parent height.
pub fn boxed(ctx: &mut EvalContext, args: &mut Args) -> Value {
let snapshot = ctx.state.clone();
let body = args.find::<SynTree>().unwrap_or_default();
let width = args.get::<_, Linear>(ctx, "width");
let height = args.get::<_, Linear>(ctx, "height");
let main = args.get::<_, Spanned<Dir>>(ctx, "main-dir");
let cross = args.get::<_, Spanned<Dir>>(ctx, "cross-dir");
let width = args.get(ctx, "width");
let height = args.get(ctx, "height");
let main = args.get(ctx, "main-dir");
let cross = args.get(ctx, "cross-dir");
ctx.set_flow(Gen::new(main, cross));
args.done(ctx);
let flow = ctx.state.flow;
let align = ctx.state.align;
ctx.start_content_group();
body.eval(ctx);
if let Some(body) = args.find::<ValueContent>(ctx) {
body.eval(ctx);
}
let children = ctx.end_content_group();
ctx.push(Fixed {
@ -227,31 +201,34 @@ pub fn boxed(mut args: Args, ctx: &mut EvalContext) -> Value {
Value::None
}
impl_type! {
Dir: "direction"
}
/// `h`: Add horizontal spacing.
///
/// # Positional arguments
/// - Spacing (`linear` relative to font size): The amount of spacing.
pub fn h(args: Args, ctx: &mut EvalContext) -> Value {
spacing(args, ctx, SpecAxis::Horizontal)
/// - Amount of spacing: of type `linear` relative to current font size.
pub fn h(ctx: &mut EvalContext, args: &mut Args) -> Value {
spacing(ctx, args, SpecAxis::Horizontal)
}
/// `v`: Add vertical spacing.
///
/// # Positional arguments
/// - Spacing (`linear` relative to font size): The amount of spacing.
pub fn v(args: Args, ctx: &mut EvalContext) -> Value {
spacing(args, ctx, SpecAxis::Vertical)
/// - Amount of spacing: of type `linear` relative to current font size.
pub fn v(ctx: &mut EvalContext, args: &mut Args) -> Value {
spacing(ctx, args, SpecAxis::Vertical)
}
/// Apply spacing along a specific axis.
fn spacing(mut args: Args, ctx: &mut EvalContext, axis: SpecAxis) -> Value {
let spacing = args.need::<_, Linear>(ctx, 0, "spacing");
args.done(ctx);
fn spacing(ctx: &mut EvalContext, args: &mut Args, axis: SpecAxis) -> Value {
let spacing: Option<Linear> = args.require(ctx, "spacing");
if let Some(linear) = spacing {
let amount = linear.resolve(ctx.state.font.font_size());
let spacing = Spacing { amount, softness: Softness::Hard };
if ctx.state.flow.main.axis() == axis {
if axis == ctx.state.flow.main.axis() {
ctx.end_par_group();
ctx.push(spacing);
ctx.start_par_group();
@ -266,79 +243,78 @@ fn spacing(mut args: Args, ctx: &mut EvalContext, axis: SpecAxis) -> Value {
/// `page`: Configure pages.
///
/// # Positional arguments
/// - Paper name (optional, `Paper`).
/// - Paper name: optional, of type `string`, see [here](crate::paper) for a
/// full list of all paper names.
///
/// # Named arguments
/// - `width` (`length`): The width of pages.
/// - `height` (`length`): The height of pages.
/// - `margins` (`linear` relative to sides): The margins for all sides.
/// - `left` (`linear` relative to width): The left margin.
/// - `right` (`linear` relative to width): The right margin.
/// - `top` (`linear` relative to height): The top margin.
/// - `bottom` (`linear` relative to height): The bottom margin.
/// - `flip` (`bool`): Flips custom or paper-defined width and height.
///
/// # Enumerations
/// - `Paper`: See [here](crate::paper) for a full list.
pub fn page(mut args: Args, ctx: &mut EvalContext) -> Value {
/// - Width of the page: `width`, of type `length`.
/// - Height of the page: `height`, of type `length`.
/// - Margins for all sides: `margins`, of type `linear` relative to sides.
/// - Left margin: `left`, of type `linear` relative to width.
/// - Right margin: `right`, of type `linear` relative to width.
/// - Top margin: `top`, of type `linear` relative to height.
/// - Bottom margin: `bottom`, of type `linear` relative to height.
/// - Flip width and height: `flip`, of type `bool`.
pub fn page(ctx: &mut EvalContext, args: &mut Args) -> Value {
let snapshot = ctx.state.clone();
let body = args.find::<SynTree>();
if let Some(paper) = args.get::<_, Paper>(ctx, 0) {
ctx.state.page.class = paper.class;
ctx.state.page.size = paper.size();
if let Some(name) = args.find::<Spanned<String>>(ctx) {
if let Some(paper) = Paper::from_name(&name.v) {
ctx.state.page.class = paper.class;
ctx.state.page.size = paper.size();
} else {
ctx.diag(error!(name.span, "invalid paper name"));
}
}
if let Some(width) = args.get::<_, Length>(ctx, "width") {
if let Some(width) = args.get(ctx, "width") {
ctx.state.page.class = PaperClass::Custom;
ctx.state.page.size.width = width;
}
if let Some(height) = args.get::<_, Length>(ctx, "height") {
if let Some(height) = args.get(ctx, "height") {
ctx.state.page.class = PaperClass::Custom;
ctx.state.page.size.height = height;
}
if let Some(margins) = args.get::<_, Linear>(ctx, "margins") {
if let Some(margins) = args.get(ctx, "margins") {
ctx.state.page.margins = Sides::uniform(Some(margins));
}
if let Some(left) = args.get::<_, Linear>(ctx, "left") {
if let Some(left) = args.get(ctx, "left") {
ctx.state.page.margins.left = Some(left);
}
if let Some(top) = args.get::<_, Linear>(ctx, "top") {
if let Some(top) = args.get(ctx, "top") {
ctx.state.page.margins.top = Some(top);
}
if let Some(right) = args.get::<_, Linear>(ctx, "right") {
if let Some(right) = args.get(ctx, "right") {
ctx.state.page.margins.right = Some(right);
}
if let Some(bottom) = args.get::<_, Linear>(ctx, "bottom") {
if let Some(bottom) = args.get(ctx, "bottom") {
ctx.state.page.margins.bottom = Some(bottom);
}
if args.get::<_, bool>(ctx, "flip").unwrap_or(false) {
if args.get(ctx, "flip").unwrap_or(false) {
let size = &mut ctx.state.page.size;
std::mem::swap(&mut size.width, &mut size.height);
}
let main = args.get::<_, Spanned<Dir>>(ctx, "main-dir");
let cross = args.get::<_, Spanned<Dir>>(ctx, "cross-dir");
let main = args.get(ctx, "main-dir");
let cross = args.get(ctx, "cross-dir");
ctx.set_flow(Gen::new(main, cross));
args.done(ctx);
let mut softness = ctx.end_page_group(|_| false);
if let Some(body) = body {
if let Some(body) = args.find::<ValueContent>(ctx) {
// TODO: Restrict body to a single page?
ctx.start_page_group(Softness::Hard);
body.eval(ctx);
ctx.end_page_group(|s| s == Softness::Hard);
ctx.state = snapshot;
softness = Softness::Soft;
ctx.state = snapshot;
}
ctx.start_page_group(softness);
@ -347,8 +323,7 @@ pub fn page(mut args: Args, ctx: &mut EvalContext) -> Value {
}
/// `pagebreak`: Start a new page.
pub fn pagebreak(args: Args, ctx: &mut EvalContext) -> Value {
args.done(ctx);
pub fn pagebreak(ctx: &mut EvalContext, _: &mut Args) -> Value {
ctx.end_page_group(|_| true);
ctx.start_page_group(Softness::Hard);
Value::None

View File

@ -8,31 +8,60 @@ pub use insert::*;
pub use layout::*;
pub use style::*;
use crate::eval::{Scope, ValueFunc};
use fontdock::{FontStretch, FontStyle, FontWeight};
macro_rules! std {
($($func:expr $(=> $name:expr)?),* $(,)?) => {
/// The scope containing all standard library functions.
pub fn _std() -> Scope {
let mut std = Scope::new();
$(
let _name = stringify!($func);
$(let _name = $name;)?
std.set(_name, ValueFunc::new($func));
)*
std
}
};
}
use crate::eval::Scope;
use crate::geom::Dir;
std! {
align,
boxed => "box",
font,
h,
image,
page,
pagebreak,
rgb,
v,
/// The scope containing the standard library.
pub fn _std() -> Scope {
let mut std = Scope::new();
// Functions.
std.set("align", align);
std.set("box", boxed);
std.set("font", font);
std.set("h", h);
std.set("image", image);
std.set("page", page);
std.set("pagebreak", pagebreak);
std.set("rgb", rgb);
std.set("v", v);
// Constants.
std.set("left", Alignment::Left);
std.set("center", Alignment::Center);
std.set("right", Alignment::Right);
std.set("top", Alignment::Top);
std.set("bottom", Alignment::Bottom);
std.set("ltr", Dir::LTR);
std.set("rtl", Dir::RTL);
std.set("ttb", Dir::TTB);
std.set("btt", Dir::BTT);
std.set("serif", FontFamily::Serif);
std.set("sans-serif", FontFamily::SansSerif);
std.set("monospace", FontFamily::Monospace);
std.set("normal", FontStyle::Normal);
std.set("italic", FontStyle::Italic);
std.set("oblique", FontStyle::Oblique);
std.set("thin", FontWeight::THIN);
std.set("extralight", FontWeight::EXTRALIGHT);
std.set("light", FontWeight::LIGHT);
std.set("regular", FontWeight::REGULAR);
std.set("medium", FontWeight::MEDIUM);
std.set("semibold", FontWeight::SEMIBOLD);
std.set("bold", FontWeight::BOLD);
std.set("extrabold", FontWeight::EXTRABOLD);
std.set("black", FontWeight::BLACK);
std.set("ultra-condensed", FontStretch::UltraCondensed);
std.set("extra-condensed", FontStretch::ExtraCondensed);
std.set("condensed", FontStretch::Condensed);
std.set("semi-condensed", FontStretch::SemiCondensed);
std.set("normal", FontStretch::Normal);
std.set("semi-expanded", FontStretch::SemiExpanded);
std.set("expanded", FontStretch::Expanded);
std.set("extra-expanded", FontStretch::ExtraExpanded);
std.set("ultra-expanded", FontStretch::UltraExpanded);
std
}

View File

@ -1,58 +1,41 @@
use std::fmt::{self, Display, Formatter};
use std::rc::Rc;
use fontdock::{FontStretch, FontStyle, FontWeight};
use crate::color::{Color, RgbaColor};
use crate::eval::StringLike;
use crate::geom::Linear;
use crate::prelude::*;
/// `font`: Configure the font.
///
/// # Positional arguments
/// - Font size (optional, `linear` relative to current font size).
/// - Font families ... (optional, variadic, `Family`)
/// - Font size: optional, of type `linear` relative to current font size.
/// - Font families: variadic, of type `font-family`.
///
/// # Named arguments
/// - `style` (`Style`): The font style.
/// - `weight` (`Weight`): The font weight.
/// - `stretch` (`Stretch`): The font stretch.
/// - `serif` (`Family` or `dict` of type `Family`): The serif family.
/// - `sans-serif` (`Family` or `dict` of type `Family`): The new sansserif family.
/// - `monospace` (`Family` or `dict` of type `Family`): The monospace family.
/// - `emoji` (`Family` or `dict` of type `Family`): The emoji family.
/// - `math` (`Family` or `dict` of type `Family`): The math family.
/// - Font Style: `style`, of type `font-style`.
/// - Font Weight: `weight`, of type `font-weight`.
/// - Font Stretch: `stretch`, of type `font-stretch`.
/// - Serif family definition: `serif`, of type `font-families`.
/// - Sans-serif family definition: `sans-serif`, of type `font-families`.
/// - Monospace family definition: `monospace`, of type `font-families`.
///
/// # Examples
/// Set font size and font families.
/// ```typst
/// [font 12pt, "Arial", "Noto Sans", sans-serif]
/// ```
///
/// Redefine the default sans-serif family to a single font family.
/// ```typst
/// [font sans-serif: "Source Sans Pro"]
/// ```
///
/// Redefine the default emoji family with a fallback.
/// ```typst
/// [font emoji: ("Segoe UI Emoji", "Noto Emoji")]
/// ```
///
/// # Enumerations
/// - `Family`
/// # Relevant types and constants
/// - Type `font-families`
/// - coerces from `string`
/// - coerces from `array`
/// - coerces from `font-family`
/// - Type `font-family`
/// - `serif`
/// - `sans-serif`
/// - `monospace`
/// - `emoji`
/// - `math`
/// - any string
/// - `Style`
/// - coerces from `string`
/// - Type `font-style`
/// - `normal`
/// - `italic`
/// - `oblique`
/// - `Weight`
/// - `thin` or `hairline` (100)
/// - Type `font-weight`
/// - `thin` (100)
/// - `extralight` (200)
/// - `light` (300)
/// - `regular` (400)
@ -61,8 +44,8 @@ use crate::prelude::*;
/// - `bold` (700)
/// - `extrabold` (800)
/// - `black` (900)
/// - any integer between 100 and 900
/// - `Stretch`
/// - coerces from `integer`
/// - Type `font-stretch`
/// - `ultra-condensed`
/// - `extra-condensed`
/// - `condensed`
@ -72,11 +55,10 @@ use crate::prelude::*;
/// - `expanded`
/// - `extra-expanded`
/// - `ultra-expanded`
pub fn font(mut args: Args, ctx: &mut EvalContext) -> Value {
pub fn font(ctx: &mut EvalContext, args: &mut Args) -> Value {
let snapshot = ctx.state.clone();
let body = args.find::<SynTree>();
if let Some(linear) = args.find::<Linear>() {
if let Some(linear) = args.find::<Linear>(ctx) {
if linear.is_absolute() {
ctx.state.font.size = linear.abs;
ctx.state.font.scale = Relative::ONE.into();
@ -85,52 +67,35 @@ pub fn font(mut args: Args, ctx: &mut EvalContext) -> Value {
}
}
let mut needs_flattening = false;
let list: Vec<_> = args.find_all::<StringLike>().map(|s| s.to_lowercase()).collect();
let list: Vec<_> = args.filter::<FontFamily>(ctx).map(|f| f.to_string()).collect();
if !list.is_empty() {
Rc::make_mut(&mut ctx.state.font.families).list = list;
needs_flattening = true;
let families = Rc::make_mut(&mut ctx.state.font.families);
families.list = list;
families.flatten();
}
if let Some(style) = args.get::<_, FontStyle>(ctx, "style") {
if let Some(style) = args.get(ctx, "style") {
ctx.state.font.variant.style = style;
}
if let Some(weight) = args.get::<_, FontWeight>(ctx, "weight") {
if let Some(weight) = args.get(ctx, "weight") {
ctx.state.font.variant.weight = weight;
}
if let Some(stretch) = args.get::<_, FontStretch>(ctx, "stretch") {
if let Some(stretch) = args.get(ctx, "stretch") {
ctx.state.font.variant.stretch = stretch;
}
struct FamilyList(Vec<String>);
try_from_match!(FamilyList["family or list of families"] @ span:
Value::Str(v) => Self(vec![v.to_lowercase()]),
Value::Dict(v) => Self(Args(v.with_span(span))
.find_all::<StringLike>()
.map(|s| s.to_lowercase())
.collect()
),
);
for &class in &["serif", "sans-serif", "monospace", "emoji", "math"] {
if let Some(list) = args.get::<_, FamilyList>(ctx, class) {
Rc::make_mut(&mut ctx.state.font.families)
.update_class_list(class.to_string(), list.0);
needs_flattening = true;
for variant in FontFamily::VARIANTS {
if let Some(FontFamilies(list)) = args.get(ctx, variant.as_str()) {
let strings = list.into_iter().map(|f| f.to_string()).collect();
let families = Rc::make_mut(&mut ctx.state.font.families);
families.update_class_list(variant.to_string(), strings);
families.flatten();
}
}
if needs_flattening {
Rc::make_mut(&mut ctx.state.font.families).flatten();
}
args.done(ctx);
if let Some(body) = body {
if let Some(body) = args.find::<ValueContent>(ctx) {
body.eval(ctx);
ctx.state = snapshot;
}
@ -138,24 +103,94 @@ pub fn font(mut args: Args, ctx: &mut EvalContext) -> Value {
Value::None
}
/// A list of font families.
#[derive(Debug, Clone, PartialEq)]
struct FontFamilies(Vec<FontFamily>);
impl_type! {
FontFamilies: "font family or array of font families",
Value::Str(string) => Self(vec![FontFamily::Named(string.to_lowercase())]),
Value::Array(values) => Self(values
.into_iter()
.filter_map(|v| v.cast().ok())
.collect()
),
#(family: FontFamily) => Self(vec![family]),
}
#[derive(Debug, Clone, Eq, PartialEq, Ord, PartialOrd)]
pub(crate) enum FontFamily {
Serif,
SansSerif,
Monospace,
Named(String),
}
impl FontFamily {
pub const VARIANTS: &'static [Self] =
&[Self::Serif, Self::SansSerif, Self::Monospace];
pub fn as_str(&self) -> &str {
match self {
Self::Serif => "serif",
Self::SansSerif => "sans-serif",
Self::Monospace => "monospace",
Self::Named(s) => s,
}
}
}
impl Display for FontFamily {
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
f.pad(self.as_str())
}
}
impl_type! {
FontFamily: "font family",
Value::Str(string) => Self::Named(string.to_lowercase())
}
impl_type! {
FontStyle: "font style"
}
impl_type! {
FontWeight: "font weight",
Value::Int(number) => {
let [min, max] = [Self::THIN, Self::BLACK];
let message = || format!("must be between {:#?} and {:#?}", min, max);
return if number < i64::from(min.to_number()) {
CastResult::Warn(min, message())
} else if number > i64::from(max.to_number()) {
CastResult::Warn(max, message())
} else {
CastResult::Ok(Self::from_number(number as u16))
};
},
}
impl_type! {
FontStretch: "font stretch"
}
/// `rgb`: Create an RGB(A) color.
///
/// # Positional arguments
/// - Red component (`float` between 0.0 and 1.0).
/// - Green component (`float` between 0.0 and 1.0).
/// - Blue component (`float` between 0.0 and 1.0).
/// - Alpha component (optional, `float` between 0.0 and 1.0).
pub fn rgb(mut args: Args, ctx: &mut EvalContext) -> Value {
let r = args.need::<_, Spanned<f64>>(ctx, 0, "red component");
let g = args.need::<_, Spanned<f64>>(ctx, 1, "green component");
let b = args.need::<_, Spanned<f64>>(ctx, 2, "blue component");
let a = args.get::<_, Spanned<f64>>(ctx, 3);
args.done(ctx);
/// - Red component: of type `float`, between 0.0 and 1.0.
/// - Green component: of type `float`, between 0.0 and 1.0.
/// - Blue component: of type `float`, between 0.0 and 1.0.
/// - Alpha component: optional, of type `float`, between 0.0 and 1.0.
pub fn rgb(ctx: &mut EvalContext, args: &mut Args) -> Value {
let r = args.require(ctx, "red component");
let g = args.require(ctx, "green component");
let b = args.require(ctx, "blue component");
let a = args.find(ctx);
let mut clamp = |component: Option<Spanned<f64>>, default| {
component.map_or(default, |c| {
if c.v < 0.0 || c.v > 1.0 {
ctx.diag(error!(c.span, "should be between 0.0 and 1.0"));
ctx.diag(warning!(c.span, "must be between 0.0 and 1.0"));
}
(c.v.max(0.0).min(1.0) * 255.0).round() as u8
})

142
src/parse/collection.rs Normal file
View File

@ -0,0 +1,142 @@
use super::*;
use crate::diag::Deco;
/// Parse the arguments to a function call.
pub fn arguments(p: &mut Parser) -> Arguments {
collection(p, vec![])
}
/// Parse a parenthesized group, which can be either of:
/// - Array literal
/// - Dictionary literal
/// - Parenthesized expression
pub fn parenthesized(p: &mut Parser) -> Expr {
p.start_group(Group::Paren);
let state = if p.eat_if(Token::Colon) {
collection(p, State::Dict(vec![]))
} else {
collection(p, State::Unknown)
};
p.end_group();
state.into_expr()
}
/// Parse a collection.
fn collection<T: Collection>(p: &mut Parser, mut collection: T) -> T {
let mut missing_coma = None;
while !p.eof() {
if let Some(arg) = p.span_if(argument) {
collection.push_arg(p, arg);
if let Some(pos) = missing_coma.take() {
p.diag_expected_at("comma", pos);
}
if p.eof() {
break;
}
let behind = p.last_end();
if p.eat_if(Token::Comma) {
collection.push_comma();
} else {
missing_coma = Some(behind);
}
}
}
collection
}
/// Parse an expression or a named pair.
fn argument(p: &mut Parser) -> Option<Argument> {
let first = p.span_if(expr)?;
if p.eat_if(Token::Colon) {
if let Expr::Lit(Lit::Ident(ident)) = first.v {
let expr = p.span_if(expr)?;
let name = ident.with_span(first.span);
p.deco(Deco::Name.with_span(name.span));
Some(Argument::Named(Named { name, expr }))
} else {
p.diag(error!(first.span, "name must be identifier"));
expr(p);
None
}
} else {
Some(Argument::Pos(first))
}
}
/// Abstraction for comma-separated list of expression / named pairs.
trait Collection {
fn push_arg(&mut self, p: &mut Parser, arg: Spanned<Argument>);
fn push_comma(&mut self) {}
}
impl Collection for Arguments {
fn push_arg(&mut self, _: &mut Parser, arg: Spanned<Argument>) {
self.push(arg.v);
}
}
/// State of collection parsing.
#[derive(Debug)]
enum State {
Unknown,
Expr(Spanned<Expr>),
Array(Array),
Dict(Dict),
}
impl State {
fn into_expr(self) -> Expr {
match self {
Self::Unknown => Expr::Lit(Lit::Array(vec![])),
Self::Expr(expr) => expr.v,
Self::Array(array) => Expr::Lit(Lit::Array(array)),
Self::Dict(dict) => Expr::Lit(Lit::Dict(dict)),
}
}
}
impl Collection for State {
fn push_arg(&mut self, p: &mut Parser, arg: Spanned<Argument>) {
match self {
Self::Unknown => match arg.v {
Argument::Pos(expr) => *self = Self::Expr(expr),
Argument::Named(named) => *self = Self::Dict(vec![named]),
},
Self::Expr(prev) => match arg.v {
Argument::Pos(expr) => *self = Self::Array(vec![take(prev), expr]),
Argument::Named(_) => diag(p, arg),
},
Self::Array(array) => match arg.v {
Argument::Pos(expr) => array.push(expr),
Argument::Named(_) => diag(p, arg),
},
Self::Dict(dict) => match arg.v {
Argument::Pos(_) => diag(p, arg),
Argument::Named(named) => dict.push(named),
},
}
}
fn push_comma(&mut self) {
if let Self::Expr(expr) = self {
*self = Self::Array(vec![take(expr)]);
}
}
}
fn take(expr: &mut Spanned<Expr>) -> Spanned<Expr> {
// Replace with anything, it's overwritten anyway.
std::mem::replace(expr, Spanned::zero(Expr::Lit(Lit::Bool(false))))
}
fn diag(p: &mut Parser, arg: Spanned<Argument>) {
p.diag(error!(arg.span, "{}", match arg.v {
Argument::Pos(_) => "expected named pair, found expression",
Argument::Named(_) => "expected expression, found named pair",
}));
}

View File

@ -1,5 +1,6 @@
//! Parsing and tokenization.
mod collection;
mod lines;
mod parser;
mod resolve;
@ -15,10 +16,11 @@ pub use tokens::*;
use std::str::FromStr;
use crate::color::RgbaColor;
use crate::diag::{Deco, Pass};
use crate::eval::DictKey;
use crate::diag::Pass;
use crate::syntax::*;
use collection::{arguments, parenthesized};
/// Parse a string of source code.
pub fn parse(src: &str) -> Pass<SynTree> {
let mut p = Parser::new(src);
@ -153,6 +155,9 @@ fn block_expr(p: &mut Parser) -> Option<Expr> {
p.push_mode(TokenMode::Header);
p.start_group(Group::Brace);
let expr = expr(p);
while !p.eof() {
p.diag_unexpected();
}
p.pop_mode();
p.end_group();
expr
@ -161,7 +166,7 @@ fn block_expr(p: &mut Parser) -> Option<Expr> {
/// Parse a parenthesized function call.
fn paren_call(p: &mut Parser, name: Spanned<Ident>) -> ExprCall {
p.start_group(Group::Paren);
let args = p.span(|p| dict_contents(p).0);
let args = p.span(arguments);
p.end_group();
ExprCall { name, args }
}
@ -184,16 +189,16 @@ fn bracket_call(p: &mut Parser) -> ExprCall {
p.end_group();
if p.peek() == Some(Token::LeftBracket) {
let expr = p.span(|p| Expr::Lit(Lit::Content(bracket_body(p))));
inner.span.expand(expr.span);
inner.v.args.v.0.push(LitDictEntry { key: None, expr });
let body = p.span(|p| Expr::Lit(Lit::Content(bracket_body(p))));
inner.span.expand(body.span);
inner.v.args.v.push(Argument::Pos(body));
}
while let Some(mut top) = outer.pop() {
let span = inner.span;
let node = inner.map(|c| SynNode::Expr(Expr::Call(c)));
let expr = Expr::Lit(Lit::Content(vec![node])).with_span(span);
top.v.args.v.0.push(LitDictEntry { key: None, expr });
top.v.args.v.push(Argument::Pos(expr));
inner = top;
}
@ -215,9 +220,9 @@ fn bracket_subheader(p: &mut Parser) -> ExprCall {
Ident(String::new()).with_span(start)
});
let args = p.span(|p| dict_contents(p).0);
let args = p.span(arguments);
p.end_group();
ExprCall { name, args }
}
@ -231,75 +236,6 @@ fn bracket_body(p: &mut Parser) -> SynTree {
tree
}
/// Parse the contents of a dictionary.
fn dict_contents(p: &mut Parser) -> (LitDict, bool) {
let mut dict = LitDict::new();
let mut missing_coma = None;
let mut comma_and_keyless = true;
while !p.eof() {
if let Some(entry) = dict_entry(p) {
let behind = entry.expr.span.end;
if let Some(pos) = missing_coma.take() {
p.diag_expected_at("comma", pos);
}
if let Some(key) = &entry.key {
comma_and_keyless = false;
p.deco(Deco::Name.with_span(key.span));
}
dict.0.push(entry);
if p.eof() {
break;
}
if p.eat_if(Token::Comma) {
comma_and_keyless = false;
} else {
missing_coma = Some(behind);
}
}
}
let coercible = comma_and_keyless && !dict.0.is_empty();
(dict, coercible)
}
/// Parse a single entry in a dictionary.
fn dict_entry(p: &mut Parser) -> Option<LitDictEntry> {
if let Some(ident) = p.span_if(ident) {
match p.peek() {
// Key-value pair.
Some(Token::Colon) => {
p.eat_assert(Token::Colon);
p.span_if(expr).map(|expr| LitDictEntry {
key: Some(ident.map(|id| DictKey::Str(id.0))),
expr,
})
}
// Function call.
Some(Token::LeftParen) => Some(LitDictEntry {
key: None,
expr: {
let start = ident.span.start;
let call = paren_call(p, ident);
Expr::Call(call).with_span(start .. p.last_end())
},
}),
// Just an identifier.
_ => Some(LitDictEntry {
key: None,
expr: ident.map(|id| Expr::Lit(Lit::Ident(id))),
}),
}
} else {
p.span_if(expr).map(|expr| LitDictEntry { key: None, expr })
}
}
/// Parse an expression: `term (+ term)*`.
fn expr(p: &mut Parser) -> Option<Expr> {
binops(p, term, |token| match token {
@ -418,19 +354,6 @@ fn content(p: &mut Parser) -> SynTree {
tree
}
/// Parse a parenthesized expression: `(a + b)`, `(1, name: "value").
fn parenthesized(p: &mut Parser) -> Expr {
p.start_group(Group::Paren);
let (dict, coercible) = dict_contents(p);
let expr = if coercible {
dict.0.into_iter().next().expect("dict is coercible").expr.v
} else {
Expr::Lit(Lit::Dict(dict))
};
p.end_group();
expr
}
/// Parse an identifier.
fn ident(p: &mut Parser) -> Option<Ident> {
p.eat_map(|token| match token {

View File

@ -5,7 +5,6 @@ use std::fmt::Debug;
use super::parse;
use crate::color::RgbaColor;
use crate::diag::{Diag, Level, Pass};
use crate::eval::DictKey;
use crate::geom::Unit;
use crate::syntax::*;
@ -154,21 +153,38 @@ fn Unary(op: impl Into<Spanned<UnOp>>, expr: impl Into<Spanned<Expr>>) -> Expr {
})
}
macro_rules! Array {
(@$($expr:expr),* $(,)?) => {
vec![$(into!($expr)),*]
};
($($tts:tt)*) => (Expr::Lit(Lit::Array(Array![@$($tts)*])));
}
macro_rules! Dict {
(@$($a:expr $(=> $b:expr)?),* $(,)?) => {
LitDict(vec![$(#[allow(unused)] {
let key: Option<Spanned<DictKey>> = None;
let expr = $a;
$(
let key = Some(into!($a).map(|s: &str| s.into()));
let expr = $b;
)?
LitDictEntry { key, expr: into!(expr) }
}),*])
(@$($name:expr => $expr:expr),* $(,)?) => {
vec![$(Named {
name: into!($name).map(|s: &str| Ident(s.into())),
expr: into!($expr)
}),*]
};
($($tts:tt)*) => (Expr::Lit(Lit::Dict(Dict![@$($tts)*])));
}
macro_rules! Args {
(@$a:expr) => {
Argument::Pos(into!($a))
};
(@$a:expr => $b:expr) => {
Argument::Named(Named {
name: into!($a).map(|s: &str| Ident(s.into())),
expr: into!($b)
})
};
($($a:expr $(=> $b:expr)?),* $(,)?) => {
vec![$(Args!(@$a $(=> $b)?)),*]
};
}
macro_rules! Content {
(@$($node:expr),* $(,)?) => (vec![$(into!($node)),*]);
($($tts:tt)*) => (Expr::Lit(Lit::Content(Content![@$($tts)*])));
@ -188,10 +204,6 @@ macro_rules! Call {
($($tts:tt)*) => (SynNode::Expr(Call!(@$($tts)*)));
}
macro_rules! Args {
($($tts:tt)*) => (Dict![@$($tts)*]);
}
#[test]
fn test_parse_comments() {
// In body.
@ -316,10 +328,9 @@ fn test_parse_groups() {
errors: [S(1..2, "expected function name, found closing paren"),
S(2..2, "expected closing bracket")]);
t!("[v {]}"
nodes: [Call!("v", Args![Content![]])],
errors: [S(4..4, "expected closing brace"),
S(5..6, "unexpected closing brace")]);
t!("[v {*]_"
nodes: [Call!("v", Args![Content![Strong]]), Emph],
errors: [S(5..5, "expected closing brace")]);
// Test brace group.
t!("{1 + [}"
@ -329,7 +340,7 @@ fn test_parse_groups() {
// Test subheader group.
t!("[v (|u )]"
nodes: [Call!("v", Args![Dict![], Content![Call!("u")]])],
nodes: [Call!("v", Args![Array![], Content![Call!("u")]])],
errors: [S(4..4, "expected closing paren"),
S(7..8, "expected expression, found closing paren")]);
}
@ -348,6 +359,12 @@ fn test_parse_blocks() {
nodes: [],
errors: [S(1..1, "expected expression"),
S(3..5, "expected expression, found invalid token")]);
// Too much stuff.
t!("{1 #{} end"
nodes: [Block(Int(1)), Space, Text("end")],
errors: [S(3..4, "unexpected hex value"),
S(4..5, "unexpected opening brace")]);
}
#[test]
@ -385,7 +402,7 @@ fn test_parse_bracket_funcs() {
nodes: [Call!("", Args![Int(1)])],
errors: [S(1..2, "expected function name, found hex value")]);
// String header eats closing bracket.
// String in header eats closing bracket.
t!(r#"[v "]"#
nodes: [Call!("v", Args![Str("]")])],
errors: [S(5..5, "expected quote"),
@ -400,8 +417,8 @@ fn test_parse_bracket_funcs() {
#[test]
fn test_parse_chaining() {
// Basic.
t!("[a | b]" Call!("a", Args![Content![Call!("b")]]));
t!("[a | b | c]" Call!("a", Args![Content![
t!("[a | b]" Call!("a", Args![Content![Call!("b")]]));
t!("[a|b|c]" Call!("a", Args![Content![
Call!("b", Args![Content![Call!("c")]])
]]));
@ -428,16 +445,14 @@ fn test_parse_chaining() {
#[test]
fn test_parse_arguments() {
// Bracket functions.
t!("[v 1]" Call!("v", Args![Int(1)]));
t!("[v 1,]" Call!("v", Args![Int(1)]));
t!("[v a]" Call!("v", Args![Id("a")]));
t!("[v a,]" Call!("v", Args![Id("a")]));
t!("[v 1,]" Call!("v", Args![Int(1)]));
t!("[v a:2]" Call!("v", Args!["a" => Int(2)]));
// Parenthesized function with nested dictionary literal.
// Parenthesized function with nested array literal.
t!(r#"{f(1, a: (2, 3), #004, b: "five")}"# Block(Call!(@"f", Args![
Int(1),
"a" => Dict![Int(2), Int(3)],
"a" => Array![Int(2), Int(3)],
Color(RgbaColor::new(0, 0, 0x44, 0xff)),
"b" => Str("five"),
])));
@ -447,56 +462,111 @@ fn test_parse_arguments() {
nodes: [Call!("v", Args![])],
errors: [S(3..5, "expected expression, found end of block comment")]);
// Bad expression.
t!("[v a:1:]"
nodes: [Call!("v", Args!["a" => Int(1)])],
errors: [S(6..7, "expected expression, found colon")]);
// Missing comma between arguments.
t!("[v 1 2]"
nodes: [Call!("v", Args![Int(1), Int(2)])],
errors: [S(4..4, "expected comma")]);
// Missing expression after name.
t!("[v a:]"
nodes: [Call!("v", Args![])],
errors: [S(5..5, "expected expression")]);
// Bad expression after name.
t!("[v a:1:]"
nodes: [Call!("v", Args!["a" => Int(1)])],
errors: [S(6..7, "expected expression, found colon")]);
// Name has to be identifier. Number parsed as positional argument.
// Name has to be identifier.
t!("[v 1:]"
nodes: [Call!("v", Args![Int(1)])],
errors: [S(4..5, "expected expression, found colon")]);
nodes: [Call!("v", Args![])],
errors: [S(3..4, "name must be identifier"),
S(5..5, "expected expression")]);
// Parsed as two positional arguments.
// Name has to be identifier.
t!("[v 1:2]"
nodes: [Call!("v", Args![Int(1), Int(2)])],
errors: [S(4..5, "expected expression, found colon"),
S(4..4, "expected comma")]);
nodes: [Call!("v", Args![])],
errors: [S(3..4, "name must be identifier")]);
}
#[test]
fn test_parse_dict_literals() {
// Basic.
t!("{()}" Block(Dict![]));
fn test_parse_arrays() {
// Empty array.
t!("{()}" Block(Array![]));
// With spans.
t!("{(1, two: 2)}"
nodes: [S(0..13, Block(Dict![
S(2..3, Int(1)),
S(5..8, "two") => S(10..11, Int(2)),
]))],
// Array with one item and trailing comma + spans.
t!("{-(1,)}"
nodes: [S(0..7, Block(Unary(
S(1..2, Neg),
S(2..6, Array![S(3..4, Int(1))])
)))],
spans: true);
// Array with three items and trailing comma.
t!(r#"{("one", 2, #003,)}"# Block(Array![
Str("one"),
Int(2),
Color(RgbaColor::new(0, 0, 0x33, 0xff))
]));
// Unclosed.
t!("{(}"
nodes: [Block(Dict![])],
nodes: [Block(Array![])],
errors: [S(2..2, "expected closing paren")]);
// Missing comma + invalid token.
t!("{(1*/2)}"
nodes: [Block(Array![Int(1), Int(2)])],
errors: [S(3..5, "expected expression, found end of block comment"),
S(3..3, "expected comma")]);
// Invalid token.
t!("{(1, 1u 2)}"
nodes: [Block(Array![Int(1), Int(2)])],
errors: [S(5..7, "expected expression, found invalid token")]);
// Coerced to expression with leading comma.
t!("{(,1)}"
nodes: [Block(Int(1))],
errors: [S(2..3, "expected expression, found comma")]);
// Missing expression after name makes this an array.
t!("{(a:)}"
nodes: [Block(Array![])],
errors: [S(4..4, "expected expression")]);
// Expected expression, found named pair.
t!("{(1, b: 2)}"
nodes: [Block(Array![Int(1)])],
errors: [S(5..9, "expected expression, found named pair")]);
}
#[test]
fn test_parse_dictionaries() {
// Empty dictionary.
t!("{(:)}" Block(Dict![]));
// Dictionary with two pairs + spans.
t!("{(one: 1, two: 2)}"
nodes: [S(0..18, Block(Dict![
S(2..5, "one") => S(7..8, Int(1)),
S(10..13, "two") => S(15..16, Int(2)),
]))],
spans: true);
// Expected named pair, found expression.
t!("{(a: 1, b)}"
nodes: [Block(Dict!["a" => Int(1)])],
errors: [S(8..9, "expected named pair, found expression")]);
// Dictionary marker followed by more stuff.
t!("{(:1 b:2, true::)}"
nodes: [Block(Dict!["b" => Int(2)])],
errors: [S(3..4, "expected named pair, found expression"),
S(4..4, "expected comma"),
S(10..14, "name must be identifier"),
S(15..16, "expected expression, found colon")]);
}
#[test]
fn test_parse_expressions() {
// Parenthesis.
t!("{(x)}" Block(Id("x")));
// Parentheses.
t!("{(x)}{(1)}" Block(Id("x")), Block(Int(1)));
// Unary operations.
t!("{-1}" Block(Unary(Neg, Int(1))));
@ -561,4 +631,12 @@ fn test_parse_values() {
t!("{#a5}"
nodes: [Block(Color(RgbaColor::new(0, 0, 0, 0xff)))],
errors: [S(1..4, "invalid color")]);
// Content.
t!("{{*Hi*}}" Block(Content![Strong, Text("Hi"), Strong]));
// Invalid tokens.
t!("{1u}"
nodes: [],
errors: [S(1..3, "expected expression, found invalid token")]);
}

View File

@ -477,13 +477,9 @@ mod tests {
}
#[test]
fn test_length_from_str_parses_correct_value_and_unit() {
fn test_length_from_str() {
assert_eq!(parse_length("2.5cm"), Some((2.5, Cm)));
assert_eq!(parse_length("1.e+2cm"), Some((100.0, Cm)));
}
#[test]
fn test_length_from_str_works_with_non_ascii_chars() {
assert_eq!(parse_length("123🚚"), None);
}

View File

@ -2,7 +2,10 @@
pub use crate::diag::{Feedback, Pass};
#[doc(no_inline)]
pub use crate::eval::{Args, Dict, Eval, EvalContext, Value, ValueDict};
pub use crate::eval::{
Args, CastResult, Eval, EvalContext, Value, ValueAny, ValueArray, ValueContent,
ValueDict,
};
pub use crate::geom::*;
#[doc(no_inline)]
pub use crate::layout::LayoutNode;

View File

@ -2,7 +2,6 @@
use super::*;
use crate::color::RgbaColor;
use crate::eval::DictKey;
use crate::geom::Unit;
/// An expression.
@ -24,10 +23,22 @@ pub struct ExprCall {
/// The name of the function.
pub name: Spanned<Ident>,
/// The arguments to the function.
///
/// In case of a bracketed invocation with a body, the body is _not_
/// included in the span for the sake of clearer error messages.
pub args: Spanned<LitDict>,
pub args: Spanned<Arguments>,
}
/// The arguments to a function: `12, draw: false`.
///
/// In case of a bracketed invocation with a body, the body is _not_
/// included in the span for the sake of clearer error messages.
pub type Arguments = Vec<Argument>;
/// An argument to a function call: `12` or `draw: false`.
#[derive(Debug, Clone, PartialEq)]
pub enum Argument {
/// A positional arguments.
Pos(Spanned<Expr>),
/// A named argument.
Named(Named),
}
/// A unary operation: `-x`.
@ -92,28 +103,25 @@ pub enum Lit {
Color(RgbaColor),
/// A string literal: `"hello!"`.
Str(String),
/// A dictionary literal: `(false, 12cm, greeting: "hi")`.
Dict(LitDict),
/// An array literal: `(1, "hi", 12cm)`.
Array(Array),
/// A dictionary literal: `(color: #f79143, pattern: dashed)`.
Dict(Dict),
/// A content literal: `{*Hello* there!}`.
Content(SynTree),
}
/// A dictionary literal: `(false, 12cm, greeting: "hi")`.
#[derive(Debug, Default, Clone, PartialEq)]
pub struct LitDict(pub Vec<LitDictEntry>);
/// An array literal: `(1, "hi", 12cm)`.
pub type Array = SpanVec<Expr>;
/// An entry in a dictionary literal: `false` or `greeting: "hi"`.
/// A dictionary literal: `(color: #f79143, pattern: dashed)`.
pub type Dict = Vec<Named>;
/// A pair of a name and an expression: `pattern: dashed`.
#[derive(Debug, Clone, PartialEq)]
pub struct LitDictEntry {
/// The key of the entry if there was one: `greeting`.
pub key: Option<Spanned<DictKey>>,
/// The value of the entry: `"hi"`.
pub struct Named {
/// The name: `pattern`.
pub name: Spanned<Ident>,
/// The right-hand side of the pair: `dashed`.
pub expr: Spanned<Expr>,
}
impl LitDict {
/// Create an empty dict literal.
pub fn new() -> Self {
Self::default()
}
}

View File

@ -30,7 +30,7 @@ pub enum SynNode {
Expr(Expr),
}
/// A section heading: `# ...`.
/// A section heading: `# Introduction`.
#[derive(Debug, Clone, PartialEq)]
pub struct NodeHeading {
/// The section depth (numer of hashtags minus 1).

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.1 KiB

After

Width:  |  Height:  |  Size: 2.7 KiB

View File

@ -6,12 +6,16 @@
// Wrong types.
[font style: bold, weight: "thin", serif: 0]
// Weight out of range.
[font weight: 2700]
// Non-existing argument.
[font something: "invalid"]
// compare-ref: false
// error: 4:7-4:12 unexpected argument
// error: 7:14-7:18 invalid font style
// error: 7:14-7:18 expected font style, found font weight
// error: 7:28-7:34 expected font weight, found string
// error: 7:43-7:44 expected family or list of families, found integer
// error: 10:7-10:27 unexpected argument
// error: 7:43-7:44 expected font family or array of font families, found integer
// warning: 10:15-10:19 must be between 100 and 900
// error: 13:7-13:27 unexpected argument

View File

@ -4,15 +4,17 @@
Emoji: 🏀
// CMU Serif + Noto Emoji.
[font "CMU Serif", "Noto Emoji"][Emoji: 🏀]
[font "CMU Serif", "Noto Emoji"][
Emoji: 🏀
]
// Class definitions.
[font math: ("CMU Serif", "Latin Modern Math", "Noto Emoji")]
[font math][Math: α + β 3]
[font serif: ("CMU Serif", "Latin Modern Math", "Noto Emoji")]
[font serif][
Math: α + β 3
]
// Class redefinition.
// Class definition reused.
[font sans-serif: "Noto Emoji"]
[font sans-serif: ("Archivo", sans-serif)]
New sans-serif. 🚀
// TODO: Add tests for other scripts.

View File

@ -7,5 +7,5 @@
[page main-dir: ltr]
// compare-ref: false
// error: 4:7-4:18 invalid paper
// error: 4:7-4:18 unknown variable
// error: 7:17-7:20 aligned axis

View File

@ -22,4 +22,4 @@
[page margins: 0pt, left: 40pt][Overriden]
// Flip the page.
[page a10, flip: true][Flipped]
[page "a10", flip: true][Flipped]

View File

@ -1,7 +1,7 @@
// Test the `rgb` function.
// Check the output.
[rgb 0.0, 0.3, 0.7] [val #004db3]
[rgb 0.0, 0.3, 0.7]
// Alpha channel.
[rgb 1.0, 0.0, 0.0, 0.5]
@ -15,9 +15,8 @@
// Missing all components.
[rgb]
// error: 4:22-4:25 unknown function
// error: 10:6-10:9 should be between 0.0 and 1.0
// error: 10:11-10:15 should be between 0.0 and 1.0
// warning: 10:6-10:9 must be between 0.0 and 1.0
// warning: 10:11-10:15 must be between 0.0 and 1.0
// error: 13:6-13:10 missing argument: blue component
// error: 16:5-16:5 missing argument: red component
// error: 16:5-16:5 missing argument: green component