Restructure value conversions 🧱

This commit is contained in:
Laurenz 2020-12-21 00:36:22 +01:00
parent 6f111f9410
commit 2b660968aa
8 changed files with 226 additions and 223 deletions

View File

@ -99,7 +99,7 @@ impl Debug for RgbaColor {
} }
} }
/// The error when parsing an `RgbaColor` fails. /// The error when parsing an [`RgbaColor`] fails.
#[derive(Debug, Copy, Clone, Eq, PartialEq)] #[derive(Debug, Copy, Clone, Eq, PartialEq)]
pub struct ParseColorError; pub struct ParseColorError;

View File

@ -1,7 +1,10 @@
//! Simplifies argument parsing. //! Simplifies argument parsing.
use super::{Convert, EvalContext, RefKey, ValueDict}; use std::mem;
use crate::syntax::{SpanWith, Spanned};
use super::{Conv, EvalContext, RefKey, TryFromValue, Value, ValueDict};
use crate::diag::Diag;
use crate::syntax::{Span, SpanVec, SpanWith, Spanned};
/// A wrapper around a dictionary value that simplifies argument parsing in /// A wrapper around a dictionary value that simplifies argument parsing in
/// functions. /// functions.
@ -16,15 +19,11 @@ impl Args {
pub fn get<'a, K, T>(&mut self, ctx: &mut EvalContext, key: K) -> Option<T> pub fn get<'a, K, T>(&mut self, ctx: &mut EvalContext, key: K) -> Option<T>
where where
K: Into<RefKey<'a>>, K: Into<RefKey<'a>>,
T: Convert, T: TryFromValue,
{ {
self.0.v.remove(key).and_then(|entry| { self.0.v.remove(key).and_then(|entry| {
let span = entry.value.span; let span = entry.value.span;
let (result, diag) = T::convert(entry.value); conv_diag(T::try_from_value(entry.value), &mut ctx.f.diags, span)
if let Some(diag) = diag {
ctx.f.diags.push(diag.span_with(span))
}
result.ok()
}) })
} }
@ -38,37 +37,29 @@ impl Args {
) -> Option<T> ) -> Option<T>
where where
K: Into<RefKey<'a>>, K: Into<RefKey<'a>>,
T: Convert, T: TryFromValue,
{ {
match self.0.v.remove(key) { if let Some(entry) = self.0.v.remove(key) {
Some(entry) => { let span = entry.value.span;
let span = entry.value.span; conv_diag(T::try_from_value(entry.value), &mut ctx.f.diags, span)
let (result, diag) = T::convert(entry.value); } else {
if let Some(diag) = diag { ctx.f.diags.push(error!(self.0.span, "missing argument: {}", name));
ctx.f.diags.push(diag.span_with(span)) None
}
result.ok()
}
None => {
ctx.f.diags.push(error!(self.0.span, "missing argument: {}", name));
None
}
} }
} }
/// Retrieve and remove the first matching positional argument. /// Retrieve and remove the first matching positional argument.
pub fn find<T>(&mut self) -> Option<T> pub fn find<T>(&mut self) -> Option<T>
where where
T: Convert, T: TryFromValue,
{ {
for (&key, entry) in self.0.v.nums_mut() { for (&key, entry) in self.0.v.nums_mut() {
let span = entry.value.span; let span = entry.value.span;
match T::convert(std::mem::take(&mut entry.value)).0 { let slot = &mut entry.value;
Ok(t) => { let conv = conv_put_back(T::try_from_value(mem::take(slot)), slot, span);
self.0.v.remove(key); if let Some(t) = conv {
return Some(t); self.0.v.remove(key);
} return Some(t);
Err(v) => entry.value = v.span_with(span),
} }
} }
None None
@ -77,18 +68,17 @@ impl Args {
/// Retrieve and remove all matching positional arguments. /// Retrieve and remove all matching positional arguments.
pub fn find_all<T>(&mut self) -> impl Iterator<Item = T> + '_ pub fn find_all<T>(&mut self) -> impl Iterator<Item = T> + '_
where where
T: Convert, T: TryFromValue,
{ {
let mut skip = 0; let mut skip = 0;
std::iter::from_fn(move || { std::iter::from_fn(move || {
for (&key, entry) in self.0.v.nums_mut().skip(skip) { for (&key, entry) in self.0.v.nums_mut().skip(skip) {
let span = entry.value.span; let span = entry.value.span;
match T::convert(std::mem::take(&mut entry.value)).0 { let slot = &mut entry.value;
Ok(t) => { let conv = conv_put_back(T::try_from_value(mem::take(slot)), slot, span);
self.0.v.remove(key); if let Some(t) = conv {
return Some(t); self.0.v.remove(key);
} return Some(t);
Err(v) => entry.value = v.span_with(span),
} }
skip += 1; skip += 1;
} }
@ -99,19 +89,18 @@ impl Args {
/// Retrieve and remove all matching keyword arguments. /// Retrieve and remove all matching keyword arguments.
pub fn find_all_str<T>(&mut self) -> impl Iterator<Item = (String, T)> + '_ pub fn find_all_str<T>(&mut self) -> impl Iterator<Item = (String, T)> + '_
where where
T: Convert, T: TryFromValue,
{ {
let mut skip = 0; let mut skip = 0;
std::iter::from_fn(move || { std::iter::from_fn(move || {
for (key, entry) in self.0.v.strs_mut().skip(skip) { for (key, entry) in self.0.v.strs_mut().skip(skip) {
let span = entry.value.span; let span = entry.value.span;
match T::convert(std::mem::take(&mut entry.value)).0 { let slot = &mut entry.value;
Ok(t) => { let conv = conv_put_back(T::try_from_value(mem::take(slot)), slot, span);
let key = key.clone(); if let Some(t) = conv {
self.0.v.remove(&key); let key = key.clone();
return Some((key, t)); self.0.v.remove(&key);
} return Some((key, t));
Err(v) => entry.value = v.span_with(span),
} }
skip += 1; skip += 1;
} }
@ -129,6 +118,31 @@ impl Args {
} }
} }
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.span_with(span));
Some(t)
}
Conv::Err(_, err) => {
diags.push(err.span_with(span));
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.span_with(span);
None
}
}
}
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use super::super::{Dict, SpannedEntry, Value}; use super::super::{Dict, SpannedEntry, Value};

View File

@ -1,170 +0,0 @@
//! Conversion from values into other types.
use std::ops::Deref;
use fontdock::{FontStretch, FontStyle, FontWeight};
use super::{Value, ValueDict, ValueFunc};
use crate::diag::Diag;
use crate::geom::{Dir, Length, Linear, Relative};
use crate::paper::Paper;
use crate::syntax::{Ident, SpanWith, Spanned, SynTree};
/// Types that values can be converted into.
pub trait Convert: Sized {
/// Convert a value into `Self`.
///
/// If the conversion works out, this should return `Ok(...)` with an
/// instance of `Self`. If it doesn't, it should return `Err(...)` giving
/// back the original value.
///
/// In addition to the result, the method can return an optional diagnostic
/// to warn even when the conversion succeeded or to explain the problem when
/// the conversion failed.
///
/// The function takes a `Spanned<Value>` instead of just a `Value` so that
/// this trait can be blanket implemented for `Spanned<T>` where `T:
/// Convert`.
fn convert(value: Spanned<Value>) -> (Result<Self, Value>, Option<Diag>);
}
impl<T: Convert> Convert for Spanned<T> {
fn convert(value: Spanned<Value>) -> (Result<Self, Value>, Option<Diag>) {
let span = value.span;
let (result, diag) = T::convert(value);
(result.map(|v| v.span_with(span)), diag)
}
}
macro_rules! convert_match {
($type:ty, $name:expr, $($p:pat => $r:expr),* $(,)?) => {
impl $crate::eval::Convert for $type {
fn convert(
value: $crate::syntax::Spanned<$crate::eval::Value>
) -> (Result<Self, $crate::eval::Value>, Option<$crate::diag::Diag>) {
#[allow(unreachable_patterns)]
match value.v {
$($p => (Ok($r), None)),*,
v => {
let err = $crate::error!("expected {}, found {}", $name, v.ty());
(Err(v), Some(err))
},
}
}
}
};
}
macro_rules! convert_ident {
($type:ty, $name:expr, $parse:expr) => {
impl $crate::eval::Convert for $type {
fn convert(
value: $crate::syntax::Spanned<$crate::eval::Value>,
) -> (
Result<Self, $crate::eval::Value>,
Option<$crate::diag::Diag>,
) {
match value.v {
Value::Ident(id) => {
if let Some(thing) = $parse(&id) {
(Ok(thing), None)
} else {
(
Err($crate::eval::Value::Ident(id)),
Some($crate::error!("invalid {}", $name)),
)
}
}
v => {
let err = $crate::error!("expected {}, found {}", $name, v.ty());
(Err(v), Some(err))
}
}
}
}
};
}
/// 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 Deref for StringLike {
type Target = str;
fn deref(&self) -> &str {
self.0.as_str()
}
}
convert_match!(Value, "value", v => v);
convert_match!(Ident, "identifier", Value::Ident(v) => v);
convert_match!(bool, "bool", Value::Bool(v) => v);
convert_match!(i64, "integer", Value::Int(v) => v);
convert_match!(f64, "float",
Value::Int(v) => v as f64,
Value::Float(v) => v,
);
convert_match!(Length, "length", Value::Length(v) => v);
convert_match!(Relative, "relative", Value::Relative(v) => v);
convert_match!(Linear, "linear",
Value::Linear(v) => v,
Value::Length(v) => v.into(),
Value::Relative(v) => v.into(),
);
convert_match!(String, "string", Value::Str(v) => v);
convert_match!(SynTree, "tree", Value::Content(v) => v);
convert_match!(ValueDict, "dictionary", Value::Dict(v) => v);
convert_match!(ValueFunc, "function", Value::Func(v) => v);
convert_match!(StringLike, "identifier or string",
Value::Ident(Ident(v)) => StringLike(v),
Value::Str(v) => StringLike(v),
);
convert_ident!(Dir, "direction", |v| match v {
"ltr" => Some(Self::LTR),
"rtl" => Some(Self::RTL),
"ttb" => Some(Self::TTB),
"btt" => Some(Self::BTT),
_ => None,
});
convert_ident!(FontStyle, "font style", Self::from_str);
convert_ident!(FontStretch, "font stretch", Self::from_str);
convert_ident!(Paper, "paper", Self::from_name);
impl Convert for FontWeight {
fn convert(value: Spanned<Value>) -> (Result<Self, Value>, Option<Diag>) {
match value.v {
Value::Int(number) => {
let [min, max] = [100, 900];
let warning = if number < min {
Some(warning!("the minimum font weight is {}", min))
} else if number > max {
Some(warning!("the maximum font weight is {}", max))
} else {
None
};
let weight = Self::from_number(number.min(max).max(min) as u16);
(Ok(weight), warning)
}
Value::Ident(id) => {
if let Some(thing) = FontWeight::from_str(&id) {
(Ok(thing), None)
} else {
(Err(Value::Ident(id)), Some(error!("invalid font weight")))
}
}
v => {
let err =
error!("expected font weight (name or number), found {}", v.ty());
(Err(v), Some(err))
}
}
}
}

View File

@ -1,15 +1,13 @@
//! Evaluation of syntax trees. //! Evaluation of syntax trees.
#[macro_use] #[macro_use]
mod convert; mod value;
mod args; mod args;
mod dict; mod dict;
mod scope; mod scope;
mod state; mod state;
mod value;
pub use args::*; pub use args::*;
pub use convert::*;
pub use dict::*; pub use dict::*;
pub use scope::*; pub use scope::*;
pub use state::*; pub use state::*;

View File

@ -4,10 +4,14 @@ use std::fmt::{self, Debug, Formatter};
use std::ops::Deref; use std::ops::Deref;
use std::rc::Rc; use std::rc::Rc;
use fontdock::{FontStretch, FontStyle, FontWeight};
use super::{Args, Dict, Eval, EvalContext, SpannedEntry}; use super::{Args, Dict, Eval, EvalContext, SpannedEntry};
use crate::color::RgbaColor; use crate::color::RgbaColor;
use crate::geom::{Length, Linear, Relative}; use crate::diag::Diag;
use crate::syntax::{Ident, SynTree}; use crate::geom::{Dir, Length, Linear, Relative};
use crate::paper::Paper;
use crate::syntax::{Ident, SpanWith, Spanned, SynTree};
/// A computational value. /// A computational value.
#[derive(Clone, PartialEq)] #[derive(Clone, PartialEq)]
@ -170,3 +174,160 @@ impl Debug for ValueFunc {
f.pad("<function>") f.pad("<function>")
} }
} }
/// 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>;
}
/// The result of a conversion.
#[derive(Debug, Clone, PartialEq)]
pub enum Conv<T> {
/// Success conversion.
Ok(T),
/// Sucessful conversion with a warning.
Warn(T, Diag),
/// Unsucessful conversion, gives back the value alongside the error.
Err(Value, Diag),
}
impl<T> Conv<T> {
/// Map the conversion result.
pub fn map<U>(self, f: impl FnOnce(T) -> U) -> Conv<U> {
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),
}
}
}
impl<T: TryFromValue> TryFromValue for Spanned<T> {
fn try_from_value(value: Spanned<Value>) -> Conv<Self> {
let span = value.span;
T::try_from_value(value).map(|v| v.span_with(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 Deref for StringLike {
type Target = str;
fn deref(&self) -> &str {
self.0.as_str()
}
}
/// 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)
}
}
}
}
};
}
/// 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)
}
}
}
};
}
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"]:
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,
Value::Length(v) => v.into(),
Value::Relative(v) => v.into(),
);
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)
}
}
}
}

