Skip to main content

htl_core/
teal.rs

1//! Rust <-> Teal type bridge used by `#[derive(TealRecord)]` and `#[host_module]`.
2//!
3//! The macros map Rust types to Teal type names *syntactically* at expansion time
4//! (`f64 -> number`, `String -> string`, `Vec<T> -> {T}`, ...). This module holds the
5//! runtime-side traits the generated code implements.
6
7/// A Rust type mirrored as a Teal declaration: a struct as a `record` (plain table with
8/// named fields), a newtype as a `type` alias, an enum as an `enum` of its variant names
9/// or, when a variant carries data, as a union of `where`-discriminated records. The
10/// lowering is in `htl_core::dts`.
11pub trait TealRecord {
12    /// Teal type name (also the module name of its `.d.tl`).
13    const NAME: &'static str;
14    /// Full `.d.tl` text: `local record NAME ... end  return NAME`, or the `enum` /
15    /// `type` form.
16    const DECL: &'static str;
17}
18
19/// Why a value coming back from Lua did not fit a `#[derive(TealRecord)]` struct.
20///
21/// `FromLua` is the only place the two sides of a record are compared — the `.d.tl` is
22/// generated from Rust, so what a host *offers* is checked at build time, while what it
23/// *receives* is checked when a value crosses. This carries enough to act on: which
24/// record, which field, the Teal type declared for it, and what arrived instead.
25///
26/// ```text
27/// Outcome.cause: expected string, got nil
28/// Outcome.depth: expected integer, got string
29/// ```
30///
31/// A record inside a record extends the path rather than nesting the message, so the
32/// innermost field is what the reader sees: `Recording.outcome.cause`.
33#[derive(Debug, Clone, PartialEq, Eq)]
34pub struct FieldError {
35    /// `Record.field`, one segment per level of nesting.
36    pub path: String,
37    /// The Teal type the record declares for the field.
38    pub expected: String,
39    /// The Lua type that arrived: `nil`, `string`, `table`, ...
40    pub got: String,
41}
42
43impl std::fmt::Display for FieldError {
44    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
45        write!(
46            f,
47            "{}: expected {}, got {}",
48            self.path, self.expected, self.got
49        )
50    }
51}
52
53impl std::error::Error for FieldError {}
54
55/// Name the record and the field a conversion failed on.
56///
57/// Called by the `FromLua` that `#[derive(TealRecord)]` generates, once per field that
58/// fails. `cause` is what the field's own conversion returned, and is only replaced when
59/// it says nothing the caller does not already know — a plain type mismatch. Anything
60/// else (a host function's own error, a borrow failure) is kept and given the field as
61/// context, because it carries more than this can reconstruct.
62pub fn field_error(
63    record: &str,
64    field: &str,
65    expected: &str,
66    got: &str,
67    cause: mlua::Error,
68) -> mlua::Error {
69    located(format!("{record}.{field}"), expected, got, cause)
70}
71
72/// Name the type a whole-value conversion failed on: a newtype (`type N = T` on the
73/// Teal side) whose inner conversion refused what arrived. The alias is what the Teal
74/// side declared, so it is the name the reader looks for: a `FieldError` from inside
75/// (the inner type is a record, an enum, or another alias) is re-rooted at the alias —
76/// `Label.y: expected number, got nil`, `Wrap: expected one of ..` — and a plain
77/// mismatch becomes `N: expected T, got <type>`.
78pub fn value_error(name: &str, expected: &str, got: &str, cause: mlua::Error) -> mlua::Error {
79    located(name.to_string(), expected, got, cause)
80}
81
82/// The one policy behind `field_error` and `value_error`, given the location the caller
83/// stands at (`Record.field`, `Enum.Variant.field`, or a bare type name):
84///
85/// - a `FieldError` from inside is re-rooted here: the head of its path is the inner
86///   type's own name, which this location already states, so it is dropped and the rest
87///   (if any — an enum or alias reports with a bare name) appended;
88/// - a plain type mismatch becomes a `FieldError` at this location;
89/// - anything else (a host function's own error, a borrow failure) keeps its message
90///   with the location as context, because it carries more than this can reconstruct.
91fn located(path: String, expected: &str, got: &str, cause: mlua::Error) -> mlua::Error {
92    if let Some(inner) = cause.downcast_ref::<FieldError>() {
93        let path = match inner.path.split_once('.') {
94            Some((_, rest)) => format!("{path}.{rest}"),
95            None => path,
96        };
97        return mlua::Error::external(FieldError {
98            path,
99            expected: inner.expected.clone(),
100            got: inner.got.clone(),
101        });
102    }
103    if !matches!(cause, mlua::Error::FromLuaConversionError { .. }) {
104        return mlua::ErrorContext::context(cause, path);
105    }
106    mlua::Error::external(FieldError {
107        path,
108        expected: expected.to_string(),
109        got: got.to_string(),
110    })
111}
112
113/// Name the enum a value did not belong to, listing what it accepts:
114/// `Mode: expected one of "Fast", "Careful", got "fst"`. `got` is the offending string
115/// in quotes, or the Lua type name when it was not a string (or, for a data-carrying
116/// enum, not a table). Reported as a `FieldError` so an enum inside a record extends the
117/// record's path the way a nested record does.
118pub fn enum_error(name: &str, variants: &[&str], got: &str) -> mlua::Error {
119    let list: Vec<String> = variants.iter().map(|v| format!("\"{v}\"")).collect();
120    mlua::Error::external(FieldError {
121        path: name.to_string(),
122        expected: format!("one of {}", list.join(", ")),
123        got: got.to_string(),
124    })
125}
126
127/// A host parameter that takes the Lua kind its Teal type names, and converts nothing.
128///
129/// mlua's `FromLua` for `i64` accepts a Lua string and coerces it (`"10"` arrives as
130/// `10`), and its `FromLua` for `String` accepts a number and formats it. The `.d.tl`
131/// that `#[host_module]` writes says `integer` and `string`, and checked Teal cannot pass
132/// the other kind — but a value from past a cast or a `load`, which the checker did not
133/// see, can, and the host has no way to tell the two apart once mlua has converted.
134/// `Strict<T>` is the parameter type that keeps the runtime to the declaration: an
135/// integer is `Value::Integer`, or a `Value::Number` with no fraction (as
136/// `math.tointeger` reads it); a float is either number kind; `bool` is
137/// `Value::Boolean`; `String` is `Value::String`. Anything else is a conversion error
138/// naming what arrived — `error converting Lua string to integer` — the same shape mlua
139/// reports, so a caller reading errors sees one kind of message.
140///
141/// ```ignore
142/// #[host_module(name = "api")]
143/// impl Api {
144///     pub fn take(&self, n: Strict<i64>) -> i64 { *n }   // `take: function(self: api, n: integer): integer`
145/// }
146/// ```
147///
148/// The declaration is the inner type's — `Strict<i64>` is `integer` in the `.d.tl` — since
149/// it already said that; this only holds the runtime to it. Derefs to `T`, and goes back
150/// to Lua as `T` would, so a return type may be `Strict<T>` as well, though there it
151/// adds nothing.
152///
153/// The other two layers of the same question are the checker, which holds checked Teal,
154/// and [`Htl::strict_strings`](crate::Htl::strict_strings), which holds the program state
155/// where arithmetic on a string would otherwise convert.
156#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Default)]
157pub struct Strict<T>(pub T);
158
159impl<T> Strict<T> {
160    /// The value, out of the wrapper.
161    pub fn into_inner(self) -> T {
162        self.0
163    }
164}
165
166impl<T> std::ops::Deref for Strict<T> {
167    type Target = T;
168    fn deref(&self) -> &T {
169        &self.0
170    }
171}
172
173impl<T> std::ops::DerefMut for Strict<T> {
174    fn deref_mut(&mut self) -> &mut T {
175        &mut self.0
176    }
177}
178
179impl<T> From<T> for Strict<T> {
180    fn from(v: T) -> Self {
181        Strict(v)
182    }
183}
184
185/// What [`Strict<T>`] accepts for a `T`: the Lua kind, read without conversion. Sealed;
186/// the integers, `f32` / `f64`, `bool` and `String`.
187pub trait StrictKind: Sized + private::Sealed {
188    /// The Teal type the `.d.tl` declares for `T`, for the error message.
189    const TEAL: &'static str;
190    /// `Some` when `v` is the kind `T` names, `None` for every other kind.
191    fn from_value(v: &mlua::Value) -> Option<Self>;
192}
193
194mod private {
195    pub trait Sealed {}
196}
197
198macro_rules! strict_int {
199    ($($t:ty),*) => {$(
200        impl private::Sealed for $t {}
201        impl StrictKind for $t {
202            const TEAL: &'static str = "integer";
203            fn from_value(v: &mlua::Value) -> Option<Self> {
204                match *v {
205                    mlua::Value::Integer(i) => <$t>::try_from(i).ok(),
206                    // What `math.tointeger` accepts: a float with no fraction. Lua's own
207                    // `3.0 == 3`, and a JSON decoder hands back a float for `3.0`.
208                    mlua::Value::Number(n) if n.fract() == 0.0 && n.is_finite() => {
209                        let i = n as i64;
210                        (i as f64 == n).then(|| <$t>::try_from(i).ok()).flatten()
211                    }
212                    _ => None,
213                }
214            }
215        }
216    )*};
217}
218strict_int!(
219    i8, i16, i32, i64, i128, isize, u8, u16, u32, u64, u128, usize
220);
221
222macro_rules! strict_float {
223    ($($t:ty),*) => {$(
224        impl private::Sealed for $t {}
225        impl StrictKind for $t {
226            const TEAL: &'static str = "number";
227            fn from_value(v: &mlua::Value) -> Option<Self> {
228                match *v {
229                    mlua::Value::Integer(i) => Some(i as $t),
230                    mlua::Value::Number(n) => Some(n as $t),
231                    _ => None,
232                }
233            }
234        }
235    )*};
236}
237strict_float!(f32, f64);
238
239impl private::Sealed for bool {}
240impl StrictKind for bool {
241    const TEAL: &'static str = "boolean";
242    fn from_value(v: &mlua::Value) -> Option<Self> {
243        match *v {
244            mlua::Value::Boolean(b) => Some(b),
245            _ => None,
246        }
247    }
248}
249
250impl private::Sealed for String {}
251impl StrictKind for String {
252    const TEAL: &'static str = "string";
253    fn from_value(v: &mlua::Value) -> Option<Self> {
254        match v {
255            mlua::Value::String(s) => s.to_str().ok().map(|s| s.to_owned()),
256            _ => None,
257        }
258    }
259}
260
261impl<T: StrictKind> mlua::FromLua for Strict<T> {
262    fn from_lua(value: mlua::Value, _lua: &mlua::Lua) -> mlua::Result<Self> {
263        T::from_value(&value)
264            .map(Strict)
265            .ok_or_else(|| mlua::Error::FromLuaConversionError {
266                from: value.type_name(),
267                to: T::TEAL.to_string(),
268                message: Some(format!(
269                    "a Strict parameter takes a Lua {} and converts nothing",
270                    T::TEAL
271                )),
272            })
273    }
274}
275
276impl<T: mlua::IntoLua> mlua::IntoLua for Strict<T> {
277    fn into_lua(self, lua: &mlua::Lua) -> mlua::Result<mlua::Value> {
278        self.0.into_lua(lua)
279    }
280}
281
282/// A Rust type exposed to Teal as a userdata module via `#[host_module]`.
283pub trait HostModule {
284    /// Module name used in `require("...")`.
285    const MODULE: &'static str;
286    /// Full `.d.tl` text for the module.
287    const DECL: &'static str;
288}