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}