Skip to content

Commit 781b4fd

Browse files
committed
feat: convert ImagePipe grammar to ZynPEG 2.0 named binding syntax
- Update ImagePipe grammar v1.0 -> v2.0: - Replace legacy JSON actions with direct TypedAST construction - Use named bindings (path:string_literal) instead of $1, $2 - Split rotate/flip rules to avoid match expressions - Actions now use ActionIR::Construct with TypedStatement/TypedExpression - Fix grammar parser for trailing commas in arrays - Update test suite (38 tests): - Verify action types are TypedAST constructs - Test print_typed_ast_actions to show AST structure - Test print_parsed_bindings to show captured values This demonstrates ZynPEG 2.0's ability to define direct TypedAST construction without legacy JSON command interpretation.
1 parent d30e806 commit 781b4fd

3 files changed

Lines changed: 694 additions & 1034 deletions

File tree

crates/zyn_peg/src/grammar/parser.rs

Lines changed: 141 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -520,21 +520,25 @@ impl<'a> GrammarParser<'a> {
520520

521521
/// Parse an action block
522522
fn parse_action(&mut self) -> ParseResult<(ActionIR, String)> {
523-
// Parse return type path
524-
let return_type = self.parse_type_path()?;
523+
// Parse first identifier/type path
524+
let first_path = self.parse_type_path()?;
525525
self.skip_ws();
526526

527-
// Check for simple pass-through
527+
// Check for simple pass-through: -> binding (no '{' follows)
528+
// If the first_path is a simple identifier (no ::) and no '{' follows,
529+
// it's a pass-through action
528530
if !self.check_str("{") {
529-
// Simple pass-through: -> binding
530-
if let Some(c) = self.peek_char() {
531-
if c.is_ascii_alphabetic() || c == '_' {
532-
let binding = self.parse_identifier()?;
533-
return Ok((ActionIR::PassThrough { binding }, return_type));
534-
}
531+
// Check if it's a simple identifier (no :: in path) - that's pass-through
532+
if !first_path.contains("::") {
533+
// No '::' means it's just a binding name, not a type path
534+
return Ok((ActionIR::PassThrough { binding: first_path.clone() }, first_path));
535535
}
536+
// If it has ::, then there should be a { with fields
537+
return Err(self.error(&format!("Expected '{{' after type path '{}'", first_path)));
536538
}
537539

540+
let return_type = first_path;
541+
538542
// Full action block
539543
self.expect_char('{')?;
540544
self.skip_ws_and_comments();
@@ -934,7 +938,7 @@ impl<'a> GrammarParser<'a> {
934938
}
935939
}
936940

937-
/// Parse argument list (comma-separated expressions)
941+
/// Parse argument list (comma-separated expressions, allows trailing comma)
938942
fn parse_arg_list(&mut self) -> ParseResult<Vec<ExprIR>> {
939943
let mut args = Vec::new();
940944
self.skip_ws();
@@ -950,6 +954,10 @@ impl<'a> GrammarParser<'a> {
950954
if self.peek_char() == Some(',') {
951955
self.advance();
952956
self.skip_ws();
957+
// Check for trailing comma (next is ) or ])
958+
if self.peek_char() == Some(')') || self.peek_char() == Some(']') {
959+
break;
960+
}
953961
args.push(self.parse_expr()?);
954962
} else {
955963
break;
@@ -1456,6 +1464,128 @@ mod tests {
14561464
}
14571465
}
14581466

1467+
#[test]
1468+
fn test_parse_nested_struct_in_list() {
1469+
let input = r#"
1470+
@language { name: "Test", version: "1.0" }
1471+
1472+
test_rule = { "test" }
1473+
-> Foo::Bar {
1474+
args: [X::Y { f: v }],
1475+
}
1476+
"#;
1477+
1478+
match parse_grammar(input) {
1479+
Ok(grammar) => {
1480+
println!("OK: {} rules", grammar.rules.len());
1481+
let rule = grammar.rules.get("test_rule").unwrap();
1482+
println!("Rule action: {:?}", rule.action);
1483+
}
1484+
Err(e) => {
1485+
panic!("Failed to parse: {:?}", e);
1486+
}
1487+
}
1488+
}
1489+
1490+
#[test]
1491+
fn test_parse_box_new_call() {
1492+
let input = r#"
1493+
@language { name: "Test", version: "1.0" }
1494+
1495+
test_rule = { "test" }
1496+
-> Foo::Bar {
1497+
callee: Box::new(X::Y { name: n }),
1498+
}
1499+
"#;
1500+
1501+
match parse_grammar(input) {
1502+
Ok(grammar) => {
1503+
println!("OK: {} rules", grammar.rules.len());
1504+
let rule = grammar.rules.get("test_rule").unwrap();
1505+
println!("Rule action: {:?}", rule.action);
1506+
}
1507+
Err(e) => {
1508+
panic!("Failed to parse: {:?}", e);
1509+
}
1510+
}
1511+
}
1512+
1513+
#[test]
1514+
fn test_parse_nested_array_in_struct() {
1515+
// Simplified test case - struct in array in struct
1516+
let input = r#"
1517+
@language { name: "Test", version: "1.0" }
1518+
1519+
test = { "test" }
1520+
-> A::B {
1521+
outer: C::D {
1522+
inner: [E::F { x: v }],
1523+
},
1524+
}
1525+
"#;
1526+
1527+
match parse_grammar(input) {
1528+
Ok(grammar) => {
1529+
println!("OK: {} rules", grammar.rules.len());
1530+
let rule = grammar.rules.get("test").unwrap();
1531+
println!("Rule action: {:?}", rule.action);
1532+
}
1533+
Err(e) => {
1534+
panic!("Failed to parse: {:?}", e);
1535+
}
1536+
}
1537+
}
1538+
1539+
#[test]
1540+
fn test_parse_array_with_function_call() {
1541+
// Test array containing struct with function call (no trailing comma)
1542+
let input = r#"
1543+
@language { name: "Test", version: "1.0" }
1544+
1545+
test = { "test" }
1546+
-> A::B {
1547+
args: [
1548+
C::D { x: intern(v) }
1549+
],
1550+
}
1551+
"#;
1552+
1553+
match parse_grammar(input) {
1554+
Ok(grammar) => {
1555+
println!("OK: {} rules", grammar.rules.len());
1556+
let rule = grammar.rules.get("test").unwrap();
1557+
println!("Rule action: {:?}", rule.action);
1558+
}
1559+
Err(e) => {
1560+
panic!("Failed to parse: {:?}", e);
1561+
}
1562+
}
1563+
}
1564+
1565+
#[test]
1566+
fn test_parse_array_with_trailing_comma() {
1567+
// Test array with trailing comma
1568+
let input = r#"
1569+
@language { name: "Test", version: "1.0" }
1570+
1571+
test = { "test" }
1572+
-> A::B {
1573+
args: [a, b,],
1574+
}
1575+
"#;
1576+
1577+
match parse_grammar(input) {
1578+
Ok(grammar) => {
1579+
println!("OK: {} rules", grammar.rules.len());
1580+
let rule = grammar.rules.get("test").unwrap();
1581+
println!("Rule action: {:?}", rule.action);
1582+
}
1583+
Err(e) => {
1584+
panic!("Failed to parse trailing comma: {:?}", e);
1585+
}
1586+
}
1587+
}
1588+
14591589
#[test]
14601590
fn test_parse_imagepipe_grammar() {
14611591
let input = include_str!("../../../../examples/imagepipe/imagepipe.zyn");
@@ -1471,7 +1601,7 @@ mod tests {
14711601

14721602
// Verify key metadata
14731603
assert_eq!(grammar.metadata.name, "ImagePipe");
1474-
assert_eq!(grammar.metadata.version, "1.0");
1604+
assert_eq!(grammar.metadata.version, "2.0");
14751605

14761606
// Verify key rules exist
14771607
assert!(grammar.rules.contains_key("program"));

0 commit comments

Comments
 (0)