Skip to main content

tachyonfx/dsl/
mod.rs

1mod arguments;
2mod completions;
3#[allow(clippy::module_inception)]
4mod dsl;
5pub(crate) mod dsl_format;
6mod dsl_writer;
7mod environment;
8mod expr_promotion;
9mod expressions;
10mod method_chains;
11mod parse_error;
12mod token_parsers;
13mod token_verification;
14mod tokenizer;
15
16use alloc::fmt;
17
18pub use arguments::Arguments;
19use compact_str::CompactString;
20pub use completions::{CompletionEngine, CompletionItem, CompletionKind};
21pub use dsl::{DslCompiler, EffectDsl};
22pub use dsl_format::DslFormat;
23use dsl_writer::DslWriter;
24pub use parse_error::DslParseError;
25
26use crate::dsl::{
27    expressions::{Expr, ExprSpan},
28    token_parsers::parse_ast,
29    tokenizer::{sanitize_tokens, tokenize},
30};
31
32/// Errors that can occur during DSL parsing, compilation, or expression conversion.
33#[derive(Debug, thiserror::Error, PartialEq)]
34pub enum DslError {
35    /// Lexical analysis failed at the given position.
36    #[error("Failed to tokenize the input at position {location}. Check for invalid characters or syntax.")]
37    TokenizationError {
38        /// Source location of the invalid token.
39        location: ExprSpan,
40    },
41
42    /// Expression parsing failed due to unexpected tokens or invalid syntax.
43    #[error("Failed to parse expression at position {location}. This could be due to unexpected tokens or invalid syntax.")]
44    TokenParseError {
45        /// Source location of the parse error.
46        location: ExprSpan,
47    },
48
49    /// An internal parser error that should not normally occur.
50    #[error("An unexpected error occurred during parsing. Please report this as a bug with your code sample.")]
51    OhNoError,
52
53    /// The referenced effect name is not registered.
54    #[error("Unknown effect '{name}'. Check the effect name or register the effect with EffectDsl::register.")]
55    UnknownEffect {
56        /// The unrecognized effect name.
57        name: CompactString,
58        /// Source location of the reference.
59        location: ExprSpan,
60    },
61
62    /// A referenced variable was not declared.
63    #[error(
64        "Variable '{name}' not found. Make sure it's declared before use with 'let {name} = ...'."
65    )]
66    UnknownArgument {
67        /// The unresolved variable name.
68        name: CompactString,
69        /// Source location of the reference.
70        location: ExprSpan,
71    },
72
73    /// A variable exists but does not match the expected type.
74    #[error("Cannot find variable '{name}' of type {expected}.")]
75    NoSuchVariable {
76        /// The variable name.
77        name: CompactString,
78        /// The type that was expected.
79        expected: &'static str,
80        /// Source location of the reference.
81        location: ExprSpan,
82    },
83
84    /// A required positional argument is missing.
85    #[error("Missing required argument '{name}' at position {position}. This function requires more arguments.")]
86    MissingArgument {
87        /// 1-based position of the missing argument.
88        position: usize,
89        /// Name or type of the expected argument.
90        name: &'static str,
91        /// Source location of the call.
92        location: ExprSpan,
93    },
94
95    /// The referenced function name is not recognized.
96    #[error("Unknown function '{name}'. Check the function name or import the required module.")]
97    UnknownFunction {
98        /// The unrecognized function name.
99        name: CompactString,
100        /// Source location of the call.
101        location: ExprSpan,
102    },
103
104    /// The referenced struct name is not recognized.
105    #[error("Unknown struct '{name}'. Check the struct name or import the required module.")]
106    UnknownStruct {
107        /// The unrecognized struct name.
108        name: CompactString,
109        /// Source location of the reference.
110        location: ExprSpan,
111    },
112
113    /// A struct field name is not valid for the given struct.
114    #[error("Unknown field '{field}' in struct '{struct_name}'. Valid fields are {valid_fields}.")]
115    UnknownField {
116        /// The struct being initialized.
117        struct_name: CompactString,
118        /// The unrecognized field name.
119        field: CompactString,
120        /// Source location of the field.
121        location: ExprSpan,
122        /// Comma-separated list of valid field names.
123        valid_fields: CompactString,
124    },
125
126    /// A required struct field was not provided.
127    #[error("Missing required field '{field}' in struct '{struct_name}'.")]
128    MissingField {
129        /// The struct being initialized.
130        struct_name: CompactString,
131        /// The missing field name.
132        field: &'static str,
133        /// Source location of the struct init expression.
134        location: ExprSpan,
135    },
136
137    /// The number of arguments does not match the expected count.
138    #[error("Invalid number of arguments. Expected {expected}, got {actual}.")]
139    InvalidArgumentLength {
140        /// Expected argument count.
141        expected: usize,
142        /// Actual argument count.
143        actual: usize,
144        /// Source location of the call.
145        location: ExprSpan,
146    },
147
148    /// An expression has the wrong type or form.
149    #[error("Invalid expression. Expected {expected}, but found {actual}.")]
150    InvalidExpression {
151        /// The type or form that was expected.
152        expected: &'static str,
153        /// The type or form that was found.
154        actual: &'static str,
155        /// Source location of the expression.
156        location: ExprSpan,
157    },
158
159    /// An opening or closing bracket has no matching counterpart.
160    #[error("Unmatched {bracket_type} '{bracket}'.")]
161    BracketMismatch {
162        /// The unmatched bracket character.
163        bracket: char,
164        /// Source location of the bracket.
165        location: ExprSpan,
166        /// Whether this is an "opening" or "closing" bracket.
167        bracket_type: &'static str,
168    },
169
170    /// A let statement is missing a terminating semicolon.
171    #[error("Missing semicolon after let statement. Add a ';' to terminate the statement.")]
172    MissingSemicolon {
173        /// Source location where the semicolon was expected.
174        location: ExprSpan,
175    },
176
177    /// A comma separator is missing between list elements.
178    #[error("Missing comma between elements. Add a ',' to separate items in the list.")]
179    MissingComma {
180        /// Source location where the comma was expected.
181        location: ExprSpan,
182    },
183
184    /// A general syntax error with a descriptive message.
185    #[error("{message}")]
186    SyntaxError {
187        /// Description of the syntax error.
188        message: CompactString,
189        /// Source location of the error.
190        location: ExprSpan,
191    },
192
193    /// A numeric value overflows when casting to a narrower type.
194    #[error("Value cannot be converted from {from} to {to}. The number is out of range for the target type.")]
195    CastOverflow {
196        /// The source numeric type.
197        from: &'static str,
198        /// The target numeric type.
199        to: &'static str,
200        /// Source location of the value.
201        location: ExprSpan,
202    },
203
204    /// An argument has the wrong type.
205    #[error("Type mismatch: expected '{expected}' but found '{actual}'.")]
206    WrongArgumentType {
207        /// The type that was expected.
208        expected: &'static str,
209        /// The type that was provided.
210        actual: CompactString,
211        /// Source location of the argument.
212        location: ExprSpan,
213    },
214
215    /// More arguments were provided than the function accepts.
216    #[error("Too many arguments for function '{name}'. Expected {count} arguments.")]
217    TooManyArguments {
218        /// The function name.
219        name: CompactString,
220        /// The maximum number of arguments expected.
221        count: usize,
222        /// Source location of the excess arguments.
223        location: ExprSpan,
224    },
225
226    /// The effect cannot be serialized to DSL format.
227    #[error("The effect '{name}' cannot be converted to DSL format.")]
228    EffectExpressionNotSupported {
229        /// The effect name.
230        name: &'static str,
231    },
232
233    /// The effect is recognized but cannot be compiled from the DSL.
234    #[error("The effect '{name}' can not be instantiated by the DSL.")]
235    UnsupportedEffect {
236        /// The effect name.
237        name: CompactString,
238        // Consider adding: similar_effects: Vec<CompactString>,
239    },
240
241    /// An array literal has the wrong number of elements.
242    #[error("Array has incorrect length. Expected {expected} elements, got {actual}.")]
243    ArrayLengthMismatch {
244        /// Expected element count.
245        expected: usize,
246        /// Actual element count.
247        actual: usize,
248        /// Source location of the array.
249        location: ExprSpan,
250    },
251
252    /// The referenced cell filter variant is not recognized.
253    #[error("Invalid cell filter: '{name}'. Valid cell filters include CellFilter::Text, CellFilter::All, etc.")]
254    UnknownCellFilter {
255        /// The unrecognized cell filter name.
256        name: CompactString,
257        /// Source location of the reference.
258        location: ExprSpan,
259    },
260}
261
262/// A parsed representation of a tachyonfx effect expression.
263///
264/// `EffectExpression` provides a way to parse and represent effect descriptions in string
265/// form. This allows effects to be defined using a domain-specific language (DSL) syntax
266/// and later converted into actual effect instances.
267///
268/// # Examples
269///
270/// ```
271/// use tachyonfx::dsl::EffectExpression;
272///
273/// // Parse a simple fade effect
274/// let expr = EffectExpression::parse("fx::fade_to(Color::from_u32(0), (500, Linear))").unwrap();
275///
276/// // Parse a more complex effect chain
277/// let expr = EffectExpression::parse(r#"
278///     fx::sequence(&[
279///         fx::fade_from(Color::Black, Color::from_u32(0), (1000, QuadOut)),
280///         fx::dissolve((500, BounceOut))
281///     ])
282/// "#);
283/// ```
284///
285/// # See Also
286///
287/// - [`Shader::to_dsl`](crate::Shader::to_dsl) for converting a shader to a DSL
288///   expression
289/// - [`DslError`] for possible error types
290pub struct EffectExpression {
291    expr: Vec<Expr>,
292}
293
294impl EffectExpression {
295    /// Parses a string into an `EffectExpression`.
296    ///
297    /// This method takes a string containing a tachyonfx effect description and attempts
298    /// to parse it into a structured `EffectExpression`. The input string should follow
299    /// the tachyonfx DSL syntax.
300    ///
301    /// # Arguments
302    ///
303    /// * `input` - A string slice containing the effect expression to parse
304    ///
305    /// # Returns
306    ///
307    /// Returns a `Result` containing either:
308    /// - `Ok(EffectExpression)` if parsing was successful
309    /// - `Err(DslError)` if the input could not be parsed
310    ///
311    /// # Errors
312    ///
313    /// Returns [`DslError`] if tokenization or AST parsing fails.
314    pub fn parse(input: &str) -> Result<Self, DslError> {
315        let expr = tokenize(input)
316            .map(sanitize_tokens)
317            .and_then(parse_ast)?;
318
319        Ok(Self { expr })
320    }
321}
322
323impl fmt::Display for EffectExpression {
324    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
325        let dsl = self
326            .expr
327            .iter()
328            .map(DslWriter::format)
329            .collect::<Vec<_>>()
330            .join("\n");
331
332        write!(f, "{dsl}")
333    }
334}
335
336#[cfg(test)]
337mod tests {
338    use indoc::indoc;
339
340    use crate::{fx, fx::RepeatMode};
341
342    #[test]
343    fn to_dsl_format_complex_tree() {
344        let expected = indoc! {
345            "fx::sequence(&[
346                fx::dissolve(EffectTimer::from_ms(100, Interpolation::Linear)),
347                fx::parallel(&[
348                    fx::dissolve(EffectTimer::from_ms(200, Interpolation::Linear)),
349                    fx::dissolve(EffectTimer::from_ms(300, Interpolation::Linear)),
350                    fx::sleep(400)
351                ]),
352                fx::repeat(
353                    fx::dissolve(EffectTimer::from_ms(500, Interpolation::Linear)),
354                    RepeatMode::Forever
355                )
356            ])"
357        };
358
359        let expr = fx::sequence(&[
360            fx::dissolve(100),
361            fx::parallel(&[fx::dissolve(200), fx::dissolve(300), fx::sleep(400)]),
362            fx::repeat(fx::dissolve(500), RepeatMode::Forever),
363        ])
364        .to_dsl()
365        .expect("dsl expression from effect");
366
367        assert_eq!(expr.to_string(), expected);
368    }
369}