Skip to main content

wolfram_serialize_macros/
lib.rs

1//! Procedural macros for `wolfram-serializer`.
2//!
3//! Provides `#[derive(ToWXF)]` and `#[derive(FromWXF)]` for structs (named,
4//! tuple, unit) and enums. Field-level type pattern matching emits the correct
5//! WXF representation for `Vec<u8>` (ByteArray), `Vec<numeric>` and rectangular
6//! nested tuples / fixed-size arrays of numerics (NumericArray), while
7//! everything else delegates through the `ToWXF` / `FromWXF` traits.
8//!
9//! See the `wolfram-serializer` crate docs for usage and the wire-format
10//! conventions emitted here.
11
12#![allow(clippy::needless_doctest_main)]
13
14use proc_macro::TokenStream;
15use syn::{parse_macro_input, DeriveInput};
16
17mod deserialize;
18mod failure_derive;
19mod serialize;
20mod shared;
21mod ty_classify;
22
23/// Derive `ToWXF` for a struct or enum.
24///
25/// # Structs
26///
27/// Named-field structs encode as a WL `Association` (`<|…|>`). Field names are
28/// converted to camelCase by default:
29///
30/// ```
31/// use wolfram_serialize::{ToWXF, to_wxf};
32///
33/// #[derive(ToWXF)]
34/// struct Point {
35///     x: f64,   // → "x" key
36///     y: f64,   // → "y" key
37/// }
38/// // Encodes as <|"x" -> 1.0, "y" -> 2.0|>
39/// let bytes = to_wxf(&Point { x: 1.0, y: 2.0 }, None).unwrap();
40/// ```
41///
42/// Tuple structs encode as a WL `List`:
43///
44/// ```
45/// use wolfram_serialize::{ToWXF, to_wxf};
46///
47/// #[derive(ToWXF)]
48/// struct Pair(i64, i64);
49/// // Encodes as {1, 2}
50/// let bytes = to_wxf(&Pair(1, 2), None).unwrap();
51/// ```
52///
53/// # Enums
54///
55/// Enum variants encode as `<|"Enum" -> "VariantName", "Data" -> {fields…}|>`.
56/// Unit variants omit the `"Data"` key:
57///
58/// ```
59/// use wolfram_serialize::{ToWXF, to_wxf};
60///
61/// #[derive(ToWXF)]
62/// enum Color {
63///     Red,
64///     Rgb(u8, u8, u8),
65/// }
66/// // Red   → <|"Enum" -> "Red"|>
67/// // Rgb   → <|"Enum" -> "Rgb", "Data" -> {255, 0, 0}|>
68/// let _ = to_wxf(&Color::Red, None).unwrap();
69/// let _ = to_wxf(&Color::Rgb(255, 0, 0), None).unwrap();
70/// ```
71///
72/// # Special field types
73///
74/// | Rust field type | WL wire encoding |
75/// |----------------|-----------------|
76/// | `Vec<u8>` | `ByteArray[…]` |
77/// | `Vec<f64>` / `Vec<i64>` / … | `NumericArray[…, "Real64"]` / `"Integer64"` / … |
78/// | `Vec<T: WxfStruct>` | `{…}` (List of Associations) |
79/// | `Option<T>` | `<\|"Enum" -> "Some"/"None", "Data" -> {v}\|>` |
80///
81/// # Field attributes
82///
83/// ```
84/// use wolfram_serialize::ToWXF;
85///
86/// #[derive(ToWXF)]
87/// struct Config {
88///     #[wolfram(rename = "MaxCount")]
89///     max_count: i64,
90/// }
91/// // Encodes the field as "MaxCount" instead of "maxCount"
92/// ```
93#[proc_macro_derive(ToWXF, attributes(wolfram))]
94pub fn derive_to_wxf(input: TokenStream) -> TokenStream {
95    let input = parse_macro_input!(input as DeriveInput);
96    serialize::expand(&input)
97        .unwrap_or_else(|err| err.to_compile_error())
98        .into()
99}
100
101/// Derive `FromWXF` for a struct or enum.
102///
103/// The lifetime parameter `'de` is the input buffer lifetime. Owned types
104/// (no reference fields) work for any `'de`; structs with `&'de str` or
105/// `&'de [u8]` fields borrow zero-copy from the input buffer.
106///
107/// # Structs
108///
109/// Named-field structs decode from a WL `Association`. Missing `Option<T>`
110/// fields default to `None`; all other fields must be present.
111///
112/// ```
113/// use wolfram_serialize::{ToWXF, FromWXF, to_wxf, from_wxf};
114///
115/// #[derive(ToWXF, FromWXF, PartialEq, Debug)]
116/// struct Point {
117///     x: f64,
118///     y: f64,
119/// }
120///
121/// let bytes = to_wxf(&Point { x: 1.0, y: 2.0 }, None).unwrap();
122/// let p: Point = from_wxf(&bytes).unwrap();
123/// assert_eq!(p, Point { x: 1.0, y: 2.0 });
124/// ```
125///
126/// # Zero-copy borrowed fields
127///
128/// Struct fields of type `&'de str` or `&'de [u8]` borrow directly from the
129/// input buffer — no heap allocation for the string data. Because the borrow
130/// is tied to the input, read them inside a `read_wxf` closure rather than
131/// returning them:
132///
133/// ```
134/// use wolfram_serialize::{ToWXF, FromWXF, to_wxf, read_wxf};
135///
136/// #[derive(ToWXF)]
137/// struct Owned { name: String }
138///
139/// #[derive(FromWXF)]
140/// struct Borrowed<'a> { name: &'a str }
141///
142/// let bytes = to_wxf(&Owned { name: "hello".into() }, None).unwrap();
143/// read_wxf(&bytes, |r| {
144///     let b = Borrowed::from_wxf(r)?;
145///     assert_eq!(b.name, "hello");  // points into `bytes`, no alloc
146///     Ok(())
147/// }).unwrap();
148/// ```
149///
150/// # Enums
151///
152/// Enums decode from `<|"Enum" -> "VariantName", "Data" -> {…}|>` (the same
153/// shape `ToWXF` emits):
154///
155/// ```
156/// use wolfram_serialize::{ToWXF, FromWXF, to_wxf, from_wxf};
157///
158/// #[derive(ToWXF, FromWXF, PartialEq, Debug)]
159/// enum Status {
160///     Ok,
161///     Err(String),
162/// }
163///
164/// let bytes = to_wxf(&Status::Err("oops".into()), None).unwrap();
165/// let s: Status = from_wxf(&bytes).unwrap();
166/// assert_eq!(s, Status::Err("oops".into()));
167/// ```
168///
169/// # Numeric widening
170///
171/// Integer and real fields accept wider WXF types: an `i32` field will accept
172/// a WXF `Integer64`, and an `f32` field will accept a WXF `Real64`.
173#[proc_macro_derive(FromWXF, attributes(wolfram))]
174pub fn derive_from_wxf(input: TokenStream) -> TokenStream {
175    let input = parse_macro_input!(input as DeriveInput);
176    deserialize::expand(&input)
177        .unwrap_or_else(|err| err.to_compile_error())
178        .into()
179}
180
181/// Derive `From<YourEnum> for Expr`, mapping each variant to a Wolfram
182/// [`Failure`](https://reference.wolfram.com/language/ref/Failure.html)
183/// expression.
184///
185/// # Wire format
186///
187/// Each enum variant becomes:
188///
189/// | Rust variant | WL expression |
190/// |---|---|
191/// | `Unit` | `Failure["Unit", <\|\|>]` |
192/// | `WithMessage(String)` | `Failure["WithMessage", <\|"0" -> "…"\|>]` |
193/// | `Named { code: i64 }` | `Failure["Named", <\|"code" -> …\|>]` |
194///
195/// # Idiomatic pattern: one error enum per exported library
196///
197/// Define a single error enum for your library, derive `Failure` on it, and
198/// return `Result<T, YourError>` from every `#[export(wxf)]` function. The
199/// macro wires the `Err` path automatically — Rust's `?` operator propagates
200/// errors from helpers, and the kernel always gets a properly structured
201/// `Failure[…]` it can pattern-match on.
202///
203/// ```ignore
204/// # mod scope {
205/// use wolfram_export::export;
206/// use wolfram_serialize::{Failure, ToWXF, FromWXF};
207///
208/// /// All errors this library can return to Wolfram.
209/// #[derive(Failure, Debug)]
210/// enum LibError {
211///     /// Failure["KeyNotFound", <|"key" -> "…"|>]
212///     KeyNotFound { key: String },
213///     /// Failure["ParseError", <|"input" -> "…", "reason" -> "…"|>]
214///     ParseError { input: String, reason: String },
215///     /// Failure["OutOfRange", <|"value" -> …, "min" -> …, "max" -> …|>]
216///     OutOfRange { value: f64, min: f64, max: f64 },
217///     /// Failure["Unsupported", <||>]
218///     Unsupported,
219/// }
220///
221/// // Helper that returns a domain error — ? propagates it automatically.
222/// fn lookup(map: &std::collections::HashMap<String, f64>, key: &str)
223///     -> Result<f64, LibError>
224/// {
225///     map.get(key)
226///        .copied()
227///        .ok_or_else(|| LibError::KeyNotFound { key: key.into() })
228/// }
229///
230/// #[derive(ToWXF, FromWXF)]
231/// struct Stats { mean: f64, count: i64 }
232///
233/// // Wolfram calls: computeStats[<|"a" -> 1.0, "b" -> 2.0|>, "a"]
234/// // On success returns Stats as an Association.
235/// // On failure returns Failure["KeyNotFound", <|"key" -> "a"|>] etc.
236/// #[export(wxf)]
237/// fn compute_stats(
238///     data: std::collections::HashMap<String, f64>,
239///     key: String,
240/// ) -> Result<Stats, LibError> {
241///     let value = lookup(&data, &key)?;   // propagates KeyNotFound
242///     if !(0.0..=1e9).contains(&value) {
243///         return Err(LibError::OutOfRange { value, min: 0.0, max: 1e9 });
244///     }
245///     Ok(Stats { mean: value, count: data.len() as i64 })
246/// }
247/// # }
248/// ```
249///
250/// # Handling errors on the Wolfram side
251///
252/// The kernel receives the `Failure` object. Standard WL idioms work directly:
253///
254/// ```wolfram
255/// result = computeStats[data, "missingKey"];
256///
257/// (* Check for failure *)
258/// FailureQ[result]   (* True *)
259///
260/// (* Pattern match on the specific error type *)
261/// Switch[result,
262///   Failure["KeyNotFound", assoc_],
263///     Print["Key not found: ", assoc["key"]],
264///   Failure["OutOfRange", assoc_],
265///     Print["Value ", assoc["value"], " outside [", assoc["min"], ", ", assoc["max"], "]"],
266///   _,
267///     Print["Unexpected error: ", result]
268/// ]
269///
270/// (* Or use Quiet + Check for simple fallback logic *)
271/// value = Check[computeStats[data, key], $Failed];
272/// ```
273///
274/// # Combining with `std::error::Error`
275///
276/// Derive both `Failure` and `thiserror::Error` to get a type that works as a
277/// proper Rust error (for `?` chains, logging, tests) *and* converts cleanly
278/// to WL when returned across the FFI boundary:
279///
280/// ```
281/// # mod scope {
282/// use wolfram_serialize::Failure;
283///
284/// #[derive(Failure, Debug, Clone, thiserror::Error)]
285/// enum DbError {
286///     #[error("connection refused: {addr}")]
287///     ConnectionRefused { addr: String },
288///     #[error("query timeout after {ms}ms")]
289///     Timeout { ms: i64 },
290/// }
291/// # }
292/// ```
293#[proc_macro_derive(Failure, attributes(wolfram))]
294pub fn derive_failure(input: TokenStream) -> TokenStream {
295    let input = parse_macro_input!(input as DeriveInput);
296    failure_derive::expand(&input)
297        .unwrap_or_else(|err| err.to_compile_error())
298        .into()
299}