View File

@ -116,7 +116,7 @@ enum SpecAlign {
Center, Center,
} }
convert_ident!(SpecAlign, "alignment", |v| match v { try_from_id!(SpecAlign["alignment"]: |v| match v {
"left" => Some(Self::Left), "left" => Some(Self::Left),
"right" => Some(Self::Right), "right" => Some(Self::Right),
"top" => Some(Self::Top), "top" => Some(Self::Top),

View File

@ -39,7 +39,7 @@ fn main() -> anyhow::Result<()> {
let mut index = FsIndex::new(); let mut index = FsIndex::new();
index.search_dir("fonts"); index.search_dir("fonts");
index.search_os(); index.search_system();
let (files, descriptors) = index.into_vecs(); let (files, descriptors) = index.into_vecs();
let env = Rc::new(RefCell::new(Env { let env = Rc::new(RefCell::new(Env {

View File

@ -7,5 +7,5 @@ pub use crate::geom::*;
#[doc(no_inline)] #[doc(no_inline)]
pub use crate::layout::LayoutNode; pub use crate::layout::LayoutNode;
#[doc(no_inline)] #[doc(no_inline)]
pub use crate::syntax::{Span, Spanned, SynTree}; pub use crate::syntax::{Span, SpanWith, Spanned, SynTree};
pub use crate::{error, warning}; pub use crate::{error, warning};