Skip to main content

tokel_engine/
session.rs

1//! The evaluation context for Tokel macro expansions.
2//!
3//! A [`Session`] represents a single, temporary run of the Tokel engine. It holds the
4//! [`Registry`] of available transformers and maintains their internal state throughout
5//! the bottom-up evaluation of an abstract syntax tree.
6//!
7//! Because transformers can be stateful (e.g., counters or accumulators), creating a
8//! fresh [`Session`] for each macro invocation ensures that state is cleanly isolated
9//! and automatically dropped when the expansion is complete.
10
11use crate::transform::Registry;
12
13/// The execution context for a Tokel expansion.
14///
15/// This struct holds the active [`Registry`] and is responsible for evaluating
16/// `TokelStream`s. By default, it is pre-loaded with Tokel's standard built-in
17/// transformers, but it can be customized or strictly sandboxed as needed.
18#[derive(Debug, Default)]
19pub struct Session(Registry);
20
21impl Session {
22    /// Creates a new [`Session`] populated with the default [`Registry`].
23    ///
24    /// This includes all standard Tokel transformers (e.g., `identity`, `case`).
25    /// This is the primary constructor used by standard `tokel::stream!` invocations.
26    #[inline]
27    #[must_use]
28    pub fn new() -> Self {
29        Self::new_with(Registry::default())
30    }
31
32    /// Creates a [`Session`] using a specific, pre-configured [`Registry`].
33    ///
34    /// This is useful if you want to initialize a registry, heavily customize it,
35    /// and then pass it into the session for execution.
36    #[inline]
37    #[must_use]
38    pub const fn new_with(registry: Registry) -> Self {
39        Self(registry)
40    }
41
42    /// Creates a completely blank [`Session`] with an empty [`Registry`].
43    ///
44    /// This session will have no built-in transformers available. It is useful
45    /// for creating strictly controlled, domain-specific token manipulation APIs
46    /// where users are only allowed to use custom transformers you explicitly provide.
47    #[inline]
48    #[must_use]
49    pub fn empty() -> Self {
50        Self(Registry::empty())
51    }
52
53    /// Borrows the active [`Registry`] for this [`Session`].
54    #[inline]
55    #[must_use]
56    pub const fn registry(&self) -> &Registry {
57        let Self(target_value) = self;
58
59        target_value
60    }
61
62    /// Mutably borrows the active [`Registry`] for this [`Session`].
63    ///
64    /// This allows you to register new custom transformers or extract state from
65    /// existing ones during or after the session's lifecycle.
66    #[inline]
67    #[must_use]
68    pub const fn registry_mut(&mut self) -> &mut Registry {
69        let Self(target_value) = self;
70
71        target_value
72    }
73}
74
75#[cfg(test)]
76mod tests {
77    use crate::prelude::*;
78    use proc_macro2::TokenStream;
79    use quote::quote;
80
81    /// A helper to assert that two token streams are syntactically identical,
82    /// ignoring raw whitespace differences generated by `to_string()`.
83    fn assert_streams_eq(actual: TokenStream, expected: TokenStream) {
84        let actual_str = actual.to_string().replace(" ", "");
85        let expected_str = expected.to_string().replace(" ", "");
86        assert_eq!(
87            actual_str,
88            expected_str,
89            "\nExpected: {}\nGot:      {}",
90            expected.to_string(),
91            actual.to_string()
92        );
93    }
94
95    /// Reverses the token stream.
96    #[derive(Debug)]
97    struct DummyReverse;
98
99    impl Transformer for DummyReverse {
100        fn transform(
101            &mut self,
102            input: TokenStream,
103            _args: TokenStream,
104        ) -> Result<TokenStream, syn::Error> {
105            let mut tokens: Vec<_> = input.into_iter().collect();
106            tokens.reverse();
107
108            Ok(tokens.into_iter().collect())
109        }
110    }
111
112    /// Appends the provided argument to the token stream.
113    #[derive(Debug)]
114    struct DummyAppend;
115    impl Transformer for DummyAppend {
116        fn transform(
117            &mut self,
118            mut input: TokenStream,
119            args: TokenStream,
120        ) -> Result<TokenStream, syn::Error> {
121            input.extend(args);
122            Ok(input)
123        }
124    }
125
126    /// A stateful counter that outputs the current count.
127    #[derive(Default, Debug)]
128    struct DummyCount(usize);
129    impl Transformer for DummyCount {
130        fn transform(
131            &mut self,
132            _input: TokenStream,
133            _args: TokenStream,
134        ) -> Result<TokenStream, syn::Error> {
135            let count = self.0;
136            self.0 += 1;
137            // Output the number as a raw token
138            let count_str = count.to_string();
139            let lit: proc_macro2::Literal = syn::parse_str(&count_str).unwrap();
140            Ok(quote!(#lit))
141        }
142    }
143
144    /// A transformer that deliberately returns an error if called.
145    #[derive(Debug)]
146    struct DummyError;
147    impl Transformer for DummyError {
148        fn transform(
149            &mut self,
150            input: TokenStream,
151            _args: TokenStream,
152        ) -> Result<TokenStream, syn::Error> {
153            // Find the first token to attach the error span to, or use call_site
154            let span = input
155                .into_iter()
156                .next()
157                .map(|t| t.span())
158                .unwrap_or_else(proc_macro2::Span::call_site);
159            Err(syn::Error::new(span, "Deliberate test error"))
160        }
161    }
162
163    #[test]
164    fn test_standard_rust_passthrough() {
165        let mut session = Session::empty();
166
167        let input = quote! {
168            pub fn process_data(data: &[u8]) -> Result<(), Error> {
169                let x = 10 + 20;
170                Ok(())
171            }
172        };
173
174        let ast: TokelStream = syn::parse2(input.clone()).unwrap();
175        let output = ast.expand(&mut session).expect("Failed to expand");
176
177        assert_streams_eq(output, input);
178    }
179
180    #[test]
181    fn test_identity_block_resolution() {
182        let mut session = Session::empty();
183
184        // An expansion block with no transformers attached should just dissolve the `< >`
185        let input = quote! {
186            let val = [< 10 + 20 >];
187        };
188
189        let ast: TokelStream = syn::parse2(input).unwrap();
190        let output = ast.expand(&mut session).unwrap();
191
192        let expected = quote! {
193            let val = 10 + 20;
194        };
195
196        assert_streams_eq(output, expected);
197    }
198
199    #[test]
200    fn test_single_transformer_with_args() {
201        let mut session = Session::empty();
202        session
203            .registry_mut()
204            .try_insert("append", DummyAppend)
205            .unwrap();
206
207        let input = quote! {
208            let word = [< hello >]:append[[ _world ]];
209        };
210
211        let ast: TokelStream = syn::parse2(input).unwrap();
212        let output = ast.expand(&mut session).unwrap();
213
214        let expected = quote! {
215            let word = hello _world;
216        };
217
218        assert_streams_eq(output, expected);
219    }
220
221    #[test]
222    fn test_pipeline_chaining() {
223        let mut session = Session::empty();
224        session
225            .registry_mut()
226            .try_insert("append", DummyAppend)
227            .unwrap();
228        session
229            .registry_mut()
230            .try_insert("reverse", DummyReverse)
231            .unwrap();
232
233        // Note the order of execution:
234        // 1. [< a b >] resolves to `a b`
235        // 2. :append[[ c ]] makes it `a b c`
236        // 3. :reverse makes it `c b a`
237        let input = quote! { [< a b >]:append[[c]]:reverse };
238
239        let ast: TokelStream = syn::parse2(input).unwrap();
240        let output = ast.expand(&mut session).unwrap();
241
242        let expected = quote! { c b a };
243        assert_streams_eq(output, expected);
244    }
245
246    #[test]
247    fn test_deep_bottom_up_nesting() {
248        let mut session = Session::empty();
249        session
250            .registry_mut()
251            .try_insert("append", DummyAppend)
252            .unwrap();
253        session
254            .registry_mut()
255            .try_insert("reverse", DummyReverse)
256            .unwrap();
257
258        // Inner block: [< a b >]:reverse -> `b a`
259        // Middle block: [< (b a) c >]:append[[d]] -> `b a c d`
260        // Outer block: reverse -> `d c a b`
261        let input = quote! {
262            [< [< [< a b >]:reverse c >]:append[[d]] >]:reverse
263        };
264
265        let ast: TokelStream = syn::parse2(input).unwrap();
266        let output = ast.expand(&mut session).unwrap();
267
268        let expected = quote! { d c a b };
269        assert_streams_eq(output, expected);
270    }
271
272    #[test]
273    fn test_stateful_session_isolation() {
274        let mut session = Session::empty();
275        session
276            .registry_mut()
277            .try_insert("count", DummyCount::default())
278            .unwrap();
279
280        // Each invocation of :count should increment the internal state.
281        // The order of bottom-up evaluation determines the assignment!
282        let input = quote! {
283            let first = [< >]:count;
284            let second = [< >]:count;
285            let inner_wins = [< [< >]:count >]:count;
286        };
287
288        let ast: TokelStream = syn::parse2(input).unwrap();
289        let output = ast.expand(&mut session).unwrap();
290
291        // first = 0
292        // second = 1
293        // inner_wins evaluates the inner block first (2), then the outer block replaces it with (3).
294        let expected = quote! {
295            let first = 0;
296            let second = 1;
297            let inner_wins = 3;
298        };
299
300        assert_streams_eq(output, expected);
301    }
302
303    #[test]
304    fn test_transformer_error_propagation() {
305        let mut session = Session::empty();
306        session
307            .registry_mut()
308            .try_insert("error_out", DummyError)
309            .unwrap();
310
311        let input = quote! {
312            fn good() {}
313            [< bad_tokens >]:error_out
314        };
315
316        let ast: TokelStream = syn::parse2(input).unwrap();
317        let result = ast.expand(&mut session);
318
319        assert!(
320            result.is_err(),
321            "Engine failed to bubble up the transformer error"
322        );
323        assert_eq!(result.unwrap_err().to_string(), "Deliberate test error");
324    }
325
326    #[test]
327    fn test_unknown_transformer_error() {
328        let mut session = Session::empty();
329        // Registry is intentionally empty
330
331        let input = quote! { [< x >]:non_existent_transformer };
332
333        let ast: TokelStream = syn::parse2(input).unwrap();
334        let result = ast.expand(&mut session);
335
336        assert!(result.is_err());
337        let err_msg = result.unwrap_err().to_string();
338        assert!(
339            err_msg.contains("non_existent_transformer"),
340            "Error message should mention the missing transformer"
341        );
342    }
343
344    #[test]
345    fn test_nested_arguments() {
346        let mut session = Session::empty();
347        session
348            .registry_mut()
349            .try_insert("append", DummyAppend)
350            .unwrap();
351        session
352            .registry_mut()
353            .try_insert("reverse", DummyReverse)
354            .unwrap();
355
356        // Testing that the `[[ ... ]]` arguments are ALSO evaluated as a TokelStream!
357        // The argument is `[< 1 2 >]:reverse`, which evaluates to `2 1`.
358        // The main block `A B` then appends `2 1`.
359        let input = quote! {
360            [< A B >]:append[[ [< 1 2 >]:reverse ]]
361        };
362
363        let ast: TokelStream = syn::parse2(input).unwrap();
364        let output = ast.expand(&mut session).unwrap();
365
366        let expected = quote! { A B 2 1 };
367        assert_streams_eq(output, expected);
368    }
369}