diff --git a/src/prism.c b/src/prism.c index 1ee3d75083..3afabc99d7 100644 --- a/src/prism.c +++ b/src/prism.c @@ -9135,12 +9135,18 @@ escape_read_warn(pm_parser_t *parser, uint8_t flags, uint8_t flag, const char *t } /** - * Read the value of an escape into the buffer. + * Read the value of an escape into the buffer. The control and meta forms can + * wrap another escape (as in \C-\M-x). Those loop back to the top with the + * accumulated flags rather than recursing, so that arbitrarily long chains of + * prefixes use constant stack space regardless of whether the compiler + * eliminates tail calls. */ static void escape_read(pm_parser_t *parser, pm_buffer_t *buffer, pm_buffer_t *regular_expression_buffer, uint8_t flags) { - uint8_t peeked = peek(parser); - switch (peeked) { + uint8_t peeked; + + escape_read_next: + switch (peeked = peek(parser)) { case '\\': { parser->current.end++; escape_write_byte(parser, buffer, regular_expression_buffer, flags, escape_byte('\\', flags)); @@ -9397,8 +9403,8 @@ escape_read(pm_parser_t *parser, pm_buffer_t *buffer, pm_buffer_t *regular_expre return; } - escape_read(parser, buffer, regular_expression_buffer, flags | PM_ESCAPE_FLAG_CONTROL); - return; + flags |= PM_ESCAPE_FLAG_CONTROL; + goto escape_read_next; case ' ': parser->current.end++; escape_read_warn(parser, flags, PM_ESCAPE_FLAG_CONTROL, "\\s"); @@ -9455,8 +9461,8 @@ escape_read(pm_parser_t *parser, pm_buffer_t *buffer, pm_buffer_t *regular_expre return; } - escape_read(parser, buffer, regular_expression_buffer, flags | PM_ESCAPE_FLAG_CONTROL); - return; + flags |= PM_ESCAPE_FLAG_CONTROL; + goto escape_read_next; case ' ': parser->current.end++; escape_read_warn(parser, flags, PM_ESCAPE_FLAG_CONTROL, "\\s"); @@ -9509,8 +9515,8 @@ escape_read(pm_parser_t *parser, pm_buffer_t *buffer, pm_buffer_t *regular_expre return; } - escape_read(parser, buffer, regular_expression_buffer, flags | PM_ESCAPE_FLAG_META); - return; + flags |= PM_ESCAPE_FLAG_META; + goto escape_read_next; case ' ': parser->current.end++; escape_read_warn(parser, flags, PM_ESCAPE_FLAG_META, "\\s"); diff --git a/test/prism/api/parse_test.rb b/test/prism/api/parse_test.rb index 68ffe33078..fcbd4fceb3 100644 --- a/test/prism/api/parse_test.rb +++ b/test/prism/api/parse_test.rb @@ -210,8 +210,21 @@ def test_scopes assert Prism.parse_success?("foo(...)", scopes: [Prism.scope(forwarding: [:"..."])]) end + def test_repeated_escape_prefixes + count = 100_000 + + assert_repeated_escape_prefixes("\"#{"\\M-" * count}a\"", "meta cannot be repeated", count - 1) + assert_repeated_escape_prefixes("?#{"\\C-" * count}a", "control cannot be repeated", count - 1) + assert_repeated_escape_prefixes("/#{"\\c" * count}a/", "control cannot be repeated", count - 1) + end + private + def assert_repeated_escape_prefixes(source, message, expected) + messages = Prism.parse(source).errors.map(&:message) + assert_equal expected, messages.count { |m| m.end_with?(message) } + end + def find_source_file_node(program) queue = [program] while (node = queue.shift)