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//
89
90/// Preprocess source to merge multiline arrow calls
91/// Example:
92///   target -> method1(arg)
93///           -> method2(arg)
94/// becomes:
95///   target -> method1(arg) -> method2(arg)
96pub fn preprocess_multiline_arrow_calls(source: &str) -> String {
97    let lines: Vec<&str> = source.lines().collect();
98    let mut result = Vec::new();
99    let mut i = 0;
100
101    while i < lines.len() {
102        let line = lines[i];
103        let trimmed = line.trim();
104
105        // Case A: line contains an arrow call and is not itself a continuation (doesn't start with ->)
106        if line.contains("->") && !trimmed.starts_with("->") {
107            // This is the start of a potential multiline arrow call
108            let mut merged = line.to_string();
109            i += 1;
110
111            // Merge all following lines that start with -> (allow interleaved comments/blank lines)
112            while i < lines.len() {
113                let next_line = lines[i];
114                let next_trimmed = next_line.trim();
115
116                // Skip comment or empty lines between arrow continuations
117                if next_trimmed.is_empty()
118                    || next_trimmed.starts_with("//")
119                    || next_trimmed.starts_with("#")
120                {
121                    i += 1;
122                    continue;
123                }
124
125                if next_trimmed.starts_with("->") {
126                    // Append this line to the merged line
127                    merged.push(' ');
128                    merged.push_str(next_trimmed);
129                    i += 1;
130                } else {
131                    // Not a continuation line, stop merging
132                    break;
133                }
134            }
135
136            result.push(merged);
137            continue; // Don't increment i again
138        }
139
140        // Case B: support definitions that set a trigger (e.g. `let t = .bank.kick`) followed by
141        // continuation lines that start with `-> effect(...)`. In this case the initial line does
142        // not contain '->' but we should treat following '->' lines as part of the same statement.
143        // Detect a trigger assignment by checking for '=' and a '.' beginning the RHS.
144        if !trimmed.starts_with("->") && line.contains('=') {
145            if let Some(rhs) = line.splitn(2, '=').nth(1) {
146                if rhs.trim_start().starts_with('.') {
147                    // This is a trigger assignment; merge following -> lines
148                    let mut merged = line.to_string();
149                    let mut j = i + 1;
150                    let mut merged_any = false;
151                    while j < lines.len() {
152                        let next_line = lines[j];
153                        let next_trimmed = next_line.trim();
154
155                        // Skip comments/blank lines
156                        if next_trimmed.is_empty()
157                            || next_trimmed.starts_with("//")
158                            || next_trimmed.starts_with("#")
159                        {
160                            j += 1;
161                            continue;
162                        }
163
164                        if next_trimmed.starts_with("->") {
165                            merged.push(' ');
166                            merged.push_str(next_trimmed);
167                            merged_any = true;
168                            j += 1;
169                        } else {
170                            break;
171                        }
172                    }
173
174                    if merged_any {
175                        result.push(merged);
176                        i = j;
177                        continue;
178                    }
179                    // If no continuation lines, fall through to default handling
180                }
181            }
182        }
183
184        // Case B2: support bare trigger lines like `.bank.kick` followed by indented `->` lines
185        // Example:
186        //   .myBank.kick
187        //       -> speed(1.0)
188        //       -> reverse(true)
189        // In this case merge the following `->` continuation lines into the trigger line so
190        // parse_trigger_line can pick up effects in a single statement.
191        if !trimmed.starts_with("->") && trimmed.starts_with('.') && !line.contains("->") {
192            let mut merged = line.to_string();
193            let mut j = i + 1;
194            let mut merged_any = false;
195            while j < lines.len() {
196                let next_line = lines[j];
197                let next_trimmed = next_line.trim();
198
199                // Skip comments/blank lines
200                if next_trimmed.is_empty()
201                    || next_trimmed.starts_with("//")
202                    || next_trimmed.starts_with("#")
203                {
204                    j += 1;
205                    continue;
206                }
207
208                if next_trimmed.starts_with("->") {
209                    merged.push(' ');
210                    merged.push_str(next_trimmed);
211                    merged_any = true;
212                    j += 1;
213                } else {
214                    break;
215                }
216            }
217
218            if merged_any {
219                result.push(merged);
220                i = j;
221                continue;
222            }
223            // If no continuation lines, fall through
224        }
225
226        // Case C: support synth declarations split across lines, e.g.
227        //   let mySynth = synth saw
228        //       -> lfo(...)
229        // If the RHS of an assignment starts with 'synth', merge following '->' continuation lines
230        if !trimmed.starts_with("->") && line.contains('=') {
231            if let Some(rhs) = line.splitn(2, '=').nth(1) {
232                if rhs.trim_start().starts_with("synth") {
233                    // This is a synth 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 D: support bare identifier lines followed by indented '->' continuations
271        // Example:
272        //   mySynth
273        //       -> note(C4)
274        //       -> duration(2000)
275        // Merge into: `mySynth -> note(C4) -> duration(2000)`
276        if !trimmed.starts_with("->") && !line.contains("->") {
277            // If the line is a single identifier-like token, merge following -> lines
278            // Skip reserved keywords (let, var, const, for, if, group, etc.) to avoid
279            // accidentally merging declarations with continuations.
280            let token = trimmed.split_whitespace().next().unwrap_or("");
281            let reserved = [
282                "let", "var", "const", "for", "if", "group", "spawn", "on", "automate", "bind",
283                "call", "emit", "bank",
284            ];
285            if !token.is_empty()
286                && !reserved.contains(&token)
287                && token
288                    .chars()
289                    .all(|c| c.is_alphanumeric() || c == '_' || c == '.')
290            {
291                let mut merged = line.to_string();
292                let mut j = i + 1;
293                let mut merged_any = false;
294                while j < lines.len() {
295                    let next_line = lines[j];
296                    let next_trimmed = next_line.trim();
297
298                    if next_trimmed.is_empty()
299                        || next_trimmed.starts_with("//")
300                        || next_trimmed.starts_with("#")
301                    {
302                        j += 1;
303                        continue;
304                    }
305
306                    if next_trimmed.starts_with("->") {
307                        merged.push(' ');
308                        merged.push_str(next_trimmed);
309                        merged_any = true;
310                        j += 1;
311                    } else {
312                        break;
313                    }
314                }
315
316                if merged_any {
317                    // If the previous pushed line was a `let <name> = synth ...` declaration for
318                    // the same identifier, attach the chain to that let line instead of
319                    // leaving a separate bare target line.
320                    // Look backwards for the nearest non-empty previous pushed line and
321                    // attempt to attach the chain to a `let <token> =` synth declaration.
322                    // Determine the first method name in the chain (the call after the first '->')
323                    let mut first_method_name = String::new();
324                    if let Some(second_part) = merged.split("->").nth(1) {
325                        let second_part = second_part.trim();
326                        if let Some(paren_idx) = second_part.find('(') {
327                            first_method_name = second_part[..paren_idx].trim().to_string();
328                        } else {
329                            first_method_name = second_part.to_string();
330                        }
331                    }
332
333                    // Allowed synth-parameter method names which should be attached to the synth
334                    // declaration when used under a bare identifier. Other methods (like `note` or
335                    // `duration`) are runtime actions and should remain as separate ArrowCall
336                    // statements targeting the identifier.
337                    let synth_param_methods =
338                        ["type", "adsr", "lfo", "filter", "filters", "options"];
339
340                    let _let_pattern1 = format!("let {} =", token);
341                    let _let_pattern2 = format!("let {}=", token);
342                    let mut attached = false;
343
344                    // Only attempt to attach the chain to a previous `let` if the chain
345                    // begins with a synth-parameter method.
346                    if synth_param_methods.contains(&first_method_name.as_str()) {
347                        if !result.is_empty() {
348                            // Find index of last non-empty previous line
349                            let mut k = result.len();
350                            while k > 0 {
351                                k -= 1;
352                                let candidate = result[k].trim();
353                                if candidate.is_empty() {
354                                    // skip empty/comment lines
355                                    continue;
356                                }
357
358                                // Be more permissive: accept any previous line that looks like a let assignment
359                                // for this token (contains 'let', the token, and an '='). This is robust to
360                                // spacing variations like 'let name =', 'let name=' or additional text.
361                                let candidate_line = result[k].to_lowercase();
362                                if candidate_line.contains("let")
363                                    && candidate_line.contains(&token.to_lowercase())
364                                    && candidate_line.contains('=')
365                                {
366                                    // Attach chain part to this candidate line
367                                    if let Some(pos) = merged.find(token) {
368                                        let chain_part = merged[pos + token.len()..].trim_start();
369                                        if !chain_part.is_empty() {
370                                            result[k].push(' ');
371                                            result[k].push_str(chain_part);
372                                        }
373                                        i = j;
374                                        attached = true;
375                                    }
376                                }
377
378                                break; // stop after first non-empty line
379                            }
380                        }
381                    }
382
383                    if attached {
384                        continue;
385                    }
386
387                    // If the first method is a synth-param, we attempted to attach above. If not,
388                    // keep the merged bare identifier arrow call as its own statement.
389                    if !synth_param_methods.contains(&first_method_name.as_str()) {
390                        result.push(merged);
391                        i = j;
392                        continue;
393                    }
394
395                    // If we reach here but not attached (e.g., no matching let found), fall through
396                    // and push the merged chain as its own statement.
397                    result.push(merged);
398                    i = j;
399                    continue;
400                }
401            }
402        }
403
404        // Fallback: if this is a let assignment (synth or otherwise) and the subsequent
405        // non-empty lines start with '->', merge them. This is a more permissive rule to
406        // handle cases where continuation lines were not merged earlier (indentation/formatting).
407        if trimmed.starts_with("let ") && !trimmed.starts_with("->") && line.contains('=') {
408            // Peek ahead to see if next meaningful line starts with '->'
409            let mut j = i + 1;
410            let mut found = false;
411            while j < lines.len() {
412                let nl = lines[j];
413                let nt = nl.trim();
414                if nt.is_empty() || nt.starts_with("//") || nt.starts_with("#") {
415                    j += 1;
416                    continue;
417                }
418                if nt.starts_with("->") {
419                    found = true;
420                }
421                break;
422            }
423
424            if found {
425                let mut merged = line.to_string();
426                i += 1;
427                let mut merged_any = false;
428                while i < lines.len() {
429                    let next_line = lines[i];
430                    let next_trimmed = next_line.trim();
431
432                    if next_trimmed.is_empty()
433                        || next_trimmed.starts_with("//")
434                        || next_trimmed.starts_with("#")
435                    {
436                        i += 1;
437                        continue;
438                    }
439
440                    if next_trimmed.starts_with("->") {
441                        merged.push(' ');
442                        merged.push_str(next_trimmed);
443                        merged_any = true;
444                        i += 1;
445                    } else {
446                        break;
447                    }
448                }
449
450                if merged_any {
451                    result.push(merged);
452                    continue;
453                }
454            }
455        }
456
457        result.push(line.to_string());
458        i += 1;
459    }
460
461    result.join("\n")
462}