From 0aaa44faf47444a8ad1b599f25e19165591267d2 Mon Sep 17 00:00:00 2001 From: Josh Holtrop Date: Sun, 12 Jul 2026 22:22:58 -0400 Subject: [PATCH] Add a test for a macro implementation using custom lex function --- spec/macros.c.propane | 25 +++++++++ spec/macros.d.propane | 31 +++++++++++ spec/propane_spec.rb | 10 ++++ spec/test_macros.c | 117 ++++++++++++++++++++++++++++++++++++++++++ spec/test_macros.d | 116 +++++++++++++++++++++++++++++++++++++++++ 5 files changed, 299 insertions(+) create mode 100644 spec/macros.c.propane create mode 100644 spec/macros.d.propane create mode 100644 spec/test_macros.c create mode 100644 spec/test_macros.d diff --git a/spec/macros.c.propane b/spec/macros.c.propane new file mode 100644 index 0000000..c81f418 --- /dev/null +++ b/spec/macros.c.propane @@ -0,0 +1,25 @@ +<< +#include +size_t mylexfn(p_context_t * context, p_token_info_t * out_token_info); +void record(int v); +>> + +ptype int; + +lex_fn mylexfn; + +drop /\s+/; +token lbrace /\{/; +token rbrace /\}/; +token plus /\+/; +token macro; +token macroname /@[a-zA-Z_]\w*/; +token num /\d+/ << char b[100]; memcpy(b, match, match_length); b[match_length] = '\0'; $$ = atoi(b); >> + +Start -> Statements; +Statements -> ; +Statements -> Statement Statements; +Statement -> Add; +Statement -> MacroStart; +Add -> num plus num << $$ = $1 + $3; record($$); >> +MacroStart -> macro macroname lbrace; diff --git a/spec/macros.d.propane b/spec/macros.d.propane new file mode 100644 index 0000000..9eaa2ff --- /dev/null +++ b/spec/macros.d.propane @@ -0,0 +1,31 @@ +<< +import test_macros; +>> + +ptype int; + +lex_fn mylexfn; + +drop /\s+/; +token lbrace /\{/; +token rbrace /\}/; +token plus /\+/; +token macro; +token macroname /@[a-zA-Z_]\w*/; +token num /\d+/ << + int n = 0; + foreach (c; match) + { + n *= 10; + n += (c - '0'); + } + $$ = n; +>> + +Start -> Statements; +Statements -> ; +Statements -> Statement Statements; +Statement -> Add; +Statement -> MacroStart; +Add -> num plus num << $$ = $1 + $3; record($$); >> +MacroStart -> macro macroname lbrace; diff --git a/spec/propane_spec.rb b/spec/propane_spec.rb index 438f633..e6ff43a 100644 --- a/spec/propane_spec.rb +++ b/spec/propane_spec.rb @@ -2164,6 +2164,16 @@ StartR start: 1, 3 StartR end: 1, 6 EOF end + + it "allows nested parses for macro expansions" do + ext = language == "cpp" ? "c" : language + write_grammar(File.read("spec/macros.#{ext}.propane")) + run_propane(language: language) + compile("spec/test_macros.#{language}", language: language) + results = run_test(language: language) + expect(results.stderr).to eq "" + expect(results.status).to eq 0 + end end end end diff --git a/spec/test_macros.c b/spec/test_macros.c new file mode 100644 index 0000000..6d0c4ae --- /dev/null +++ b/spec/test_macros.c @@ -0,0 +1,117 @@ +#include "testparser.h" +#include "testutils.h" +#include +#include +#include + +static p_context_t * context; +size_t n_tokens; +p_token_info_t token_infos[10]; + +/* Capture the macro body tokens (everything up to the closing '}') into + * token_infos[]. Called from mylexfn() right after the definition's '{' has + * been lexed, so the input cursor is positioned at the first body token. */ +static void capture_macro_body(void) +{ + n_tokens = 0u; + for (;;) + { + size_t result = p_lex(context, &token_infos[n_tokens]); + assert_eq(result, P_SUCCESS); + if (token_infos[n_tokens].token == TOKEN_rbrace) + { + break; + } + n_tokens++; + assert(n_tokens < sizeof(token_infos) / sizeof(token_infos[0])); + } +} + +size_t mylexfn(p_context_t * context, p_token_info_t * out_token_info) +{ + static bool defining; + static bool expanding; + static size_t expand_i; + + for (;;) + { + if (expanding) + { + size_t ei = expand_i++; + if (expand_i >= n_tokens) + { + expanding = false; + } + *out_token_info = token_infos[ei]; + return P_SUCCESS; + } + + size_t lex_result = p_lex(context, out_token_info); + if (lex_result != P_SUCCESS) + { + return lex_result; + } + + switch (out_token_info->token) + { + case TOKEN_macro: + /* Start of a macro definition: "macro macroname { ... }". */ + defining = true; + break; + case TOKEN_macroname: + if (!defining) + { + /* Use of a macro: replay its captured body tokens instead of + * returning the macroname to the parser. */ + expanding = true; + expand_i = 0u; + continue; + } + /* Definition name: pass through and keep waiting for '{'. */ + break; + case TOKEN_lbrace: + if (defining) + { + /* Consume and store the macro body now, before the parser gets + * a chance to read its lookahead token (which would otherwise + * swallow the first body token). */ + capture_macro_body(); + defining = false; + } + break; + default: + defining = false; + break; + } + return lex_result; + } +} + +size_t n_nums; +int nums[10]; + +void record(int v) +{ + nums[n_nums++] = v; +} + +int main() +{ + char const * input = + "macro @m { 23 + 200 }\n" + "66 + 100\n" + "@m\n" + "33 + 55\n" + "@m\n"; + context = p_context_new((uint8_t const *)input, strlen(input)); + assert(p_parse(context) == P_SUCCESS); + p_context_delete(context); + + assert_eq(n_nums, 4); + assert_eq(nums[0], 166); + assert_eq(nums[1], 223); + assert_eq(nums[2], 88); + assert_eq(nums[3], 223); + + return 0; +} diff --git a/spec/test_macros.d b/spec/test_macros.d new file mode 100644 index 0000000..3790a95 --- /dev/null +++ b/spec/test_macros.d @@ -0,0 +1,116 @@ +import testparser; +import testutils; + +size_t n_tokens; +p_token_info_t[10] token_infos; + +// Capture the macro body tokens (everything up to the closing '}') into +// token_infos[]. Called from mylexfn() right after the definition's '{' has +// been lexed, so the input cursor is positioned at the first body token. +void capture_macro_body(p_context_t * context) +{ + n_tokens = 0u; + for (;;) + { + size_t result = p_lex(context, &token_infos[n_tokens]); + assert(result == P_SUCCESS); + if (token_infos[n_tokens].token == TOKEN_rbrace) + { + break; + } + n_tokens++; + assert(n_tokens < token_infos.length); + } +} + +size_t mylexfn(p_context_t * context, p_token_info_t * out_token_info) +{ + static bool defining; + static bool expanding; + static size_t expand_i; + + for (;;) + { + if (expanding) + { + size_t ei = expand_i++; + if (expand_i >= n_tokens) + { + expanding = false; + } + *out_token_info = token_infos[ei]; + return P_SUCCESS; + } + + size_t lex_result = p_lex(context, out_token_info); + if (lex_result != P_SUCCESS) + { + return lex_result; + } + + switch (out_token_info.token) + { + case TOKEN_macro: + // Start of a macro definition: "macro macroname { ... }". + defining = true; + break; + case TOKEN_macroname: + if (!defining) + { + // Use of a macro: replay its captured body tokens instead of + // returning the macroname to the parser. + expanding = true; + expand_i = 0u; + continue; + } + // Definition name: pass through and keep waiting for '{'. + break; + case TOKEN_lbrace: + if (defining) + { + // Consume and store the macro body now, before the parser gets + // a chance to read its lookahead token (which would otherwise + // swallow the first body token). + capture_macro_body(context); + defining = false; + } + break; + default: + defining = false; + break; + } + return lex_result; + } +} + +size_t n_nums; +int[10] nums; + +void record(int v) +{ + nums[n_nums++] = v; +} + +int main() +{ + return 0; +} + +unittest +{ + string input = + "macro @m { 23 + 200 }\n" ~ + "66 + 100\n" ~ + "@m\n" ~ + "33 + 55\n" ~ + "@m\n"; + p_context_t * context = p_context_new(input); + assert(p_parse(context) == P_SUCCESS); + p_context_delete(context); + + assert(n_nums == 4); + assert(nums[0] == 166); + assert(nums[1] == 223); + assert(nums[2] == 88); + assert(nums[3] == 223); +}