devalang_wasm/language/syntax/parser/driver/
mod.rs

1pub mod directive;
2pub mod duration;
3pub mod effects;
4pub mod helpers;
5pub mod preprocessing;
6pub mod routing;
7pub mod statements;
8pub mod trigger;
9// Re-export statement-level parsers so they are available at driver root
10use crate::language::syntax::ast::nodes::{Statement, StatementKind, Value};
11use anyhow::{Result, anyhow};
12pub use statements::*;
13use std::path::{Path, PathBuf};
14
15/// Find the closest keyword suggestion using Levenshtein distance
16/// Returns the suggestion if distance is <= 2 (typo-like)
17fn find_keyword_suggestion(input: &str, keywords: &[&str]) -> Option<String> {
18    // Calculate Levenshtein distance between two strings
19    fn levenshtein(s1: &str, s2: &str) -> usize {
20        let len1 = s1.len();
21        let len2 = s2.len();
22        let mut matrix = vec![vec![0; len2 + 1]; len1 + 1];
23
24        for i in 0..=len1 {
25            matrix[i][0] = i;
26        }
27        for j in 0..=len2 {
28            matrix[0][j] = j;
29        }
30
31        for (i, c1) in s1.chars().enumerate() {
32            for (j, c2) in s2.chars().enumerate() {
33                let cost = if c1 == c2 { 0 } else { 1 };
34                matrix[i + 1][j + 1] = std::cmp::min(
35                    std::cmp::min(
36                        matrix[i][j + 1] + 1, // deletion
37                        matrix[i + 1][j] + 1, // insertion
38                    ),
39                    matrix[i][j] + cost, // substitution
40                );
41            }
42        }
43        matrix[len1][len2]
44    }
45
46    let mut best = (usize::MAX, "");
47    for &keyword in keywords {
48        let distance = levenshtein(input, keyword);
49        if distance < best.0 && distance <= 2 {
50            best = (distance, keyword);
51        }
52    }
53
54    if best.0 <= 2 {
55        Some(best.1.to_string())
56    } else {
57        None
58    }
59}
60
61/// Entry point: parse source into a list of Statements
62pub fn parse(source: &str, path: PathBuf) -> Result<Vec<Statement>> {
63    // Pre-process: merge ALL multiline statements with braces
64    let braces_pre = preprocessing::preprocess_multiline_braces(source);
65
66    // Then merge multiline arrow calls (without braces)
67    let preprocessed = preprocessing::preprocess_multiline_arrow_calls(&braces_pre);
68
69    let lines: Vec<_> = preprocessed.lines().collect();
70    parse_lines(&lines, 0, lines.len(), 0, &path)
71}
72
73/// Parse a range of lines into statements, handling indentation for blocks.
74fn parse_lines(
75    lines: &Vec<&str>,
76    start: usize,
77    end: usize,
78    indent: usize,
79    path: &Path,
80) -> Result<Vec<Statement>> {
81    use crate::language::syntax::ast::nodes::StatementKind;
82
83    let mut i = start;
84    let mut statements: Vec<Statement> = Vec::new();
85
86    while i < end {
87        let raw = lines[i];
88        let trimmed = raw.trim();
89
90        if trimmed.is_empty() || trimmed.starts_with('#') {
91            i += 1;
92            continue;
93        }
94
95        let current_indent = raw.len() - raw.trim_start().len();
96        if current_indent < indent {
97            break;
98        }
99
100        // parse header line
101        let mut statement = parse_line(trimmed, i + 1, path)?;
102        statement.indent = current_indent;
103        statement.line = i + 1;
104
105        // determine body range for block statements
106        let body_start = i + 1;
107        let mut body_end = body_start;
108        while body_end < end {
109            let l = lines[body_end];
110            if l.trim().is_empty() || l.trim().starts_with('#') {
111                body_end += 1;
112                continue;
113            }
114            let indent_l = l.len() - l.trim_start().len();
115            if indent_l <= current_indent {
116                break;
117            }
118            body_end += 1;
119        }
120
121        // If we found a body, parse it and attach appropriately based on kind
122        if body_end > body_start {
123            let body = parse_lines(lines, body_start, body_end, current_indent + 1, path)?;
124
125            // To avoid borrowing `statement.kind` and then assigning to it
126            // (which the borrow checker forbids), take ownership of the kind
127            // temporarily and then replace it with the updated version.
128            let orig_kind = std::mem::replace(&mut statement.kind, StatementKind::Unknown);
129            match orig_kind {
130                StatementKind::If { condition, .. } => {
131                    // Keep If simple here; attach else/else-if later in post-processing
132                    statement.kind = StatementKind::If {
133                        condition,
134                        body: body.clone(),
135                        else_body: None,
136                    };
137                }
138                StatementKind::For {
139                    variable, iterable, ..
140                } => {
141                    statement.kind = StatementKind::For {
142                        variable,
143                        iterable,
144                        body: body.clone(),
145                    };
146                }
147                StatementKind::Loop { count, .. } => {
148                    statement.kind = StatementKind::Loop {
149                        count,
150                        body: body.clone(),
151                    };
152                }
153                StatementKind::On { event, args, .. } => {
154                    statement.kind = StatementKind::On {
155                        event,
156                        args,
157                        body: body.clone(),
158                    };
159                }
160                StatementKind::Automate { target } => {
161                    statement.kind = StatementKind::Automate { target };
162                    // store raw body as value map if needed elsewhere
163                    let raw_lines: Vec<String> = lines[body_start..body_end]
164                        .iter()
165                        .map(|s| s.to_string())
166                        .collect();
167                    let raw_body = raw_lines.join("\n");
168                    let mut map = std::collections::HashMap::new();
169                    map.insert("body".to_string(), Value::String(raw_body));
170                    statement.value = Value::Map(map);
171                }
172                StatementKind::Function {
173                    name, parameters, ..
174                } => {
175                    statement.kind = StatementKind::Function {
176                        name: name.clone(),
177                        parameters,
178                        body: body.clone(),
179                    };
180                    statement.value = Value::Identifier(name.clone());
181                }
182                StatementKind::Group { .. } => {
183                    // attach body for groups, keep name in value if present
184                    let group_name = match &statement.value {
185                        Value::Identifier(s) => s.clone(),
186                        _ => "".to_string(),
187                    };
188                    statement.kind = StatementKind::Group {
189                        name: group_name,
190                        body: body.clone(),
191                    };
192                }
193                StatementKind::Routing { .. } => {
194                    // Parse routing body statements
195                    statement.kind = StatementKind::Routing { body: body.clone() };
196                }
197                StatementKind::Tempo {
198                    value,
199                    body: Some(_),
200                } => {
201                    // Attach body to tempo block
202                    statement.kind = StatementKind::Tempo {
203                        value,
204                        body: Some(body.clone()),
205                    };
206                }
207                other => {
208                    // keep original kind for other statements
209                    statement.kind = other;
210                }
211            }
212
213            // If this statement was a plain 'else' marker (Comment with value "else"),
214            // the parsed body was not attached to the marker (we intentionally
215            // keep the marker lightweight). We must append the parsed body
216            // statements into the parent statements vector so the post-process
217            // pass can collect them and attach to the previous If.
218            i = body_end;
219            statements.push(statement);
220
221            if let StatementKind::Comment = &statements.last().unwrap().kind {
222                if let Value::String(s) = &statements.last().unwrap().value {
223                    if s == "else" {
224                        // append the body statements after the marker
225                        for stmt in body.into_iter() {
226                            statements.push(stmt);
227                        }
228                    }
229                }
230            }
231            continue;
232        }
233
234        // No body found
235        statements.push(statement);
236        i += 1;
237    }
238
239    // Post-process to attach else/else-if blocks
240    fn attach_else_blocks(statements: &mut Vec<Statement>) {
241        use crate::language::syntax::ast::StatementKind;
242
243        // Helper: attach a new else-body (Vec<Statement>) to an existing If Statement.
244        // If the If already has a nested else-if chain (encoded as a single If in
245        // its else_body vector), descend into that chain and attach to the deepest
246        // nested If instead of overwriting the entire else_body.
247        fn attach_else_to_if_statement(target: Statement, new_else: Vec<Statement>) -> Statement {
248            use crate::language::syntax::ast::StatementKind;
249
250            match target.kind {
251                StatementKind::If {
252                    condition,
253                    body,
254                    else_body,
255                } => {
256                    match else_body {
257                        None => Statement::new(
258                            StatementKind::If {
259                                condition,
260                                body,
261                                else_body: Some(new_else),
262                            },
263                            target.value,
264                            target.indent,
265                            target.line,
266                            target.column,
267                        ),
268                        Some(mut eb) => {
269                            if eb.len() == 1 {
270                                let inner = eb.remove(0);
271                                // If inner is an If, recurse into it
272                                if let StatementKind::If { .. } = inner.kind {
273                                    let updated_inner =
274                                        attach_else_to_if_statement(inner, new_else);
275                                    Statement::new(
276                                        StatementKind::If {
277                                            condition,
278                                            body,
279                                            else_body: Some(vec![updated_inner]),
280                                        },
281                                        target.value,
282                                        target.indent,
283                                        target.line,
284                                        target.column,
285                                    )
286                                } else {
287                                    // Not an If inside else_body; overwrite
288                                    Statement::new(
289                                        StatementKind::If {
290                                            condition,
291                                            body,
292                                            else_body: Some(new_else),
293                                        },
294                                        target.value,
295                                        target.indent,
296                                        target.line,
297                                        target.column,
298                                    )
299                                }
300                            } else {
301                                // Multiple statements in existing else_body; overwrite for now
302                                Statement::new(
303                                    StatementKind::If {
304                                        condition,
305                                        body,
306                                        else_body: Some(new_else),
307                                    },
308                                    target.value,
309                                    target.indent,
310                                    target.line,
311                                    target.column,
312                                )
313                            }
314                        }
315                    }
316                }
317                _ => target,
318            }
319        }
320
321        let mut idx = 0;
322        while idx < statements.len() {
323            // Handle plain 'else' marker: Comment with value "else"
324            if let StatementKind::Comment = &statements[idx].kind {
325                if let Value::String(s) = &statements[idx].value {
326                    if s == "else" {
327                        let else_indent = statements[idx].indent;
328
329                        // Collect following statements that are part of the else body
330                        let mut body: Vec<Statement> = Vec::new();
331                        let j = idx + 1;
332                        while j < statements.len() && statements[j].indent > else_indent {
333                            body.push(statements.remove(j));
334                        }
335
336                        // Remove the 'else' marker itself
337                        statements.remove(idx);
338
339                        // Find previous If to attach to (search backwards) and attach
340                        // the parsed else body to the deepest nested If in the chain.
341                        let mut k = idx as isize - 1;
342                        while k >= 0 {
343                            if let StatementKind::If { .. } = &statements[k as usize].kind {
344                                // Take ownership of the previous If statement, attach into it,
345                                // then put back the updated statement.
346                                let prev = std::mem::replace(
347                                    &mut statements[k as usize],
348                                    Statement::new(StatementKind::Unknown, Value::Null, 0, 0, 1),
349                                );
350                                let updated = attach_else_to_if_statement(prev, body);
351                                statements[k as usize] = updated;
352                                break;
353                            }
354                            k -= 1;
355                        }
356
357                        continue;
358                    }
359                }
360            }
361
362            // Handle 'else if' which was parsed as an If with value == "else-if"
363            if let StatementKind::If { .. } = &statements[idx].kind {
364                if let Value::String(s) = &statements[idx].value {
365                    if s == "else-if" {
366                        // Remove this else-if statement and attach it as the
367                        // else_body (single-statement vector) of the previous If.
368                        let else_if_stmt = statements.remove(idx);
369
370                        // find previous If and attach this else-if into its nested chain
371                        let mut k = idx as isize - 1;
372                        while k >= 0 {
373                            if let StatementKind::If { .. } = &statements[k as usize].kind {
374                                let prev = std::mem::replace(
375                                    &mut statements[k as usize],
376                                    Statement::new(StatementKind::Unknown, Value::Null, 0, 0, 1),
377                                );
378                                let updated = attach_else_to_if_statement(prev, vec![else_if_stmt]);
379                                statements[k as usize] = updated;
380                                break;
381                            }
382                            k -= 1;
383                        }
384
385                        continue;
386                    }
387                }
388            }
389
390            idx += 1;
391        }
392    }
393
394    attach_else_blocks(&mut statements);
395
396    Ok(statements)
397}
398
399fn parse_line(line: &str, line_number: usize, path: &Path) -> Result<Statement> {
400    use crate::language::syntax::parser::driver::statements::*;
401
402    if line.starts_with('.') {
403        return trigger::parse_trigger_line(line, line_number);
404    }
405
406    let mut parts = line.split_whitespace();
407    // Extract first token as keyword and strip a trailing ':' if present
408    let first_token = parts
409        .next()
410        .ok_or_else(|| anyhow!("empty line"))?
411        .to_string();
412    let keyword = first_token.trim_end_matches(':').to_lowercase();
413
414    // Check for routing statements (node, fx, route, duck, sidechain)
415    let routing_keywords = ["node", "fx", "route", "duck", "sidechain"];
416    if routing_keywords.contains(&keyword.as_str()) {
417        return crate::language::syntax::parser::driver::routing::parse_routing_statement(
418            line,
419            line_number,
420        );
421    }
422
423    // Check if this is a property assignment first: target.property = value
424    if line.contains('=') && keyword.contains('.') {
425        return parse_assign(line, line_number);
426    }
427
428    // Check if this looks like a trigger (contains a dot like "drums.kick")
429    if keyword.contains('.') && !keyword.contains('(') {
430        // Reconstruct the line and parse as trigger
431        return trigger::parse_trigger_line(line, line_number);
432    }
433
434    // Check if this is a bind statement first (before arrow call)
435    if keyword == "bind" && line.contains("->") {
436        return parse_bind(line, line_number);
437    }
438
439    // If this line contains an arrow call, parse it as an ArrowCall, UNLESS the
440    // statement starts with a reserved keyword that must be handled (let/var/const/etc.).
441    // This ensures constructs like `let name = .bank.kick -> reverse(...)` are
442    // parsed by `parse_let` rather than being mis-parsed as an ArrowCall.
443    let reserved_keywords = [
444        "bpm", "tempo", "print", "sleep", "rest", "wait", "pattern", "bank", "let", "const", "for",
445        "foreach", "loop", "if", "else", "group", "automate", "call", "spawn", "sequence", "layer",
446        "on", "emit", "routing", "return", "break", "import", "export", "use", "load",
447    ];
448    if line.contains("->") && !reserved_keywords.contains(&keyword.as_str()) {
449        return statements::parse_arrow_call(line, line_number);
450    }
451
452    return match keyword.as_str() {
453        "bpm" | "tempo" => statements::core::parse_tempo(line, line_number),
454        "print" => statements::core::parse_print(line, line_number),
455        "sleep" | "rest" | "wait" => statements::core::parse_sleep(parts, line_number),
456        "trigger" => Err(anyhow!(
457            "keyword 'trigger' is deprecated; use dot notation like '.alias' instead"
458        )),
459        "pattern" => parse_pattern(parts, line_number),
460        "bank" => parse_bank(parts, line_number),
461        "let" => parse_let(line, parts, line_number),
462        "const" => parse_const(line, parts, line_number),
463        "for" | "foreach" => parse_for(parts, line_number),
464        "loop" => parse_loop(parts, line_number),
465        "if" => statements::structure::parse_if(parts, line_number),
466        "else" => statements::structure::parse_else(line, line_number),
467        "group" => statements::structure::parse_group(parts, line_number),
468        "automate" => {
469            crate::language::syntax::parser::driver::statements::structure::parse_automate(
470                parts,
471                line_number,
472            )
473        }
474        "call" | "sequence" => parse_call(line, parts, line_number),
475        "break" => statements::structure::parse_break(parts, line_number),
476        "function" => statements::structure::parse_function(line, line_number),
477        "spawn" | "layer" => parse_spawn(parts, line_number),
478        "on" => parse_on(parts, line_number),
479        "emit" => parse_emit(line, parts, line_number),
480        "return" => statements::core::parse_return(line, line_number),
481        "routing" => {
482            crate::language::syntax::parser::driver::routing::parse_routing_command(line_number)
483        }
484        "import" => directive::parse_directive_keyword(line, "import", line_number, path),
485        "export" => directive::parse_directive_keyword(line, "export", line_number, path),
486        "use" => directive::parse_directive_keyword(line, "use", line_number, path),
487        "load" => directive::parse_directive_keyword(line, "load", line_number, path),
488        _ => {
489            // Provide helpful suggestions for common typos FIRST
490            let suggestion = find_keyword_suggestion(&keyword, &reserved_keywords);
491
492            // If we found a suggestion, it's likely a typo, not a trigger call
493            if suggestion.is_some() {
494                let suggestion_str = suggestion.unwrap();
495                // Store message in a structured format: "MESSAGE|||FILE:LINE|||SUGGESTION"
496                // This allows the collector to parse it and build a StructuredError
497                let error_msg = format!(
498                    "Unknown statement '{}' at {}:{}|||{}|||{}",
499                    keyword,
500                    path.display(),
501                    line_number,
502                    path.display(),
503                    suggestion_str
504                );
505
506                // Push structured error to WASM registry if available
507                #[cfg(feature = "wasm")]
508                {
509                    use crate::web::registry::debug;
510                    if debug::is_debug_errors_enabled() {
511                        debug::push_parse_error_from_parts(
512                            format!(
513                                "Unknown statement '{}'. Did you mean '{}' ?",
514                                keyword, suggestion_str
515                            ),
516                            line_number,
517                            1,
518                            "UnknownStatement".to_string(),
519                        );
520                    }
521                }
522
523                return Ok(Statement::new(
524                    StatementKind::Unknown,
525                    Value::String(error_msg),
526                    0,
527                    line_number,
528                    1,
529                ));
530            }
531
532            // Check if this looks like a potential trigger identifier (single word, no special chars except dots)
533            // This handles cases like variable references: let kick = drums.kick; kick
534            if keyword
535                .chars()
536                .all(|c| c.is_alphanumeric() || c == '_' || c == '.')
537            {
538                // Parse as trigger
539                return trigger::parse_trigger_line(line, line_number);
540            }
541
542            // Generic error for truly unknown statements
543            // Format: "MESSAGE|||FILE:LINE|||NO_SUGGESTION"
544            let error_msg = format!(
545                "Unknown statement '{}' at {}:{}|||{}|||",
546                keyword,
547                path.display(),
548                line_number,
549                path.display()
550            );
551
552            // Push structured error to WASM registry if available
553            #[cfg(feature = "wasm")]
554            {
555                use crate::web::registry::debug;
556                if debug::is_debug_errors_enabled() {
557                    debug::push_parse_error_from_parts(
558                        format!("Unknown statement '{}'", keyword),
559                        line_number,
560                        1,
561                        "UnknownStatement".to_string(),
562                    );
563                }
564            }
565
566            return Ok(Statement::new(
567                StatementKind::Unknown,
568                Value::String(error_msg),
569                0,
570                line_number,
571                1,
572            ));
573        }
574    };
575}
576
577/// Re-export helper parsing functions from helpers.rs so other modules can call them
578pub use helpers::{
579    parse_array_value, parse_condition, parse_function_args, parse_map_value, parse_single_arg,
580    parse_synth_definition,
581};
582
583/// SimpleParser is a small wrapper used by other modules/tests in the crate.
584/// It forwards to the top-level parse implementation above.
585pub struct SimpleParser;
586
587impl SimpleParser {
588    pub fn parse(source: &str, path: PathBuf) -> Result<Vec<Statement>> {
589        crate::language::syntax::parser::driver::parse(source, path)
590    }
591
592    pub fn parse_file<P: AsRef<Path>>(path: P) -> Result<Vec<Statement>> {
593        let buf = std::path::PathBuf::from(path.as_ref());
594        let s = std::fs::read_to_string(&buf)?;
595        Self::parse(&s, buf)
596    }
597}