use flutmax_ast::{
AttrPair, AttrValue, CallArg, DestructuringWire, DirectConnection, Expr, FeedbackAssignment,
FeedbackDecl, InDecl, InputPortAccess, LitValue, MsgDecl, OutAssignment, OutDecl,
OutputPortAccess, PortType, Program, Span, StateAssignment, StateDecl, Wire,
};
use tree_sitter::{Node, Parser};
use tree_sitter_flutmax::LANGUAGE;
#[derive(Debug)]
pub enum ParseError {
TreeSitter(String),
InvalidSyntax {
message: String,
line: usize,
column: usize,
},
}
impl std::fmt::Display for ParseError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
ParseError::TreeSitter(msg) => write!(f, "Tree-sitter error: {}", msg),
ParseError::InvalidSyntax {
message,
line,
column,
} => {
write!(f, "Syntax error at {}:{}: {}", line, column, message)
}
}
}
}
impl std::error::Error for ParseError {}
pub fn parse(source: &str) -> Result<Program, ParseError> {
let mut parser = Parser::new();
parser
.set_language(&LANGUAGE.into())
.map_err(|e| ParseError::TreeSitter(e.to_string()))?;
let tree = parser
.parse(source, None)
.ok_or_else(|| ParseError::TreeSitter("Failed to parse source".to_string()))?;
let root = tree.root_node();
if root.has_error() {
if let Some(err_node) = find_error_node(root) {
return Err(ParseError::InvalidSyntax {
message: format!("Unexpected syntax near '{}'", node_text(err_node, source)),
line: err_node.start_position().row + 1,
column: err_node.start_position().column + 1,
});
}
}
let mut program = Program::new();
let mut implicit_in_index: u32 = 0;
let mut implicit_out_index: u32 = 0;
let mut cursor = root.walk();
for child in root.named_children(&mut cursor) {
match child.kind() {
"port_declaration" => {
convert_port_declaration(
child,
source,
&mut program,
&mut implicit_in_index,
&mut implicit_out_index,
)?;
}
"destructuring_wire" => {
let dw = convert_destructuring_wire(child, source)?;
program.destructuring_wires.push(dw);
}
"wire_declaration" => {
let wire = convert_wire_declaration(child, source)?;
program.wires.push(wire);
}
"out_assignment" => {
let assignment = convert_out_assignment(child, source)?;
program.out_assignments.push(assignment);
}
"direct_connection" => {
let connection = convert_direct_connection(child, source)?;
program.direct_connections.push(connection);
}
"feedback_declaration" => {
let decl = convert_feedback_declaration(child, source)?;
program.feedback_decls.push(decl);
}
"feedback_assignment" => {
let assign = convert_feedback_assignment(child, source)?;
program.feedback_assignments.push(assign);
}
"msg_declaration" => {
let md = convert_msg_declaration(child, source)?;
program.msg_decls.push(md);
}
"state_declaration" => {
let decl = convert_state_declaration(child, source)?;
program.state_decls.push(decl);
}
"state_assignment" => {
let assign = convert_state_assignment(child, source)?;
program.state_assignments.push(assign);
}
_ => {
}
}
}
Ok(program)
}
fn find_error_node(node: Node) -> Option<Node> {
if node.is_error() || node.is_missing() {
return Some(node);
}
let mut cursor = node.walk();
for child in node.children(&mut cursor) {
if let Some(err) = find_error_node(child) {
return Some(err);
}
}
None
}
fn node_text<'a>(node: Node, source: &'a str) -> &'a str {
&source[node.byte_range()]
}
fn convert_port_declaration(
node: Node,
source: &str,
program: &mut Program,
implicit_in_index: &mut u32,
implicit_out_index: &mut u32,
) -> Result<(), ParseError> {
let direction_node = required_field(node, "direction", source)?;
let name_node = required_field(node, "name", source)?;
let type_node = required_field(node, "type", source)?;
let direction = node_text(direction_node, source);
let name = node_text(name_node, source).to_string();
let type_text = node_text(type_node, source);
let index = if let Some(index_node) = node.child_by_field_name("index") {
parse_u32(index_node, source)?
} else {
match direction {
"in" => {
let idx = *implicit_in_index;
*implicit_in_index += 1;
idx
}
"out" => {
let idx = *implicit_out_index;
*implicit_out_index += 1;
idx
}
_ => 0,
}
};
let port_type = PortType::parse(type_text).ok_or_else(|| ParseError::InvalidSyntax {
message: format!("Unknown port type '{}'", type_text),
line: type_node.start_position().row + 1,
column: type_node.start_position().column + 1,
})?;
let value = if direction == "out" {
if let Some(value_node) = node.child_by_field_name("value") {
Some(convert_expression(value_node, source)?)
} else {
None
}
} else {
None
};
match direction {
"in" => {
program.in_decls.push(InDecl {
index,
name,
port_type,
});
}
"out" => {
program.out_decls.push(OutDecl {
index,
name,
port_type,
value,
});
}
_ => {
return Err(ParseError::InvalidSyntax {
message: format!("Expected 'in' or 'out', got '{}'", direction),
line: direction_node.start_position().row + 1,
column: direction_node.start_position().column + 1,
});
}
}
Ok(())
}
fn make_span(node: Node) -> Option<Span> {
Some(Span {
start_line: node.start_position().row + 1,
start_column: node.start_position().column + 1,
end_line: node.end_position().row + 1,
end_column: node.end_position().column + 1,
})
}
fn convert_destructuring_wire(node: Node, source: &str) -> Result<DestructuringWire, ParseError> {
let names_node = required_field(node, "names", source)?;
let value_node = required_field(node, "value", source)?;
let mut names = Vec::new();
let mut cursor = names_node.walk();
for child in names_node.named_children(&mut cursor) {
if child.kind() == "plain_identifier" {
names.push(node_text(child, source).to_string());
}
}
let value = convert_expression(value_node, source)?;
let span = make_span(node);
Ok(DestructuringWire { names, value, span })
}
fn convert_wire_declaration(node: Node, source: &str) -> Result<Wire, ParseError> {
let name_node = required_field(node, "name", source)?;
let value_node = required_field(node, "value", source)?;
let name = node_text(name_node, source).to_string();
let value = convert_expression(value_node, source)?;
let span = make_span(node);
let attrs = if let Some(attr_node) = node.child_by_field_name("attrs") {
convert_attr_chain(attr_node, source)?
} else {
vec![]
};
Ok(Wire {
name,
value,
span,
attrs,
})
}
fn convert_out_assignment(node: Node, source: &str) -> Result<OutAssignment, ParseError> {
let index_node = required_field(node, "index", source)?;
let value_node = required_field(node, "value", source)?;
let index = parse_u32(index_node, source)?;
let value = convert_expression(value_node, source)?;
let span = make_span(node);
Ok(OutAssignment { index, value, span })
}
fn convert_direct_connection(node: Node, source: &str) -> Result<DirectConnection, ParseError> {
let target_node = required_field(node, "target", source)?;
let value_node = required_field(node, "value", source)?;
let target = convert_input_port_access(target_node, source)?;
let value = convert_expression(value_node, source)?;
Ok(DirectConnection { target, value })
}
fn convert_feedback_declaration(node: Node, source: &str) -> Result<FeedbackDecl, ParseError> {
let name_node = required_field(node, "name", source)?;
let type_node = required_field(node, "type", source)?;
let name = node_text(name_node, source).to_string();
let type_text = node_text(type_node, source);
let port_type = PortType::parse(type_text).ok_or_else(|| ParseError::InvalidSyntax {
message: format!("Unknown port type '{}'", type_text),
line: type_node.start_position().row + 1,
column: type_node.start_position().column + 1,
})?;
let span = make_span(node);
Ok(FeedbackDecl {
name,
port_type,
span,
})
}
fn convert_feedback_assignment(node: Node, source: &str) -> Result<FeedbackAssignment, ParseError> {
let target_node = required_field(node, "target", source)?;
let value_node = required_field(node, "value", source)?;
let target = node_text(target_node, source).to_string();
let value = convert_expression(value_node, source)?;
let span = make_span(node);
Ok(FeedbackAssignment {
target,
value,
span,
})
}
fn convert_msg_declaration(node: Node, source: &str) -> Result<MsgDecl, ParseError> {
let name_node = required_field(node, "name", source)?;
let content_node = required_field(node, "content", source)?;
let name = node_text(name_node, source).to_string();
let raw = node_text(content_node, source);
let inner = &raw[1..raw.len() - 1];
let content = unescape_string(inner);
let span = make_span(node);
let attrs = if let Some(attr_node) = node.child_by_field_name("attrs") {
convert_attr_chain(attr_node, source)?
} else {
vec![]
};
Ok(MsgDecl {
name,
content,
span,
attrs,
})
}
fn convert_state_declaration(node: Node, source: &str) -> Result<StateDecl, ParseError> {
let name_node = required_field(node, "name", source)?;
let type_node = required_field(node, "type", source)?;
let init_node = required_field(node, "init", source)?;
let name = node_text(name_node, source).to_string();
let type_text = node_text(type_node, source);
let port_type = PortType::parse(type_text).ok_or_else(|| ParseError::InvalidSyntax {
message: format!("Unknown control type '{}'", type_text),
line: type_node.start_position().row + 1,
column: type_node.start_position().column + 1,
})?;
let init_value = convert_expression(init_node, source)?;
let span = make_span(node);
Ok(StateDecl {
name,
port_type,
init_value,
span,
})
}
fn convert_state_assignment(node: Node, source: &str) -> Result<StateAssignment, ParseError> {
let target_node = required_field(node, "target", source)?;
let value_node = required_field(node, "value", source)?;
let name = node_text(target_node, source).to_string();
let value = convert_expression(value_node, source)?;
let span = make_span(node);
Ok(StateAssignment { name, value, span })
}
fn convert_input_port_access(node: Node, source: &str) -> Result<InputPortAccess, ParseError> {
let object_node = required_field(node, "object", source)?;
let index_node = required_field(node, "index", source)?;
let object = node_text(object_node, source).to_string();
let index = parse_u32(index_node, source)?;
Ok(InputPortAccess { object, index })
}
fn convert_output_port_access(node: Node, source: &str) -> Result<OutputPortAccess, ParseError> {
let object_node = required_field(node, "object", source)?;
let index_node = required_field(node, "index", source)?;
let object = node_text(object_node, source).to_string();
let index = parse_u32(index_node, source)?;
Ok(OutputPortAccess { object, index })
}
fn convert_expression(node: Node, source: &str) -> Result<Expr, ParseError> {
match node.kind() {
"object_call" => convert_object_call(node, source),
"output_port_access" => {
let opa = convert_output_port_access(node, source)?;
Ok(Expr::OutputPortAccess(opa))
}
"tuple_expression" => {
let mut elements = Vec::new();
let mut cursor = node.walk();
for child in node.named_children(&mut cursor) {
elements.push(convert_expression(child, source)?);
}
Ok(Expr::Tuple(elements))
}
"identifier" => {
let text = node_text(node, source).to_string();
Ok(Expr::Ref(text))
}
"number" => {
let text = node_text(node, source);
if text.contains('.') || text.contains('e') || text.contains('E') {
let val: f64 = text.parse().map_err(|_| ParseError::InvalidSyntax {
message: format!("Invalid float literal '{}'", text),
line: node.start_position().row + 1,
column: node.start_position().column + 1,
})?;
Ok(Expr::Lit(LitValue::Float(val)))
} else {
let val: i64 = text.parse().map_err(|_| ParseError::InvalidSyntax {
message: format!("Invalid integer literal '{}'", text),
line: node.start_position().row + 1,
column: node.start_position().column + 1,
})?;
Ok(Expr::Lit(LitValue::Int(val)))
}
}
"string" => {
let raw = node_text(node, source);
let inner = &raw[1..raw.len() - 1];
let unescaped = unescape_string(inner);
Ok(Expr::Lit(LitValue::Str(unescaped)))
}
_ => Err(ParseError::InvalidSyntax {
message: format!("Unexpected expression kind '{}'", node.kind()),
line: node.start_position().row + 1,
column: node.start_position().column + 1,
}),
}
}
fn convert_object_call(node: Node, source: &str) -> Result<Expr, ParseError> {
let object_node = required_field(node, "object", source)?;
let object = node_text(object_node, source).to_string();
let mut args = Vec::new();
if let Some(arg_list_node) = node.child_by_field_name("arguments") {
let mut cursor = arg_list_node.walk();
for arg_child in arg_list_node.named_children(&mut cursor) {
let expr = convert_expression(arg_child, source)?;
args.push(CallArg::positional(expr));
}
}
Ok(Expr::Call { object, args })
}
fn convert_attr_chain(node: Node, source: &str) -> Result<Vec<AttrPair>, ParseError> {
let pairs_node = required_field(node, "pairs", source)?;
convert_attr_list(pairs_node, source)
}
fn convert_attr_list(node: Node, source: &str) -> Result<Vec<AttrPair>, ParseError> {
let mut pairs = Vec::new();
let mut cursor = node.walk();
for child in node.named_children(&mut cursor) {
if child.kind() == "attr_pair" {
pairs.push(convert_attr_pair(child, source)?);
}
}
Ok(pairs)
}
fn convert_attr_pair(node: Node, source: &str) -> Result<AttrPair, ParseError> {
let key_node = required_field(node, "key", source)?;
let value_node = required_field(node, "value", source)?;
let key = node_text(key_node, source).to_string();
let value = convert_attr_value(value_node, source)?;
Ok(AttrPair { key, value })
}
fn convert_attr_value(node: Node, source: &str) -> Result<AttrValue, ParseError> {
match node.kind() {
"number" => {
let text = node_text(node, source);
if text.contains('.') || text.contains('e') || text.contains('E') {
let val: f64 = text.parse().map_err(|_| ParseError::InvalidSyntax {
message: format!("Invalid float literal '{}'", text),
line: node.start_position().row + 1,
column: node.start_position().column + 1,
})?;
Ok(AttrValue::Float(val))
} else {
let val: i64 = text.parse().map_err(|_| ParseError::InvalidSyntax {
message: format!("Invalid integer literal '{}'", text),
line: node.start_position().row + 1,
column: node.start_position().column + 1,
})?;
Ok(AttrValue::Int(val))
}
}
"string" => {
let raw = node_text(node, source);
let inner = &raw[1..raw.len() - 1];
let unescaped = unescape_string(inner);
Ok(AttrValue::Str(unescaped))
}
"plain_identifier" => {
let text = node_text(node, source).to_string();
Ok(AttrValue::Ident(text))
}
_ => Err(ParseError::InvalidSyntax {
message: format!("Unexpected attr value kind '{}'", node.kind()),
line: node.start_position().row + 1,
column: node.start_position().column + 1,
}),
}
}
fn unescape_string(s: &str) -> String {
let mut result = String::with_capacity(s.len());
let mut chars = s.chars();
while let Some(ch) = chars.next() {
if ch == '\\' {
match chars.next() {
Some('n') => result.push('\n'),
Some('t') => result.push('\t'),
Some('\\') => result.push('\\'),
Some('"') => result.push('"'),
Some(other) => {
result.push('\\');
result.push(other);
}
None => result.push('\\'),
}
} else {
result.push(ch);
}
}
result
}
fn required_field<'a>(
node: Node<'a>,
field_name: &str,
source: &str,
) -> Result<Node<'a>, ParseError> {
node.child_by_field_name(field_name)
.ok_or_else(|| ParseError::InvalidSyntax {
message: format!(
"Missing required field '{}' in '{}'",
field_name,
node_text(node, source)
),
line: node.start_position().row + 1,
column: node.start_position().column + 1,
})
}
fn parse_u32(node: Node, source: &str) -> Result<u32, ParseError> {
let text = node_text(node, source);
text.parse().map_err(|_| ParseError::InvalidSyntax {
message: format!("Expected integer, got '{}'", text),
line: node.start_position().row + 1,
column: node.start_position().column + 1,
})
}
#[cfg(test)]
mod tests {
use super::*;
use flutmax_ast::{
DirectConnection, Expr, InDecl, InputPortAccess, LitValue, OutDecl, PortType,
};
#[test]
fn test_l1_minimal() {
let source = r#"
out 0 (audio): signal;
wire osc = cycle~(440);
out[0] = osc;
"#;
let prog = parse(source).expect("parse failed");
assert_eq!(prog.out_decls.len(), 1);
assert_eq!(
prog.out_decls[0],
OutDecl {
index: 0,
name: "audio".to_string(),
port_type: PortType::Signal,
value: None,
}
);
assert_eq!(prog.wires.len(), 1);
assert_eq!(prog.wires[0].name, "osc");
assert_eq!(
prog.wires[0].value,
Expr::Call {
object: "cycle~".to_string(),
args: vec![CallArg::positional(Expr::Lit(LitValue::Int(440)))],
}
);
assert!(prog.wires[0].span.is_some());
assert_eq!(prog.out_assignments.len(), 1);
assert_eq!(prog.out_assignments[0].index, 0);
assert_eq!(prog.out_assignments[0].value, Expr::Ref("osc".to_string()));
assert!(prog.out_assignments[0].span.is_some());
assert!(prog.in_decls.is_empty());
assert!(prog.direct_connections.is_empty());
}
#[test]
fn test_l2_simple_synth() {
let source = r#"
// L2_simple_synth.flutmax
in 0 (freq): float;
out 0 (audio): signal;
wire osc = cycle~(freq);
wire amp = mul~(osc, 0.5);
out[0] = amp;
"#;
let prog = parse(source).expect("parse failed");
assert_eq!(prog.in_decls.len(), 1);
assert_eq!(
prog.in_decls[0],
InDecl {
index: 0,
name: "freq".to_string(),
port_type: PortType::Float,
}
);
assert_eq!(prog.out_decls.len(), 1);
assert_eq!(
prog.out_decls[0],
OutDecl {
index: 0,
name: "audio".to_string(),
port_type: PortType::Signal,
value: None,
}
);
assert_eq!(prog.wires.len(), 2);
assert_eq!(prog.wires[0].name, "osc");
assert_eq!(
prog.wires[0].value,
Expr::Call {
object: "cycle~".to_string(),
args: vec![CallArg::positional(Expr::Ref("freq".to_string()))],
}
);
assert!(prog.wires[0].span.is_some());
assert_eq!(prog.wires[1].name, "amp");
assert_eq!(
prog.wires[1].value,
Expr::Call {
object: "mul~".to_string(),
args: vec![
CallArg::positional(Expr::Ref("osc".to_string())),
CallArg::positional(Expr::Lit(LitValue::Float(0.5))),
],
}
);
assert!(prog.wires[1].span.is_some());
assert_eq!(prog.out_assignments.len(), 1);
assert_eq!(prog.out_assignments[0].index, 0);
assert_eq!(prog.out_assignments[0].value, Expr::Ref("amp".to_string()));
assert!(prog.out_assignments[0].span.is_some());
}
#[test]
fn test_l3b_control_fanout() {
let source = r#"
wire trigger = button();
wire counter = counter(trigger);
wire msg = print(counter);
node_a.in[0] = trigger;
node_b.in[0] = trigger;
"#;
let prog = parse(source).expect("parse failed");
assert_eq!(prog.wires.len(), 3);
assert_eq!(prog.wires[0].name, "trigger");
assert_eq!(
prog.wires[0].value,
Expr::Call {
object: "button".to_string(),
args: vec![],
}
);
assert_eq!(prog.wires[1].name, "counter");
assert_eq!(
prog.wires[1].value,
Expr::Call {
object: "counter".to_string(),
args: vec![CallArg::positional(Expr::Ref("trigger".to_string()))],
}
);
assert_eq!(prog.wires[2].name, "msg");
assert_eq!(
prog.wires[2].value,
Expr::Call {
object: "print".to_string(),
args: vec![CallArg::positional(Expr::Ref("counter".to_string()))],
}
);
assert_eq!(prog.direct_connections.len(), 2);
assert_eq!(
prog.direct_connections[0],
DirectConnection {
target: InputPortAccess {
object: "node_a".to_string(),
index: 0,
},
value: Expr::Ref("trigger".to_string()),
}
);
assert_eq!(
prog.direct_connections[1],
DirectConnection {
target: InputPortAccess {
object: "node_b".to_string(),
index: 0,
},
value: Expr::Ref("trigger".to_string()),
}
);
}
#[test]
fn test_multiple_in_out_ports() {
let source = r#"
in 0 (input_sig): signal;
in 1 (cutoff): float;
in 2 (q_factor): float;
out 0 (lowpass): signal;
out 1 (highpass): signal;
"#;
let prog = parse(source).expect("parse failed");
assert_eq!(prog.in_decls.len(), 3);
assert_eq!(prog.in_decls[0].name, "input_sig");
assert_eq!(prog.in_decls[0].port_type, PortType::Signal);
assert_eq!(prog.in_decls[1].name, "cutoff");
assert_eq!(prog.in_decls[1].port_type, PortType::Float);
assert_eq!(prog.in_decls[2].name, "q_factor");
assert_eq!(prog.in_decls[2].port_type, PortType::Float);
assert_eq!(prog.out_decls.len(), 2);
assert_eq!(prog.out_decls[0].name, "lowpass");
assert_eq!(prog.out_decls[1].name, "highpass");
}
#[test]
fn test_nested_object_call() {
let source = r#"
wire sig = biquad~(cycle~(440), 1000, 0.7);
"#;
let prog = parse(source).expect("parse failed");
assert_eq!(prog.wires.len(), 1);
assert_eq!(prog.wires[0].name, "sig");
assert_eq!(
prog.wires[0].value,
Expr::Call {
object: "biquad~".to_string(),
args: vec![
CallArg::positional(Expr::Call {
object: "cycle~".to_string(),
args: vec![CallArg::positional(Expr::Lit(LitValue::Int(440)))],
}),
CallArg::positional(Expr::Lit(LitValue::Int(1000))),
CallArg::positional(Expr::Lit(LitValue::Float(0.7))),
],
}
);
assert!(prog.wires[0].span.is_some());
}
#[test]
fn test_string_literal() {
let source = r#"
wire msg = print("hello world");
"#;
let prog = parse(source).expect("parse failed");
assert_eq!(prog.wires.len(), 1);
assert_eq!(
prog.wires[0].value,
Expr::Call {
object: "print".to_string(),
args: vec![CallArg::positional(Expr::Lit(LitValue::Str(
"hello world".to_string()
)))],
}
);
}
#[test]
fn test_zero_arg_object_call() {
let source = r#"
wire btn = button();
"#;
let prog = parse(source).expect("parse failed");
assert_eq!(
prog.wires[0].value,
Expr::Call {
object: "button".to_string(),
args: vec![],
}
);
}
#[test]
fn test_empty_source() {
let source = "";
let prog = parse(source).expect("parse failed");
assert!(prog.in_decls.is_empty());
assert!(prog.out_decls.is_empty());
assert!(prog.wires.is_empty());
assert!(prog.out_assignments.is_empty());
assert!(prog.direct_connections.is_empty());
}
#[test]
fn test_comments_ignored() {
let source = r#"
// This is a comment
wire osc = cycle~(440);
// Another comment
out 0 (audio): signal;
"#;
let prog = parse(source).expect("parse failed");
assert_eq!(prog.wires.len(), 1);
assert_eq!(prog.out_decls.len(), 1);
}
#[test]
fn test_span_populated() {
let source = "wire osc = cycle~(440);";
let prog = parse(source).expect("parse failed");
assert!(prog.wires[0].span.is_some());
let span = prog.wires[0].span.as_ref().unwrap();
assert_eq!(span.start_line, 1);
assert_eq!(span.start_column, 1);
}
#[test]
fn test_out_assignment_span_populated() {
let source = "out 0 (audio): signal;\nwire osc = cycle~(440);\nout[0] = osc;";
let prog = parse(source).expect("parse failed");
assert!(prog.out_assignments[0].span.is_some());
let span = prog.out_assignments[0].span.as_ref().unwrap();
assert_eq!(span.start_line, 3);
}
#[test]
fn test_tuple_expression() {
let source = "wire t = (a, b, c);";
let prog = parse(source).expect("parse failed");
assert_eq!(prog.wires.len(), 1);
assert_eq!(prog.wires[0].name, "t");
assert_eq!(
prog.wires[0].value,
Expr::Tuple(vec![
Expr::Ref("a".to_string()),
Expr::Ref("b".to_string()),
Expr::Ref("c".to_string()),
])
);
}
#[test]
fn test_tuple_two_elements() {
let source = "wire pair = (x, y);";
let prog = parse(source).expect("parse failed");
assert_eq!(prog.wires.len(), 1);
assert_eq!(
prog.wires[0].value,
Expr::Tuple(vec![Expr::Ref("x".to_string()), Expr::Ref("y".to_string()),])
);
}
#[test]
fn test_tuple_with_literals() {
let source = "wire nums = (1, 2, 3);";
let prog = parse(source).expect("parse failed");
assert_eq!(
prog.wires[0].value,
Expr::Tuple(vec![
Expr::Lit(LitValue::Int(1)),
Expr::Lit(LitValue::Int(2)),
Expr::Lit(LitValue::Int(3)),
])
);
}
#[test]
fn test_destructuring_wire() {
let source = "wire (a, b, c) = unpack(coords);";
let prog = parse(source).expect("parse failed");
assert_eq!(prog.destructuring_wires.len(), 1);
let dw = &prog.destructuring_wires[0];
assert_eq!(dw.names, vec!["a", "b", "c"]);
assert_eq!(
dw.value,
Expr::Call {
object: "unpack".to_string(),
args: vec![CallArg::positional(Expr::Ref("coords".to_string()))],
}
);
assert!(dw.span.is_some());
}
#[test]
fn test_destructuring_wire_two_names() {
let source = "wire (x, y) = data;";
let prog = parse(source).expect("parse failed");
assert_eq!(prog.destructuring_wires.len(), 1);
let dw = &prog.destructuring_wires[0];
assert_eq!(dw.names, vec!["x", "y"]);
assert_eq!(dw.value, Expr::Ref("data".to_string()));
}
#[test]
fn test_destructuring_wire_with_tuple_value() {
let source = "wire (a, b) = (x, y);";
let prog = parse(source).expect("parse failed");
assert_eq!(prog.destructuring_wires.len(), 1);
let dw = &prog.destructuring_wires[0];
assert_eq!(dw.names, vec!["a", "b"]);
assert_eq!(
dw.value,
Expr::Tuple(vec![Expr::Ref("x".to_string()), Expr::Ref("y".to_string()),])
);
}
#[test]
fn test_l4_tuple_full() {
let source = r#"
in 0 (x): float;
in 1 (y): float;
in 2 (z): float;
out 0 (coords): list;
wire packed = (x, y, z);
out[0] = packed;
"#;
let prog = parse(source).expect("parse failed");
assert_eq!(prog.in_decls.len(), 3);
assert_eq!(prog.out_decls.len(), 1);
assert_eq!(prog.wires.len(), 1);
assert_eq!(prog.wires[0].name, "packed");
assert_eq!(
prog.wires[0].value,
Expr::Tuple(vec![
Expr::Ref("x".to_string()),
Expr::Ref("y".to_string()),
Expr::Ref("z".to_string()),
])
);
assert_eq!(prog.out_assignments.len(), 1);
}
#[test]
fn test_l5_destructure_full() {
let source = r#"
in 0 (coords): list;
out 0 (x): float;
out 1 (y): float;
wire (a, b) = unpack(coords);
out[0] = a;
out[1] = b;
"#;
let prog = parse(source).expect("parse failed");
assert_eq!(prog.in_decls.len(), 1);
assert_eq!(prog.out_decls.len(), 2);
assert_eq!(prog.destructuring_wires.len(), 1);
assert_eq!(prog.destructuring_wires[0].names, vec!["a", "b"]);
assert_eq!(prog.out_assignments.len(), 2);
}
#[test]
fn test_feedback_declaration() {
let source = "feedback fb: signal;";
let prog = parse(source).expect("parse failed");
assert_eq!(prog.feedback_decls.len(), 1);
assert_eq!(prog.feedback_decls[0].name, "fb");
assert_eq!(prog.feedback_decls[0].port_type, PortType::Signal);
assert!(prog.feedback_decls[0].span.is_some());
}
#[test]
fn test_feedback_assignment() {
let source = "feedback fb = tapin~(mixed, 1000);";
let prog = parse(source).expect("parse failed");
assert_eq!(prog.feedback_assignments.len(), 1);
assert_eq!(prog.feedback_assignments[0].target, "fb");
assert_eq!(
prog.feedback_assignments[0].value,
Expr::Call {
object: "tapin~".to_string(),
args: vec![
CallArg::positional(Expr::Ref("mixed".to_string())),
CallArg::positional(Expr::Lit(LitValue::Int(1000))),
],
}
);
assert!(prog.feedback_assignments[0].span.is_some());
}
#[test]
fn test_feedback_full_patch() {
let source = r#"
in 0 (input): signal;
out 0 (output): signal;
feedback fb: signal;
wire delayed = tapout~(fb, 500);
wire mixed = add~(input, mul~(delayed, 0.3));
feedback fb = tapin~(mixed, 1000);
out[0] = mixed;
"#;
let prog = parse(source).expect("parse failed");
assert_eq!(prog.in_decls.len(), 1);
assert_eq!(prog.out_decls.len(), 1);
assert_eq!(prog.feedback_decls.len(), 1);
assert_eq!(prog.feedback_decls[0].name, "fb");
assert_eq!(prog.feedback_decls[0].port_type, PortType::Signal);
assert_eq!(prog.wires.len(), 2);
assert_eq!(prog.wires[0].name, "delayed");
assert_eq!(prog.wires[1].name, "mixed");
assert_eq!(prog.feedback_assignments.len(), 1);
assert_eq!(prog.feedback_assignments[0].target, "fb");
assert_eq!(prog.out_assignments.len(), 1);
}
#[test]
fn test_output_port_access_in_wire() {
let source = "wire x = node.out[0];";
let prog = parse(source).expect("parse failed");
assert_eq!(prog.wires.len(), 1);
assert_eq!(prog.wires[0].name, "x");
match &prog.wires[0].value {
Expr::OutputPortAccess(flutmax_ast::OutputPortAccess { object, index }) => {
assert_eq!(object, "node");
assert_eq!(*index, 0);
}
other => panic!("expected OutputPortAccess, got {:?}", other),
}
}
#[test]
fn test_output_port_access_in_call_arg() {
let source = "wire y = mul~(node.out[0], 0.5);";
let prog = parse(source).expect("parse failed");
assert_eq!(prog.wires.len(), 1);
if let Expr::Call { args, .. } = &prog.wires[0].value {
assert_eq!(args.len(), 2);
match &args[0].value {
Expr::OutputPortAccess(flutmax_ast::OutputPortAccess { object, index }) => {
assert_eq!(object, "node");
assert_eq!(*index, 0);
}
other => panic!("expected OutputPortAccess, got {:?}", other),
}
} else {
panic!("expected Call");
}
}
#[test]
fn test_output_port_access_higher_index() {
let source = "wire z = node.out[2];";
let prog = parse(source).expect("parse failed");
match &prog.wires[0].value {
Expr::OutputPortAccess(flutmax_ast::OutputPortAccess { object, index }) => {
assert_eq!(object, "node");
assert_eq!(*index, 2);
}
other => panic!("expected OutputPortAccess, got {:?}", other),
}
}
#[test]
fn test_input_port_access_in_direct_connection() {
let source = "node_a.in[0] = trigger;";
let prog = parse(source).expect("parse failed");
assert_eq!(prog.direct_connections.len(), 1);
assert_eq!(prog.direct_connections[0].target.object, "node_a");
assert_eq!(prog.direct_connections[0].target.index, 0);
}
#[test]
fn test_in_not_in_expression() {
let source = "wire x = node.in[0];";
let result = parse(source);
assert!(
result.is_err(),
".in[N] should not be valid in expression position"
);
}
#[test]
fn test_state_declaration_int() {
let source = "state counter: int = 0;";
let prog = parse(source).expect("parse failed");
assert_eq!(prog.state_decls.len(), 1);
assert_eq!(prog.state_decls[0].name, "counter");
assert_eq!(prog.state_decls[0].port_type, PortType::Int);
assert_eq!(prog.state_decls[0].init_value, Expr::Lit(LitValue::Int(0)));
assert!(prog.state_decls[0].span.is_some());
}
#[test]
fn test_state_declaration_float() {
let source = "state volume: float = 0.5;";
let prog = parse(source).expect("parse failed");
assert_eq!(prog.state_decls.len(), 1);
assert_eq!(prog.state_decls[0].name, "volume");
assert_eq!(prog.state_decls[0].port_type, PortType::Float);
assert_eq!(
prog.state_decls[0].init_value,
Expr::Lit(LitValue::Float(0.5))
);
}
#[test]
fn test_state_assignment() {
let source = "state counter = next;";
let prog = parse(source).expect("parse failed");
assert_eq!(prog.state_assignments.len(), 1);
assert_eq!(prog.state_assignments[0].name, "counter");
assert_eq!(
prog.state_assignments[0].value,
Expr::Ref("next".to_string())
);
assert!(prog.state_assignments[0].span.is_some());
}
#[test]
fn test_state_assignment_with_call() {
let source = "state counter = add(counter, 1);";
let prog = parse(source).expect("parse failed");
assert_eq!(prog.state_assignments.len(), 1);
assert_eq!(prog.state_assignments[0].name, "counter");
assert_eq!(
prog.state_assignments[0].value,
Expr::Call {
object: "add".to_string(),
args: vec![
CallArg::positional(Expr::Ref("counter".to_string())),
CallArg::positional(Expr::Lit(LitValue::Int(1))),
],
}
);
}
#[test]
fn test_state_full_counter_patch() {
let source = r#"
state counter: int = 0;
wire next = add(counter, 1);
state counter = next;
out 0 (count): int;
out[0] = next;
"#;
let prog = parse(source).expect("parse failed");
assert_eq!(prog.state_decls.len(), 1);
assert_eq!(prog.state_decls[0].name, "counter");
assert_eq!(prog.state_decls[0].port_type, PortType::Int);
assert_eq!(prog.wires.len(), 1);
assert_eq!(prog.wires[0].name, "next");
assert_eq!(prog.state_assignments.len(), 1);
assert_eq!(prog.state_assignments[0].name, "counter");
assert_eq!(prog.out_decls.len(), 1);
assert_eq!(prog.out_assignments.len(), 1);
}
#[test]
fn test_dotted_identifier_object_call() {
let source = r#"
wire vid = jit.gl.videoplane();
"#;
let prog = parse(source).expect("parse failed");
assert_eq!(prog.wires.len(), 1);
assert_eq!(prog.wires[0].name, "vid");
assert_eq!(
prog.wires[0].value,
Expr::Call {
object: "jit.gl.videoplane".to_string(),
args: vec![],
}
);
}
#[test]
fn test_dotted_identifier_with_args() {
let source = r#"
wire dial = live.dial(0.5);
"#;
let prog = parse(source).expect("parse failed");
assert_eq!(prog.wires.len(), 1);
assert_eq!(
prog.wires[0].value,
Expr::Call {
object: "live.dial".to_string(),
args: vec![CallArg::positional(Expr::Lit(LitValue::Float(0.5)))],
}
);
}
#[test]
fn test_dotted_identifier_does_not_conflict_with_port_access() {
let source = r#"
wire node = cycle~(440);
wire x = node.out[0];
node.in[0] = 440;
"#;
let prog = parse(source).expect("parse failed");
assert_eq!(prog.wires.len(), 2);
assert_eq!(
prog.wires[1].value,
Expr::OutputPortAccess(OutputPortAccess {
object: "node".to_string(),
index: 0,
})
);
assert_eq!(prog.direct_connections.len(), 1);
assert_eq!(prog.direct_connections[0].target.object, "node");
assert_eq!(prog.direct_connections[0].target.index, 0);
}
#[test]
fn test_hyphenated_identifier() {
let source = r#"
wire x = drunk-walk(10);
"#;
let prog = parse(source).expect("parse failed");
assert_eq!(prog.wires.len(), 1);
assert_eq!(
prog.wires[0].value,
Expr::Call {
object: "drunk-walk".to_string(),
args: vec![CallArg::positional(Expr::Lit(LitValue::Int(10)))],
}
);
}
#[test]
fn test_msg_declaration() {
let source = r#"
msg click = "bang";
"#;
let prog = parse(source).expect("parse failed");
assert_eq!(prog.msg_decls.len(), 1);
assert_eq!(prog.msg_decls[0].name, "click");
assert_eq!(prog.msg_decls[0].content, "bang");
assert!(prog.msg_decls[0].span.is_some());
}
#[test]
fn test_msg_declaration_with_template() {
let source = r#"
msg format = "set $1 $2";
"#;
let prog = parse(source).expect("parse failed");
assert_eq!(prog.msg_decls.len(), 1);
assert_eq!(prog.msg_decls[0].name, "format");
assert_eq!(prog.msg_decls[0].content, "set $1 $2");
}
#[test]
fn test_msg_declaration_multiple() {
let source = r#"
msg bang_msg = "bang";
msg set_msg = "set 42";
"#;
let prog = parse(source).expect("parse failed");
assert_eq!(prog.msg_decls.len(), 2);
assert_eq!(prog.msg_decls[0].name, "bang_msg");
assert_eq!(prog.msg_decls[0].content, "bang");
assert_eq!(prog.msg_decls[1].name, "set_msg");
assert_eq!(prog.msg_decls[1].content, "set 42");
}
#[test]
fn test_msg_with_wire_and_connection() {
let source = r#"
msg click = "bang";
wire btn = button();
btn.in[0] = click;
"#;
let prog = parse(source).expect("parse failed");
assert_eq!(prog.msg_decls.len(), 1);
assert_eq!(prog.wires.len(), 1);
assert_eq!(prog.direct_connections.len(), 1);
assert_eq!(
prog.direct_connections[0].value,
Expr::Ref("click".to_string())
);
}
#[test]
fn test_wire_with_attrs() {
let source = r#"wire w = flonum(x).attr(minimum: 0., maximum: 100.);"#;
let prog = parse(source).expect("parse failed");
assert_eq!(prog.wires.len(), 1);
let wire = &prog.wires[0];
assert_eq!(wire.name, "w");
assert_eq!(wire.attrs.len(), 2);
assert_eq!(wire.attrs[0].key, "minimum");
assert_eq!(wire.attrs[0].value, AttrValue::Float(0.0));
assert_eq!(wire.attrs[1].key, "maximum");
assert_eq!(wire.attrs[1].value, AttrValue::Float(100.0));
}
#[test]
fn test_wire_without_attrs() {
let source = r#"wire osc = cycle~(440);"#;
let prog = parse(source).expect("parse failed");
assert_eq!(prog.wires.len(), 1);
assert!(prog.wires[0].attrs.is_empty());
}
#[test]
fn test_wire_with_string_attr() {
let source = r#"wire dial = live.dial().attr(parameter_longname: "Cutoff");"#;
let prog = parse(source).expect("parse failed");
assert_eq!(prog.wires.len(), 1);
let wire = &prog.wires[0];
assert_eq!(wire.attrs.len(), 1);
assert_eq!(wire.attrs[0].key, "parameter_longname");
assert_eq!(wire.attrs[0].value, AttrValue::Str("Cutoff".to_string()));
}
#[test]
fn test_wire_with_ident_attr() {
let source = r#"wire osc = cycle~(freq).attr(phase: half);"#;
let prog = parse(source).expect("parse failed");
assert_eq!(prog.wires.len(), 1);
let wire = &prog.wires[0];
assert_eq!(wire.attrs.len(), 1);
assert_eq!(wire.attrs[0].key, "phase");
assert_eq!(wire.attrs[0].value, AttrValue::Ident("half".to_string()));
}
#[test]
fn test_wire_with_int_attr() {
let source = r#"wire w = flonum(x).attr(minimum: 0, maximum: 100);"#;
let prog = parse(source).expect("parse failed");
assert_eq!(prog.wires.len(), 1);
let wire = &prog.wires[0];
assert_eq!(wire.attrs.len(), 2);
assert_eq!(wire.attrs[0].value, AttrValue::Int(0));
assert_eq!(wire.attrs[1].value, AttrValue::Int(100));
}
#[test]
fn test_msg_with_attrs() {
let source = r#"msg click = "bang".attr(patching_rect: 100.);"#;
let prog = parse(source).expect("parse failed");
assert_eq!(prog.msg_decls.len(), 1);
let msg = &prog.msg_decls[0];
assert_eq!(msg.name, "click");
assert_eq!(msg.content, "bang");
assert_eq!(msg.attrs.len(), 1);
assert_eq!(msg.attrs[0].key, "patching_rect");
assert_eq!(msg.attrs[0].value, AttrValue::Float(100.0));
}
#[test]
fn test_msg_without_attrs() {
let source = r#"msg click = "bang";"#;
let prog = parse(source).expect("parse failed");
assert_eq!(prog.msg_decls.len(), 1);
assert!(prog.msg_decls[0].attrs.is_empty());
}
#[test]
fn test_wire_multiline_attrs() {
let source = r#"
wire dial = live.dial().attr(
parameter_longname: "Cutoff",
parameter_shortname: "Cut",
minimum: 20.,
maximum: 20000.
);
"#;
let prog = parse(source).expect("parse failed");
assert_eq!(prog.wires.len(), 1);
let wire = &prog.wires[0];
assert_eq!(wire.attrs.len(), 4);
assert_eq!(wire.attrs[0].key, "parameter_longname");
assert_eq!(wire.attrs[1].key, "parameter_shortname");
assert_eq!(wire.attrs[2].key, "minimum");
assert_eq!(wire.attrs[3].key, "maximum");
}
#[test]
fn test_implicit_in_single() {
let source = "in freq: float;";
let prog = parse(source).expect("parse failed");
assert_eq!(prog.in_decls.len(), 1);
assert_eq!(prog.in_decls[0].index, 0);
assert_eq!(prog.in_decls[0].name, "freq");
assert_eq!(prog.in_decls[0].port_type, PortType::Float);
}
#[test]
fn test_implicit_in_multiple() {
let source = "in freq: float;\nin cutoff: float;";
let prog = parse(source).expect("parse failed");
assert_eq!(prog.in_decls.len(), 2);
assert_eq!(prog.in_decls[0].index, 0);
assert_eq!(prog.in_decls[0].name, "freq");
assert_eq!(prog.in_decls[1].index, 1);
assert_eq!(prog.in_decls[1].name, "cutoff");
}
#[test]
fn test_implicit_out_single() {
let source = "out audio: signal;";
let prog = parse(source).expect("parse failed");
assert_eq!(prog.out_decls.len(), 1);
assert_eq!(prog.out_decls[0].index, 0);
assert_eq!(prog.out_decls[0].name, "audio");
assert_eq!(prog.out_decls[0].port_type, PortType::Signal);
}
#[test]
fn test_implicit_out_multiple() {
let source = "out left: signal;\nout right: signal;";
let prog = parse(source).expect("parse failed");
assert_eq!(prog.out_decls.len(), 2);
assert_eq!(prog.out_decls[0].index, 0);
assert_eq!(prog.out_decls[0].name, "left");
assert_eq!(prog.out_decls[1].index, 1);
assert_eq!(prog.out_decls[1].name, "right");
}
#[test]
fn test_implicit_separate_counters_in_out() {
let source = "in a: float;\nout x: signal;\nin b: float;\nout y: signal;";
let prog = parse(source).expect("parse failed");
assert_eq!(prog.in_decls.len(), 2);
assert_eq!(prog.in_decls[0].index, 0);
assert_eq!(prog.in_decls[1].index, 1);
assert_eq!(prog.out_decls.len(), 2);
assert_eq!(prog.out_decls[0].index, 0);
assert_eq!(prog.out_decls[1].index, 1);
}
#[test]
fn test_explicit_index_still_works() {
let source = "in 5 (freq): float;";
let prog = parse(source).expect("parse failed");
assert_eq!(prog.in_decls.len(), 1);
assert_eq!(prog.in_decls[0].index, 5);
assert_eq!(prog.in_decls[0].name, "freq");
}
#[test]
fn test_implicit_full_patch() {
let source = r#"
in carrier_freq: float;
in harmonicity: float;
out fm_signal: signal;
wire osc = cycle~(carrier_freq);
out[0] = osc;
"#;
let prog = parse(source).expect("parse failed");
assert_eq!(prog.in_decls.len(), 2);
assert_eq!(prog.in_decls[0].index, 0);
assert_eq!(prog.in_decls[0].name, "carrier_freq");
assert_eq!(prog.in_decls[1].index, 1);
assert_eq!(prog.in_decls[1].name, "harmonicity");
assert_eq!(prog.out_decls.len(), 1);
assert_eq!(prog.out_decls[0].index, 0);
assert_eq!(prog.out_decls[0].name, "fm_signal");
assert_eq!(prog.wires.len(), 1);
assert_eq!(prog.out_assignments.len(), 1);
}
#[test]
fn test_out_decl_inline_value_implicit_ts() {
let source = r#"
wire osc = cycle~(440);
out audio: signal = osc;
"#;
let prog = parse(source).expect("parse failed");
assert_eq!(prog.out_decls.len(), 1);
assert_eq!(prog.out_decls[0].index, 0);
assert_eq!(prog.out_decls[0].name, "audio");
assert_eq!(prog.out_decls[0].port_type, PortType::Signal);
assert_eq!(prog.out_decls[0].value, Some(Expr::Ref("osc".to_string())));
assert!(prog.out_assignments.is_empty());
}
#[test]
fn test_out_decl_inline_value_explicit_ts() {
let source = r#"
wire osc = cycle~(440);
out 0 (audio): signal = osc;
"#;
let prog = parse(source).expect("parse failed");
assert_eq!(prog.out_decls.len(), 1);
assert_eq!(prog.out_decls[0].index, 0);
assert_eq!(prog.out_decls[0].name, "audio");
assert_eq!(prog.out_decls[0].port_type, PortType::Signal);
assert_eq!(prog.out_decls[0].value, Some(Expr::Ref("osc".to_string())));
assert!(prog.out_assignments.is_empty());
}
#[test]
fn test_out_decl_without_value_ts() {
let source = r#"
out audio: signal;
wire osc = cycle~(440);
out[0] = osc;
"#;
let prog = parse(source).expect("parse failed");
assert_eq!(prog.out_decls.len(), 1);
assert_eq!(prog.out_decls[0].value, None);
assert_eq!(prog.out_assignments.len(), 1);
}
}