From 193666d4997c665d8a7342fd82d688f4b6e6dad7 Mon Sep 17 00:00:00 2001 From: Josh Holtrop Date: Mon, 9 Feb 2026 21:18:32 -0500 Subject: [PATCH] Test multiple starting rules --- doc/user_guide.md | 54 ++++++++++++++++++++++++++++++++++ lib/propane/grammar.rb | 7 +++-- spec/propane_spec.rb | 41 ++++++++++++++++++++++++++ spec/test_starting_rules.c | 27 +++++++++++++++++ spec/test_starting_rules.d | 29 ++++++++++++++++++ spec/test_starting_rules_ast.c | 37 +++++++++++++++++++++++ spec/test_starting_rules_ast.d | 36 +++++++++++++++++++++++ spec/testutils.c | 18 ++++++++++++ spec/testutils.h | 10 +++++++ 9 files changed, 257 insertions(+), 2 deletions(-) create mode 100644 spec/test_starting_rules.c create mode 100644 spec/test_starting_rules.d create mode 100644 spec/test_starting_rules_ast.c create mode 100644 spec/test_starting_rules_ast.d diff --git a/doc/user_guide.md b/doc/user_guide.md index 0e4cb45..adb4101 100644 --- a/doc/user_guide.md +++ b/doc/user_guide.md @@ -738,6 +738,22 @@ Example: start MyStartRule; ``` +Multiple start rules can be specified, either with multiple `start` statements +or one `start` statement listing multiple start rules. +Example: + +``` +start Module ModuleItem Statement Expression; +``` + +When multiple start rules are specified, multiple `p_parse_*()` functions, +`p_result_*()`, and `p_free_ast_*()` functions (in AST mode) are generated. +A default `p_parse()`, `p_result()`, `p_free_ast()` are generated corresponding +to the first start rule. +Additionally, each start rule causes the generation of another version of each +of these functions, for example `p_parse_Statement()`, `p_result_Statement()`, +and `p_free_ast_Statement()`. + ##> Specifying the parser module name - the `module` statement The `module` statement can be used to specify the module name for a generated @@ -1018,6 +1034,16 @@ p_context_init(&context, input, input_length); size_t result = p_parse(&context); ``` +When multiple start rules are specified, a separate parse function is generated +for each which starts parsing at the given rule. +For example, if `Statement` is specified as a start rule: + +``` +size_t result = p_parse_Statement(&context); +``` + +In this case, the parser will start parsing with the `Statement` rule. + ### `p_position_valid` The `p_position_valid()` function is only generated for C targets. @@ -1056,6 +1082,23 @@ if (p_parse(&context) == P_SUCCESS) If AST generation mode is active, then the `p_result()` function returns a `Start *` pointing to the `Start` AST structure. +When multiple start rules are specified, a separate result function is generated +for each which returns the parse result for the corresponding rule. +For example, if `Statement` is specified as a start rule: + +``` +p_context_t context; +p_context_init(&context, input, input_length); +size_t result = p_parse(&context); +if (p_parse_Statement(&context) == P_SUCCESS) +{ + result = p_result_Statement(&context); +} +``` + +In this case, the parser will start parsing with the `Statement` rule and the +parse result from the `Statement` rule will be returned. + ### `p_position` The `p_position()` function can be used to retrieve the parser position where @@ -1142,6 +1185,17 @@ If specified, the `free_token_node` function will be called during the `p_free_ast()` process to allow user code to free any memory associated with a token node's `pvalue`. +When multiple start rules are specified, a separate `p_free_ast` function is +generated for each which frees the AST resulting from parsing the given rule. +For example, if `Statement` is specified as a start rule: + +``` +p_free_ast_Statement(statement_ast); +``` + +In this case, Propane will free a `Statement` AST structure returned by the +`p_parse_Statement(&context)` function. + ##> Data ### `p_token_names` diff --git a/lib/propane/grammar.rb b/lib/propane/grammar.rb index 4679eb8..8572c28 100644 --- a/lib/propane/grammar.rb +++ b/lib/propane/grammar.rb @@ -242,8 +242,11 @@ class Propane end def parse_start_statement! - if md = consume!(/start\s+(\w+)\s*;/) - @start_rules << md[1] unless @start_rules.include?(md[1]) + if md = consume!(/start\s+([\w\s]*);/) + start_rules = md[1].split(/\s+/).map(&:strip) + start_rules.each do |start_rule| + @start_rules << start_rule unless @start_rules.include?(start_rule) + end end end diff --git a/spec/propane_spec.rb b/spec/propane_spec.rb index 4e83d7a..3a100ff 100644 --- a/spec/propane_spec.rb +++ b/spec/propane_spec.rb @@ -1440,6 +1440,47 @@ EOF expect(results.status).to eq 0 end + it "allows multiple starting rules" do + write_grammar <> +token b << $$ = 2; >> +token c << $$ = 3; >> +Start -> a b R; +Start -> Bs:bs << $$ = $1; >> +R -> c:c << $$ = $1; >> +Bs -> << $$ = 0; >> +Bs -> b:b Bs:bs << $$ = $1 + $2; >> +start Start R Bs; +EOF + run_propane(language: language) + compile("spec/test_starting_rules.#{language}", language: language) + results = run_test(language: language) + expect(results.stderr).to eq "" + expect(results.status).to eq 0 + end + + it "allows multiple starting rules in AST mode" do + write_grammar <> +token b << $$ = 2; >> +token c << $$ = 3; >> +Start -> a b R; +Start -> Bs:bs; +R -> c:c; +Bs -> ; +Bs -> b:b Bs:bs; +start Start R Bs; +EOF + run_propane(language: language) + compile("spec/test_starting_rules_ast.#{language}", language: language) + results = run_test(language: language) + expect(results.stderr).to eq "" + expect(results.status).to eq 0 + end + if %w[c cpp].include?(language) it "allows a user function to free token node memory in AST mode" do write_grammar < +#include +#include "testutils.h" + +int main() +{ + char const * input = "bbbb"; + p_context_t context; + p_context_init(&context, (uint8_t const *)input, strlen(input)); + assert(p_parse(&context) == P_SUCCESS); + int result = p_result(&context); + assert_eq(8, result); + + p_context_init(&context, (uint8_t const *)input, strlen(input)); + assert(p_parse_Bs(&context) == P_SUCCESS); + result = p_result_Bs(&context); + assert_eq(8, result); + + input = "c"; + p_context_init(&context, (uint8_t const *)input, strlen(input)); + assert(p_parse_R(&context) == P_SUCCESS); + result = p_result_R(&context); + assert_eq(3, result); + + return 0; +} diff --git a/spec/test_starting_rules.d b/spec/test_starting_rules.d new file mode 100644 index 0000000..bb32022 --- /dev/null +++ b/spec/test_starting_rules.d @@ -0,0 +1,29 @@ +import testparser; +import std.stdio; +import testutils; + +int main() +{ + return 0; +} + +unittest +{ + string input = "bbbb"; + p_context_t context; + p_context_init(&context, input); + assert(p_parse(&context) == P_SUCCESS); + int result = p_result(&context); + assert(result == 8); + + p_context_init(&context, input); + assert(p_parse_Bs(&context) == P_SUCCESS); + result = p_result_Bs(&context); + assert(result == 8); + + input = "c"; + p_context_init(&context, input); + assert(p_parse_R(&context) == P_SUCCESS); + result = p_result_R(&context); + assert(result == 3); +} diff --git a/spec/test_starting_rules_ast.c b/spec/test_starting_rules_ast.c new file mode 100644 index 0000000..8a2ca00 --- /dev/null +++ b/spec/test_starting_rules_ast.c @@ -0,0 +1,37 @@ +#include "testparser.h" +#include +#include +#include "testutils.h" + +int main() +{ + char const * input = "bbbb"; + p_context_t context; + p_context_init(&context, (uint8_t const *)input, strlen(input)); + assert(p_parse(&context) == P_SUCCESS); + Start * start = p_result(&context); + assert_not_null(start->bs); + assert_not_null(start->bs->b); + assert_not_null(start->bs->bs->b); + assert_not_null(start->bs->bs->bs->b); + assert_not_null(start->bs->bs->bs->bs->b); + p_free_ast(start); + + p_context_init(&context, (uint8_t const *)input, strlen(input)); + assert(p_parse_Bs(&context) == P_SUCCESS); + Bs * bs = p_result_Bs(&context); + assert_not_null(bs->b); + assert_not_null(bs->bs->b); + assert_not_null(bs->bs->bs->b); + assert_not_null(bs->bs->bs->bs->b); + p_free_ast_Bs(bs); + + input = "c"; + p_context_init(&context, (uint8_t const *)input, strlen(input)); + assert(p_parse_R(&context) == P_SUCCESS); + R * r = p_result_R(&context); + assert_not_null(r->c); + p_free_ast_R(r); + + return 0; +} diff --git a/spec/test_starting_rules_ast.d b/spec/test_starting_rules_ast.d new file mode 100644 index 0000000..4738132 --- /dev/null +++ b/spec/test_starting_rules_ast.d @@ -0,0 +1,36 @@ +import testparser; +import std.stdio; +import testutils; + +int main() +{ + return 0; +} + +unittest +{ + string input = "bbbb"; + p_context_t context; + p_context_init(&context, input); + assert(p_parse(&context) == P_SUCCESS); + Start * start = p_result(&context); + assert(start.bs); + assert(start.bs.b); + assert(start.bs.bs.b); + assert(start.bs.bs.bs.b); + assert(start.bs.bs.bs.bs.b); + + p_context_init(&context, input); + assert(p_parse_Bs(&context) == P_SUCCESS); + Bs * bs = p_result_Bs(&context); + assert(bs.b); + assert(bs.bs.b); + assert(bs.bs.bs.b); + assert(bs.bs.bs.bs.b); + + input = "c"; + p_context_init(&context, input); + assert(p_parse_R(&context) == P_SUCCESS); + R * r = p_result_R(&context); + assert(r.c); +} diff --git a/spec/testutils.c b/spec/testutils.c index 071dbd9..417fde0 100644 --- a/spec/testutils.c +++ b/spec/testutils.c @@ -14,6 +14,24 @@ void assert_eq_size_t_i(size_t expected, size_t actual, char const * file, size_ } } +void assert_ne_size_t_i(size_t expected, size_t actual, char const * file, size_t line) +{ + if (expected == actual) + { + fprintf(stderr, "%s:%lu: expected not %lu, got %lu\n", file, line, expected, actual); + assert(false); + } +} + +void assert_not_null_i(void * ptr, char const * file, size_t line) +{ + if (ptr == NULL) + { + fprintf(stderr, "%s:%lu: expected not NULL\n", file, line); + assert(false); + } +} + void str_init(str_t * str, char const * cs) { size_t length = strlen(cs); diff --git a/spec/testutils.h b/spec/testutils.h index c93ffc9..4a402a9 100644 --- a/spec/testutils.h +++ b/spec/testutils.h @@ -5,6 +5,16 @@ void assert_eq_size_t_i(size_t expected, size_t actual, char const * file, size_ #define assert_eq(expected, actual) \ assert_eq_size_t_i(expected, actual, __FILE__, __LINE__) +void assert_ne_size_t_i(size_t expected, size_t actual, char const * file, size_t line); + +#define assert_ne(expected, actual) \ + assert_ne_size_t_i(expected, actual, __FILE__, __LINE__) + +void assert_not_null_i(void * ptr, char const * file, size_t line); + +#define assert_not_null(ptr) \ + assert_not_null_i(ptr, __FILE__, __LINE__) + typedef struct { char * cs;