diff --git a/examples/50-smoke.sx b/examples/50-smoke.sx index f6010a8..7897cb3 100644 --- a/examples/50-smoke.sx +++ b/examples/50-smoke.sx @@ -3120,5 +3120,31 @@ END; } } + // ── Trailing commas ────────────────────────────────────────── + print("=== Trailing Commas ===\n"); + { + // Struct literal with trailing comma + Vec4 :: struct { x: f64; y: f64; z: f64; w: f64; } + v := Vec4.{ + x = 1.0, + y = 2.0, + z = 3.0, + w = 4.0, + }; + assert(v.x == 1.0); + assert(v.w == 4.0); + + // Function call with trailing comma + add :: (a: s64, b: s64) -> s64 { return a + b; } + r := add(10, 20,); + assert(r == 30); + + // Array literal with trailing comma + arr := s64.[1, 2, 3,]; + assert(arr[2] == 3); + + print("trailing commas ok\n"); + } + print("=== DONE ===\n"); } diff --git a/specs.md b/specs.md index f1b4185..da4ad7b 100644 --- a/specs.md +++ b/specs.md @@ -92,7 +92,7 @@ GLSL; | `:` | type annotation | | `=` | assignment (in typed var decl) | | `;` | statement terminator | -| `,` | separator | +| `,` | separator (trailing commas allowed) | | `.` | field access / enum literal prefix | | `->` | return type annotation | | `=>` | lambda arrow | @@ -244,6 +244,14 @@ v3 := Vec4.{ w=0, x=2, y=3, z=4 }; z := 5.0; w := 6.0; v4 := Vec4.{ y=3, x=9, w, z }; + +// Trailing commas are allowed in all comma-separated lists +v5 := Vec4.{ + x = 1.0, + y = 2.0, + z = 3.0, + w = 4.0, +}; ``` #### Field Access and Assignment @@ -1779,7 +1787,7 @@ enum_decl = IDENT '::' 'enum' '{' (IDENT ';')* '}' struct_decl = IDENT '::' 'struct' '{' struct_member* '}' struct_member = field_group | '#using' IDENT ';' field_group = IDENT (',' IDENT)* ':' type ('=' expr)? ';' -params = param (',' param)* +params = param (',' param)* ','? param = IDENT ':' type block = '{' stmt* '}' stmt = decl | assignment ';' | multi_assign ';' | return_stmt | defer_stmt | insert_stmt @@ -1803,7 +1811,7 @@ postfix = primary ('(' args? ')' | '.' IDENT | '.{' field_init_list '}') primary = INT | HEX_INT | BIN_INT | FLOAT | STRING | BOOL | IDENT | '---' | '.' IDENT | '.' '{' field_init_list '}' | '(' expr ')' | block | '#run' expr -field_init_list = field_init (',' field_init)* +field_init_list = field_init (',' field_init)* ','? field_init = IDENT '=' expr | IDENT | expr if_expr = 'if' expr 'then' expr ('else' expr)? | 'if' expr block ('else' block)? @@ -1812,7 +1820,7 @@ case_arm = 'case' pattern ':' (stmt* | 'break' ';') else_arm = 'else' ':' stmt* pattern = '.' IDENT | INT | BOOL | IDENT lambda = '(' params? ')' ('->' type)? '=>' expr -args = expr (',' expr)* +args = expr (',' expr)* ','? type = '$' IDENT | 's32' | 'f32' | 'f64' | 'bool' | 'string' | 'Any' | 'Type' | '..' type | '[' expr ']' type | IDENT ``` diff --git a/src/parser.zig b/src/parser.zig index e443f2d..610dce6 100644 --- a/src/parser.zig +++ b/src/parser.zig @@ -714,6 +714,7 @@ pub const Parser = struct { while (self.current.tag != .r_paren and self.current.tag != .eof) { if (type_params.items.len > 0) { try self.expect(.comma); + if (self.current.tag == .r_paren) break; } // Expect $name : constraint try self.expect(.dollar); @@ -899,6 +900,7 @@ pub const Parser = struct { while (self.current.tag != .r_paren and self.current.tag != .eof) { if (param_types.items.len > 0) { try self.expect(.comma); + if (self.current.tag == .r_paren) break; } // Parse: name: type if (self.current.tag != .identifier and self.current.tag != .kw_Self) { @@ -976,6 +978,7 @@ pub const Parser = struct { while (self.current.tag != .r_paren and self.current.tag != .eof) { if (target_type_params.items.len > 0) { try self.expect(.comma); + if (self.current.tag == .r_paren) break; } try self.expect(.dollar); if (self.current.tag != .identifier) { @@ -1036,6 +1039,7 @@ pub const Parser = struct { while (self.current.tag != .r_brace and self.current.tag != .eof) { if (field_inits.items.len > 0) { try self.expect(.comma); + if (self.current.tag == .r_brace) break; } // Check if this is a named field: identifier followed by '=' @@ -1104,6 +1108,7 @@ pub const Parser = struct { while (self.current.tag != .r_paren and self.current.tag != .eof) { if (params.items.len > 0) { try self.expect(.comma); + if (self.current.tag == .r_paren) break; } var is_ct_param = false; if (self.current.tag == .dollar) { @@ -1606,6 +1611,7 @@ pub const Parser = struct { while (self.current.tag != .r_paren and self.current.tag != .eof) { if (args.items.len > 0) { try self.expect(.comma); + if (self.current.tag == .r_paren) break; } // Spread operator: ..expr if (self.current.tag == .dot_dot) { @@ -1641,6 +1647,7 @@ pub const Parser = struct { while (self.current.tag != .r_bracket and self.current.tag != .eof) { if (elements.items.len > 0) { try self.expect(.comma); + if (self.current.tag == .r_bracket) break; } const elem = try self.parseExpr(); try elements.append(self.allocator, elem); @@ -1805,6 +1812,7 @@ pub const Parser = struct { while (self.current.tag != .r_bracket and self.current.tag != .eof) { if (elements.items.len > 0) { try self.expect(.comma); + if (self.current.tag == .r_bracket) break; } const elem = try self.parseExpr(); try elements.append(self.allocator, elem); @@ -2203,6 +2211,7 @@ pub const Parser = struct { try elements.append(self.allocator, .{ .name = name, .value = value }); if (self.current.tag == .comma) { self.advance(); + if (self.current.tag == .r_paren) break; } else break; } try self.expect(.r_paren); diff --git a/tests/expected/50-smoke.txt b/tests/expected/50-smoke.txt index 4568456..fcfa3de 100644 --- a/tests/expected/50-smoke.txt +++ b/tests/expected/50-smoke.txt @@ -602,4 +602,6 @@ usize->s64: 42 not wasm known os desktop 64-bit +=== Trailing Commas === +trailing commas ok === DONE ===