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