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}