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}