From c0016f6d58780aa1d7b6a02306c0b6009e8ea2f0 Mon Sep 17 00:00:00 2001 From: Josh Holtrop Date: Sat, 11 Jul 2026 22:44:40 -0400 Subject: [PATCH] Add p_set_position() --- CHANGELOG.md | 3 ++ assets/parser.c.erb | 17 +++++++++ assets/parser.d.erb | 17 +++++++++ assets/parser.h.erb | 2 + doc/user_guide.md | 19 ++++++++++ spec/propane_spec.rb | 13 +++++++ spec/test_set_position.c | 81 ++++++++++++++++++++++++++++++++++++++++ spec/test_set_position.d | 66 ++++++++++++++++++++++++++++++++ 8 files changed, 218 insertions(+) create mode 100644 spec/test_set_position.c create mode 100644 spec/test_set_position.d diff --git a/CHANGELOG.md b/CHANGELOG.md index 7f7fe62..2f65657 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,9 @@ given start rule, but instead of expecting the rest of the input to match the start rule they allow specifying a set of tokens that may follow the start rule. +- Add `p_set_position()` API to set the current text position stored in the + context. Useful for setting the initial text position to something other + than `(1, 1)` for a nested parse operation. ## v4.7.0 diff --git a/assets/parser.c.erb b/assets/parser.c.erb index a27d686..0aaf133 100644 --- a/assets/parser.c.erb +++ b/assets/parser.c.erb @@ -1346,6 +1346,23 @@ size_t <%= @grammar.prefix %>parse_inner_<%= start_rule %>(<%= @grammar.prefix % return context->text_position; } +/** + * Set the current text input position. + * + * This can be used to set the initial text position to something other than + * (1, 1) for a nested parse operation so that error positions reported by + * subsequent lexer/parser calls are relative to a larger enclosing document. + * + * @param context + * Lexer/parser context structure. + * @param position + * Text position to set. + */ +void <%= @grammar.prefix %>set_position(<%= @grammar.prefix %>context_t * context, <%= @grammar.prefix %>position_t position) +{ + context->text_position = position; +} + /** * Get the user terminate code. * diff --git a/assets/parser.d.erb b/assets/parser.d.erb index a33e6bf..eb36c35 100644 --- a/assets/parser.d.erb +++ b/assets/parser.d.erb @@ -1425,6 +1425,23 @@ public <%= @grammar.prefix %>position_t <%= @grammar.prefix %>position(<%= @gram return context.text_position; } +/** + * Set the current text input position. + * + * This can be used to set the initial text position to something other than + * (1, 1) for a nested parse operation so that error positions reported by + * subsequent lexer/parser calls are relative to a larger enclosing document. + * + * @param context + * Lexer/parser context structure. + * @param position + * Text position to set. + */ +public void <%= @grammar.prefix %>set_position(<%= @grammar.prefix %>context_t * context, <%= @grammar.prefix %>position_t position) +{ + context.text_position = position; +} + /** * Get the user terminate code. * diff --git a/assets/parser.h.erb b/assets/parser.h.erb index 8f9dfca..a4803e3 100644 --- a/assets/parser.h.erb +++ b/assets/parser.h.erb @@ -237,6 +237,8 @@ void <%= @grammar.prefix %>tree_delete_<%= start_rule %>(<%= @grammar.tree_prefi <%= @grammar.prefix %>position_t <%= @grammar.prefix %>position(<%= @grammar.prefix %>context_t * context); +void <%= @grammar.prefix %>set_position(<%= @grammar.prefix %>context_t * context, <%= @grammar.prefix %>position_t position); + size_t <%= @grammar.prefix %>user_terminate_code(<%= @grammar.prefix %>context_t * context); <%= @grammar.prefix %>token_t <%= @grammar.prefix %>token(<%= @grammar.prefix %>context_t * context); diff --git a/doc/user_guide.md b/doc/user_guide.md index 0477558..1c3bd02 100644 --- a/doc/user_guide.md +++ b/doc/user_guide.md @@ -1474,6 +1474,25 @@ if (p_parse(context) == P_UNEXPECTED_TOKEN) } ``` +### `p_set_position` + +The `p_set_position()` function sets the current text position stored in the +context. +This is useful when performing a nested parse operation over a slice of a +larger enclosing document: by setting the initial text position to something +other than the default `(1, 1)`, any token positions or error positions +subsequently reported by the lexer/parser are relative to the enclosing +document rather than the slice. + +Example: + +``` +p_context_t * context = p_context_new(input, input_length); +p_position_t start = { .row = 5, .col = 20 }; +p_set_position(context, start); +p_parse(context); +``` + ### `p_user_terminate_code` The `p_user_terminate_code()` function can be used to retrieve the user diff --git a/spec/propane_spec.rb b/spec/propane_spec.rb index 26f4ecf..696b53a 100644 --- a/spec/propane_spec.rb +++ b/spec/propane_spec.rb @@ -938,6 +938,19 @@ EOF expect(results.status).to eq 0 end + it "allows setting the text position via p_set_position()" do + write_grammar < a b; +EOF + run_propane(language: language) + compile("spec/test_set_position.#{language}", language: language) + results = run_test(language: language) + expect(results.stderr).to eq "" + expect(results.status).to eq 0 + end + it "allows creating a JSON parser" do ext = language == "cpp" ? "c" : language write_grammar(File.read("spec/json_parser.#{ext}.propane")) diff --git a/spec/test_set_position.c b/spec/test_set_position.c new file mode 100644 index 0000000..9da510e --- /dev/null +++ b/spec/test_set_position.c @@ -0,0 +1,81 @@ +#include "testparser.h" +#include +#include +#include "testutils.h" + +int main() +{ + /* Grammar (simple): + * token a; token b; + * Start -> a b; + * + * Verifies that p_set_position() overrides the default (1, 1) starting + * position so that lexed tokens and error positions are reported + * relative to the caller-supplied position. */ + + /* Baseline: without p_set_position(), positions start at (1, 1). */ + { + char const * input = "ab"; + p_context_t * context = p_context_new((uint8_t const *)input, strlen(input)); + p_position_t pos = p_position(context); + assert_eq(1u, (size_t)pos.row); + assert_eq(1u, (size_t)pos.col); + p_token_info_t token_info; + assert(p_lex(context, &token_info) == P_SUCCESS); + assert_eq((size_t)TOKEN_a, (size_t)token_info.token); + assert_eq(1u, (size_t)token_info.position.row); + assert_eq(1u, (size_t)token_info.position.col); + p_context_delete(context); + } + + /* p_set_position() overrides the initial position; subsequent lex calls + * report token positions relative to the set position. */ + { + char const * input = "ab"; + p_context_t * context = p_context_new((uint8_t const *)input, strlen(input)); + p_position_t initial = {5u, 20u}; + p_set_position(context, initial); + p_position_t pos = p_position(context); + assert_eq(5u, (size_t)pos.row); + assert_eq(20u, (size_t)pos.col); + p_token_info_t token_info; + assert(p_lex(context, &token_info) == P_SUCCESS); + assert_eq((size_t)TOKEN_a, (size_t)token_info.token); + assert_eq(5u, (size_t)token_info.position.row); + assert_eq(20u, (size_t)token_info.position.col); + assert(p_lex(context, &token_info) == P_SUCCESS); + assert_eq((size_t)TOKEN_b, (size_t)token_info.token); + assert_eq(5u, (size_t)token_info.position.row); + assert_eq(21u, (size_t)token_info.position.col); + p_context_delete(context); + } + + /* p_set_position() before a full parse: successful parse still works and + * text_position tracking is relative to the set starting point. */ + { + char const * input = "ab"; + p_context_t * context = p_context_new((uint8_t const *)input, strlen(input)); + p_position_t initial = {3u, 7u}; + p_set_position(context, initial); + assert(p_parse_Start(context) == P_SUCCESS); + p_context_delete(context); + } + + /* p_set_position() before a parse that fails: the reported error + * position is relative to the set starting point. */ + { + char const * input = "aa"; + p_context_t * context = p_context_new((uint8_t const *)input, strlen(input)); + p_position_t initial = {10u, 2u}; + p_set_position(context, initial); + assert(p_parse_Start(context) == P_UNEXPECTED_TOKEN); + p_position_t err_pos = p_position(context); + /* Error is at the second `a`, which is one column past the initial + * column. */ + assert_eq(10u, (size_t)err_pos.row); + assert_eq(3u, (size_t)err_pos.col); + p_context_delete(context); + } + + return 0; +} diff --git a/spec/test_set_position.d b/spec/test_set_position.d new file mode 100644 index 0000000..1858df8 --- /dev/null +++ b/spec/test_set_position.d @@ -0,0 +1,66 @@ +import testparser; +import std.stdio; +import testutils; + +int main() +{ + return 0; +} + +unittest +{ + /* See test_set_position.c for details. */ + + /* Baseline: without p_set_position(), positions start at (1, 1). */ + { + string input = "ab"; + p_context_t * context = p_context_new(input); + p_position_t pos = p_position(context); + assert(pos.row == 1); + assert(pos.col == 1); + p_token_info_t token_info; + assert(p_lex(context, &token_info) == P_SUCCESS); + assert(token_info.token == TOKEN_a); + assert(token_info.position.row == 1); + assert(token_info.position.col == 1); + } + + /* p_set_position() overrides the initial position. */ + { + string input = "ab"; + p_context_t * context = p_context_new(input); + p_set_position(context, p_position_t(5u, 20u)); + p_position_t pos = p_position(context); + assert(pos.row == 5); + assert(pos.col == 20); + p_token_info_t token_info; + assert(p_lex(context, &token_info) == P_SUCCESS); + assert(token_info.token == TOKEN_a); + assert(token_info.position.row == 5); + assert(token_info.position.col == 20); + assert(p_lex(context, &token_info) == P_SUCCESS); + assert(token_info.token == TOKEN_b); + assert(token_info.position.row == 5); + assert(token_info.position.col == 21); + } + + /* p_set_position() before a full parse still parses successfully. */ + { + string input = "ab"; + p_context_t * context = p_context_new(input); + p_set_position(context, p_position_t(3u, 7u)); + assert(p_parse_Start(context) == P_SUCCESS); + } + + /* p_set_position() before a parse that fails: error position is + * relative to the set starting point. */ + { + string input = "aa"; + p_context_t * context = p_context_new(input); + p_set_position(context, p_position_t(10u, 2u)); + assert(p_parse_Start(context) == P_UNEXPECTED_TOKEN); + p_position_t err_pos = p_position(context); + assert(err_pos.row == 10); + assert(err_pos.col == 3); + } +}