Skip to main content

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

1use crate::language::syntax::ast::{Statement, StatementKind, Value};
2use crate::language::syntax::parser::driver::effects::parse_chained_effects;
3use anyhow::{Result, anyhow};
4
5pub fn parse_routing_command(line_number: usize) -> Result<Statement> {
6    // The "routing" keyword itself indicates a block-based routing declaration
7    // The body will be filled in during indentation parsing in the main driver
8    Ok(Statement::new(
9        StatementKind::Routing { body: Vec::new() },
10        Value::Null,
11        0,
12        line_number,
13        1,
14    ))
15}
16
17/// Parse routing block statements (node, fx, route, duck, sidechain)
18pub fn parse_routing_statement<'a>(line: &str, line_number: usize) -> Result<Statement> {
19    let trimmed = line.trim();
20
21    // node <name> [= <alias>]
22    if trimmed.starts_with("node ") {
23        let rest = trimmed[5..].trim();
24        if let Some((name, alias)) = rest.split_once('=') {
25            let name = name.trim().trim_end_matches(':').to_string();
26            let alias = alias.trim().trim_end_matches(':').to_string();
27            return Ok(Statement::new(
28                StatementKind::RoutingNode {
29                    name,
30                    alias: Some(alias),
31                },
32                Value::Null,
33                0,
34                line_number,
35                1,
36            ));
37        } else {
38            let name = rest.trim_end_matches(':').to_string();
39            return Ok(Statement::new(
40                StatementKind::RoutingNode { name, alias: None },
41                Value::Null,
42                0,
43                line_number,
44                1,
45            ));
46        }
47    }
48
49    // fx <target> -> effect1 -> effect2 ...
50    if trimmed.starts_with("fx ") {
51        let rest = trimmed[3..].trim();
52        if let Some((target, effects_str)) = rest.split_once("->") {
53            let target = target.trim().to_string();
54            let effects_chain = format!("->{}", effects_str);
55            let effects = parse_chained_effects(&effects_chain)?;
56            return Ok(Statement::new(
57                StatementKind::RoutingFx { target, effects },
58                Value::Null,
59                0,
60                line_number,
61                1,
62            ));
63        } else {
64            return Err(anyhow!(
65                "fx statement requires effects after '->': {}",
66                trimmed
67            ));
68        }
69    }
70
71    // route <source> to <dest> [with effect(...)]
72    // route <source> -> <dest> [@ level]
73    if trimmed.starts_with("route ") {
74        let rest = trimmed[6..].trim();
75        let source;
76        let remaining;
77
78        if let Some((source_part, rest)) = rest.split_once(" to ") {
79            source = source_part.trim().to_string();
80            remaining = rest.trim();
81        } else if let Some((source_part, rest)) = rest.split_once("->") {
82            source = source_part.trim().to_string();
83            remaining = rest.trim();
84        } else {
85            return Err(anyhow!(
86                "route statement requires format: route <source> to <dest> [with effect(...)] or route <source> -> <dest> [@ level]: {}",
87                trimmed
88            ));
89        }
90
91        if let Some((dest_part, effect_part)) = remaining.split_once(" with ") {
92            let destination = normalize_routing_target(dest_part.trim().trim_end_matches(':'));
93            let effect_str = effect_part.trim().trim_end_matches(':');
94            let effects = parse_single_routing_effect(effect_str)?;
95            return Ok(Statement::new(
96                StatementKind::RoutingRoute {
97                    source,
98                    destination,
99                    effects: Some(effects),
100                },
101                Value::Null,
102                0,
103                line_number,
104                1,
105            ));
106        }
107
108        let (destination, effects) = parse_destination_and_gain(remaining)?;
109        return Ok(Statement::new(
110            StatementKind::RoutingRoute {
111                source,
112                destination,
113                effects,
114            },
115            Value::Null,
116            0,
117            line_number,
118            1,
119        ));
120    }
121
122    // duck <source> to <dest> with effect(...)
123    // duck <source> -> <dest> with effect(...)
124    if trimmed.starts_with("duck ") {
125        let rest = trimmed[5..].trim();
126        let source;
127        let remaining;
128
129        if let Some((source_part, rest)) = rest.split_once(" to ") {
130            source = source_part.trim().to_string();
131            remaining = rest.trim();
132        } else if let Some((source_part, rest)) = rest.split_once("->") {
133            source = source_part.trim().to_string();
134            remaining = rest.trim();
135        } else {
136            return Err(anyhow!(
137                "duck statement requires format: duck <source> to <dest> with effect(...): {}",
138                trimmed
139            ));
140        }
141
142        if let Some((dest_part, effect_part)) = remaining.split_once(" with ") {
143            let destination = normalize_routing_target(dest_part.trim().trim_end_matches(':'));
144            let effect_str = effect_part.trim().trim_end_matches(':');
145            let effect = parse_single_routing_effect(effect_str)?;
146            return Ok(Statement::new(
147                StatementKind::RoutingDuck {
148                    source,
149                    destination,
150                    effect,
151                },
152                Value::Null,
153                0,
154                line_number,
155                1,
156            ));
157        }
158
159        return Err(anyhow!(
160            "duck statement requires 'with' clause: {}",
161            trimmed
162        ));
163    }
164
165    // sidechain <source> to <dest> with effect(...)
166    // sidechain <source> -> <dest> with effect(...)
167    if trimmed.starts_with("sidechain ") {
168        let rest = trimmed[10..].trim();
169        let source;
170        let remaining;
171
172        if let Some((source_part, rest)) = rest.split_once(" to ") {
173            source = source_part.trim().to_string();
174            remaining = rest.trim();
175        } else if let Some((source_part, rest)) = rest.split_once("->") {
176            source = source_part.trim().to_string();
177            remaining = rest.trim();
178        } else {
179            return Err(anyhow!(
180                "sidechain statement requires format: sidechain <source> to <dest> with effect(...): {}",
181                trimmed
182            ));
183        }
184
185        if let Some((dest_part, effect_part)) = remaining.split_once(" with ") {
186            let destination = normalize_routing_target(dest_part.trim().trim_end_matches(':'));
187            let effect_str = effect_part.trim().trim_end_matches(':');
188            let effect = parse_single_routing_effect(effect_str)?;
189            return Ok(Statement::new(
190                StatementKind::RoutingSidechain {
191                    source,
192                    destination,
193                    effect,
194                },
195                Value::Null,
196                0,
197                line_number,
198                1,
199            ));
200        }
201
202        return Err(anyhow!(
203            "sidechain statement requires 'with' clause: {}",
204            trimmed
205        ));
206    }
207
208    Err(anyhow!("Unknown routing statement: {}", trimmed))
209}
210
211/// Parse a single routing effect like "effect({ param: value, ... })"
212fn parse_single_routing_effect(effect_str: &str) -> Result<Value> {
213    let effect_str = effect_str.trim();
214    if effect_str.is_empty() {
215        return Err(anyhow!("Empty effect definition"));
216    }
217
218    // Handle function-like syntax: effect_name(param1, param2, ...)
219    if let Some((name, params_str)) = effect_str.split_once('(') {
220        let name = name.trim().to_string();
221        let params_str = params_str.trim_end_matches(')');
222
223        // Handle parameter map
224        if params_str.contains('{') && params_str.contains('}') {
225            let map_str = params_str.trim_matches(|c| c == '{' || c == '}');
226            let mut params_map = std::collections::HashMap::new();
227
228            for pair in map_str.split(',') {
229                if let Some((key, value)) = pair.split_once(':') {
230                    let key = key.trim().to_string();
231                    let value = value.trim();
232
233                    let value = if value == "true" {
234                        Value::Boolean(true)
235                    } else if value == "false" {
236                        Value::Boolean(false)
237                    } else if let Ok(num) = value.parse::<f32>() {
238                        Value::Number(num)
239                    } else {
240                        Value::String(value.trim_matches('"').to_string())
241                    };
242
243                    params_map.insert(key, value);
244                }
245            }
246
247            let mut result = std::collections::HashMap::new();
248            result.insert(name, Value::Map(params_map));
249            return Ok(Value::Map(result));
250        } else if let Ok(num) = params_str.parse::<f32>() {
251            let mut result = std::collections::HashMap::new();
252            result.insert(name, Value::Number(num));
253            return Ok(Value::Map(result));
254        } else {
255            let mut result = std::collections::HashMap::new();
256            result.insert(
257                name,
258                Value::String(params_str.trim_matches('"').to_string()),
259            );
260            return Ok(Value::Map(result));
261        }
262    } else {
263        let mut result = std::collections::HashMap::new();
264        result.insert(effect_str.to_string(), Value::Null);
265        return Ok(Value::Map(result));
266    }
267}
268
269fn normalize_routing_target(target: &str) -> String {
270    if target == "master" || target == "$master" {
271        "master".to_string()
272    } else {
273        target.to_string()
274    }
275}
276
277fn parse_destination_and_gain(dest_expr: &str) -> Result<(String, Option<Value>)> {
278    let mut parts = dest_expr.trim().split('@');
279    let destination =
280        normalize_routing_target(parts.next().unwrap_or("").trim().trim_end_matches(':'));
281    let effects = if let Some(gain_str) = parts.next() {
282        let gain_str = gain_str.trim();
283        if gain_str.is_empty() {
284            None
285        } else {
286            let gain = gain_str
287                .parse::<f32>()
288                .map_err(|_| anyhow!("Invalid mix level: {}", gain_str))?;
289            let mut volume_map = std::collections::HashMap::new();
290            volume_map.insert("gain".to_string(), Value::Number(gain));
291            let mut map = std::collections::HashMap::new();
292            map.insert("volume".to_string(), Value::Map(volume_map));
293            Some(Value::Map(map))
294        }
295    } else {
296        None
297    };
298    Ok((destination, effects))
299}
300
301#[cfg(test)]
302mod tests {
303    use super::*;
304
305    #[test]
306    fn test_parse_routing_fx_eq_chain() {
307        let statement =
308            parse_routing_statement("fx master -> eq({ low: 1.2, mid: 0.8, high: 1.1 })", 1)
309                .unwrap();
310
311        match statement.kind {
312            StatementKind::RoutingFx { target, effects } => {
313                assert_eq!(target, "master");
314                match effects {
315                    Value::Map(map) => {
316                        assert!(matches!(map.get("eq"), Some(Value::Map(_))));
317                    }
318                    other => panic!("expected effects map, got {:?}", other),
319                }
320            }
321            other => panic!("expected routing fx, got {:?}", other),
322        }
323    }
324
325    #[test]
326    fn test_parse_routing_arrow_with_gain() {
327        let statement = parse_routing_statement("route drums -> master @ 0.8", 1).unwrap();
328
329        match statement.kind {
330            StatementKind::RoutingRoute {
331                source,
332                destination,
333                effects,
334            } => {
335                assert_eq!(source, "drums");
336                assert_eq!(destination, "master");
337                match effects {
338                    Some(Value::Map(map)) => match map.get("volume") {
339                        Some(Value::Map(volume_map)) => match volume_map.get("gain") {
340                            Some(Value::Number(gain)) => {
341                                assert!((gain - 0.8).abs() < f32::EPSILON)
342                            }
343                            other => panic!("expected gain number, got {:?}", other),
344                        },
345                        other => panic!("expected volume map, got {:?}", other),
346                    },
347                    other => panic!("expected route effects map, got {:?}", other),
348                }
349            }
350            other => panic!("expected routing route, got {:?}", other),
351        }
352    }
353}