use super::super::helpers::{parse_function_args, parse_map_value};
use crate::language::syntax::ast::{Statement, StatementKind, Value};
use anyhow::{Result, anyhow};
use std::collections::HashMap;
fn normalize_arrow_method_name(method_name: &str) -> String {
match method_name.trim() {
"dur" | "len" => "duration",
"vel" => "velocity",
"amp" | "vol" | "volume" => "gain",
"wave" | "osc" => "waveform",
"env" | "envelope" => "adsr",
"verb" => "reverb",
"echo" => "delay",
"comp" => "compressor",
"lpf" => "lowpass",
"hpf" => "highpass",
"bpf" => "bandpass",
"dist" => "distortion",
other => other,
}
.to_string()
}
pub fn parse_arrow_call(line: &str, line_number: usize) -> Result<Statement> {
let parts: Vec<&str> = line.split("->").map(|s| s.trim()).collect();
if parts.len() < 2 {
return Err(anyhow!("Arrow call requires at least one '->' operator"));
}
let target = parts[0].to_string();
let mut calls = Vec::new();
for method_call in &parts[1..] {
if let Some(paren_idx) = method_call.find('(') {
let method_name = normalize_arrow_method_name(&method_call[..paren_idx]);
let args_str = &method_call[paren_idx + 1..];
let close_paren = args_str
.rfind(')')
.ok_or_else(|| anyhow!("Missing closing parenthesis in arrow call"))?;
let args_str = &args_str[..close_paren];
let args = if args_str.trim().is_empty() {
Vec::new()
} else {
parse_function_args(args_str)?
};
calls.push((method_name, args));
} else {
calls.push((normalize_arrow_method_name(method_call), Vec::new()));
}
}
if calls.is_empty() {
return Err(anyhow!("No method calls found in arrow call"));
}
let (method, args) = calls[0].clone();
let mut chain_value = HashMap::new();
chain_value.insert("target".to_string(), Value::String(target.clone()));
chain_value.insert("method".to_string(), Value::String(method.clone()));
chain_value.insert("args".to_string(), Value::Array(args.clone()));
if calls.len() > 1 {
let chain_calls: Vec<Value> = calls[1..]
.iter()
.map(|(m, a)| {
let mut call_map = HashMap::new();
call_map.insert("method".to_string(), Value::String(m.clone()));
call_map.insert("args".to_string(), Value::Array(a.clone()));
Value::Map(call_map)
})
.collect();
chain_value.insert("chain".to_string(), Value::Array(chain_calls));
}
Ok(Statement::new(
StatementKind::ArrowCall {
target,
method,
args,
},
Value::Map(chain_value),
0,
line_number,
1,
))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_arrow_aliases_are_normalized() {
let statement = parse_arrow_call(
r#"lead -> dur(1/4) -> vel(0.8) -> verb({ mix: 0.2 }) -> lpf({ cutoff: 900 })"#,
1,
)
.unwrap();
match statement.kind {
StatementKind::ArrowCall { method, .. } => {
assert_eq!(method, "duration");
}
other => panic!("expected arrow call, got {:?}", other),
}
match statement.value {
Value::Map(map) => {
assert!(matches!(
map.get("method"),
Some(Value::String(method)) if method == "duration"
));
let chain = match map.get("chain") {
Some(Value::Array(chain)) => chain,
other => panic!("expected chain array, got {:?}", other),
};
assert!(matches!(
&chain[0],
Value::Map(call) if matches!(call.get("method"), Some(Value::String(method)) if method == "velocity")
));
assert!(matches!(
&chain[1],
Value::Map(call) if matches!(call.get("method"), Some(Value::String(method)) if method == "reverb")
));
assert!(matches!(
&chain[2],
Value::Map(call) if matches!(call.get("method"), Some(Value::String(method)) if method == "lowpass")
));
}
other => panic!("expected arrow value map, got {:?}", other),
}
}
#[test]
fn test_synth_config_aliases_are_normalized() {
let statement = parse_arrow_call(
r#"synth -> wave("saw") -> env({ attack: 0.01 }) -> amp(0.8)"#,
1,
)
.unwrap();
match statement.value {
Value::Map(map) => {
assert!(matches!(
map.get("method"),
Some(Value::String(method)) if method == "waveform"
));
let chain = match map.get("chain") {
Some(Value::Array(chain)) => chain,
other => panic!("expected chain array, got {:?}", other),
};
assert!(matches!(
&chain[0],
Value::Map(call) if matches!(call.get("method"), Some(Value::String(method)) if method == "adsr")
));
assert!(matches!(
&chain[1],
Value::Map(call) if matches!(call.get("method"), Some(Value::String(method)) if method == "gain")
));
}
other => panic!("expected arrow value map, got {:?}", other),
}
}
}
pub fn parse_assign(line: &str, line_number: usize) -> Result<Statement> {
let assign_parts: Vec<&str> = line.splitn(2, '=').collect();
if assign_parts.len() != 2 {
return Err(anyhow!("Invalid assignment syntax"));
}
let left = assign_parts[0].trim();
let right = assign_parts[1].trim();
let prop_parts: Vec<&str> = left.splitn(2, '.').collect();
if prop_parts.len() != 2 {
return Err(anyhow!("Assignment requires target.property syntax"));
}
let target = prop_parts[0].trim().to_string();
let property = prop_parts[1].trim().to_string();
let value = if let Ok(num) = right.parse::<f32>() {
Value::Number(num)
} else if right.starts_with('"') && right.ends_with('"') {
Value::String(right.trim_matches('"').to_string())
} else {
Value::Identifier(right.to_string())
};
Ok(Statement::new(
StatementKind::Assign { target, property },
value,
0,
line_number,
1,
))
}
pub fn parse_bind(line: &str, line_number: usize) -> Result<Statement> {
let after_bind = line
.trim()
.strip_prefix("bind")
.ok_or_else(|| anyhow!("bind parsing error"))?
.trim();
let arrow_parts: Vec<&str> = after_bind.splitn(2, "->").collect();
if arrow_parts.len() != 2 {
return Err(anyhow!("bind requires source -> target syntax"));
}
let left_raw = arrow_parts[0].trim();
let right_raw = arrow_parts[1].trim();
fn extract_with(side: &str) -> Result<(String, Option<Value>)> {
let s = side.trim();
if let Some(with_pos) = s.find("with") {
let before = s[..with_pos].trim().to_string();
let after = s[with_pos + "with".len()..].trim();
if after.starts_with('{') {
if let Some(close_brace) = after.rfind('}') {
let options_str = &after[..close_brace + 1];
let options = parse_map_value(options_str)?;
return Ok((before, Some(options)));
} else {
return Err(anyhow!("unclosed brace in bind options"));
}
}
}
Ok((s.to_string(), None))
}
let (source_candidate, left_opts) = extract_with(left_raw)?;
let (target_candidate, right_opts) = extract_with(right_raw)?;
let merged_options = match (left_opts, right_opts) {
(Some(mut l), Some(r)) => {
if let (Value::Map(lm), Value::Map(rm)) = (&mut l, &r) {
for (k, v) in rm.iter() {
lm.insert(k.clone(), v.clone());
}
Some(l)
} else {
Some(r)
}
}
(Some(l), None) => Some(l),
(None, Some(r)) => Some(r),
(None, None) => None,
};
let source = source_candidate;
let target = target_candidate;
Ok(Statement::new(
StatementKind::Bind { source, target },
merged_options.unwrap_or(Value::Null),
0,
line_number,
1,
))
}