Skip to main content

devalang_wasm/engine/audio/interpreter/driver/
handler.rs

1// Full handler implementation copied from top-level module
2use anyhow::Result;
3use std::collections::HashMap;
4
5use crate::engine::audio::events::AudioEvent;
6use crate::language::syntax::ast::{Statement, StatementKind, Value};
7
8use super::AudioInterpreter;
9use super::patterns::execute_pattern_with_source;
10
11pub fn handle_let(interpreter: &mut AudioInterpreter, name: &str, value: &Value) -> Result<()> {
12    // Check if this is a synth definition. v0.2 syntax allows bare `synth`
13    // configured only through chained calls such as `-> preset("bass")`.
14    if let Value::Map(orig_map) = value {
15        let is_synth_like = orig_map.contains_key("waveform")
16            || orig_map.contains_key("_plugin_ref")
17            || orig_map.contains_key("synth_type")
18            || matches!(
19                orig_map.get("type"),
20                Some(Value::String(s)) if s.trim_matches('"').trim_matches('\'') == "synth"
21            )
22            || matches!(
23                orig_map.get("type"),
24                Some(Value::Identifier(s)) if s.trim_matches('"').trim_matches('\'') == "synth"
25            );
26
27        // Work on a normalized copy before storing it in the interpreter state.
28        let mut map = orig_map.clone();
29
30        // Normalize older key `synth_type` into `type` so downstream logic reads the same key.
31        // Prefer chained synth_type over default "synth" placeholder when present.
32        if let Some(synth_val) = map.get("synth_type").cloned() {
33            let should_replace = match map.get("type") {
34                Some(Value::String(s)) => {
35                    let clean = s.trim_matches('"').trim_matches('\'');
36                    clean == "synth" || clean.is_empty()
37                }
38                Some(Value::Identifier(id)) => {
39                    let clean = id.trim_matches('"').trim_matches('\'');
40                    clean == "synth" || clean.is_empty()
41                }
42                None => true,
43                _ => false,
44            };
45            if should_replace {
46                map.insert("type".to_string(), synth_val.clone());
47                map.remove("synth_type");
48            } else {
49                // If we didn't replace, still keep synth_type (back-compat) but don't remove it
50            }
51        }
52
53        // Merge chained parameter sub-maps (e.g. "params", "adsr", "envelope")
54        let chain_keys = ["params", "adsr", "envelope"];
55        for key in &chain_keys {
56            if let Some(Value::Map(submap)) = map.get(*key) {
57                // Temporary buffer to avoid mutable/immutable borrow conflict
58                let to_insert: Vec<(String, Value)> =
59                    submap.iter().map(|(k, v)| (k.clone(), v.clone())).collect();
60                for (k, v) in to_insert {
61                    map.insert(k, v);
62                }
63                map.remove(*key);
64            }
65        }
66
67        // Ensure default properties are present on instantiated objects (synths/plugins)
68        // so that dotted access like `mySynth.volume` resolves to a concrete Value.
69        // These defaults are safe no-ops for objects that don't use them.
70        map.entry("volume".to_string())
71            .or_insert(Value::Number(1.0));
72        map.entry("gain".to_string()).or_insert(Value::Number(1.0));
73        map.entry("pan".to_string()).or_insert(Value::Number(0.0));
74        map.entry("detune".to_string())
75            .or_insert(Value::Number(0.0));
76        // Ensure a visible type key exists for prints; prefer existing value if present
77        map.entry("type".to_string())
78            .or_insert(Value::String("synth".to_string()));
79
80        // Ensure defaults for synth-like objects
81        crate::utils::props::ensure_default_properties(&mut map, Some("synth"));
82
83        if is_synth_like {
84            // plugin handling simplified: reuse existing logic
85            let mut is_plugin = false;
86            let mut plugin_author: Option<String> = None;
87            let mut plugin_name: Option<String> = None;
88            let mut plugin_export: Option<String> = None;
89
90            if let Some(Value::String(plugin_ref)) = map.get("_plugin_ref") {
91                let parts: Vec<&str> = plugin_ref.split('.').collect();
92                if parts.len() == 2 {
93                    let (var_name, prop_name_raw) = (parts[0], parts[1]);
94                    let prop_name = prop_name_raw.trim_end_matches(';'); // Remove semicolon
95                    if let Some(var_value) = interpreter.variables.get(var_name) {
96                        if let Value::Map(var_map) = var_value {
97                            if let Some(Value::String(resolved_plugin)) = var_map.get(prop_name) {
98                                if resolved_plugin.starts_with("plugin:") {
99                                    let ref_parts: Vec<&str> =
100                                        resolved_plugin["plugin:".len()..].split(':').collect();
101                                    if ref_parts.len() == 2 {
102                                        let full_plugin_name = ref_parts[0];
103                                        let export_name = ref_parts[1];
104                                        let plugin_parts: Vec<&str> =
105                                            full_plugin_name.split('.').collect();
106                                        if plugin_parts.len() == 2 {
107                                            plugin_author = Some(plugin_parts[0].to_string());
108                                            plugin_name = Some(plugin_parts[1].to_string());
109                                            plugin_export = Some(export_name.to_string());
110                                            is_plugin = true;
111                                        }
112                                    }
113                                }
114                            } else if let Some(Value::Map(export_map)) = var_map.get(prop_name) {
115                                if let (
116                                    Some(Value::String(author)),
117                                    Some(Value::String(name)),
118                                    Some(Value::String(export)),
119                                ) = (
120                                    export_map.get("_plugin_author"),
121                                    export_map.get("_plugin_name"),
122                                    export_map.get("_export_name"),
123                                ) {
124                                    plugin_author = Some(author.clone());
125                                    plugin_name = Some(name.clone());
126                                    plugin_export = Some(export.clone());
127                                    is_plugin = true;
128                                }
129                            }
130                        }
131                    }
132                }
133            }
134
135            let waveform = crate::engine::audio::events::extract_string(&map, "waveform", "sine");
136            let attack = crate::engine::audio::events::extract_number(&map, "attack", 0.01);
137            let decay = crate::engine::audio::events::extract_number(&map, "decay", 0.1);
138            let sustain = crate::engine::audio::events::extract_number(&map, "sustain", 0.7);
139            let release = crate::engine::audio::events::extract_number(&map, "release", 0.2);
140
141            // Accept both String and Identifier for type (parser may emit Identifier for bare words)
142            let synth_type = if let Some(v) = map.get("type") {
143                match v {
144                    Value::String(t) => {
145                        let clean = t.trim_matches('"').trim_matches('\'');
146                        if clean.is_empty() || clean == "synth" {
147                            None
148                        } else {
149                            Some(clean.to_string())
150                        }
151                    }
152                    Value::Identifier(id) => {
153                        let clean = id.trim_matches('"').trim_matches('\'');
154                        if clean.is_empty() || clean == "synth" {
155                            None
156                        } else {
157                            Some(clean.to_string())
158                        }
159                    }
160                    _ => None,
161                }
162            } else {
163                None
164            };
165
166            let filters = if let Some(Value::Array(filters_arr)) = map.get("filters") {
167                crate::engine::audio::events::extract_filters(filters_arr)
168            } else {
169                Vec::new()
170            };
171
172            // Extract LFO configuration if present
173            let lfo = if let Some(Value::Map(lfo_map)) = map.get("lfo") {
174                use crate::engine::audio::lfo::{LfoParams, LfoRate, LfoTarget, LfoWaveform};
175
176                // Parse rate (Hz or tempo-synced like "1/4")
177                let rate_str = if let Some(Value::Number(n)) = lfo_map.get("rate") {
178                    n.to_string()
179                } else if let Some(Value::String(s)) = lfo_map.get("rate") {
180                    s.clone()
181                } else {
182                    "5.0".to_string() // Default rate
183                };
184                let rate = LfoRate::from_value(&rate_str);
185
186                // Parse depth (0-1)
187                let depth = if let Some(Value::Number(n)) = lfo_map.get("depth") {
188                    (*n).clamp(0.0, 1.0)
189                } else {
190                    0.5 // Default depth
191                };
192
193                // Parse waveform (sine, triangle, square, saw)
194                let waveform_str = if let Some(Value::String(s)) = lfo_map.get("shape") {
195                    s.clone()
196                } else if let Some(Value::String(s)) = lfo_map.get("waveform") {
197                    s.clone()
198                } else {
199                    "sine".to_string() // Default waveform
200                };
201                let lfo_waveform = LfoWaveform::from_str(&waveform_str);
202
203                // Parse target (volume, pitch, filter, pan)
204                let target = if let Some(Value::String(s)) = lfo_map.get("target") {
205                    LfoTarget::from_str(s).unwrap_or(LfoTarget::Volume)
206                } else {
207                    LfoTarget::Volume // Default target
208                };
209
210                // Parse initial phase (0-1)
211                let phase = if let Some(Value::Number(n)) = lfo_map.get("phase") {
212                    (*n).fract().abs() // Ensure 0-1 range
213                } else {
214                    0.0 // Default phase
215                };
216
217                Some(LfoParams {
218                    rate,
219                    depth,
220                    waveform: lfo_waveform,
221                    target,
222                    phase,
223                })
224            } else {
225                None
226            };
227
228            let mut options = std::collections::HashMap::new();
229            let reserved_keys = if is_plugin {
230                vec![
231                    "attack",
232                    "decay",
233                    "sustain",
234                    "release",
235                    "type",
236                    "filters",
237                    "_plugin_ref",
238                    "lfo",
239                    "waveform", // Don't auto-convert waveform for plugins - let plugin defaults handle it
240                    // Audio parameters (applied post-render, NOT plugin options)
241                    "pan",
242                    "gain",
243                    "volume",
244                    "detune",
245                    "spread",
246                    // Other metadata keys
247                    "synth_type", // Alias for type
248                    "chain",      // Effects chain
249                    "effects",    // Effects (deprecated)
250                ]
251            } else {
252                vec![
253                    "waveform",
254                    "attack",
255                    "decay",
256                    "sustain",
257                    "release",
258                    "type",
259                    "filters",
260                    "_plugin_ref",
261                    "lfo",
262                    // Audio parameters (applied post-render, NOT synth options)
263                    "pan",
264                    "gain",
265                    "volume",
266                    "detune",
267                    "spread",
268                    // Other metadata keys
269                    "synth_type", // Alias for type
270                    "chain",      // Effects chain
271                    "effects",    // Effects (deprecated)
272                ]
273            };
274
275            for (key, val) in map.iter() {
276                if !reserved_keys.contains(&key.as_str()) {
277                    match val {
278                        Value::Number(n) => {
279                            options.insert(key.clone(), *n);
280                        }
281                        Value::String(s) => {
282                            if is_plugin && key == "waveform" {
283                                let waveform_id = match s
284                                    .trim_matches('"')
285                                    .trim_matches('\'')
286                                    .to_lowercase()
287                                    .as_str()
288                                {
289                                    "sine" => 0.0,
290                                    "saw" => 1.0,
291                                    "square" => 2.0,
292                                    "triangle" => 3.0,
293                                    _ => 1.0,
294                                };
295                                options.insert(key.clone(), waveform_id);
296                            }
297                        }
298                        _ => {}
299                    }
300                }
301            }
302
303            if is_plugin && map.contains_key("decay") {
304                options.insert("decay".to_string(), decay);
305            }
306
307            // Extract parameters from chain if present (e.g., from synth acid.synth -> cutoff(800) -> resonance(0.75) -> ...)
308            if let Some(Value::Array(chain_arr)) = map.get("chain") {
309                for chain_item in chain_arr {
310                    if let Value::Map(chain_map) = chain_item {
311                        // Each chain item has "type" (method name) and "value" (parameter value)
312                        if let Some(Value::String(method)) = chain_map.get("type") {
313                            // Handle string values (e.g., decay("auto"))
314                            if let Some(Value::String(str_val)) = chain_map.get("value") {
315                                if method == "decay" && str_val == "auto" {
316                                    options.insert("decay_mode".to_string(), 1.0); // 1.0 = auto mode
317                                }
318                            }
319                            // Handle numeric values
320                            else if let Some(Value::Number(val)) = chain_map.get("value") {
321                                options.insert(method.clone(), *val);
322                            }
323                        }
324                    }
325                }
326            }
327
328            let final_waveform = if is_plugin {
329                "plugin".to_string()
330            } else {
331                waveform
332            };
333
334            let synth_def = crate::engine::audio::events::SynthDefinition {
335                waveform: final_waveform,
336                attack,
337                decay,
338                sustain,
339                release,
340                synth_type,
341                filters,
342                options,
343                plugin_author,
344                plugin_name,
345                plugin_export,
346                lfo,
347                bpm: interpreter.bpm,
348            };
349
350            interpreter.events.add_synth(name.to_string(), synth_def);
351        }
352
353        // Insert the normalized/augmented map into variables so dotted-property reads
354        // (e.g., `mySynth.volume`) return concrete values instead of Identifier("...").
355        interpreter
356            .variables
357            .insert(name.to_string(), Value::Map(map.clone()));
358        return Ok(());
359    }
360
361    // Non-map values: fall back to storing the original value
362    // If the value is a stored Trigger statement, normalize it into a Map so dotted
363    // property access like `myTrigger.effects.reverb.size` works and `print myTrigger`
364    // displays a clean map rather than a Statement debug dump.
365    if let Value::Statement(stmt_box) = value {
366        if let StatementKind::Trigger {
367            entity,
368            duration,
369            effects,
370        } = &stmt_box.kind
371        {
372            let mut map = HashMap::new();
373            map.insert("kind".to_string(), Value::String("Trigger".to_string()));
374            map.insert("entity".to_string(), Value::String(entity.clone()));
375            map.insert("duration".to_string(), Value::Duration(duration.clone()));
376            if let Some(eff) = effects {
377                map.insert("effects".to_string(), eff.clone());
378            }
379
380            // Ensure defaults for trigger objects
381            crate::utils::props::ensure_default_properties(&mut map, Some("trigger"));
382
383            interpreter
384                .variables
385                .insert(name.to_string(), Value::Map(map));
386            return Ok(());
387        }
388    }
389
390    interpreter
391        .variables
392        .insert(name.to_string(), value.clone());
393    Ok(())
394}
395
396pub fn handle_call(interpreter: &mut AudioInterpreter, name: &str, args: &[Value]) -> Result<()> {
397    // ============================================================================
398    // CALL EXECUTION (Sequential)
399    // ============================================================================
400    // Call accepts:
401    // - Patterns (defined with `pattern name with trigger = "x---"`)
402    // - Groups (defined with `group name:`)
403    // ============================================================================
404
405    // Check for user-defined function stored as a variable
406    if let Some(var_val) = interpreter.variables.get(name).cloned() {
407        if let Value::Statement(stmt_box) = var_val {
408            if let StatementKind::Function {
409                name: _fname,
410                parameters,
411                body,
412            } = &stmt_box.kind
413            {
414                // Create a local variable scope for function execution
415                let vars_snapshot = interpreter.variables.clone();
416
417                // Bind parameters: resolve passed args into actual values
418                for (i, param) in parameters.iter().enumerate() {
419                    let bound = args.get(i).cloned().unwrap_or(Value::Null);
420                    // If the bound value is an Identifier, resolve it to its actual value
421                    let bound_val = match bound {
422                        Value::Identifier(ref id) => {
423                            interpreter.resolve_value(&Value::Identifier(id.clone()))?
424                        }
425                        other => other,
426                    };
427                    interpreter.variables.insert(param.clone(), bound_val);
428                }
429
430                // Execute the function body. Track function call depth so 'return'
431                // statements are only valid within a function context.
432                interpreter.function_call_depth += 1;
433                let exec_result = super::collector::collect_events(interpreter, body);
434                // decrement depth regardless of success or error
435                interpreter.function_call_depth = interpreter.function_call_depth.saturating_sub(1);
436                exec_result?;
437
438                // Capture return value if the function executed a `return` statement.
439                let mut captured_return: Option<Value> = None;
440                if interpreter.returning_flag {
441                    captured_return = interpreter.return_value.clone();
442                    // clear the interpreter return state now that we've captured it
443                    interpreter.returning_flag = false;
444                    interpreter.return_value = None;
445                }
446
447                // Restore variables (local scope ends). We deliberately do not touch interpreter.events
448                // so synth definitions or other global registrations performed by the function persist.
449                interpreter.variables = vars_snapshot;
450
451                // If there was a returned value, expose it to the caller scope via a special variable
452                // named "__return" so callers can inspect the result. This is a simple mechanism
453                // for now; higher-level expression support can be added later.
454                if let Some(rv) = captured_return {
455                    interpreter.variables.insert("__return".to_string(), rv);
456                }
457
458                return Ok(());
459            }
460            // If it's a stored pattern (inline pattern stored as Statement), handle below
461            if let StatementKind::Pattern { target, .. } = &stmt_box.kind {
462                if let Some(tgt) = target.as_ref() {
463                    let (pattern_str, options) = interpreter.extract_pattern_data(&stmt_box.value);
464                    if let Some(pat) = pattern_str {
465                        // Pass pattern name as source for routing
466                        let previous = interpreter.current_event_node.clone();
467                        interpreter.current_event_node =
468                            interpreter.routed_node_for_alias(name).or(previous.clone());
469                        execute_pattern_with_source(
470                            interpreter,
471                            tgt.as_str(),
472                            &pat,
473                            options,
474                            Some(name),
475                        )?;
476                        interpreter.current_event_node = previous;
477                        return Ok(());
478                    }
479                }
480            }
481        }
482    }
483
484    // If it's a group call, execute the group body
485    if let Some(body) = interpreter.groups.get(name).cloned() {
486        let node_id = interpreter.routed_node_for_alias(name);
487        interpreter.collect_with_event_node(node_id, &body)?;
488        return Ok(());
489    }
490
491    println!(
492        "⚠️  Warning: Group, pattern or function '{}' not found",
493        name
494    );
495    Ok(())
496}
497
498/// Execute a call as an expression and return its resulting Value.
499/// This is similar to `handle_call` but returns the captured `return` value
500/// from a function when present. Groups and patterns return `Value::Null`.
501pub fn call_function(
502    interpreter: &mut AudioInterpreter,
503    name: &str,
504    args: &[Value],
505) -> Result<Value> {
506    // If it's a stored variable that is a function statement, execute and capture return
507    if let Some(var_val) = interpreter.variables.get(name).cloned() {
508        if let Value::Statement(stmt_box) = var_val {
509            if let StatementKind::Function {
510                name: _fname,
511                parameters,
512                body,
513            } = &stmt_box.kind
514            {
515                // create local variable snapshot
516                let vars_snapshot = interpreter.variables.clone();
517
518                // Bind parameters: use provided args (they are already resolved by caller)
519                for (i, param) in parameters.iter().enumerate() {
520                    let bound = args.get(i).cloned().unwrap_or(Value::Null);
521                    // If the bound value is an Identifier, resolve it to its actual value
522                    let bound_val = match bound {
523                        Value::Identifier(ref id) => {
524                            interpreter.resolve_value(&Value::Identifier(id.clone()))?
525                        }
526                        other => other,
527                    };
528                    interpreter.variables.insert(param.clone(), bound_val);
529                }
530
531                // Execute body in function context
532                interpreter.function_call_depth += 1;
533
534                let exec_result = super::collector::collect_events(interpreter, body);
535                interpreter.function_call_depth = interpreter.function_call_depth.saturating_sub(1);
536                exec_result?;
537
538                // Capture return value if present
539                let mut captured_return: Option<Value> = None;
540                if interpreter.returning_flag {
541                    captured_return = interpreter.return_value.clone();
542                    interpreter.returning_flag = false;
543                    interpreter.return_value = None;
544                }
545
546                // Restore variables (local scope ends)
547                interpreter.variables = vars_snapshot;
548
549                if let Some(rv) = captured_return {
550                    return Ok(rv);
551                }
552
553                return Ok(Value::Null);
554            }
555            // Patterns fallthrough to below handling
556            if let StatementKind::Pattern { target, .. } = &stmt_box.kind {
557                if let Some(tgt) = target.as_ref() {
558                    let (pattern_str, options) = interpreter.extract_pattern_data(&stmt_box.value);
559                    if let Some(pat) = pattern_str {
560                        let previous = interpreter.current_event_node.clone();
561                        interpreter.current_event_node =
562                            interpreter.routed_node_for_alias(name).or(previous.clone());
563                        interpreter.execute_pattern_with_source(
564                            tgt.as_str(),
565                            &pat,
566                            options,
567                            Some(name),
568                        )?;
569                        interpreter.current_event_node = previous;
570                        return Ok(Value::Null);
571                    }
572                }
573            }
574        }
575    }
576
577    // If it's a group call, execute and return null
578    if let Some(body) = interpreter.groups.get(name).cloned() {
579        let node_id = interpreter.routed_node_for_alias(name);
580        interpreter.collect_with_event_node(node_id, &body)?;
581        return Ok(Value::Null);
582    }
583
584    println!(
585        "⚠️  Warning: Group, pattern or function '{}' not found",
586        name
587    );
588    Ok(Value::Null)
589}
590
591pub fn execute_print(interpreter: &mut AudioInterpreter, value: &Value) -> Result<()> {
592    let message = match value {
593        Value::Call { name: _, args: _ } => {
594            // Evaluate the call expression and convert the returned value to string
595            let resolved = interpreter.resolve_value(value)?;
596            interpreter.value_to_string(&resolved)
597        }
598
599        Value::String(s) => {
600            if s.contains('{') && s.contains('}') {
601                interpreter.interpolate_string(s)
602            } else {
603                s.clone()
604            }
605        }
606        Value::Identifier(id) => {
607            // Resolve identifier values (supports post-increment shorthand)
608            // Support post-increment shorthand in prints: `i++` should mutate the variable.
609            if id.ends_with("++") {
610                let varname = id[..id.len() - 2].trim();
611                // read current
612                let cur = match interpreter.variables.get(varname) {
613                    Some(Value::Number(n)) => *n as isize,
614                    _ => 0,
615                };
616                // set new value
617                interpreter
618                    .variables
619                    .insert(varname.to_string(), Value::Number((cur + 1) as f32));
620                cur.to_string()
621            } else {
622                // Resolve the identifier using the interpreter resolver so dotted paths
623                // (e.g., mySynth.volume) are properly traversed.
624                let resolved = interpreter.resolve_value(&Value::Identifier(id.clone()))?;
625                match resolved {
626                    Value::String(s) => s.clone(),
627                    Value::Number(n) => n.to_string(),
628                    Value::Boolean(b) => b.to_string(),
629                    Value::Array(arr) => format!("{:?}", arr),
630                    Value::Map(map) => format!("{:?}", map),
631                    Value::Null => format!("Identifier(\"{}\")", id),
632                    other => format!("{:?}", other),
633                }
634            }
635        }
636        Value::Number(n) => n.to_string(),
637        Value::Boolean(b) => b.to_string(),
638        Value::Array(arr) => {
639            // Treat arrays used in print as concatenation parts: resolve each part and join
640            let mut parts = Vec::new();
641            for v in arr.iter() {
642                // If identifier with ++, handle mutation here
643                if let Value::Identifier(idtok) = v {
644                    if idtok.ends_with("++") {
645                        let varname = idtok[..idtok.len() - 2].trim();
646                        let cur = match interpreter.variables.get(varname) {
647                            Some(Value::Number(n)) => *n as isize,
648                            _ => 0,
649                        };
650                        interpreter
651                            .variables
652                            .insert(varname.to_string(), Value::Number((cur + 1) as f32));
653                        parts.push(cur.to_string());
654                        continue;
655                    }
656                }
657                let resolved = interpreter.resolve_value(v)?;
658                parts.push(interpreter.value_to_string(&resolved));
659            }
660            parts.join("")
661        }
662        Value::Map(map) => format!("{:?}", map),
663        _ => format!("{:?}", value),
664    };
665    // Always record prints as scheduled log events so they can be replayed at playback time.
666    // Record the scheduled log message (no debug-origin annotation in production)
667    let log_message = message.clone();
668    // Use the interpreter cursor_time as the event time.
669    interpreter
670        .events
671        .add_log_event(log_message.clone(), interpreter.cursor_time);
672    // If the interpreter is explicitly allowed to print (runtime interpreter), print immediately.
673    // For offline renders (`suppress_print == true`) we do NOT print now; prints are scheduled
674    // into `interpreter.events.logs` and written to a `.printlog` sidecar during the build.
675    // The live playback engine replays that sidecar so prints appear in real time during playback.
676    if !interpreter.suppress_print {
677        // Use the global CLI logger formatting for prints so they appear as [PRINT]
678        // Guard CLI-only logger behind the `cli` feature so WASM builds don't
679        // reference the `crate::tools` module (which is only available for native builds).
680        #[cfg(feature = "cli")]
681        {
682            crate::tools::logger::Logger::new().print(message.clone());
683        }
684
685        // For non-CLI builds (wasm/plugins) try forwarding to the realtime print
686        // channel if provided so prints are surfaced in UIs that consume them.
687        #[cfg(not(feature = "cli"))]
688        {
689            if let Some(tx) = &interpreter.realtime_print_tx {
690                let _ = tx.send((interpreter.cursor_time, log_message.clone()));
691            }
692        }
693    } else {
694        // If printing is suppressed (offline render) but a realtime replay channel was provided,
695        // forward the scheduled print to the replay thread so it can be displayed in real-time
696        // while the offline render proceeds. This is a best-effort path used by some callers
697        // that pipe scheduled prints directly into a playback session (when set).
698        if let Some(tx) = &interpreter.realtime_print_tx {
699            // forward the scheduled log message to realtime replayers (best-effort)
700            let _ = tx.send((interpreter.cursor_time, log_message.clone()));
701        }
702    }
703    Ok(())
704}
705
706pub fn execute_if(
707    interpreter: &mut AudioInterpreter,
708    condition: &Value,
709    body: &[Statement],
710    else_body: &Option<Vec<Statement>>,
711) -> Result<()> {
712    let condition_result = interpreter.evaluate_condition(condition)?;
713
714    if condition_result {
715        super::collector::collect_events(interpreter, body)?;
716    } else if let Some(else_stmts) = else_body {
717        super::collector::collect_events(interpreter, else_stmts)?;
718    }
719
720    Ok(())
721}
722
723pub fn execute_while(
724    interpreter: &mut AudioInterpreter,
725    condition: &Value,
726    body: &[Statement],
727) -> Result<()> {
728    let mut iter_count: usize = 0;
729    let hard_iter_cap: usize = 100_000; // Prevent infinite loops
730
731    loop {
732        let condition_result = interpreter.evaluate_condition(condition)?;
733
734        if !condition_result {
735            break;
736        }
737
738        super::collector::collect_events(interpreter, body)?;
739
740        // Break signalled inside while body -> exit
741        if interpreter.break_flag {
742            interpreter.break_flag = false;
743            break;
744        }
745
746        // Check schedule deadline (from "at X and during Y:")
747        if let Some(deadline) = interpreter.schedule_deadline {
748            if interpreter.cursor_time >= deadline {
749                break;
750            }
751        }
752
753        iter_count = iter_count.saturating_add(1);
754        if iter_count > hard_iter_cap {
755            break;
756        }
757    }
758
759    Ok(())
760}
761
762pub fn execute_event_handlers(interpreter: &mut AudioInterpreter, event_name: &str) -> Result<()> {
763    let handlers = interpreter.event_registry.get_handlers_matching(event_name);
764
765    for (index, handler) in handlers.iter().enumerate() {
766        // If handler has args, allow numeric interval to gate execution for 'beat'/'bar'
767        if let Some(args) = &handler.args {
768            if let Some(num_val) = args.iter().find(|v| matches!(v, Value::Number(_))) {
769                if let Value::Number(n) = num_val {
770                    let interval = (*n as usize).max(1);
771                    if event_name == "beat" {
772                        let cur = interpreter.special_vars.current_beat.floor() as usize;
773                        if cur % interval != 0 {
774                            continue;
775                        }
776                    } else if event_name == "bar" {
777                        let cur = interpreter.special_vars.current_bar.floor() as usize;
778                        if cur % interval != 0 {
779                            continue;
780                        }
781                    }
782                }
783            }
784        }
785
786        if handler.once
787            && !interpreter
788                .event_registry
789                .should_execute_once(event_name, index)
790        {
791            continue;
792        }
793
794        let body_clone = handler.body.clone();
795        super::collector::collect_events(interpreter, &body_clone)?;
796    }
797
798    Ok(())
799}
800
801pub fn handle_assign(
802    interpreter: &mut AudioInterpreter,
803    target: &str,
804    property: &str,
805    value: &Value,
806) -> Result<()> {
807    // Support dotted targets like "myTrigger.effects.reverb" as the target string.
808    // The parser may pass the full path as `target` and the last path segment as `property`.
809    if target.contains('.') {
810        let parts: Vec<&str> = target.split('.').collect();
811        let root = parts[0];
812        if let Some(root_val) = interpreter.variables.get_mut(root) {
813            // Traverse into nested maps to reach the parent map where to insert `property`.
814            let mut current = root_val;
815            for seg in parts.iter().skip(1) {
816                match current {
817                    Value::Map(map) => {
818                        if !map.contains_key(*seg) {
819                            // create nested map if missing
820                            map.insert((*seg).to_string(), Value::Map(HashMap::new()));
821                        }
822                        current = map.get_mut(*seg).ok_or_else(|| {
823                            anyhow::anyhow!(
824                                "Failed to access nested segment '{}' while assigning to '{}'",
825                                seg,
826                                target
827                            )
828                        })?;
829                    }
830                    _ => {
831                        return Err(anyhow::anyhow!(
832                            "Cannot traverse into non-map segment '{}' when assigning to '{}'",
833                            seg,
834                            target
835                        ));
836                    }
837                }
838            }
839
840            // Now `current` should be a Value::Map where we insert `property`.
841            if let Value::Map(map) = current {
842                map.insert(property.to_string(), value.clone());
843
844                // If the root object is a synth, update its synth definition
845                if interpreter.events.synths.contains_key(root) {
846                    if let Some(Value::Map(root_map)) = interpreter.variables.get(root) {
847                        let map_clone = root_map.clone();
848                        let updated_def = interpreter.extract_synth_def_from_map(&map_clone)?;
849                        interpreter
850                            .events
851                            .synths
852                            .insert(root.to_string(), updated_def);
853                    }
854                }
855            } else {
856                return Err(anyhow::anyhow!(
857                    "Cannot assign property '{}' to non-map target '{}'",
858                    property,
859                    target
860                ));
861            }
862        } else {
863            return Err(anyhow::anyhow!("Variable '{}' not found", root));
864        }
865    } else {
866        if let Some(var) = interpreter.variables.get_mut(target) {
867            if let Value::Map(map) = var {
868                map.insert(property.to_string(), value.clone());
869
870                if interpreter.events.synths.contains_key(target) {
871                    let map_clone = map.clone();
872                    let updated_def = interpreter.extract_synth_def_from_map(&map_clone)?;
873                    interpreter
874                        .events
875                        .synths
876                        .insert(target.to_string(), updated_def);
877                }
878            } else {
879                return Err(anyhow::anyhow!(
880                    "Cannot assign property '{}' to non-map variable '{}'",
881                    property,
882                    target
883                ));
884            }
885        } else {
886            return Err(anyhow::anyhow!("Variable '{}' not found", target));
887        }
888    }
889
890    Ok(())
891}
892
893pub fn extract_synth_def_from_map(
894    interpreter: &AudioInterpreter,
895    map: &HashMap<String, Value>,
896) -> Result<crate::engine::audio::events::SynthDefinition> {
897    use crate::engine::audio::events::extract_filters;
898    use crate::engine::audio::lfo::{LfoParams, LfoRate, LfoTarget, LfoWaveform};
899
900    // v0.2.0: Warn if synth <waveform> shorthand was used
901    if let Some(Value::Boolean(true)) = map.get("_deprecated_shorthand") {
902        eprintln!(
903            "⚠️  WARNING: synth <waveform> shorthand is deprecated in v0.2.0.\n   \
904             Use arrow chain instead: synth -> wave(\"<waveform>\") -> preset(...)\n   \
905             Example: synth -> wave(\"saw\") -> preset(\"bass\")"
906        );
907    }
908
909    let waveform = crate::engine::audio::events::extract_string(map, "waveform", "sine");
910    let attack = crate::engine::audio::events::extract_number(map, "attack", 0.01);
911    let decay = crate::engine::audio::events::extract_number(map, "decay", 0.1);
912    let sustain = crate::engine::audio::events::extract_number(map, "sustain", 0.7);
913    let release = crate::engine::audio::events::extract_number(map, "release", 0.2);
914
915    // Accept both String and Identifier for type (and synth_type alias)
916    let synth_type = if let Some(v) = map.get("type") {
917        match v {
918            Value::String(t) => {
919                let clean = t.trim_matches('"').trim_matches('\'');
920                if clean.is_empty() || clean == "synth" {
921                    None
922                } else {
923                    Some(clean.to_string())
924                }
925            }
926            Value::Identifier(id) => {
927                let clean = id.trim_matches('"').trim_matches('\'');
928                if clean.is_empty() || clean == "synth" {
929                    None
930                } else {
931                    Some(clean.to_string())
932                }
933            }
934            _ => None,
935        }
936    } else if let Some(v2) = map.get("synth_type") {
937        match v2 {
938            Value::String(t2) => {
939                let clean = t2.trim_matches('"').trim_matches('\'');
940                if clean.is_empty() || clean == "synth" {
941                    None
942                } else {
943                    Some(clean.to_string())
944                }
945            }
946            Value::Identifier(id2) => {
947                let clean = id2.trim_matches('"').trim_matches('\'');
948                if clean.is_empty() || clean == "synth" {
949                    None
950                } else {
951                    Some(clean.to_string())
952                }
953            }
954            _ => None,
955        }
956    } else {
957        None
958    };
959
960    let filters = if let Some(Value::Array(filters_arr)) = map.get("filters") {
961        extract_filters(filters_arr)
962    } else {
963        Vec::new()
964    };
965
966    let plugin_author = if let Some(Value::String(s)) = map.get("plugin_author") {
967        Some(s.clone())
968    } else {
969        None
970    };
971    let plugin_name = if let Some(Value::String(s)) = map.get("plugin_name") {
972        Some(s.clone())
973    } else {
974        None
975    };
976    let plugin_export = if let Some(Value::String(s)) = map.get("plugin_export") {
977        Some(s.clone())
978    } else {
979        None
980    };
981
982    // Extract LFO configuration if present
983    let lfo = if let Some(Value::Map(lfo_map)) = map.get("lfo") {
984        // Parse rate (Hz or tempo-synced like "1/4")
985        let rate_str = if let Some(Value::Number(n)) = lfo_map.get("rate") {
986            n.to_string()
987        } else if let Some(Value::String(s)) = lfo_map.get("rate") {
988            s.clone()
989        } else {
990            "5.0".to_string() // Default rate
991        };
992        let rate = LfoRate::from_value(&rate_str);
993
994        // Parse depth (0-1)
995        let depth = if let Some(Value::Number(n)) = lfo_map.get("depth") {
996            (*n).clamp(0.0, 1.0)
997        } else {
998            0.5 // Default depth
999        };
1000
1001        // Parse waveform (sine, triangle, square, saw)
1002        let waveform_str = if let Some(Value::String(s)) = lfo_map.get("shape") {
1003            s.clone()
1004        } else if let Some(Value::String(s)) = lfo_map.get("waveform") {
1005            s.clone()
1006        } else {
1007            "sine".to_string() // Default waveform
1008        };
1009        let waveform = LfoWaveform::from_str(&waveform_str);
1010
1011        // Parse target (volume, pitch, filter, pan)
1012        let target = if let Some(Value::String(s)) = lfo_map.get("target") {
1013            LfoTarget::from_str(s).unwrap_or(LfoTarget::Volume)
1014        } else {
1015            LfoTarget::Volume // Default target
1016        };
1017
1018        // Parse initial phase (0-1)
1019        let phase = if let Some(Value::Number(n)) = lfo_map.get("phase") {
1020            (*n).fract().abs() // Ensure 0-1 range
1021        } else {
1022            0.0 // Default phase
1023        };
1024
1025        Some(LfoParams {
1026            rate,
1027            depth,
1028            waveform,
1029            target,
1030            phase,
1031        })
1032    } else {
1033        None
1034    };
1035
1036    let mut options = HashMap::new();
1037
1038    // List of SYNTH ADSR keys (standard parameters, not plugin options)
1039    let synth_keys = [
1040        "waveform",
1041        "attack",
1042        "decay",
1043        "sustain",
1044        "release",
1045        "type",
1046        "filters",
1047        "plugin_author",
1048        "plugin_name",
1049        "plugin_export",
1050        "lfo",
1051    ];
1052
1053    // List of AUDIO parameters (applied post-render, not plugin options)
1054    let audio_keys = [
1055        "pan",
1056        "gain",
1057        "volume",
1058        "detune",
1059        "spread",
1060        "synth_type", // Alias for type
1061        "chain",      // Effects chain
1062        "effects",    // Effects (deprecated name)
1063    ];
1064
1065    for (key, val) in map.iter() {
1066        // Skip all synth standard keys AND audio parameters
1067        if synth_keys.contains(&key.as_str()) || audio_keys.contains(&key.as_str()) {
1068            continue;
1069        }
1070
1071        // Skip keys starting with underscore (internal keys)
1072        if key.starts_with("_") {
1073            continue;
1074        }
1075
1076        // Now add everything else to options (plugin parameters)
1077        if let Value::Number(n) = val {
1078            options.insert(key.clone(), *n);
1079        } else if let Value::String(s) = val {
1080            // Try to parse string numbers
1081            if let Ok(n) = s.parse::<f32>() {
1082                options.insert(key.clone(), n);
1083            }
1084        }
1085    }
1086
1087    Ok(crate::engine::audio::events::SynthDefinition {
1088        waveform,
1089        attack,
1090        decay,
1091        sustain,
1092        release,
1093        synth_type,
1094        filters,
1095        options,
1096        plugin_author,
1097        plugin_name,
1098        plugin_export,
1099        lfo,
1100        bpm: interpreter.bpm,
1101    })
1102}
1103
1104pub fn handle_load(interpreter: &mut AudioInterpreter, source: &str, alias: &str) -> Result<()> {
1105    use std::path::Path;
1106
1107    let path = Path::new(source);
1108    // Determine extension
1109    if let Some(ext) = path
1110        .extension()
1111        .and_then(|s| s.to_str())
1112        .map(|s| s.to_lowercase())
1113    {
1114        match ext.as_str() {
1115            "mid" | "midi" => {
1116                use crate::engine::audio::midi::load_midi_file;
1117                let midi_data = load_midi_file(path)?;
1118                interpreter.variables.insert(alias.to_string(), midi_data);
1119                // MIDI file loaded (silent)
1120                Ok(())
1121            }
1122            "wav" | "flac" | "mp3" | "ogg" => {
1123                // For native/CLI builds, register sample via the samples subsystem.
1124                #[cfg(feature = "cli")]
1125                {
1126                    use crate::engine::audio::samples;
1127                    let registered = samples::register_sample_from_path(path)?;
1128                    // Record the sample URI under the alias variable as a string (consistent with triggers)
1129                    interpreter
1130                        .variables
1131                        .insert(alias.to_string(), Value::String(registered.clone()));
1132                    // Sample file loaded (silent)
1133                    return Ok(());
1134                }
1135
1136                // For non-CLI builds (WASM/plugins), fallback to storing the original path as a string.
1137                #[cfg(not(feature = "cli"))]
1138                {
1139                    interpreter
1140                        .variables
1141                        .insert(alias.to_string(), Value::String(source.to_string()));
1142                    return Ok(());
1143                }
1144            }
1145            _ => Err(anyhow::anyhow!("Unsupported file type for @load: {}", ext)),
1146        }
1147    } else {
1148        Err(anyhow::anyhow!(
1149            "Cannot determine file extension for {}",
1150            source
1151        ))
1152    }
1153}
1154
1155pub fn handle_bind(
1156    interpreter: &mut AudioInterpreter,
1157    source: &str,
1158    target: &str,
1159    options: &Value,
1160) -> Result<()> {
1161    use std::collections::HashMap as StdHashMap;
1162
1163    // Support bindings that reference runtime MIDI device mappings like:
1164    //   bind myKickPattern -> mapping.out.myDeviceA with { port: 1, channel: 10 }
1165    //   bind mapping.in.myDeviceB with { port: 2, channel: 10 } -> mySynth
1166    // When a 'mapping.*' path is used, register a lightweight mapping entry in the
1167    // interpreter variables and expose convenience variables:
1168    //   mapping.<in|out>.<device>.<noteOn|noteOff|rest>
1169    if source.starts_with("mapping.") || target.starts_with("mapping.") {
1170        // extract options if present
1171        let opts_map: StdHashMap<String, Value> = if let Value::Map(m) = options {
1172            m.clone()
1173        } else {
1174            StdHashMap::new()
1175        };
1176
1177        // Helper function to create mapping variables and bookkeeping
1178        fn create_and_insert(
1179            path: &str,
1180            opts_map: &StdHashMap<String, Value>,
1181            interpreter: &mut AudioInterpreter,
1182        ) -> Option<(String, String)> {
1183            let parts: Vec<&str> = path.split('.').collect();
1184            if parts.len() >= 3 {
1185                let direction = parts[1]; // "in" or "out"
1186                let device = parts[2];
1187
1188                let mut map = StdHashMap::new();
1189                map.insert(
1190                    "_type".to_string(),
1191                    Value::String("midi_mapping".to_string()),
1192                );
1193                map.insert(
1194                    "direction".to_string(),
1195                    Value::String(direction.to_string()),
1196                );
1197                map.insert("device".to_string(), Value::String(device.to_string()));
1198
1199                // merge provided options
1200                for (k, v) in opts_map.iter() {
1201                    map.insert(k.clone(), v.clone());
1202                }
1203
1204                // Ensure mapping defaults
1205                crate::utils::props::ensure_default_properties(&mut map, Some("mapping"));
1206
1207                interpreter
1208                    .variables
1209                    .insert(path.to_string(), Value::Map(map.clone()));
1210
1211                // Expose event variables for convenience
1212                let note_on = format!("mapping.{}.{}.noteOn", direction, device);
1213                let note_off = format!("mapping.{}.{}.noteOff", direction, device);
1214                let rest = format!("mapping.{}.{}.rest", direction, device);
1215                interpreter
1216                    .variables
1217                    .insert(note_on.clone(), Value::String(note_on.clone()));
1218                interpreter
1219                    .variables
1220                    .insert(note_off.clone(), Value::String(note_off.clone()));
1221                interpreter
1222                    .variables
1223                    .insert(rest.clone(), Value::String(rest.clone()));
1224
1225                return Some((direction.to_string(), device.to_string()));
1226            }
1227            None
1228        }
1229
1230        // If source is mapping.* (incoming mapping binds to target instrument)
1231        if source.starts_with("mapping.") {
1232            if let Some((direction, device)) = create_and_insert(source, &opts_map, interpreter) {
1233                // Record association when target is not also mapping.*
1234                if !target.starts_with("mapping.") {
1235                    let mut bmap = StdHashMap::new();
1236                    bmap.insert("instrument".to_string(), Value::String(target.to_string()));
1237                    bmap.insert("direction".to_string(), Value::String(direction.clone()));
1238                    bmap.insert("device".to_string(), Value::String(device.clone()));
1239                    for (k, v) in opts_map.iter() {
1240                        bmap.insert(k.clone(), v.clone());
1241                    }
1242                    interpreter
1243                        .variables
1244                        .insert(format!("__mapping_bind::{}", source), Value::Map(bmap));
1245
1246                    // If we have a midi_manager, try to open the input port (for incoming mappings)
1247                    #[cfg(feature = "cli")]
1248                    if let Some(manager) = &mut interpreter.midi_manager {
1249                        if let Some(Value::Number(port_num)) = opts_map.get("port") {
1250                            let idx = *port_num as usize;
1251                            if let Ok(mut mgr) = manager.lock() {
1252                                // use device as name for identification
1253                                let _ = mgr.open_input_by_index(idx, &device);
1254                            }
1255                        }
1256                    }
1257                }
1258            }
1259        }
1260
1261        // If target is mapping.* (binding a sequence/instrument to an external device)
1262        if target.starts_with("mapping.") {
1263            if let Some((direction, device)) = create_and_insert(target, &opts_map, interpreter) {
1264                if !source.starts_with("mapping.") {
1265                    let mut bmap = StdHashMap::new();
1266                    bmap.insert("source".to_string(), Value::String(source.to_string()));
1267                    bmap.insert("direction".to_string(), Value::String(direction.clone()));
1268                    bmap.insert("device".to_string(), Value::String(device.clone()));
1269                    for (k, v) in opts_map.iter() {
1270                        bmap.insert(k.clone(), v.clone());
1271                    }
1272                    interpreter
1273                        .variables
1274                        .insert(format!("__mapping_bind::{}", target), Value::Map(bmap));
1275
1276                    // If we have a midi_manager, try to open the output port (for outgoing mappings)
1277                    #[cfg(feature = "cli")]
1278                    if let Some(manager) = &mut interpreter.midi_manager {
1279                        if let Some(Value::Number(port_num)) = opts_map.get("port") {
1280                            let idx = *port_num as usize;
1281                            if let Ok(mut mgr) = manager.lock() {
1282                                let _ = mgr.open_output_by_name(&device, idx);
1283                            }
1284                        }
1285                    }
1286                }
1287            }
1288        }
1289
1290        // Nothing more to schedule here at audio event level; actual MIDI I/O handlers
1291        // will be responsible for reacting to incoming messages and emitting events into
1292        // the interpreter event registry, and for flushing outgoing bound sequences to
1293        // MIDI device ports when appropriate.
1294        return Ok(());
1295    }
1296
1297    // Fallback: existing behaviour (binding MIDI file data to a synth)
1298    let midi_data = interpreter
1299        .variables
1300        .get(source)
1301        .ok_or_else(|| anyhow::anyhow!("MIDI source '{}' not found", source))?
1302        .clone();
1303
1304    if let Value::Map(midi_map) = &midi_data {
1305        let notes = midi_map
1306            .get("notes")
1307            .ok_or_else(|| anyhow::anyhow!("MIDI data has no notes"))?;
1308
1309        if let Value::Array(notes_array) = notes {
1310            let _synth_def = interpreter
1311                .events
1312                .synths
1313                .get(target)
1314                .ok_or_else(|| anyhow::anyhow!("Synth '{}' not found", target))?
1315                .clone();
1316
1317            let default_velocity = 100;
1318            let mut velocity = default_velocity;
1319
1320            if let Value::Map(opts) = options {
1321                if let Some(Value::Number(v)) = opts.get("velocity") {
1322                    velocity = *v as u8;
1323                }
1324            }
1325
1326            // Determine MIDI file BPM (if present) so we can rescale times to interpreter BPM
1327            // Default to interpreter.bpm when the MIDI file has no BPM metadata
1328            let midi_bpm =
1329                crate::engine::audio::events::extract_number(midi_map, "bpm", interpreter.bpm);
1330
1331            for note_val in notes_array {
1332                if let Value::Map(note_map) = note_val {
1333                    let time = crate::engine::audio::events::extract_number(note_map, "time", 0.0);
1334                    let note =
1335                        crate::engine::audio::events::extract_number(note_map, "note", 60.0) as u8;
1336                    let note_velocity = crate::engine::audio::events::extract_number(
1337                        note_map,
1338                        "velocity",
1339                        velocity as f32,
1340                    ) as u8;
1341                    // Duration may be present (ms) from MIDI loader; fallback to 500 ms
1342                    let duration_ms =
1343                        crate::engine::audio::events::extract_number(note_map, "duration", 500.0);
1344
1345                    use crate::engine::audio::events::AudioEvent;
1346                    let synth_def = interpreter
1347                        .events
1348                        .get_synth(target)
1349                        .cloned()
1350                        .unwrap_or_default();
1351                    // Rescale times according to interpreter BPM vs MIDI file BPM.
1352                    // If midi_bpm == interpreter.bpm this is a no-op. We compute factor = midi_bpm / interpreter.bpm
1353                    let interp_bpm = interpreter.bpm;
1354                    let factor = if interp_bpm > 0.0 {
1355                        midi_bpm / interp_bpm
1356                    } else {
1357                        1.0
1358                    };
1359
1360                    let start_time_s = (time / 1000.0) * factor;
1361                    let duration_s = (duration_ms / 1000.0) * factor;
1362
1363                    let event = AudioEvent::Note {
1364                        node_id: interpreter.resolve_event_node_id(target),
1365                        midi: note,
1366                        start_time: start_time_s,
1367                        duration: duration_s,
1368                        velocity: note_velocity as f32,
1369                        synth_id: target.to_string(),
1370                        synth_def,
1371                        pan: 0.0,
1372                        detune: 0.0,
1373                        gain: 1.0,
1374                        attack: None,
1375                        release: None,
1376                        delay_time: None,
1377                        delay_feedback: None,
1378                        delay_mix: None,
1379                        reverb_amount: None,
1380                        drive_amount: None,
1381                        drive_color: None,
1382                        effects: None,
1383                        use_per_note_automation: false,
1384                    };
1385
1386                    // bound note scheduled
1387                    interpreter.events.events.push(event);
1388                }
1389            }
1390
1391            // Bound notes from source to target
1392        }
1393    }
1394
1395    Ok(())
1396}
1397
1398#[cfg(feature = "cli")]
1399pub fn handle_use_plugin(
1400    interpreter: &mut AudioInterpreter,
1401    author: &str,
1402    name: &str,
1403    alias: &str,
1404) -> Result<()> {
1405    use crate::engine::plugin::loader::load_plugin;
1406
1407    match load_plugin(author, name) {
1408        Ok((info, _wasm_bytes)) => {
1409            let mut plugin_map = HashMap::new();
1410            plugin_map.insert("_type".to_string(), Value::String("plugin".to_string()));
1411            plugin_map.insert("_author".to_string(), Value::String(info.author.clone()));
1412            plugin_map.insert("_name".to_string(), Value::String(info.name.clone()));
1413
1414            if let Some(version) = &info.version {
1415                plugin_map.insert("_version".to_string(), Value::String(version.clone()));
1416            }
1417
1418            for export in &info.exports {
1419                let mut export_map = HashMap::new();
1420                export_map.insert(
1421                    "_plugin_author".to_string(),
1422                    Value::String(info.author.clone()),
1423                );
1424                export_map.insert("_plugin_name".to_string(), Value::String(info.name.clone()));
1425                export_map.insert(
1426                    "_export_name".to_string(),
1427                    Value::String(export.name.clone()),
1428                );
1429                export_map.insert(
1430                    "_export_kind".to_string(),
1431                    Value::String(export.kind.clone()),
1432                );
1433
1434                plugin_map.insert(export.name.clone(), Value::Map(export_map));
1435            }
1436            // Ensure plugin map has default properties available for dotted access
1437            crate::utils::props::ensure_default_properties(&mut plugin_map, Some("plugin"));
1438
1439            interpreter
1440                .variables
1441                .insert(alias.to_string(), Value::Map(plugin_map));
1442        }
1443        Err(e) => {
1444            eprintln!("❌ Failed to load plugin {}.{}: {}", author, name, e);
1445            return Err(anyhow::anyhow!("Failed to load plugin: {}", e));
1446        }
1447    }
1448
1449    Ok(())
1450}
1451
1452#[cfg(not(feature = "cli"))]
1453pub fn handle_use_plugin(
1454    interpreter: &mut AudioInterpreter,
1455    author: &str,
1456    name: &str,
1457    alias: &str,
1458) -> Result<()> {
1459    // Plugin loading not supported in this build (WASM/plugin builds). Insert a minimal placeholder so scripts can still reference the alias.
1460    let mut plugin_map = HashMap::new();
1461    plugin_map.insert(
1462        "_type".to_string(),
1463        Value::String("plugin_stub".to_string()),
1464    );
1465    plugin_map.insert("_author".to_string(), Value::String(author.to_string()));
1466    plugin_map.insert("_name".to_string(), Value::String(name.to_string()));
1467    // Ensure stubs also expose defaults for dotted access
1468    crate::utils::props::ensure_default_properties(&mut plugin_map, Some("plugin"));
1469
1470    interpreter
1471        .variables
1472        .insert(alias.to_string(), Value::Map(plugin_map));
1473    Ok(())
1474}
1475
1476pub fn handle_bank(
1477    interpreter: &mut AudioInterpreter,
1478    name: &str,
1479    alias: &Option<String>,
1480) -> Result<()> {
1481    let target_alias = alias
1482        .clone()
1483        .unwrap_or_else(|| name.split('.').last().unwrap_or(name).to_string());
1484
1485    if let Some(existing_value) = interpreter.variables.get(name) {
1486        interpreter
1487            .variables
1488            .insert(target_alias.clone(), existing_value.clone());
1489    } else {
1490        #[cfg(feature = "wasm")]
1491        {
1492            use crate::web::registry::banks::REGISTERED_BANKS;
1493            REGISTERED_BANKS.with(|banks| {
1494                for bank in banks.borrow().iter() {
1495                    if bank.full_name == *name {
1496                        if let Some(Value::Map(bank_map)) = interpreter.variables.get(&bank.alias) {
1497                            interpreter
1498                                .variables
1499                                .insert(target_alias.clone(), Value::Map(bank_map.clone()));
1500                        }
1501                    }
1502                }
1503            });
1504        }
1505
1506        #[cfg(not(feature = "wasm"))]
1507        {
1508            if let Ok(current_dir) = std::env::current_dir() {
1509                match interpreter.banks.register_bank(
1510                    target_alias.clone(),
1511                    &name,
1512                    &current_dir,
1513                    &current_dir,
1514                ) {
1515                    Ok(_) => {
1516                        let mut bank_map = HashMap::new();
1517                        bank_map.insert("_name".to_string(), Value::String(name.to_string()));
1518                        bank_map.insert("_alias".to_string(), Value::String(target_alias.clone()));
1519                        interpreter
1520                            .variables
1521                            .insert(target_alias.clone(), Value::Map(bank_map));
1522                    }
1523                    Err(e) => {
1524                        eprintln!("⚠️ Failed to register bank '{}': {}", name, e);
1525                        let mut bank_map = HashMap::new();
1526                        bank_map.insert("_name".to_string(), Value::String(name.to_string()));
1527                        bank_map.insert("_alias".to_string(), Value::String(target_alias.clone()));
1528                        interpreter
1529                            .variables
1530                            .insert(target_alias.clone(), Value::Map(bank_map));
1531                    }
1532                }
1533            } else {
1534                let mut bank_map = HashMap::new();
1535                bank_map.insert("_name".to_string(), Value::String(name.to_string()));
1536                bank_map.insert("_alias".to_string(), Value::String(target_alias.clone()));
1537                interpreter
1538                    .variables
1539                    .insert(target_alias.clone(), Value::Map(bank_map));
1540                eprintln!(
1541                    "⚠️ Could not determine cwd to register bank '{}', registered minimal alias.",
1542                    name
1543                );
1544            }
1545        }
1546    }
1547
1548    Ok(())
1549}
1550
1551pub fn handle_trigger(
1552    interpreter: &mut AudioInterpreter,
1553    entity: &str,
1554    effects: Option<&crate::language::syntax::ast::Value>,
1555) -> Result<()> {
1556    let resolved_entity = if entity.starts_with('.') {
1557        &entity[1..]
1558    } else {
1559        entity
1560    };
1561
1562    // If this resolved entity refers to a variable that contains a Trigger statement,
1563    // execute that stored trigger instead (supports: let t = .bank.kick -> reverse(true); .t )
1564    if let Some(var_val) = interpreter.variables.get(resolved_entity).cloned() {
1565        if let crate::language::syntax::ast::Value::Statement(stmt_box) = var_val {
1566            if let crate::language::syntax::ast::StatementKind::Trigger {
1567                entity: inner_entity,
1568                duration: _,
1569                effects: stored_effects,
1570            } = &stmt_box.kind
1571            {
1572                // Avoid direct recursion if someone stored a trigger that points to itself
1573                if inner_entity != resolved_entity {
1574                    // Prefer stored effects when present, otherwise fall back to effects passed in
1575                    let chosen_effects = stored_effects.as_ref().or(effects);
1576                    return handle_trigger(interpreter, inner_entity, chosen_effects);
1577                } else {
1578                    return Ok(());
1579                }
1580            }
1581        }
1582    }
1583
1584    if resolved_entity.contains('.') {
1585        let parts: Vec<&str> = resolved_entity.split('.').collect();
1586        if parts.len() == 2 {
1587            let (var_name, property) = (parts[0], parts[1]);
1588
1589            if let Some(Value::Map(map)) = interpreter.variables.get(var_name) {
1590                if let Some(Value::String(sample_uri)) = map.get(property) {
1591                    let uri = sample_uri.trim_matches('"').trim_matches('\'');
1592                    // scheduling sample at current cursor_time
1593                    interpreter.events.add_sample_event_with_effects(
1594                        uri,
1595                        interpreter.cursor_time,
1596                        1.0,
1597                        effects.cloned(),
1598                    );
1599                    let beat_duration = interpreter.beat_duration();
1600                    interpreter.cursor_time += beat_duration;
1601                } else {
1602                    #[cfg(not(feature = "wasm"))]
1603                    {
1604                        // First try to produce an internal devalang://bank URI (preferred, supports lazy loading)
1605                        let resolved_uri = interpreter.resolve_sample_uri(resolved_entity);
1606                        if resolved_uri != resolved_entity {
1607                            // scheduling resolved sample
1608                            interpreter.events.add_sample_event_with_effects(
1609                                &resolved_uri,
1610                                interpreter.cursor_time,
1611                                1.0,
1612                                effects.cloned(),
1613                            );
1614                            let beat_duration = interpreter.beat_duration();
1615                            interpreter.cursor_time += beat_duration;
1616                        } else if let Some(pathbuf) =
1617                            interpreter.banks.resolve_trigger(var_name, property)
1618                        {
1619                            if let Some(path_str) = pathbuf.to_str() {
1620                                // scheduling sample via bank path
1621                                interpreter.events.add_sample_event_with_effects(
1622                                    path_str,
1623                                    interpreter.cursor_time,
1624                                    1.0,
1625                                    effects.cloned(),
1626                                );
1627                                let beat_duration = interpreter.beat_duration();
1628                                interpreter.cursor_time += beat_duration;
1629                            } else {
1630                                println!(
1631                                    "⚠️ Resolution failed for {}.{} (invalid path)",
1632                                    var_name, property
1633                                );
1634                            }
1635                        } else {
1636                            // no path found in BankRegistry
1637                        }
1638                    }
1639                }
1640            }
1641        }
1642    } else {
1643        if let Some(Value::String(sample_uri)) = interpreter.variables.get(resolved_entity) {
1644            let uri = sample_uri.trim_matches('"').trim_matches('\'');
1645            interpreter.events.add_sample_event_with_effects(
1646                uri,
1647                interpreter.cursor_time,
1648                1.0,
1649                effects.cloned(),
1650            );
1651            let beat_duration = interpreter.beat_duration();
1652            interpreter.cursor_time += beat_duration;
1653        }
1654    }
1655
1656    // Note: do not call interpreter.render_audio() here - rendering is handled by the build pipeline.
1657    // Trigger queued for rendering (events collected)
1658
1659    Ok(())
1660}
1661
1662/// Handle fade statement: automate a parameter in or out over a duration
1663pub fn handle_fade(
1664    interpreter: &mut AudioInterpreter,
1665    direction: &str,
1666    target: &str,
1667    duration: &crate::language::syntax::ast::DurationValue,
1668) -> Result<()> {
1669    // Parse the target (entity.property format, with optional leading dot)
1670    let normalized_target = if target.starts_with('.') {
1671        &target[1..] // Remove leading dot if present
1672    } else {
1673        target
1674    };
1675
1676    let parts: Vec<&str> = normalized_target.split('.').collect();
1677    if parts.len() != 2 {
1678        return Err(anyhow::anyhow!(
1679            "Fade target must be in format 'entity.property', got '{}'",
1680            target
1681        ));
1682    }
1683
1684    let entity = parts[0];
1685    let property = parts[1];
1686
1687    let dur_secs = interpreter.duration_to_seconds(duration)?;
1688
1689    // Determine start and end values based on direction
1690    let (from_value, to_value) = match direction.to_lowercase().as_str() {
1691        "in" => (0.0, 1.0),  // Fade in: 0 -> 1
1692        "out" => (1.0, 0.0), // Fade out: 1 -> 0
1693        _ => {
1694            return Err(anyhow::anyhow!(
1695                "Fade direction must be 'in' or 'out', got '{}'",
1696                direction
1697            ));
1698        }
1699    };
1700
1701    // Create an automation parameter that modulates the target property
1702    // We'll add a fade event to the automation registry
1703    let mut fade_envelope = crate::engine::audio::automation::AutomationEnvelope::new(format!(
1704        "{}.{}",
1705        entity, property
1706    ));
1707
1708    // Use Linear curve for fade
1709    use crate::engine::audio::automation::AutomationCurve;
1710    fade_envelope.add_param(crate::engine::audio::automation::AutomationParam {
1711        param_name: property.to_string(),
1712        from_value,
1713        to_value,
1714        start_time: interpreter.cursor_time,
1715        duration: dur_secs,
1716        curve: AutomationCurve::Linear,
1717    });
1718
1719    interpreter.automation_registry.register(fade_envelope);
1720
1721    // Advance cursor time by fade duration
1722    interpreter.cursor_time += dur_secs;
1723
1724    Ok(())
1725}
1726
1727/// Handle stop statement: cease audio playback
1728pub fn handle_stop(
1729    interpreter: &mut AudioInterpreter,
1730    target: Option<&str>,
1731    delay: f32,
1732) -> Result<()> {
1733    // Create a stop event at the absolute time (delay from start, not from cursor_time)
1734    // Stop is a global instruction that applies from absolute position 0
1735    let stop_time = delay;
1736
1737    if let Some(entity_name) = target {
1738        // Stop specific entity: create a filter event that silences this entity
1739        let stop_event = AudioEvent::Stop {
1740            target: Some(entity_name.to_string()),
1741            time: stop_time,
1742        };
1743        interpreter.events.events.push(stop_event);
1744    } else {
1745        // Stop all: create a master stop event
1746        let stop_event = AudioEvent::Stop {
1747            target: None,
1748            time: stop_time,
1749        };
1750        interpreter.events.events.push(stop_event);
1751    }
1752
1753    Ok(())
1754}
1755
1756#[cfg(test)]
1757mod tests {
1758    use super::*;
1759
1760    #[test]
1761    fn test_bare_synth_with_preset_registers_synth() {
1762        let mut interpreter = AudioInterpreter::new(44_100);
1763        let mut map = HashMap::new();
1764        map.insert("type".to_string(), Value::String("synth".to_string()));
1765        map.insert("synth_type".to_string(), Value::String("bass".to_string()));
1766
1767        handle_let(&mut interpreter, "bass", &Value::Map(map)).unwrap();
1768
1769        let synth = interpreter
1770            .events
1771            .get_synth("bass")
1772            .expect("bare synth with preset should be registered");
1773        assert_eq!(synth.waveform, "sine");
1774        assert_eq!(synth.synth_type.as_deref(), Some("bass"));
1775    }
1776}