summaryrefslogtreecommitdiff
path: root/crates/typst-library/src/foundations/cast.rs
diff options
context:
space:
mode:
Diffstat (limited to 'crates/typst-library/src/foundations/cast.rs')
-rw-r--r--crates/typst-library/src/foundations/cast.rs499
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,
+}