diff options
Diffstat (limited to 'crates/typst-library/src/foundations/cast.rs')
| -rw-r--r-- | crates/typst-library/src/foundations/cast.rs | 499 |
1 files changed, 499 insertions, 0 deletions
diff --git a/crates/typst-library/src/foundations/cast.rs b/crates/typst-library/src/foundations/cast.rs new file mode 100644 index 00000000..84f38f36 --- /dev/null +++ b/crates/typst-library/src/foundations/cast.rs @@ -0,0 +1,499 @@ +#[rustfmt::skip] +#[doc(inline)] +pub use typst_macros::{cast, Cast}; + +use std::borrow::Cow; +use std::fmt::Write; +use std::hash::Hash; +use std::ops::Add; + +use ecow::eco_format; +use smallvec::SmallVec; +use typst_syntax::{Span, Spanned}; +use unicode_math_class::MathClass; + +use crate::diag::{At, HintedStrResult, HintedString, SourceResult, StrResult}; +use crate::foundations::{array, repr, NativeElement, Packed, Repr, Str, Type, Value}; + +/// Determine details of a type. +/// +/// Type casting works as follows: +/// - [`Reflect for T`](Reflect) describes the possible Typst values for `T` +/// (for documentation and autocomplete). +/// - [`IntoValue for T`](IntoValue) is for conversion from `T -> Value` +/// (infallible) +/// - [`FromValue for T`](FromValue) is for conversion from `Value -> T` +/// (fallible). +/// +/// We can't use `TryFrom<Value>` due to conflicting impls. We could use +/// `From<T> for Value`, but that inverses the impl and leads to tons of +/// `.into()` all over the place that become hard to decipher. +pub trait Reflect { + /// Describe what can be cast into this value. + fn input() -> CastInfo; + + /// Describe what this value can be cast into. + fn output() -> CastInfo; + + /// Whether the given value can be converted to `T`. + /// + /// This exists for performance. The check could also be done through the + /// [`CastInfo`], but it would be much more expensive (heap allocation + + /// dynamic checks instead of optimized machine code for each type). + fn castable(value: &Value) -> bool; + + /// Produce an error message for an unacceptable value type. + /// + /// ```ignore + /// assert_eq!( + /// <i64 as Reflect>::error(&Value::None), + /// "expected integer, found none", + /// ); + /// ``` + fn error(found: &Value) -> HintedString { + Self::input().error(found) + } +} + +impl Reflect for Value { + fn input() -> CastInfo { + CastInfo::Any + } + + fn output() -> CastInfo { + CastInfo::Any + } + + fn castable(_: &Value) -> bool { + true + } +} + +impl<T: Reflect> Reflect for Spanned<T> { + fn input() -> CastInfo { + T::input() + } + + fn output() -> CastInfo { + T::output() + } + + fn castable(value: &Value) -> bool { + T::castable(value) + } +} + +impl<T: NativeElement + Reflect> Reflect for Packed<T> { + fn input() -> CastInfo { + T::input() + } + + fn output() -> CastInfo { + T::output() + } + + fn castable(value: &Value) -> bool { + T::castable(value) + } +} + +impl<T: Reflect> Reflect for StrResult<T> { + fn input() -> CastInfo { + T::input() + } + + fn output() -> CastInfo { + T::output() + } + + fn castable(value: &Value) -> bool { + T::castable(value) + } +} + +impl<T: Reflect> Reflect for HintedStrResult<T> { + fn input() -> CastInfo { + T::input() + } + + fn output() -> CastInfo { + T::output() + } + + fn castable(value: &Value) -> bool { + T::castable(value) + } +} + +impl<T: Reflect> Reflect for SourceResult<T> { + fn input() -> CastInfo { + T::input() + } + + fn output() -> CastInfo { + T::output() + } + + fn castable(value: &Value) -> bool { + T::castable(value) + } +} + +impl<T: Reflect> Reflect for &T { + fn input() -> CastInfo { + T::input() + } + + fn output() -> CastInfo { + T::output() + } + + fn castable(value: &Value) -> bool { + T::castable(value) + } +} + +impl<T: Reflect> Reflect for &mut T { + fn input() -> CastInfo { + T::input() + } + + fn output() -> CastInfo { + T::output() + } + + fn castable(value: &Value) -> bool { + T::castable(value) + } +} + +/// Cast a Rust type into a Typst [`Value`]. +/// +/// See also: [`Reflect`]. +pub trait IntoValue { + /// Cast this type into a value. + fn into_value(self) -> Value; +} + +impl IntoValue for Value { + fn into_value(self) -> Value { + self + } +} + +impl IntoValue for (&Str, &Value) { + fn into_value(self) -> Value { + Value::Array(array![self.0.clone(), self.1.clone()]) + } +} + +impl<T: IntoValue + Clone> IntoValue for Cow<'_, T> { + fn into_value(self) -> Value { + self.into_owned().into_value() + } +} + +impl<T: NativeElement + IntoValue> IntoValue for Packed<T> { + fn into_value(self) -> Value { + Value::Content(self.pack()) + } +} + +impl<T: IntoValue> IntoValue for Spanned<T> { + fn into_value(self) -> Value { + self.v.into_value() + } +} + +/// Cast a Rust type or result into a [`SourceResult<Value>`]. +/// +/// Converts `T`, [`StrResult<T>`], or [`SourceResult<T>`] into +/// [`SourceResult<Value>`] by `Ok`-wrapping or adding span information. +pub trait IntoResult { + /// Cast this type into a value. + fn into_result(self, span: Span) -> SourceResult<Value>; +} + +impl<T: IntoValue> IntoResult for T { + fn into_result(self, _: Span) -> SourceResult<Value> { + Ok(self.into_value()) + } +} + +impl<T: IntoValue> IntoResult for StrResult<T> { + fn into_result(self, span: Span) -> SourceResult<Value> { + self.map(IntoValue::into_value).at(span) + } +} + +impl<T: IntoValue> IntoResult for HintedStrResult<T> { + fn into_result(self, span: Span) -> SourceResult<Value> { + self.map(IntoValue::into_value).at(span) + } +} + +impl<T: IntoValue> IntoResult for SourceResult<T> { + fn into_result(self, _: Span) -> SourceResult<Value> { + self.map(IntoValue::into_value) + } +} + +impl<T: IntoValue> IntoValue for fn() -> T { + fn into_value(self) -> Value { + self().into_value() + } +} + +/// Try to cast a Typst [`Value`] into a Rust type. +/// +/// See also: [`Reflect`]. +pub trait FromValue<V = Value>: Sized + Reflect { + /// Try to cast the value into an instance of `Self`. + fn from_value(value: V) -> HintedStrResult<Self>; +} + +impl FromValue for Value { + fn from_value(value: Value) -> HintedStrResult<Self> { + Ok(value) + } +} + +impl<T: NativeElement + FromValue> FromValue for Packed<T> { + fn from_value(mut value: Value) -> HintedStrResult<Self> { + if let Value::Content(content) = value { + match content.into_packed::<T>() { + Ok(packed) => return Ok(packed), + Err(content) => value = Value::Content(content), + } + } + let val = T::from_value(value)?; + Ok(Packed::new(val)) + } +} + +impl<T: FromValue> FromValue<Spanned<Value>> for T { + fn from_value(value: Spanned<Value>) -> HintedStrResult<Self> { + T::from_value(value.v) + } +} + +impl<T: FromValue> FromValue<Spanned<Value>> for Spanned<T> { + fn from_value(value: Spanned<Value>) -> HintedStrResult<Self> { + let span = value.span; + T::from_value(value.v).map(|t| Spanned::new(t, span)) + } +} + +/// Describes a possible value for a cast. +#[derive(Debug, Clone, PartialEq, Hash, PartialOrd)] +pub enum CastInfo { + /// Any value is okay. + Any, + /// A specific value, plus short documentation for that value. + Value(Value, &'static str), + /// Any value of a type. + Type(Type), + /// Multiple alternatives. + Union(Vec<Self>), +} + +impl CastInfo { + /// Produce an error message describing what was expected and what was + /// found. + pub fn error(&self, found: &Value) -> HintedString { + let mut matching_type = false; + let mut parts = vec![]; + + self.walk(|info| match info { + CastInfo::Any => parts.push("anything".into()), + CastInfo::Value(value, _) => { + parts.push(value.repr()); + if value.ty() == found.ty() { + matching_type = true; + } + } + CastInfo::Type(ty) => parts.push(eco_format!("{ty}")), + CastInfo::Union(_) => {} + }); + + let mut msg = String::from("expected "); + if parts.is_empty() { + msg.push_str(" nothing"); + } + + msg.push_str(&repr::separated_list(&parts, "or")); + + if !matching_type { + msg.push_str(", found "); + write!(msg, "{}", found.ty()).unwrap(); + } + + let mut msg: HintedString = msg.into(); + + if let Value::Int(i) = found { + if !matching_type && parts.iter().any(|p| p == "length") { + msg.hint(eco_format!("a length needs a unit - did you mean {i}pt?")); + } + } else if let Value::Str(s) = found { + if !matching_type && parts.iter().any(|p| p == "label") { + if typst_syntax::is_valid_label_literal_id(s) { + msg.hint(eco_format!( + "use `<{s}>` or `label({})` to create a label", + s.repr() + )); + } else { + msg.hint(eco_format!("use `label({})` to create a label", s.repr())); + } + } + } else if let Value::Decimal(_) = found { + if !matching_type && parts.iter().any(|p| p == "float") { + msg.hint(eco_format!( + "if loss of precision is acceptable, explicitly cast the \ + decimal to a float with `float(value)`" + )); + } + } + + msg + } + + /// Walk all contained non-union infos. + pub fn walk<F>(&self, mut f: F) + where + F: FnMut(&Self), + { + fn inner<F>(info: &CastInfo, f: &mut F) + where + F: FnMut(&CastInfo), + { + if let CastInfo::Union(infos) = info { + for child in infos { + inner(child, f); + } + } else { + f(info); + } + } + + inner(self, &mut f) + } +} + +impl Add for CastInfo { + type Output = Self; + + fn add(self, rhs: Self) -> Self { + Self::Union(match (self, rhs) { + (Self::Union(mut lhs), Self::Union(rhs)) => { + for cast in rhs { + if !lhs.contains(&cast) { + lhs.push(cast); + } + } + lhs + } + (Self::Union(mut lhs), rhs) => { + if !lhs.contains(&rhs) { + lhs.push(rhs); + } + lhs + } + (lhs, Self::Union(mut rhs)) => { + if !rhs.contains(&lhs) { + rhs.insert(0, lhs); + } + rhs + } + (lhs, rhs) => vec![lhs, rhs], + }) + } +} + +/// A container for an argument. +pub trait Container { + /// The contained type. + type Inner; +} + +impl<T> Container for Option<T> { + type Inner = T; +} + +impl<T> Container for Vec<T> { + type Inner = T; +} + +impl<T, const N: usize> Container for SmallVec<[T; N]> { + type Inner = T; +} + +/// An uninhabitable type. +#[derive(Debug, Clone, Copy, Eq, PartialEq, Hash)] +pub enum Never {} + +impl Reflect for Never { + fn input() -> CastInfo { + CastInfo::Union(vec![]) + } + + fn output() -> CastInfo { + CastInfo::Union(vec![]) + } + + fn castable(_: &Value) -> bool { + false + } +} + +impl IntoValue for Never { + fn into_value(self) -> Value { + match self {} + } +} + +impl FromValue for Never { + fn from_value(value: Value) -> HintedStrResult<Self> { + Err(Self::error(&value)) + } +} + +cast! { + MathClass, + self => IntoValue::into_value(match self { + MathClass::Normal => "normal", + MathClass::Alphabetic => "alphabetic", + MathClass::Binary => "binary", + MathClass::Closing => "closing", + MathClass::Diacritic => "diacritic", + MathClass::Fence => "fence", + MathClass::GlyphPart => "glyph-part", + MathClass::Large => "large", + MathClass::Opening => "opening", + MathClass::Punctuation => "punctuation", + MathClass::Relation => "relation", + MathClass::Space => "space", + MathClass::Unary => "unary", + MathClass::Vary => "vary", + MathClass::Special => "special", + }), + /// The default class for non-special things. + "normal" => MathClass::Normal, + /// Punctuation, e.g. a comma. + "punctuation" => MathClass::Punctuation, + /// An opening delimiter, e.g. `(`. + "opening" => MathClass::Opening, + /// A closing delimiter, e.g. `)`. + "closing" => MathClass::Closing, + /// A delimiter that is the same on both sides, e.g. `|`. + "fence" => MathClass::Fence, + /// A large operator like `sum`. + "large" => MathClass::Large, + /// A relation like `=` or `prec`. + "relation" => MathClass::Relation, + /// A unary operator like `not`. + "unary" => MathClass::Unary, + /// A binary operator like `times`. + "binary" => MathClass::Binary, + /// An operator that can be both unary or binary like `+`. + "vary" => MathClass::Vary, +} |
