From a89e744ed0e8c759caf041a580112365bbfe4303 Mon Sep 17 00:00:00 2001 From: radiohotline Date: Tue, 25 Aug 2026 16:33:56 +0300 Subject: escoria update --- .../testing/ashes/parser/esc_parser_test.gd | 586 +++++++++++++++++++++ .../testing/ashes/parser/esc_parser_test.gd.uid | 1 + .../parser/fixtures/blank_lines_before_dedent.esc | 9 + .../fixtures/blank_lines_before_dedent.esc.uid | 1 + .../fixtures/blank_lines_in_nested_blocks.esc | 11 + .../fixtures/blank_lines_in_nested_blocks.esc.uid | 1 + .../fixtures/dialog_error_then_top_level_done.esc | 10 + .../dialog_error_then_top_level_done.esc.uid | 1 + .../dialog_option_missing_indented_body.esc | 7 + .../dialog_option_missing_indented_body.esc.uid | 1 + .../dialog_option_missing_right_square.esc | 9 + .../fixtures/dialog_option_translation_key.esc | 6 + .../fixtures/dialog_option_translation_key.esc.uid | 1 + .../fixtures/dialog_start_args_with_parens.esc | 9 + .../fixtures/dialog_start_args_with_parens.esc.uid | 1 + .../fixtures/dialog_start_args_without_parens.esc | 9 + .../dialog_start_args_without_parens.esc.uid | 1 + .../ashes/parser/fixtures/done_partial_match.esc | 5 + .../parser/fixtures/global_missing_newline.esc | 5 + .../parser/fixtures/global_missing_newline.esc.uid | 1 + .../fixtures/grouping_missing_right_paren.esc | 5 + .../fixtures/grouping_missing_right_paren.esc.uid | 1 + .../parser/fixtures/happy_path_full_language.esc | 41 ++ .../fixtures/happy_path_full_language.esc.uid | 1 + .../fixtures/hyphenated_global_identifier.esc | 2 + .../fixtures/hyphenated_global_identifier.esc.uid | 1 + .../fixtures/identifier_keyword_prefixes.esc | 6 + .../fixtures/identifier_keyword_prefixes.esc.uid | 1 + .../fixtures/synchronize_after_invalid_done.esc | 6 + .../fixtures/synchronize_after_invalid_if.esc | 7 + .../fixtures/var_grouping_missing_right_paren.esc | 5 + .../var_grouping_missing_right_paren.esc.uid | 1 + .../ashes/parser/fixtures/var_missing_newline.esc | 5 + .../parser/fixtures/var_missing_newline.esc.uid | 1 + .../fixtures/while_error_then_top_level_break.esc | 9 + .../while_error_then_top_level_break.esc.uid | 1 + 36 files changed, 768 insertions(+) create mode 100644 addons/escoria-core/testing/ashes/parser/esc_parser_test.gd create mode 100644 addons/escoria-core/testing/ashes/parser/esc_parser_test.gd.uid create mode 100644 addons/escoria-core/testing/ashes/parser/fixtures/blank_lines_before_dedent.esc create mode 100644 addons/escoria-core/testing/ashes/parser/fixtures/blank_lines_before_dedent.esc.uid create mode 100644 addons/escoria-core/testing/ashes/parser/fixtures/blank_lines_in_nested_blocks.esc create mode 100644 addons/escoria-core/testing/ashes/parser/fixtures/blank_lines_in_nested_blocks.esc.uid create mode 100644 addons/escoria-core/testing/ashes/parser/fixtures/dialog_error_then_top_level_done.esc create mode 100644 addons/escoria-core/testing/ashes/parser/fixtures/dialog_error_then_top_level_done.esc.uid create mode 100644 addons/escoria-core/testing/ashes/parser/fixtures/dialog_option_missing_indented_body.esc create mode 100644 addons/escoria-core/testing/ashes/parser/fixtures/dialog_option_missing_indented_body.esc.uid create mode 100644 addons/escoria-core/testing/ashes/parser/fixtures/dialog_option_missing_right_square.esc create mode 100644 addons/escoria-core/testing/ashes/parser/fixtures/dialog_option_translation_key.esc create mode 100644 addons/escoria-core/testing/ashes/parser/fixtures/dialog_option_translation_key.esc.uid create mode 100644 addons/escoria-core/testing/ashes/parser/fixtures/dialog_start_args_with_parens.esc create mode 100644 addons/escoria-core/testing/ashes/parser/fixtures/dialog_start_args_with_parens.esc.uid create mode 100644 addons/escoria-core/testing/ashes/parser/fixtures/dialog_start_args_without_parens.esc create mode 100644 addons/escoria-core/testing/ashes/parser/fixtures/dialog_start_args_without_parens.esc.uid create mode 100644 addons/escoria-core/testing/ashes/parser/fixtures/done_partial_match.esc create mode 100644 addons/escoria-core/testing/ashes/parser/fixtures/global_missing_newline.esc create mode 100644 addons/escoria-core/testing/ashes/parser/fixtures/global_missing_newline.esc.uid create mode 100644 addons/escoria-core/testing/ashes/parser/fixtures/grouping_missing_right_paren.esc create mode 100644 addons/escoria-core/testing/ashes/parser/fixtures/grouping_missing_right_paren.esc.uid create mode 100644 addons/escoria-core/testing/ashes/parser/fixtures/happy_path_full_language.esc create mode 100644 addons/escoria-core/testing/ashes/parser/fixtures/happy_path_full_language.esc.uid create mode 100644 addons/escoria-core/testing/ashes/parser/fixtures/hyphenated_global_identifier.esc create mode 100644 addons/escoria-core/testing/ashes/parser/fixtures/hyphenated_global_identifier.esc.uid create mode 100644 addons/escoria-core/testing/ashes/parser/fixtures/identifier_keyword_prefixes.esc create mode 100644 addons/escoria-core/testing/ashes/parser/fixtures/identifier_keyword_prefixes.esc.uid create mode 100644 addons/escoria-core/testing/ashes/parser/fixtures/synchronize_after_invalid_done.esc create mode 100644 addons/escoria-core/testing/ashes/parser/fixtures/synchronize_after_invalid_if.esc create mode 100644 addons/escoria-core/testing/ashes/parser/fixtures/var_grouping_missing_right_paren.esc create mode 100644 addons/escoria-core/testing/ashes/parser/fixtures/var_grouping_missing_right_paren.esc.uid create mode 100644 addons/escoria-core/testing/ashes/parser/fixtures/var_missing_newline.esc create mode 100644 addons/escoria-core/testing/ashes/parser/fixtures/var_missing_newline.esc.uid create mode 100644 addons/escoria-core/testing/ashes/parser/fixtures/while_error_then_top_level_break.esc create mode 100644 addons/escoria-core/testing/ashes/parser/fixtures/while_error_then_top_level_break.esc.uid (limited to 'addons/escoria-core/testing/ashes/parser') diff --git a/addons/escoria-core/testing/ashes/parser/esc_parser_test.gd b/addons/escoria-core/testing/ashes/parser/esc_parser_test.gd new file mode 100644 index 0000000..8a2539c --- /dev/null +++ b/addons/escoria-core/testing/ashes/parser/esc_parser_test.gd @@ -0,0 +1,586 @@ +# GdUnit generated TestSuite +class_name ESCParserTest +extends GdUnitTestSuite + +@warning_ignore("unused_parameter") +@warning_ignore("return_value_discarded") + + +func test_done_statement_does_not_consume_tokens_on_partial_match() -> void: + # This fixture is intentionally valid up to the dialog option body: + # `done` is in a dialog context, but `done x` is invalid because the parser + # only accepts `done` followed immediately by a newline. + var source := _load_fixture("done_partial_match.esc") + var compiler := ESCCompiler.new() + var scanner := ESCScanner.new() + scanner.set_source(source) + scanner.set_filename(_fixture_path("done_partial_match.esc")) + + var tokens: Array = scanner.scan_tokens() + var parser := ESCParser.new() + parser.init(compiler, tokens, "") + + var done_index := _find_token_index(tokens, ESCTokenType.TokenType.DONE) + var print_index := _find_token_index(tokens, ESCTokenType.TokenType.IDENTIFIER, "print") + assert_int(done_index).is_greater_equal(0) + assert_int(print_index).is_greater_equal(0) + + # Jump directly to the `done` token so this test exercises the statement + # parser in isolation. We also force dialog context because `done` is only + # legal inside dialogs. + parser._current = done_index + parser._dialog_level = 1 + + var statement = parser._statement() + + # We expect a parse error, but we specifically do not want the parser to + # consume the `done` token before it discovers that the following token is + # not a newline. If it advances here, later recovery code starts from the + # wrong token and can corrupt the remaining parse. + assert_object(statement).is_instanceof(ESCParseError) + assert_bool(compiler.had_error).is_true() + assert_int(parser._current).is_equal(done_index) + assert_int(parser._peek().get_type()).is_equal(ESCTokenType.TokenType.DONE) + assert_int(tokens[print_index].get_type()).is_equal(ESCTokenType.TokenType.IDENTIFIER) + assert_str(tokens[print_index].get_lexeme()).is_equal("print") + + # Tightening the regression: the next statement must still be present in the + # token stream after the failed `done` parse attempt. This protects against + # accidental over-consumption beyond the failing statement. + assert_int(_find_token_index_after(tokens, parser._current, ESCTokenType.TokenType.IDENTIFIER, "print")).is_equal(print_index) + + +func test_parser_synchronizes_after_invalid_done_and_keeps_following_event() -> void: + # This fixture places an invalid `done` at the start of an event body and + # then follows it with a valid statement and a second event. The purpose is + # to verify parser recovery at ASHES statement and event boundaries. + var source := _load_fixture("synchronize_after_invalid_done.esc") + var compiler := ESCCompiler.new() + var scanner := ESCScanner.new() + scanner.set_source(source) + scanner.set_filename(_fixture_path("synchronize_after_invalid_done.esc")) + + var tokens: Array = scanner.scan_tokens() + var parser := ESCParser.new() + parser.init(compiler, tokens, "") + + var statements := parser.parse() + + # The parser should report an error for `done x`, but it should still recover + # well enough to continue parsing later declarations. In particular, the + # following event `:second` must survive recovery. + assert_bool(compiler.had_error).is_true() + assert_int(statements.size()).is_greater_equal(2) + assert_object(statements[0]).is_not_null() + assert_object(statements[1]).is_not_null() + assert_object(statements[1]).is_instanceof(ESCGrammarStmts.Event) + assert_str(statements[1].get_event_name()).is_equal("second") + + +func test_parser_synchronizes_after_invalid_if_and_keeps_following_event() -> void: + # This fixture uses a malformed `if` statement inside an event body. The + # missing colon forces a real statement parse error after the parser has + # already committed to the `if` branch. Recovery must still preserve the + # following top-level event declaration. + var source := _load_fixture("synchronize_after_invalid_if.esc") + var compiler := ESCCompiler.new() + var scanner := ESCScanner.new() + scanner.set_source(source) + scanner.set_filename(_fixture_path("synchronize_after_invalid_if.esc")) + + var tokens: Array = scanner.scan_tokens() + var parser := ESCParser.new() + parser.init(compiler, tokens, "") + + var statements := parser.parse() + + assert_bool(compiler.had_error).is_true() + assert_int(statements.size()).is_greater_equal(2) + assert_object(statements[0]).is_not_null() + assert_object(statements[1]).is_not_null() + assert_object(statements[1]).is_instanceof(ESCGrammarStmts.Event) + assert_str(statements[1].get_event_name()).is_equal("second") + + +func test_dialog_option_missing_right_square_returns_parse_error() -> void: + # This fixture omits the closing `]` from a dialog option predicate. The + # parser should report a parse error here rather than silently building a + # malformed dialog option node and continuing as if the condition was valid. + # Recovery should still preserve the following top-level event declaration. + var source := _load_fixture("dialog_option_missing_right_square.esc") + var compiler := ESCCompiler.new() + var scanner := ESCScanner.new() + scanner.set_source(source) + scanner.set_filename(_fixture_path("dialog_option_missing_right_square.esc")) + + var tokens: Array = scanner.scan_tokens() + var parser := ESCParser.new() + parser.init(compiler, tokens, "") + + var statements := parser.parse() + + assert_bool(compiler.had_error).is_true() + assert_int(statements.size()).is_greater_equal(2) + assert_object(statements[0]).is_not_null() + assert_object(statements[0]).is_instanceof(ESCGrammarStmts.Event) + assert_str(statements[0].get_event_name()).is_equal("test") + + var found_second := false + for statement in statements: + if statement is ESCGrammarStmts.Event and statement.get_event_name() == "second": + found_second = true + break + + assert_bool(found_second).is_true() + + +func test_dialog_option_missing_indented_body_returns_parse_error() -> void: + # This fixture omits the required indented body after a dialog option. The + # parser should surface the missing block-start error instead of continuing + # into `_block()` and constructing a malformed option node. + var source := _load_fixture("dialog_option_missing_indented_body.esc") + var compiler := ESCCompiler.new() + var scanner := ESCScanner.new() + scanner.set_source(source) + scanner.set_filename(_fixture_path("dialog_option_missing_indented_body.esc")) + + var tokens: Array = scanner.scan_tokens() + var parser := ESCParser.new() + parser.init(compiler, tokens, "") + + var statements := parser.parse() + + assert_bool(compiler.had_error).is_true() + assert_int(statements.size()).is_greater_equal(2) + assert_object(statements[0]).is_not_null() + assert_object(statements[0]).is_instanceof(ESCGrammarStmts.Event) + assert_str(statements[0].get_event_name()).is_equal("test") + + var found_second := false + for statement in statements: + if statement is ESCGrammarStmts.Event and statement.get_event_name() == "second": + found_second = true + break + + assert_bool(found_second).is_true() + + +func test_dialog_start_arguments_without_parens_parse_and_are_preserved() -> void: + # The public docs show dialog-start arguments in space-separated form: + # `?! "res://avatar" 5 2`. This regression locks that syntax down so the + # parser preserves those arguments instead of requiring the older `?!(...)` + # form. + var source := _load_fixture("dialog_start_args_without_parens.esc") + var compiler := ESCCompiler.new() + var scanner := ESCScanner.new() + scanner.set_source(source) + scanner.set_filename(_fixture_path("dialog_start_args_without_parens.esc")) + + var tokens: Array = scanner.scan_tokens() + var parser := ESCParser.new() + parser.init(compiler, tokens, "") + + var statements := parser.parse() + + assert_bool(compiler.had_error).is_false() + assert_int(statements.size()).is_equal(2) + assert_object(statements[0]).is_instanceof(ESCGrammarStmts.Event) + assert_object(statements[1]).is_instanceof(ESCGrammarStmts.Event) + + var talk_event := statements[0] as ESCGrammarStmts.Event + var talk_block := talk_event.get_body() as ESCGrammarStmts.Block + assert_int(talk_block.get_statements().size()).is_equal(1) + assert_object(talk_block.get_statements()[0]).is_instanceof(ESCGrammarStmts.Dialog) + + var dialog := talk_block.get_statements()[0] as ESCGrammarStmts.Dialog + var args := dialog.get_args() + assert_int(args.size()).is_equal(3) + assert_object(args[0]).is_instanceof(ESCGrammarExprs.Literal) + assert_str(args[0].get_value()).is_equal("res://game/dialog_avatars/player.tres") + assert_object(args[1]).is_instanceof(ESCGrammarExprs.Literal) + assert_float(args[1].get_value()).is_equal(5.0) + assert_object(args[2]).is_instanceof(ESCGrammarExprs.Literal) + assert_float(args[2].get_value()).is_equal(2.0) + + +func test_dialog_start_arguments_with_parens_parse_and_are_preserved() -> void: + # Preserve the older parenthesized positional form as well, so both + # positional dialog-start syntaxes remain supported. + var source := _load_fixture("dialog_start_args_with_parens.esc") + var compiler := ESCCompiler.new() + var scanner := ESCScanner.new() + scanner.set_source(source) + scanner.set_filename(_fixture_path("dialog_start_args_with_parens.esc")) + + var tokens: Array = scanner.scan_tokens() + var parser := ESCParser.new() + parser.init(compiler, tokens, "") + + var statements := parser.parse() + + assert_bool(compiler.had_error).is_false() + assert_int(statements.size()).is_equal(2) + assert_object(statements[0]).is_instanceof(ESCGrammarStmts.Event) + assert_object(statements[1]).is_instanceof(ESCGrammarStmts.Event) + + var talk_event := statements[0] as ESCGrammarStmts.Event + var talk_block := talk_event.get_body() as ESCGrammarStmts.Block + assert_int(talk_block.get_statements().size()).is_equal(1) + assert_object(talk_block.get_statements()[0]).is_instanceof(ESCGrammarStmts.Dialog) + + var dialog := talk_block.get_statements()[0] as ESCGrammarStmts.Dialog + var args := dialog.get_args() + assert_int(args.size()).is_equal(3) + assert_object(args[0]).is_instanceof(ESCGrammarExprs.Literal) + assert_str(args[0].get_value()).is_equal("res://game/dialog_avatars/player.tres") + assert_object(args[1]).is_instanceof(ESCGrammarExprs.Literal) + assert_float(args[1].get_value()).is_equal(5.0) + assert_object(args[2]).is_instanceof(ESCGrammarExprs.Literal) + assert_float(args[2].get_value()).is_equal(2.0) + + +func test_dialog_option_translation_key_is_parsed_explicitly() -> void: + var source := _load_fixture("dialog_option_translation_key.esc") + var compiler := ESCCompiler.new() + var scanner := ESCScanner.new() + scanner.set_source(source) + scanner.set_filename(_fixture_path("dialog_option_translation_key.esc")) + + var tokens: Array = scanner.scan_tokens() + var parser := ESCParser.new() + parser.init(compiler, tokens, "") + + var statements := parser.parse() + + assert_bool(compiler.had_error).is_false() + assert_int(statements.size()).is_equal(1) + assert_object(statements[0]).is_instanceof(ESCGrammarStmts.Event) + + var talk_event := statements[0] as ESCGrammarStmts.Event + var talk_block := talk_event.get_body() as ESCGrammarStmts.Block + var dialog := talk_block.get_statements()[0] as ESCGrammarStmts.Dialog + var first_option := dialog.get_options()[0] as ESCGrammarStmts.DialogOption + var second_option := dialog.get_options()[1] as ESCGrammarStmts.DialogOption + + assert_str(first_option.get_translation_key()).is_equal("UNCLE_SVEN") + assert_str(second_option.get_translation_key()).is_equal("") + + +func test_grouping_missing_right_paren_returns_parse_error() -> void: + # This fixture omits the closing `)` from a grouped expression inside a + # function call. The parser must not silently accept the grouping and build + # a malformed AST; it should report the error and still preserve the + # following event declaration after recovery. + var source := _load_fixture("grouping_missing_right_paren.esc") + var compiler := ESCCompiler.new() + var scanner := ESCScanner.new() + scanner.set_source(source) + scanner.set_filename(_fixture_path("grouping_missing_right_paren.esc")) + + var tokens: Array = scanner.scan_tokens() + var parser := ESCParser.new() + parser.init(compiler, tokens, "") + + var statements := parser.parse() + + assert_bool(compiler.had_error).is_true() + assert_int(statements.size()).is_greater_equal(2) + assert_object(statements[0]).is_not_null() + assert_object(statements[0]).is_instanceof(ESCGrammarStmts.Event) + assert_str(statements[0].get_event_name()).is_equal("test") + assert_object(statements[1]).is_not_null() + assert_object(statements[1]).is_instanceof(ESCGrammarStmts.Event) + assert_str(statements[1].get_event_name()).is_equal("second") + + +func test_var_grouping_missing_right_paren_returns_parse_error() -> void: + # This fixture omits the closing `)` from a grouped expression in a variable + # initializer. Unlike the call-expression regression above, this path goes + # straight through `_primary()`, so the parser must reject the malformed + # grouping instead of silently constructing a `Grouping` node. + var source := _load_fixture("var_grouping_missing_right_paren.esc") + var compiler := ESCCompiler.new() + var scanner := ESCScanner.new() + scanner.set_source(source) + scanner.set_filename(_fixture_path("var_grouping_missing_right_paren.esc")) + + var tokens: Array = scanner.scan_tokens() + var parser := ESCParser.new() + parser.init(compiler, tokens, "") + + var statements := parser.parse() + + assert_bool(compiler.had_error).is_true() + assert_int(statements.size()).is_greater_equal(2) + assert_object(statements[0]).is_not_null() + assert_object(statements[0]).is_instanceof(ESCGrammarStmts.Event) + assert_str(statements[0].get_event_name()).is_equal("test") + assert_object(statements[1]).is_not_null() + assert_object(statements[1]).is_instanceof(ESCGrammarStmts.Event) + assert_str(statements[1].get_event_name()).is_equal("second") + + +func test_var_declaration_missing_newline_returns_parse_error() -> void: + # This fixture continues a variable declaration onto the same line with a + # second statement. The parser should not silently accept the declaration + # when the required trailing newline is missing, and recovery should still + # preserve the following event. + var source := _load_fixture("var_missing_newline.esc") + var compiler := ESCCompiler.new() + var scanner := ESCScanner.new() + scanner.set_source(source) + scanner.set_filename(_fixture_path("var_missing_newline.esc")) + + var tokens: Array = scanner.scan_tokens() + var parser := ESCParser.new() + parser.init(compiler, tokens, "") + + var statements := parser.parse() + + assert_bool(compiler.had_error).is_true() + assert_int(statements.size()).is_greater_equal(2) + assert_object(statements[0]).is_not_null() + assert_object(statements[0]).is_instanceof(ESCGrammarStmts.Event) + assert_str(statements[0].get_event_name()).is_equal("test") + assert_object(statements[1]).is_not_null() + assert_object(statements[1]).is_instanceof(ESCGrammarStmts.Event) + assert_str(statements[1].get_event_name()).is_equal("second") + + +func test_global_declaration_missing_newline_returns_parse_error() -> void: + # This fixture continues a global declaration onto the same line with a + # second statement. The parser should reject the malformed declaration + # instead of constructing a partial `Global` node, and recovery should still + # preserve the following event. + var source := _load_fixture("global_missing_newline.esc") + var compiler := ESCCompiler.new() + var scanner := ESCScanner.new() + scanner.set_source(source) + scanner.set_filename(_fixture_path("global_missing_newline.esc")) + + var tokens: Array = scanner.scan_tokens() + var parser := ESCParser.new() + parser.init(compiler, tokens, "") + + var statements := parser.parse() + + assert_bool(compiler.had_error).is_true() + assert_int(statements.size()).is_greater_equal(2) + assert_object(statements[0]).is_not_null() + assert_object(statements[0]).is_instanceof(ESCGrammarStmts.Event) + assert_str(statements[0].get_event_name()).is_equal("test") + assert_object(statements[1]).is_not_null() + assert_object(statements[1]).is_instanceof(ESCGrammarStmts.Event) + assert_str(statements[1].get_event_name()).is_equal("second") + + +func test_while_error_does_not_leak_loop_level_into_later_event() -> void: + # This fixture forces `_while_statement()` to fail after incrementing + # `_loop_level`. The later `break` in `:second` is top-level within that + # event body and must still be rejected. If the loop counter leaks across the + # error path, the parser will incorrectly accept it as a valid `Break`. + var source := _load_fixture("while_error_then_top_level_break.esc") + var compiler := ESCCompiler.new() + var scanner := ESCScanner.new() + scanner.set_source(source) + scanner.set_filename(_fixture_path("while_error_then_top_level_break.esc")) + + var tokens: Array = scanner.scan_tokens() + var parser := ESCParser.new() + parser.init(compiler, tokens, "") + + var statements := parser.parse() + + assert_bool(compiler.had_error).is_true() + assert_int(statements.size()).is_greater_equal(3) + assert_object(statements[0]).is_instanceof(ESCGrammarStmts.Event) + assert_str(statements[0].get_event_name()).is_equal("test") + assert_object(statements[1]).is_instanceof(ESCGrammarStmts.Event) + assert_str(statements[1].get_event_name()).is_equal("second") + assert_object(statements[2]).is_instanceof(ESCGrammarStmts.Event) + assert_str(statements[2].get_event_name()).is_equal("third") + + var second_body: Array = statements[1].get_body().get_statements() + assert_int(second_body.size()).is_equal(0) + + +func test_dialog_error_does_not_leak_dialog_level_into_later_event() -> void: + # This fixture forces `_dialog_statement()` to fail after incrementing + # `_dialog_level`. The later `done` in `:second` is top-level within that + # event body and must still be rejected. If the dialog counter leaks across + # the error path, the parser will incorrectly accept it as a valid `Done`. + var source := _load_fixture("dialog_error_then_top_level_done.esc") + var compiler := ESCCompiler.new() + var scanner := ESCScanner.new() + scanner.set_source(source) + scanner.set_filename(_fixture_path("dialog_error_then_top_level_done.esc")) + + var tokens: Array = scanner.scan_tokens() + var parser := ESCParser.new() + parser.init(compiler, tokens, "") + + var statements := parser.parse() + + assert_bool(compiler.had_error).is_true() + assert_int(statements.size()).is_greater_equal(3) + assert_object(statements[0]).is_instanceof(ESCGrammarStmts.Event) + assert_str(statements[0].get_event_name()).is_equal("test") + assert_object(statements[1]).is_instanceof(ESCGrammarStmts.Event) + assert_str(statements[1].get_event_name()).is_equal("second") + assert_object(statements[2]).is_instanceof(ESCGrammarStmts.Event) + assert_str(statements[2].get_event_name()).is_equal("third") + + var second_body: Array = statements[1].get_body().get_statements() + assert_int(second_body.size()).is_equal(0) + + +func test_happy_path_full_language_fixture_parses_without_errors() -> void: + # This smoke fixture exercises a broad cross-section of ASHES syntax on the + # parser happy path: event flags, targets, globals, locals, branching, + # looping, built-ins, dialog, nested dialog, conditional dialog options, + # `break`, `done`, `in inventory`, `is active`, and `is "state"`. + var source := _load_fixture("happy_path_full_language.esc") + var compiler := ESCCompiler.new() + var scanner := ESCScanner.new() + scanner.set_source(source) + scanner.set_filename(_fixture_path("happy_path_full_language.esc")) + + var tokens: Array = scanner.scan_tokens() + var parser := ESCParser.new() + parser.init(compiler, tokens, "") + + var statements := parser.parse() + + assert_bool(compiler.had_error).is_false() + assert_int(statements.size()).is_equal(2) + assert_object(statements[0]).is_instanceof(ESCGrammarStmts.Event) + assert_str(statements[0].get_event_name()).is_equal("setup") + assert_int(statements[0].get_num_statements_in_block()).is_equal(6) + assert_object(statements[1]).is_instanceof(ESCGrammarStmts.Event) + assert_str(statements[1].get_event_name()).is_equal("look") + assert_str(statements[1].get_target_name()).is_equal("worker") + assert_int(statements[1].get_num_statements_in_block()).is_equal(3) + + +func test_keyword_prefixed_names_scan_as_identifiers() -> void: + # ASHES keywords should only match on exact identifier text. Names that + # merely start with keyword text must still scan as ordinary identifiers. + var source := _load_fixture("identifier_keyword_prefixes.esc") + var compiler := ESCCompiler.new() + var scanner := ESCScanner.new() + scanner.set_source(source) + scanner.set_filename(_fixture_path("identifier_keyword_prefixes.esc")) + + var tokens: Array = scanner.scan_tokens() + var parser := ESCParser.new() + parser.init(compiler, tokens, "") + var statements := parser.parse() + + assert_bool(compiler.had_error).is_false() + assert_int(_find_token_index(tokens, ESCTokenType.TokenType.IDENTIFIER, "breakfast")).is_greater_equal(0) + assert_int(_find_token_index(tokens, ESCTokenType.TokenType.IDENTIFIER, "donee")).is_greater_equal(0) + assert_int(_find_token_index(tokens, ESCTokenType.TokenType.IDENTIFIER, "elseify")).is_greater_equal(0) + assert_int(_find_token_index(tokens, ESCTokenType.TokenType.IDENTIFIER, "notable")).is_greater_equal(0) + assert_int(_find_token_index(tokens, ESCTokenType.TokenType.BREAK, "break")).is_equal(-1) + assert_int(_find_token_index(tokens, ESCTokenType.TokenType.DONE, "done")).is_equal(-1) + assert_int(_find_token_index(tokens, ESCTokenType.TokenType.ELSE, "else")).is_equal(-1) + assert_int(_find_token_index(tokens, ESCTokenType.TokenType.NOT, "not")).is_equal(-1) + assert_int(statements.size()).is_equal(1) + assert_object(statements[0]).is_instanceof(ESCGrammarStmts.Event) + assert_str(statements[0].get_event_name()).is_equal("test") + + +func test_hyphenated_global_identifier_scans_as_single_identifier() -> void: + # `$`-prefixed global-ID shorthand may contain hyphens, but ordinary `-` + # characters must still scan as subtraction operators outside that prefix. + var source := _load_fixture("hyphenated_global_identifier.esc") + var compiler := ESCCompiler.new() + var scanner := ESCScanner.new() + scanner.set_source(source) + scanner.set_filename(_fixture_path("hyphenated_global_identifier.esc")) + + var tokens: Array = scanner.scan_tokens() + var parser := ESCParser.new() + parser.init(compiler, tokens, "") + var statements := parser.parse() + + assert_bool(compiler.had_error).is_false() + assert_int(_find_token_index(tokens, ESCTokenType.TokenType.IDENTIFIER, "$closet-door")).is_greater_equal(0) + assert_int(_find_token_index(tokens, ESCTokenType.TokenType.MINUS, "-")).is_greater_equal(0) + assert_int(_find_token_index(tokens, ESCTokenType.TokenType.NUMBER, "1")).is_greater_equal(0) + assert_int(statements.size()).is_equal(1) + assert_object(statements[0]).is_instanceof(ESCGrammarStmts.Event) + assert_str(statements[0].get_event_name()).is_equal("test") + + +func test_blank_lines_in_nested_blocks_do_not_break_later_events() -> void: + # Blank lines inside nested blocks should not corrupt INDENT/DEDENT handling. + # The parser must still recover the later top-level event after the nested + # block ends. + var source := _load_fixture("blank_lines_in_nested_blocks.esc") + var compiler := ESCCompiler.new() + var scanner := ESCScanner.new() + scanner.set_source(source) + scanner.set_filename(_fixture_path("blank_lines_in_nested_blocks.esc")) + + var tokens: Array = scanner.scan_tokens() + var parser := ESCParser.new() + parser.init(compiler, tokens, "") + var statements := parser.parse() + + assert_bool(compiler.had_error).is_false() + assert_int(statements.size()).is_equal(2) + assert_object(statements[0]).is_instanceof(ESCGrammarStmts.Event) + assert_str(statements[0].get_event_name()).is_equal("test") + assert_object(statements[1]).is_instanceof(ESCGrammarStmts.Event) + assert_str(statements[1].get_event_name()).is_equal("second") + + +func test_blank_lines_before_dedent_do_not_hide_block_termination() -> void: + # Blank lines immediately before a block-closing dedent should not be eaten + # in a way that hides the dedent from `_block()`. The outer statement after + # the nested block, plus the later top-level event, must still parse. + var source := _load_fixture("blank_lines_before_dedent.esc") + var compiler := ESCCompiler.new() + var scanner := ESCScanner.new() + scanner.set_source(source) + scanner.set_filename(_fixture_path("blank_lines_before_dedent.esc")) + + var tokens: Array = scanner.scan_tokens() + var parser := ESCParser.new() + parser.init(compiler, tokens, "") + var statements := parser.parse() + + assert_bool(compiler.had_error).is_false() + assert_int(statements.size()).is_equal(2) + assert_object(statements[0]).is_instanceof(ESCGrammarStmts.Event) + assert_str(statements[0].get_event_name()).is_equal("test") + assert_object(statements[1]).is_instanceof(ESCGrammarStmts.Event) + assert_str(statements[1].get_event_name()).is_equal("second") + assert_int(statements[0].get_num_statements_in_block()).is_equal(2) + + +func _load_fixture(name: String) -> String: + return FileAccess.get_file_as_string(_fixture_path(name)) + + +func _fixture_path(name: String) -> String: + return "res://addons/escoria-core/testing/ashes/parser/fixtures/%s" % name + + +func _find_token_index(tokens: Array, token_type: int, lexeme: String = "") -> int: + for index in range(tokens.size()): + var token = tokens[index] + if token.get_type() == token_type and (lexeme.is_empty() or token.get_lexeme() == lexeme): + return index + + return -1 + + +func _find_token_index_after(tokens: Array, current_index: int, token_type: int, lexeme: String = "") -> int: + for index in range(current_index + 1, tokens.size()): + var token = tokens[index] + if token.get_type() == token_type and (lexeme.is_empty() or token.get_lexeme() == lexeme): + return index + + return -1 diff --git a/addons/escoria-core/testing/ashes/parser/esc_parser_test.gd.uid b/addons/escoria-core/testing/ashes/parser/esc_parser_test.gd.uid new file mode 100644 index 0000000..e97a2bb --- /dev/null +++ b/addons/escoria-core/testing/ashes/parser/esc_parser_test.gd.uid @@ -0,0 +1 @@ +uid://di2o7dahssqsc diff --git a/addons/escoria-core/testing/ashes/parser/fixtures/blank_lines_before_dedent.esc b/addons/escoria-core/testing/ashes/parser/fixtures/blank_lines_before_dedent.esc new file mode 100644 index 0000000..0ba7826 --- /dev/null +++ b/addons/escoria-core/testing/ashes/parser/fixtures/blank_lines_before_dedent.esc @@ -0,0 +1,9 @@ +:test + if true: + print("inside") + + + print("after") + +:second + print("ok") diff --git a/addons/escoria-core/testing/ashes/parser/fixtures/blank_lines_before_dedent.esc.uid b/addons/escoria-core/testing/ashes/parser/fixtures/blank_lines_before_dedent.esc.uid new file mode 100644 index 0000000..4287f2f --- /dev/null +++ b/addons/escoria-core/testing/ashes/parser/fixtures/blank_lines_before_dedent.esc.uid @@ -0,0 +1 @@ +uid://dc3kyx3rnh13d diff --git a/addons/escoria-core/testing/ashes/parser/fixtures/blank_lines_in_nested_blocks.esc b/addons/escoria-core/testing/ashes/parser/fixtures/blank_lines_in_nested_blocks.esc new file mode 100644 index 0000000..f1bf7dc --- /dev/null +++ b/addons/escoria-core/testing/ashes/parser/fixtures/blank_lines_in_nested_blocks.esc @@ -0,0 +1,11 @@ +:test + if true: + var value = 1 + + if true: + print(value) + + print("done") + +:second + print("ok") diff --git a/addons/escoria-core/testing/ashes/parser/fixtures/blank_lines_in_nested_blocks.esc.uid b/addons/escoria-core/testing/ashes/parser/fixtures/blank_lines_in_nested_blocks.esc.uid new file mode 100644 index 0000000..5d9dc6a --- /dev/null +++ b/addons/escoria-core/testing/ashes/parser/fixtures/blank_lines_in_nested_blocks.esc.uid @@ -0,0 +1 @@ +uid://c6pkm7n2t4wyf diff --git a/addons/escoria-core/testing/ashes/parser/fixtures/dialog_error_then_top_level_done.esc b/addons/escoria-core/testing/ashes/parser/fixtures/dialog_error_then_top_level_done.esc new file mode 100644 index 0000000..a6aa528 --- /dev/null +++ b/addons/escoria-core/testing/ashes/parser/fixtures/dialog_error_then_top_level_done.esc @@ -0,0 +1,10 @@ +:test + ?!(1, + - "Option" + print("broken") + +:second + done + +:third + print("ok") diff --git a/addons/escoria-core/testing/ashes/parser/fixtures/dialog_error_then_top_level_done.esc.uid b/addons/escoria-core/testing/ashes/parser/fixtures/dialog_error_then_top_level_done.esc.uid new file mode 100644 index 0000000..13ee072 --- /dev/null +++ b/addons/escoria-core/testing/ashes/parser/fixtures/dialog_error_then_top_level_done.esc.uid @@ -0,0 +1 @@ +uid://f4q8q8et7mbo2 diff --git a/addons/escoria-core/testing/ashes/parser/fixtures/dialog_option_missing_indented_body.esc b/addons/escoria-core/testing/ashes/parser/fixtures/dialog_option_missing_indented_body.esc new file mode 100644 index 0000000..51c525c --- /dev/null +++ b/addons/escoria-core/testing/ashes/parser/fixtures/dialog_option_missing_indented_body.esc @@ -0,0 +1,7 @@ +:test + ?! + - "Option" + print("after") + +:second + print("ok") diff --git a/addons/escoria-core/testing/ashes/parser/fixtures/dialog_option_missing_indented_body.esc.uid b/addons/escoria-core/testing/ashes/parser/fixtures/dialog_option_missing_indented_body.esc.uid new file mode 100644 index 0000000..7bd0fe8 --- /dev/null +++ b/addons/escoria-core/testing/ashes/parser/fixtures/dialog_option_missing_indented_body.esc.uid @@ -0,0 +1 @@ +uid://blnqmv4wjsoi8 diff --git a/addons/escoria-core/testing/ashes/parser/fixtures/dialog_option_missing_right_square.esc b/addons/escoria-core/testing/ashes/parser/fixtures/dialog_option_missing_right_square.esc new file mode 100644 index 0000000..32f407a --- /dev/null +++ b/addons/escoria-core/testing/ashes/parser/fixtures/dialog_option_missing_right_square.esc @@ -0,0 +1,9 @@ +:test + ?! + - "Option" [some_flag < 3 + print("after") + - "Second option" + print("ok") + +:second + print("after_dialog") diff --git a/addons/escoria-core/testing/ashes/parser/fixtures/dialog_option_translation_key.esc b/addons/escoria-core/testing/ashes/parser/fixtures/dialog_option_translation_key.esc new file mode 100644 index 0000000..69f88db --- /dev/null +++ b/addons/escoria-core/testing/ashes/parser/fixtures/dialog_option_translation_key.esc @@ -0,0 +1,6 @@ +:talk + ?! + - UNCLE_SVEN:"Uncle Sven sends regards." + break + - "Fallback" + break diff --git a/addons/escoria-core/testing/ashes/parser/fixtures/dialog_option_translation_key.esc.uid b/addons/escoria-core/testing/ashes/parser/fixtures/dialog_option_translation_key.esc.uid new file mode 100644 index 0000000..7ac79a6 --- /dev/null +++ b/addons/escoria-core/testing/ashes/parser/fixtures/dialog_option_translation_key.esc.uid @@ -0,0 +1 @@ +uid://doadggrcx5l6l diff --git a/addons/escoria-core/testing/ashes/parser/fixtures/dialog_start_args_with_parens.esc b/addons/escoria-core/testing/ashes/parser/fixtures/dialog_start_args_with_parens.esc new file mode 100644 index 0000000..0b3c59e --- /dev/null +++ b/addons/escoria-core/testing/ashes/parser/fixtures/dialog_start_args_with_parens.esc @@ -0,0 +1,9 @@ +:talk + ?!("res://game/dialog_avatars/player.tres", 5, 2) + - "First option" + say($player, "First option") + - "Second option" + say($player, "Second option") + +:second + print("ok") diff --git a/addons/escoria-core/testing/ashes/parser/fixtures/dialog_start_args_with_parens.esc.uid b/addons/escoria-core/testing/ashes/parser/fixtures/dialog_start_args_with_parens.esc.uid new file mode 100644 index 0000000..076398a --- /dev/null +++ b/addons/escoria-core/testing/ashes/parser/fixtures/dialog_start_args_with_parens.esc.uid @@ -0,0 +1 @@ +uid://d0xvqlf18ax11 diff --git a/addons/escoria-core/testing/ashes/parser/fixtures/dialog_start_args_without_parens.esc b/addons/escoria-core/testing/ashes/parser/fixtures/dialog_start_args_without_parens.esc new file mode 100644 index 0000000..cdb2090 --- /dev/null +++ b/addons/escoria-core/testing/ashes/parser/fixtures/dialog_start_args_without_parens.esc @@ -0,0 +1,9 @@ +:talk + ?! "res://game/dialog_avatars/player.tres" 5 2 + - "First option" + say($player, "First option") + - "Second option" + say($player, "Second option") + +:second + print("ok") diff --git a/addons/escoria-core/testing/ashes/parser/fixtures/dialog_start_args_without_parens.esc.uid b/addons/escoria-core/testing/ashes/parser/fixtures/dialog_start_args_without_parens.esc.uid new file mode 100644 index 0000000..d0dd054 --- /dev/null +++ b/addons/escoria-core/testing/ashes/parser/fixtures/dialog_start_args_without_parens.esc.uid @@ -0,0 +1 @@ +uid://cb2w2jdpxy8sr diff --git a/addons/escoria-core/testing/ashes/parser/fixtures/done_partial_match.esc b/addons/escoria-core/testing/ashes/parser/fixtures/done_partial_match.esc new file mode 100644 index 0000000..a36a85b --- /dev/null +++ b/addons/escoria-core/testing/ashes/parser/fixtures/done_partial_match.esc @@ -0,0 +1,5 @@ +:test + ?! + - "Option" + done x + print("after") diff --git a/addons/escoria-core/testing/ashes/parser/fixtures/global_missing_newline.esc b/addons/escoria-core/testing/ashes/parser/fixtures/global_missing_newline.esc new file mode 100644 index 0000000..f51f093 --- /dev/null +++ b/addons/escoria-core/testing/ashes/parser/fixtures/global_missing_newline.esc @@ -0,0 +1,5 @@ +:test + global value = 1 print("oops") + +:second + print("ok") diff --git a/addons/escoria-core/testing/ashes/parser/fixtures/global_missing_newline.esc.uid b/addons/escoria-core/testing/ashes/parser/fixtures/global_missing_newline.esc.uid new file mode 100644 index 0000000..bcb4fd1 --- /dev/null +++ b/addons/escoria-core/testing/ashes/parser/fixtures/global_missing_newline.esc.uid @@ -0,0 +1 @@ +uid://di8w10jb2c7u4 diff --git a/addons/escoria-core/testing/ashes/parser/fixtures/grouping_missing_right_paren.esc b/addons/escoria-core/testing/ashes/parser/fixtures/grouping_missing_right_paren.esc new file mode 100644 index 0000000..97e2e1e --- /dev/null +++ b/addons/escoria-core/testing/ashes/parser/fixtures/grouping_missing_right_paren.esc @@ -0,0 +1,5 @@ +:test + print((1 + 2) + +:second + print("ok") diff --git a/addons/escoria-core/testing/ashes/parser/fixtures/grouping_missing_right_paren.esc.uid b/addons/escoria-core/testing/ashes/parser/fixtures/grouping_missing_right_paren.esc.uid new file mode 100644 index 0000000..a872107 --- /dev/null +++ b/addons/escoria-core/testing/ashes/parser/fixtures/grouping_missing_right_paren.esc.uid @@ -0,0 +1 @@ +uid://dnj6r3c3d8m5x diff --git a/addons/escoria-core/testing/ashes/parser/fixtures/happy_path_full_language.esc b/addons/escoria-core/testing/ashes/parser/fixtures/happy_path_full_language.esc new file mode 100644 index 0000000..0a82acd --- /dev/null +++ b/addons/escoria-core/testing/ashes/parser/fixtures/happy_path_full_language.esc @@ -0,0 +1,41 @@ +:setup | NO_UI | NO_SAVE + global has_met_worker = false + global ask_count = 0 + var local_count = 0 + var target_name = $worker + + if !has_met_worker and ESC_LAST_SCENE == "room1": + print("first visit") + elif $worker is active: + print("worker active") + else: + print("fallback") + + while local_count < 3: + local_count = local_count + 1 + if local_count == 2: + break + +:look "worker" + if $badge in inventory: + print("has badge") + elif $worker is "busy": + print("busy") + else: + print("idle") + + ?! + - "Who are you?" [!has_met_worker] + print("ask name") + has_met_worker = true + - "Tell me more." [ask_count < 2] + ask_count = ask_count + 1 + ?! + - "Enough." + break 1 + - "I am done." + done + - "Bye." + break + + print("after dialog") diff --git a/addons/escoria-core/testing/ashes/parser/fixtures/happy_path_full_language.esc.uid b/addons/escoria-core/testing/ashes/parser/fixtures/happy_path_full_language.esc.uid new file mode 100644 index 0000000..d6713b5 --- /dev/null +++ b/addons/escoria-core/testing/ashes/parser/fixtures/happy_path_full_language.esc.uid @@ -0,0 +1 @@ +uid://cbv6x0w8g17ap diff --git a/addons/escoria-core/testing/ashes/parser/fixtures/hyphenated_global_identifier.esc b/addons/escoria-core/testing/ashes/parser/fixtures/hyphenated_global_identifier.esc new file mode 100644 index 0000000..0c86561 --- /dev/null +++ b/addons/escoria-core/testing/ashes/parser/fixtures/hyphenated_global_identifier.esc @@ -0,0 +1,2 @@ +:test + var total = $closet-door - 1 diff --git a/addons/escoria-core/testing/ashes/parser/fixtures/hyphenated_global_identifier.esc.uid b/addons/escoria-core/testing/ashes/parser/fixtures/hyphenated_global_identifier.esc.uid new file mode 100644 index 0000000..f2e55d8 --- /dev/null +++ b/addons/escoria-core/testing/ashes/parser/fixtures/hyphenated_global_identifier.esc.uid @@ -0,0 +1 @@ +uid://b2mxj0r3fk4yy diff --git a/addons/escoria-core/testing/ashes/parser/fixtures/identifier_keyword_prefixes.esc b/addons/escoria-core/testing/ashes/parser/fixtures/identifier_keyword_prefixes.esc new file mode 100644 index 0000000..dd86b8f --- /dev/null +++ b/addons/escoria-core/testing/ashes/parser/fixtures/identifier_keyword_prefixes.esc @@ -0,0 +1,6 @@ +:test + var breakfast = 1 + var donee = 2 + var elseify = 3 + var notable = 4 + print(breakfast + donee + elseify + notable) diff --git a/addons/escoria-core/testing/ashes/parser/fixtures/identifier_keyword_prefixes.esc.uid b/addons/escoria-core/testing/ashes/parser/fixtures/identifier_keyword_prefixes.esc.uid new file mode 100644 index 0000000..3bdfa1b --- /dev/null +++ b/addons/escoria-core/testing/ashes/parser/fixtures/identifier_keyword_prefixes.esc.uid @@ -0,0 +1 @@ +uid://d1s8r2n3kq1vh diff --git a/addons/escoria-core/testing/ashes/parser/fixtures/synchronize_after_invalid_done.esc b/addons/escoria-core/testing/ashes/parser/fixtures/synchronize_after_invalid_done.esc new file mode 100644 index 0000000..0203977 --- /dev/null +++ b/addons/escoria-core/testing/ashes/parser/fixtures/synchronize_after_invalid_done.esc @@ -0,0 +1,6 @@ +:test + done x + print("after") + +:second + print("ok") diff --git a/addons/escoria-core/testing/ashes/parser/fixtures/synchronize_after_invalid_if.esc b/addons/escoria-core/testing/ashes/parser/fixtures/synchronize_after_invalid_if.esc new file mode 100644 index 0000000..d871c98 --- /dev/null +++ b/addons/escoria-core/testing/ashes/parser/fixtures/synchronize_after_invalid_if.esc @@ -0,0 +1,7 @@ +:test + if some_flag + print("broken") + print("after") + +:second + print("ok") diff --git a/addons/escoria-core/testing/ashes/parser/fixtures/var_grouping_missing_right_paren.esc b/addons/escoria-core/testing/ashes/parser/fixtures/var_grouping_missing_right_paren.esc new file mode 100644 index 0000000..05fd749 --- /dev/null +++ b/addons/escoria-core/testing/ashes/parser/fixtures/var_grouping_missing_right_paren.esc @@ -0,0 +1,5 @@ +:test + var x = (1 + 2 + +:second + print("ok") diff --git a/addons/escoria-core/testing/ashes/parser/fixtures/var_grouping_missing_right_paren.esc.uid b/addons/escoria-core/testing/ashes/parser/fixtures/var_grouping_missing_right_paren.esc.uid new file mode 100644 index 0000000..567b162 --- /dev/null +++ b/addons/escoria-core/testing/ashes/parser/fixtures/var_grouping_missing_right_paren.esc.uid @@ -0,0 +1 @@ +uid://b2d1d5qj4o8t7 diff --git a/addons/escoria-core/testing/ashes/parser/fixtures/var_missing_newline.esc b/addons/escoria-core/testing/ashes/parser/fixtures/var_missing_newline.esc new file mode 100644 index 0000000..c62d37d --- /dev/null +++ b/addons/escoria-core/testing/ashes/parser/fixtures/var_missing_newline.esc @@ -0,0 +1,5 @@ +:test + var x = 1 print("bad") + +:second + print("ok") diff --git a/addons/escoria-core/testing/ashes/parser/fixtures/var_missing_newline.esc.uid b/addons/escoria-core/testing/ashes/parser/fixtures/var_missing_newline.esc.uid new file mode 100644 index 0000000..f65cfb9 --- /dev/null +++ b/addons/escoria-core/testing/ashes/parser/fixtures/var_missing_newline.esc.uid @@ -0,0 +1 @@ +uid://dssmdd6b0cwut diff --git a/addons/escoria-core/testing/ashes/parser/fixtures/while_error_then_top_level_break.esc b/addons/escoria-core/testing/ashes/parser/fixtures/while_error_then_top_level_break.esc new file mode 100644 index 0000000..dd6ef8b --- /dev/null +++ b/addons/escoria-core/testing/ashes/parser/fixtures/while_error_then_top_level_break.esc @@ -0,0 +1,9 @@ +:test + while true + print("broken") + +:second + break + +:third + print("ok") diff --git a/addons/escoria-core/testing/ashes/parser/fixtures/while_error_then_top_level_break.esc.uid b/addons/escoria-core/testing/ashes/parser/fixtures/while_error_then_top_level_break.esc.uid new file mode 100644 index 0000000..1a1f894 --- /dev/null +++ b/addons/escoria-core/testing/ashes/parser/fixtures/while_error_then_top_level_break.esc.uid @@ -0,0 +1 @@ +uid://dgo0jap62bmth -- cgit v1.3