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

1//! Preprocessing utilities for multiline statement merging
2/// Preprocess source to merge ALL multiline statements with braces
3/// Handles: synth, bind, pattern, let with map/array, emit, etc.
4/// Example:
5///   bind myMidi -> myBassline {
6///       velocity: 80,
7///       bpm: 150
8///   }
9/// becomes:
10///   bind myMidi -> myBassline { velocity: 80, bpm: 150 }
11pub fn preprocess_multiline_braces(source: &str) -> String {
12    let lines: Vec<&str> = source.lines().collect();
13    let mut result = Vec::new();
14    let mut i = 0;
15
16    while i < lines.len() {
17        let line = lines[i];
18        let trimmed = line.trim();
19
20        // Skip empty lines and comments
21        if trimmed.is_empty() || trimmed.starts_with("//") || trimmed.starts_with("#") {
22            result.push(line.to_string());
23            i += 1;
24            continue;
25        }
26
27        // Check if this line contains an opening brace
28        if line.contains('{') {
29            // Count braces to see if it's complete on one line
30            let open_braces = line.matches('{').count();
31            let close_braces = line.matches('}').count();
32
33            if open_braces > close_braces {
34                // Multiline detected - collect all lines until braces are balanced
35                let mut merged = line.to_string();
36                let mut brace_depth = open_braces - close_braces;
37                i += 1;
38
39                while i < lines.len() && brace_depth > 0 {
40                    let next_line = lines[i];
41                    let next_trimmed = next_line.trim();
42
43                    // Skip comments inside multiline blocks
44                    if next_trimmed.starts_with("//") || next_trimmed.starts_with("#") {
45                        i += 1;
46                        continue;
47                    }
48
49                    // Remove inline comments before merging
50                    let clean_line = if let Some(comment_pos) = next_line.find("//") {
51                        &next_line[..comment_pos]
52                    } else if let Some(comment_pos) = next_line.find("#") {
53                        &next_line[..comment_pos]
54                    } else {
55                        next_line
56                    };
57
58                    let clean_trimmed = clean_line.trim();
59                    if !clean_trimmed.is_empty() {
60                        // Add space before appending (unless it's a closing brace)
61                        if !clean_trimmed.starts_with('}') && !merged.ends_with('{') {
62                            merged.push(' ');
63                        } else if clean_trimmed.starts_with('}') {
64                            merged.push(' ');
65                        }
66                        merged.push_str(clean_trimmed);
67                    }
68
69                    // Update brace depth (use original line for brace counting)
70                    brace_depth += next_line.matches('{').count();
71                    brace_depth -= next_line.matches('}').count();
72
73                    i += 1;
74                }
75
76                result.push(merged);
77                continue; // Don't increment i again
78            }
79        }
80
81        result.push(line.to_string());
82        i += 1;
83    }
84
85    result.join("\n")
86}
87
88/// Preprocess source to merge multiline statements with brackets
89/// Handles: timeline, etc.
90/// Example:
91///   timeline [
92///       section1 during 4 bars,
93///       section2 during 8 bars
94///   ]
95/// becomes:
96///   timeline [section1 during 4 bars, section2 during 8 bars]
97pub fn preprocess_multiline_brackets(source: &str) -> String {
98    let lines: Vec<&str> = source.lines().collect();
99    let mut result = Vec::new();
100    let mut i = 0;
101
102    while i < lines.len() {
103        let line = lines[i];
104        let trimmed = line.trim();
105
106        // Skip empty lines and comments
107        if trimmed.is_empty() || trimmed.starts_with("//") || trimmed.starts_with("#") {
108            result.push(line.to_string());
109            i += 1;
110            continue;
111        }
112
113        // Check if this line contains an opening bracket
114        if line.contains('[') {
115            // Count brackets to see if it's complete on one line
116            let open_brackets = line.matches('[').count();
117            let close_brackets = line.matches(']').count();
118
119            if open_brackets > close_brackets {
120                // Multiline detected - collect all lines until brackets are balanced
121                let mut merged = line.to_string();
122                let mut bracket_depth = open_brackets - close_brackets;
123                i += 1;
124
125                while i < lines.len() && bracket_depth > 0 {
126                    let next_line = lines[i];
127                    let next_trimmed = next_line.trim();
128
129                    // Skip comments inside multiline blocks
130                    if next_trimmed.starts_with("//") || next_trimmed.starts_with("#") {
131                        i += 1;
132                        continue;
133                    }
134
135                    // Remove inline comments before merging
136                    let clean_line = if let Some(comment_pos) = next_line.find("//") {
137                        &next_line[..comment_pos]
138                    } else if let Some(comment_pos) = next_line.find("#") {
139                        &next_line[..comment_pos]
140                    } else {
141                        next_line
142                    };
143
144                    let clean_trimmed = clean_line.trim();
145                    if !clean_trimmed.is_empty() {
146                        // Add space before appending
147                        if !merged.ends_with('[') && !merged.ends_with(',') {
148                            merged.push(' ');
149                        } else if merged.ends_with(',') {
150                            merged.push(' ');
151                        }
152                        merged.push_str(clean_trimmed);
153                    }
154
155                    // Update bracket depth
156                    bracket_depth += next_line.matches('[').count();
157                    bracket_depth -= next_line.matches(']').count();
158
159                    i += 1;
160                }
161
162                result.push(merged);
163                continue; // Don't increment i again
164            }
165        }
166
167        result.push(line.to_string());
168        i += 1;
169    }
170
171    result.join("\n")
172}
173
174//
175
176/// Preprocess source to merge multiline arrow calls
177/// Example:
178///   target -> method1(arg)
179///           -> method2(arg)
180/// becomes:
181///   target -> method1(arg) -> method2(arg)
182pub fn preprocess_multiline_arrow_calls(source: &str) -> String {
183    let lines: Vec<&str> = source.lines().collect();
184    let mut result = Vec::new();
185    let mut i = 0;
186
187    while i < lines.len() {
188        let line = lines[i];
189        let trimmed = line.trim();
190
191        // Case A: line contains an arrow call and is not itself a continuation (doesn't start with ->)
192        if line.contains("->") && !trimmed.starts_with("->") {
193            // This is the start of a potential multiline arrow call
194            let mut merged = line.to_string();
195            i += 1;
196
197            // Merge all following lines that start with -> (allow interleaved comments/blank lines)
198            while i < lines.len() {
199                let next_line = lines[i];
200                let next_trimmed = next_line.trim();
201
202                // Skip comment or empty lines between arrow continuations
203                if next_trimmed.is_empty()
204                    || next_trimmed.starts_with("//")
205                    || next_trimmed.starts_with("#")
206                {
207                    i += 1;
208                    continue;
209                }
210
211                if next_trimmed.starts_with("->") {
212                    // Append this line to the merged line
213                    merged.push(' ');
214                    merged.push_str(next_trimmed);
215                    i += 1;
216                } else {
217                    // Not a continuation line, stop merging
218                    break;
219                }
220            }
221
222            result.push(merged);
223            continue; // Don't increment i again
224        }
225
226        // Case B: support definitions that set a trigger (e.g. `let t = .bank.kick`) followed by
227        // continuation lines that start with `-> effect(...)`. In this case the initial line does
228        // not contain '->' but we should treat following '->' lines as part of the same statement.
229        // Detect a trigger assignment by checking for '=' and a '.' beginning the RHS.
230        if !trimmed.starts_with("->") && line.contains('=') {
231            if let Some(rhs) = line.splitn(2, '=').nth(1) {
232                if rhs.trim_start().starts_with('.') {
233                    // This is a trigger assignment; merge following -> lines
234                    let mut merged = line.to_string();
235                    let mut j = i + 1;
236                    let mut merged_any = false;
237                    while j < lines.len() {
238                        let next_line = lines[j];
239                        let next_trimmed = next_line.trim();
240
241                        // Skip comments/blank lines
242                        if next_trimmed.is_empty()
243                            || next_trimmed.starts_with("//")
244                            || next_trimmed.starts_with("#")
245                        {
246                            j += 1;
247                            continue;
248                        }
249
250                        if next_trimmed.starts_with("->") {
251                            merged.push(' ');
252                            merged.push_str(next_trimmed);
253                            merged_any = true;
254                            j += 1;
255                        } else {
256                            break;
257                        }
258                    }
259
260                    if merged_any {
261                        result.push(merged);
262                        i = j;
263                        continue;
264                    }
265                    // If no continuation lines, fall through to default handling
266                }
267            }
268        }
269
270        // Case B2: support bare trigger lines like `.bank.kick` followed by indented `->` lines
271        // Example:
272        //   .myBank.kick
273        //       -> speed(1.0)
274        //       -> reverse(true)
275        // In this case merge the following `->` continuation lines into the trigger line so
276        // parse_trigger_line can pick up effects in a single statement.
277        if !trimmed.starts_with("->") && trimmed.starts_with('.') && !line.contains("->") {
278            let mut merged = line.to_string();
279            let mut j = i + 1;
280            let mut merged_any = false;
281            while j < lines.len() {
282                let next_line = lines[j];
283                let next_trimmed = next_line.trim();
284
285                // Skip comments/blank lines
286                if next_trimmed.is_empty()
287                    || next_trimmed.starts_with("//")
288                    || next_trimmed.starts_with("#")
289                {
290                    j += 1;
291                    continue;
292                }
293
294                if next_trimmed.starts_with("->") {
295                    merged.push(' ');
296                    merged.push_str(next_trimmed);
297                    merged_any = true;
298                    j += 1;
299                } else {
300                    break;
301                }
302            }
303
304            if merged_any {
305                result.push(merged);
306                i = j;
307                continue;
308            }
309            // If no continuation lines, fall through
310        }
311
312        // Case C: support synth declarations split across lines, e.g.
313        //   let mySynth = synth saw
314        //       -> lfo(...)
315        // If the RHS of an assignment starts with 'synth', merge following '->' continuation lines
316        if !trimmed.starts_with("->") && line.contains('=') {
317            if let Some(rhs) = line.splitn(2, '=').nth(1) {
318                if rhs.trim_start().starts_with("synth") {
319                    // This is a synth assignment; merge following -> lines
320                    let mut merged = line.to_string();
321                    let mut j = i + 1;
322                    let mut merged_any = false;
323                    while j < lines.len() {
324                        let next_line = lines[j];
325                        let next_trimmed = next_line.trim();
326
327                        // Skip comments/blank lines
328                        if next_trimmed.is_empty()
329                            || next_trimmed.starts_with("//")
330                            || next_trimmed.starts_with("#")
331                        {
332                            j += 1;
333                            continue;
334                        }
335
336                        if next_trimmed.starts_with("->") {
337                            merged.push(' ');
338                            merged.push_str(next_trimmed);
339                            merged_any = true;
340                            j += 1;
341                        } else {
342                            break;
343                        }
344                    }
345
346                    if merged_any {
347                        result.push(merged);
348                        i = j;
349                        continue;
350                    }
351                    // If no continuation lines, fall through to default handling
352                }
353            }
354        }
355
356        // Case D: support bare identifier lines followed by indented '->' continuations
357        // Example:
358        //   mySynth
359        //       -> note(C4)
360        //       -> duration(2000)
361        // Merge into: `mySynth -> note(C4) -> duration(2000)`
362        if !trimmed.starts_with("->") && !line.contains("->") {
363            // If the line is a single identifier-like token, merge following -> lines
364            // Skip reserved keywords (let, var, const, for, if, group, etc.) to avoid
365            // accidentally merging declarations with continuations.
366            let token = trimmed.split_whitespace().next().unwrap_or("");
367            let reserved = [
368                "let", "var", "const", "for", "if", "group", "spawn", "on", "automate", "bind",
369                "call", "emit", "bank",
370            ];
371            if !token.is_empty()
372                && !reserved.contains(&token)
373                && token
374                    .chars()
375                    .all(|c| c.is_alphanumeric() || c == '_' || c == '.')
376            {
377                let mut merged = line.to_string();
378                let mut j = i + 1;
379                let mut merged_any = false;
380                while j < lines.len() {
381                    let next_line = lines[j];
382                    let next_trimmed = next_line.trim();
383
384                    if next_trimmed.is_empty()
385                        || next_trimmed.starts_with("//")
386                        || next_trimmed.starts_with("#")
387                    {
388                        j += 1;
389                        continue;
390                    }
391
392                    if next_trimmed.starts_with("->") {
393                        merged.push(' ');
394                        merged.push_str(next_trimmed);
395                        merged_any = true;
396                        j += 1;
397                    } else {
398                        break;
399                    }
400                }
401
402                if merged_any {
403                    // If the previous pushed line was a `let <name> = synth ...` declaration for
404                    // the same identifier, attach the chain to that let line instead of
405                    // leaving a separate bare target line.
406                    // Look backwards for the nearest non-empty previous pushed line and
407                    // attempt to attach the chain to a `let <token> =` synth declaration.
408                    // Determine the first method name in the chain (the call after the first '->')
409                    let mut first_method_name = String::new();
410                    if let Some(second_part) = merged.split("->").nth(1) {
411                        let second_part = second_part.trim();
412                        if let Some(paren_idx) = second_part.find('(') {
413                            first_method_name = second_part[..paren_idx].trim().to_string();
414                        } else {
415                            first_method_name = second_part.to_string();
416                        }
417                    }
418
419                    // Allowed synth-parameter method names which should be attached to the synth
420                    // declaration when used under a bare identifier. Other methods (like `note` or
421                    // `duration`) are runtime actions and should remain as separate ArrowCall
422                    // statements targeting the identifier.
423                    let synth_param_methods =
424                        ["type", "adsr", "lfo", "filter", "filters", "options"];
425
426                    let _let_pattern1 = format!("let {} =", token);
427                    let _let_pattern2 = format!("let {}=", token);
428                    let mut attached = false;
429
430                    // Only attempt to attach the chain to a previous `let` if the chain
431                    // begins with a synth-parameter method.
432                    if synth_param_methods.contains(&first_method_name.as_str()) {
433                        if !result.is_empty() {
434                            // Find index of last non-empty previous line
435                            let mut k = result.len();
436                            while k > 0 {
437                                k -= 1;
438                                let candidate = result[k].trim();
439                                if candidate.is_empty() {
440                                    // skip empty/comment lines
441                                    continue;
442                                }
443
444                                // Be more permissive: accept any previous line that looks like a let assignment
445                                // for this token (contains 'let', the token, and an '='). This is robust to
446                                // spacing variations like 'let name =', 'let name=' or additional text.
447                                let candidate_line = result[k].to_lowercase();
448                                if candidate_line.contains("let")
449                                    && candidate_line.contains(&token.to_lowercase())
450                                    && candidate_line.contains('=')
451                                {
452                                    // Attach chain part to this candidate line
453                                    if let Some(pos) = merged.find(token) {
454                                        let chain_part = merged[pos + token.len()..].trim_start();
455                                        if !chain_part.is_empty() {
456                                            result[k].push(' ');
457                                            result[k].push_str(chain_part);
458                                        }
459                                        i = j;
460                                        attached = true;
461                                    }
462                                }
463
464                                break; // stop after first non-empty line
465                            }
466                        }
467                    }
468
469                    if attached {
470                        continue;
471                    }
472
473                    // If the first method is a synth-param, we attempted to attach above. If not,
474                    // keep the merged bare identifier arrow call as its own statement.
475                    if !synth_param_methods.contains(&first_method_name.as_str()) {
476                        result.push(merged);
477                        i = j;
478                        continue;
479                    }
480
481                    // If we reach here but not attached (e.g., no matching let found), fall through
482                    // and push the merged chain as its own statement.
483                    result.push(merged);
484                    i = j;
485                    continue;
486                }
487            }
488        }
489
490        // Fallback: if this is a let assignment (synth or otherwise) and the subsequent
491        // non-empty lines start with '->', merge them. This is a more permissive rule to
492        // handle cases where continuation lines were not merged earlier (indentation/formatting).
493        if trimmed.starts_with("let ") && !trimmed.starts_with("->") && line.contains('=') {
494            // Peek ahead to see if next meaningful line starts with '->'
495            let mut j = i + 1;
496            let mut found = false;
497            while j < lines.len() {
498                let nl = lines[j];
499                let nt = nl.trim();
500                if nt.is_empty() || nt.starts_with("//") || nt.starts_with("#") {
501                    j += 1;
502                    continue;
503                }
504                if nt.starts_with("->") {
505                    found = true;
506                }
507                break;
508            }
509
510            if found {
511                let mut merged = line.to_string();
512                i += 1;
513                let mut merged_any = false;
514                while i < lines.len() {
515                    let next_line = lines[i];
516                    let next_trimmed = next_line.trim();
517
518                    if next_trimmed.is_empty()
519                        || next_trimmed.starts_with("//")
520                        || next_trimmed.starts_with("#")
521                    {
522                        i += 1;
523                        continue;
524                    }
525
526                    if next_trimmed.starts_with("->") {
527                        merged.push(' ');
528                        merged.push_str(next_trimmed);
529                        merged_any = true;
530                        i += 1;
531                    } else {
532                        break;
533                    }
534                }
535
536                if merged_any {
537                    result.push(merged);
538                    continue;
539                }
540            }
541        }
542
543        result.push(line.to_string());
544        i += 1;
545    }
546
547    result.join("\n")
548}