devalang_wasm/language/syntax/parser/driver/
routing.rs1use 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 Ok(Statement::new(
9 StatementKind::Routing { body: Vec::new() },
10 Value::Null,
11 0,
12 line_number,
13 1,
14 ))
15}
16
17pub fn parse_routing_statement<'a>(line: &str, line_number: usize) -> Result<Statement> {
19 let trimmed = line.trim();
20
21 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 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 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 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 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
211fn 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 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 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}