diff --git a/jac/jaclang/compiler/jac.lark b/jac/jaclang/compiler/jac.lark index db0306d4b2..2a31e4f88a 100644 --- a/jac/jaclang/compiler/jac.lark +++ b/jac/jaclang/compiler/jac.lark @@ -25,7 +25,7 @@ from_path: (DOT | ELLIPSIS)* import_path | (DOT | ELLIPSIS)+ import_path: named_ref (DOT named_ref)* (KW_AS NAME)? -import_items: (import_item COMMA)* import_item +import_items: (import_item COMMA)* import_item COMMA? import_item: named_ref (KW_AS NAME)? sub_name: COLON NAME include_stmt: KW_INCLUDE sub_name? import_path SEMI @@ -63,7 +63,7 @@ enum: decorators? enum_decl enum_decl: KW_ENUM access_tag? STRING? NAME inherited_archs? (enum_block | SEMI) enum_def: arch_to_enum_chain enum_block -enum_block: LBRACE ((enum_stmt COMMA)* enum_stmt)? RBRACE +enum_block: LBRACE ((enum_stmt COMMA)* enum_stmt COMMA?)? RBRACE enum_stmt: NAME (COLON STRING)? EQ expression | NAME (COLON STRING)? @@ -83,7 +83,7 @@ abstract_ability: KW_OVERRIDE? KW_STATIC? KW_CAN access_tag? STRING? named_ref ( genai_ability: KW_OVERRIDE? KW_STATIC? KW_CAN access_tag? STRING? named_ref (func_decl) KW_BY atomic_call SEMI event_clause: KW_WITH expression? (KW_EXIT | KW_ENTRY) (STRING? RETURN_HINT expression)? func_decl: (LPAREN func_decl_params? RPAREN)? (RETURN_HINT (STRING COLON)? expression)? -func_decl_params: (param_var COMMA)* param_var +func_decl_params: (param_var COMMA)* param_var COMMA? param_var: (STAR_POW | STAR_MUL)? NAME (COLON STRING)? type_tag (EQ expression)? // Global variables @@ -364,9 +364,9 @@ index_slice: LSQUARE expression? COLON expression? (COLON expression?)? RSQUARE // Function calls atomic_call: atomic_chain LPAREN param_list? (KW_BY atomic_call)? RPAREN -param_list: expr_list COMMA kw_expr_list - | kw_expr_list - | expr_list +param_list: expr_list COMMA kw_expr_list COMMA? + | kw_expr_list COMMA? + | expr_list COMMA? // Atom atom: named_ref @@ -412,20 +412,20 @@ set_compr: LBRACE expression inner_compr+ RBRACE dict_compr: LBRACE kv_pair inner_compr+ RBRACE inner_compr: KW_ASYNC? KW_FOR atomic_chain KW_IN pipe_call (KW_IF walrus_assign)* -dict_val: LBRACE ((kv_pair COMMA)* kv_pair)? RBRACE -list_val: LSQUARE expr_list? RSQUARE +dict_val: LBRACE ((kv_pair COMMA)* kv_pair COMMA?)? RBRACE +list_val: LSQUARE (expr_list COMMA?)? RSQUARE tuple_val: LPAREN tuple_list? RPAREN -set_val: LBRACE expr_list RBRACE +set_val: LBRACE expr_list COMMA? RBRACE kv_pair: expression COLON expression | STAR_POW expression expr_list: (expr_list COMMA)? expression // Tuples and Jac Tuples -tuple_list: expression COMMA expr_list COMMA kw_expr_list - | expression COMMA kw_expr_list - | expression COMMA expr_list +tuple_list: expression COMMA expr_list COMMA kw_expr_list COMMA? + | expression COMMA kw_expr_list COMMA? + | expression COMMA expr_list COMMA? | expression COMMA - | kw_expr_list + | kw_expr_list COMMA? kw_expr_list: (kw_expr_list COMMA)? kw_expr kw_expr: named_ref EQ expression | STAR_POW expression @@ -662,4 +662,4 @@ FSTR_BESC.1: /{{|}}/ COMMENT: /#\*(.|\n|\r)*?\*#|#.*/ WS.-2: /[ \t\f\r\n]/+ %ignore COMMENT -%ignore WS \ No newline at end of file +%ignore WS diff --git a/jac/jaclang/compiler/parser.py b/jac/jaclang/compiler/parser.py index 4f37c85dca..0ff74ec41c 100644 --- a/jac/jaclang/compiler/parser.py +++ b/jac/jaclang/compiler/parser.py @@ -429,7 +429,7 @@ def import_items( ) -> ast.SubNodeList[ast.ModuleItem]: """Grammar rule. - import_items: (import_item COMMA)* import_item + import_items: (import_item COMMA)* import_item COMMA? """ ret = ast.SubNodeList[ast.ModuleItem]( items=[i for i in kid if isinstance(i, ast.ModuleItem)], @@ -693,7 +693,7 @@ def enum_block( ) -> ast.SubNodeList[ast.EnumBlockStmt]: """Grammar rule. - enum_block: LBRACE ((enum_stmt COMMA)* enum_stmt)? RBRACE + enum_block: LBRACE ((enum_stmt COMMA)* enum_stmt COMMA?)? RBRACE """ ret = ast.SubNodeList[ast.EnumBlockStmt](items=[], delim=Tok.COMMA, kid=kid) ret.items = [i for i in kid if isinstance(i, ast.EnumBlockStmt)] @@ -1022,7 +1022,7 @@ def func_decl_params( ) -> ast.SubNodeList[ast.ParamVar]: """Grammar rule. - func_decl_params: (param_var COMMA)* param_var + func_decl_params: (param_var COMMA)* param_var COMMA? """ ret = ast.SubNodeList[ast.ParamVar]( items=[i for i in kid if isinstance(i, ast.ParamVar)], @@ -1267,29 +1267,30 @@ def code_block( def statement(self, kid: list[ast.AstNode]) -> ast.CodeBlockStmt: """Grammar rule. - statement: py_code_block - | walker_stmt - | return_stmt SEMI - | report_stmt SEMI - | delete_stmt SEMI - | ctrl_stmt SEMI - | assert_stmt SEMI - | raise_stmt SEMI - | with_stmt + statement: import_stmt + | ability + | architype + | if_stmt | while_stmt | for_stmt | try_stmt - | if_stmt - | expression SEMI - | (yield_expr | KW_YIELD) SEMI - | static_assignment - | assignment SEMI + | match_stmt + | with_stmt | global_ref SEMI | nonlocal_ref SEMI | typed_ctx_block - | ability - | architype - | import_stmt + | return_stmt SEMI + | (yield_expr | KW_YIELD) SEMI + | raise_stmt SEMI + | assert_stmt SEMI + | check_stmt SEMI + | assignment SEMI + | delete_stmt SEMI + | report_stmt SEMI + | expression SEMI + | ctrl_stmt SEMI + | py_code_block + | walker_stmt | SEMI """ if isinstance(kid[0], ast.CodeBlockStmt) and len(kid) < 2: @@ -2180,30 +2181,21 @@ def cmp_op(self, kid: list[ast.AstNode]) -> ast.Token: def arithmetic(self, kid: list[ast.AstNode]) -> ast.Expr: """Grammar rule. - arithmetic: term MINUS arithmetic - | term PLUS arithmetic - | term + arithmetic: (arithmetic (MINUS | PLUS))? term """ return self.binary_expr_unwind(kid) def term(self, kid: list[ast.AstNode]) -> ast.Expr: """Grammar rule. - term: factor MOD term - | factor DIV term - | factor FLOOR_DIV term - | factor STAR_MUL term - | factor + term: (term (MOD | DIV | FLOOR_DIV | STAR_MUL | DECOR_OP))? power """ return self.binary_expr_unwind(kid) def factor(self, kid: list[ast.AstNode]) -> ast.Expr: """Grammar rule. - factor: power - | BW_NOT factor - | MINUS factor - | PLUS factor + factor: (BW_NOT | MINUS | PLUS) factor | connect """ if len(kid) == 2: if isinstance(kid[0], ast.Token) and isinstance(kid[1], ast.Expr): @@ -2221,8 +2213,7 @@ def factor(self, kid: list[ast.AstNode]) -> ast.Expr: def power(self, kid: list[ast.AstNode]) -> ast.Expr: """Grammar rule. - power: connect STAR_POW power - | connect + power: (power STAR_POW)? factor """ return self.binary_expr_unwind(kid) @@ -2513,8 +2504,7 @@ def atom(self, kid: list[ast.AstNode]) -> ast.Expr: def yield_expr(self, kid: list[ast.AstNode]) -> ast.YieldExpr: """Grammar rule. - yield_expr: - | KW_YIELD KW_FROM? expression + yield_expr: KW_YIELD KW_FROM? expression """ if isinstance(kid[-1], ast.Expr): return self.nu( @@ -2565,7 +2555,7 @@ def atom_collection(self, kid: list[ast.AstNode]) -> ast.AtomExpr: def multistring(self, kid: list[ast.AstNode]) -> ast.AtomExpr: """Grammar rule. - multistring: (fstring | STRING)+ + multistring: (fstring | STRING | DOC_STRING)+ """ valid_strs = [i for i in kid if isinstance(i, (ast.String, ast.FString))] if len(valid_strs) == len(kid): @@ -2582,6 +2572,7 @@ def fstring(self, kid: list[ast.AstNode]) -> ast.FString: """Grammar rule. fstring: FSTR_START fstr_parts FSTR_END + | FSTR_SQ_START fstr_sq_parts FSTR_SQ_END """ if len(kid) == 2: return self.nu( @@ -2605,7 +2596,7 @@ def fstr_parts( ) -> ast.SubNodeList[ast.String | ast.ExprStmt]: """Grammar rule. - fstr_parts: (FSTR_PIECE | FSTR_BESC | LBRACE expression RBRACE | fstring)* + fstr_parts: (FSTR_PIECE | FSTR_BESC | LBRACE expression RBRACE )* """ valid_parts: list[ast.String | ast.ExprStmt] = [ ( @@ -2651,7 +2642,7 @@ def fstr_sq_parts( def list_val(self, kid: list[ast.AstNode]) -> ast.ListVal: """Grammar rule. - list_val: LSQUARE expr_list? RSQUARE + list_val: LSQUARE (expr_list COMMA?)? RSQUARE """ if len(kid) == 2: return self.nu( @@ -2695,7 +2686,7 @@ def tuple_val(self, kid: list[ast.AstNode]) -> ast.TupleVal: def set_val(self, kid: list[ast.AstNode]) -> ast.SetVal: """Grammar rule. - set_val: LBRACE expr_list RBRACE + set_val: LBRACE expr_list COMMA? RBRACE """ if len(kid) == 2: return self.nu( @@ -2822,25 +2813,37 @@ def tuple_list( ) -> ast.SubNodeList[ast.Expr | ast.KWPair]: """Grammar rule. - tuple_list: expression COMMA expr_list COMMA kw_expr_list - | expression COMMA kw_expr_list - | expression COMMA expr_list - | expression COMMA - | kw_expr_list + tuple_list: expression COMMA expr_list COMMA kw_expr_list COMMA? + | expression COMMA kw_expr_list COMMA? + | expression COMMA expr_list COMMA? + | expression COMMA + | kw_expr_list COMMA? """ chomp = [*kid] first_expr = None if isinstance(chomp[0], ast.SubNodeList): + # The chomp will be like this: + # kw_expr_list, [COMMA] + if len(chomp) > 1: + # Add the comma to the subnode list if it exists, otherwise the last comma will not be a part of + # the ast, we need it for formatting. + chomp[0].kid.append(chomp[1]) return self.nu(chomp[0]) else: - first_expr = chomp[0] - chomp = chomp[2:] + # The chomp will be like this: + # expression, COMMA, [subnode_list, [COMMA, [kw_expr_list, [COMMA]]]] + # Pop the first expression from chomp. + first_expr = chomp[0] # Get the first expression. + chomp = chomp[2:] # Get rid of expr and comma. + + # The chomp will be like this: + # [subnode_list, [COMMA, [kw_expr_list, [COMMA]]]] expr_list = [] if len(chomp): - expr_list = chomp[0].kid - chomp = chomp[1:] + expr_list = chomp[0].kid # Get the kids subnode list. + chomp = chomp[2:] # Get rid of the subnode list and a comma if exists. if len(chomp): - chomp = chomp[1:] + # The chomp will be like this: [kw_expr_list, [COMMA]] expr_list = [*expr_list, *chomp[0].kid] expr_list = [first_expr, *expr_list] valid_kid = [i for i in expr_list if isinstance(i, (ast.Expr, ast.KWPair))] @@ -2855,7 +2858,7 @@ def tuple_list( def dict_val(self, kid: list[ast.AstNode]) -> ast.DictVal: """Grammar rule. - dict_val: LBRACE ((kv_pair COMMA)* kv_pair)? RBRACE + dict_val: LBRACE ((kv_pair COMMA)* kv_pair COMMA?)? RBRACE """ ret = ast.DictVal( kv_pairs=[], @@ -2912,7 +2915,7 @@ def list_compr(self, kid: list[ast.AstNode]) -> ast.ListCompr: def gen_compr(self, kid: list[ast.AstNode]) -> ast.GenCompr: """Grammar rule. - gen_compr: LSQUARE expression inner_compr+ RSQUARE + gen_compr: LPAREN expression inner_compr+ RPAREN """ comprs = [i for i in kid if isinstance(i, ast.InnerCompr)] if isinstance(kid[1], ast.Expr): @@ -2929,7 +2932,7 @@ def gen_compr(self, kid: list[ast.AstNode]) -> ast.GenCompr: def set_compr(self, kid: list[ast.AstNode]) -> ast.SetCompr: """Grammar rule. - set_compr: LSQUARE expression inner_compr+ RSQUARE + set_compr: LBRACE expression inner_compr+ RBRACE """ comprs = [i for i in kid if isinstance(i, ast.InnerCompr)] if isinstance(kid[1], ast.Expr) and isinstance(kid[2], ast.InnerCompr): @@ -2993,12 +2996,21 @@ def param_list( ) -> ast.SubNodeList[ast.Expr | ast.KWPair]: """Grammar rule. - param_list: expr_list COMMA kw_expr_list - | kw_expr_list - | expr_list + param_list: expr_list COMMA kw_expr_list COMMA? + | kw_expr_list COMMA? + | expr_list COMMA? """ - if len(kid) == 1: + ends_with_comma = ( + len(kid) > 1 + and isinstance(kid[-1], ast.Token) + and kid[-1].name == "COMMA" + ) + if len(kid) == 1 or (len(kid) == 2 and ends_with_comma): if isinstance(kid[0], ast.SubNodeList): + if ( + ends_with_comma + ): # Append the trailing comma to the subnode list. + kid[0].kid.append(kid[1]) return self.nu(kid[0]) else: raise self.ice() @@ -3146,7 +3158,7 @@ def object_ref(self, kid: list[ast.AstNode]) -> ast.ArchRef: def type_ref(self, kid: list[ast.AstNode]) -> ast.ArchRef: """Grammar rule. - type_ref: TYPE_OP name_ref + type_ref: TYPE_OP (named_ref | builtin_type) """ if isinstance(kid[0], ast.Token) and isinstance(kid[1], ast.NameAtom): return self.nu( diff --git a/jac/jaclang/compiler/passes/tool/jac_formatter_pass.py b/jac/jaclang/compiler/passes/tool/jac_formatter_pass.py index 578c8778a2..86ddede47c 100644 --- a/jac/jaclang/compiler/passes/tool/jac_formatter_pass.py +++ b/jac/jaclang/compiler/passes/tool/jac_formatter_pass.py @@ -404,7 +404,9 @@ def exit_func_call(self, node: ast.FuncCall) -> None: indented = True for count, j in enumerate(i.kid): if j.gen.jac == ",": - if i.kid[count + 1].gen.jac.startswith("#"): + if len(i.kid) > count + 1 and i.kid[ + count + 1 + ].gen.jac.startswith("#"): self.indent_level -= 1 self.emit(node, f"{j.gen.jac} ") self.indent_level += 1 diff --git a/jac/jaclang/tests/fixtures/trailing_comma.jac b/jac/jaclang/tests/fixtures/trailing_comma.jac new file mode 100644 index 0000000000..df65f74b41 --- /dev/null +++ b/jac/jaclang/tests/fixtures/trailing_comma.jac @@ -0,0 +1,88 @@ +""" +This test file is to ensure the valid syntax of jac lang. +Add new jac syntax here to test if it compile without any issue. +""" + + +# Import statement without trailing comma. +import:py from time { sleep, timezone, tzname } +# Import statement with trailing comma. +import:py from os { path, getenv, getpid, } + +enum WithoutTrailComma { + FOO = "FOO", + BAR = "BAR" +} + +enum WithTrailComma { + FOO = "FOO", + BAR = "BAR", +} + +can without_trail_comma(a: int, b: int) {} + +can with_trail_comma(a: int, b: int, c: int, ) {} + +with entry { + + dict_without_trail_comma = {"key": "value"}; + dict_with_trail_comma = {"key": "val",}; + + list_without_trail_comma = ["foo", "bar"]; + list_with_trail_comma = ["foo", "bar",]; + + set_without_trail_comma = {"foo", "bar"}; + set_with_trail_comma = {"foo", "bar"}; + obj foo11 { + has val: int, + val2: int = 9, + val3: int = 8, + val4: int = 8, + val5: int = 8; + } + empty_tuple = (); + single_tuple = ('single', ); + + tuple_without_trail_comma = ("foo", "bar"); + tuple_with_trail_comma = ("foo", "bar", ); + + mixed_tuple = (1, 'two', 3.0, False); + mixed_tuple_with_trail_comma = (1, 'two', 3.0, False, ); + + nested_tuple = (1, (2, 3), 'four'); + nested_tuple_with_trail_comma = (1, (2, 3), 'four', ); + + deeply_nested_tuple = (1, (2, (3, (4, 5)))); + deeply_nested_tuple_with_trail_comma = (1, (2, (3, (4, 5))), ); + + tuple1 = ("foo", ); + tuple1_2 = ("foo", "bar", "baz"); + tuple_2_2 = ("foo", "bar", "baz", ); + + foo_instance1 = foo11(val=90); + foo_instance1_2 = foo11(val=90, ); + + foo_instance3 = foo11(2, 34, val3=90); + foo_instance3_2 = foo11(2, 34, val3=90, ); + + foo_instance4 = foo11(2, 34, val3=90, val4=90); + foo_instance4_2 = foo11(2, 34, val3=90, val4=90, ); + + func_tuple = (foo_instance1, foo_instance3_2, len(tuple1_2)); + func_tuple_with_trail_comma = (foo_instance1, foo_instance3_2, len(tuple1_2), ); + + unpack_tuple = (1, 2, 3); + (a, b, c, ) = unpack_tuple; + values = (a, b, c); + + (1, 2); + (1, 2, ); + (1, 2, 3, 4); + (1, 2, 3, 4, ); + + val_f = (foo_instance3_2.val, foo_instance3_2.val2, foo_instance3_2.val3); + expression_tuple = (1 + 2, len("abc"), foo_instance1.val * 2); + tuple_with_list = (1, [2, 3], 4); + print("Code compiled and ran successfully!"); + +} diff --git a/jac/jaclang/tests/test_language.py b/jac/jaclang/tests/test_language.py index 5bcb1fab6a..154947c88f 100644 --- a/jac/jaclang/tests/test_language.py +++ b/jac/jaclang/tests/test_language.py @@ -677,6 +677,15 @@ def test_tuple_unpack(self) -> None: self.assertIn("1", stdout_value[0]) self.assertIn("[2, 3, 4]", stdout_value[1]) + def test_trailing_comma(self) -> None: + """Test trailing comma.""" + captured_output = io.StringIO() + sys.stdout = captured_output + jac_import("trailing_comma", base_path=self.fixture_abs_path("./")) + sys.stdout = sys.__stdout__ + stdout_value = captured_output.getvalue() + self.assertIn("Code compiled and ran successfully!", stdout_value) + def test_try_finally(self) -> None: """Test try finally.""" captured_output = io.StringIO()