From 0d0e8e45d22fae7e3632d81252a4b15d1f3fac4f Mon Sep 17 00:00:00 2001 From: Josh Holtrop Date: Mon, 22 Jul 2024 17:29:54 -0400 Subject: [PATCH] Add field aliases WIP --- lib/propane/grammar.rb | 2 +- lib/propane/rule.rb | 18 ++++++++++- lib/propane/rule_set.rb | 16 +++++++++- spec/propane_spec.rb | 56 +++++++++++++++++++++++++++++++++++ spec/test_ast_field_aliases.c | 19 ++++++++++++ spec/test_ast_field_aliases.d | 21 +++++++++++++ 6 files changed, 129 insertions(+), 3 deletions(-) create mode 100644 spec/test_ast_field_aliases.c create mode 100644 spec/test_ast_field_aliases.d diff --git a/lib/propane/grammar.rb b/lib/propane/grammar.rb index de5d93a..0e8f090 100644 --- a/lib/propane/grammar.rb +++ b/lib/propane/grammar.rb @@ -198,7 +198,7 @@ class Propane if @ast && ptypename raise Error.new("Multiple ptypes are unsupported in AST mode") end - md = consume!(/((?:#{IDENTIFIER_REGEX}\??\s*)*)\s*/, "expected rule component list") + md = consume!(/((?:#{IDENTIFIER_REGEX}(?::#{IDENTIFIER_REGEX})?\??\s*)*)\s*/, "expected rule component list") components = md[1].strip.split(/\s+/) if @ast consume!(/;/, "expected `;'") diff --git a/lib/propane/rule.rb b/lib/propane/rule.rb index 20047b9..82bf860 100644 --- a/lib/propane/rule.rb +++ b/lib/propane/rule.rb @@ -6,6 +6,10 @@ class Propane # Rule components. attr_reader :components + # @return [Hash] + # Field aliases. + attr_reader :aliases + # @return [String] # User code associated with the rule. attr_reader :code @@ -49,7 +53,19 @@ class Propane # Line number where the rule was defined in the input grammar. def initialize(name, components, code, ptypename, line_number) @name = name - @components = components + @aliases = {} + @components = components.each_with_index.map do |component, i| + if component =~ /(\S+):(\S+)/ + c, aliasname = $1, $2 + if @aliases[aliasname] + raise Error.new("Error: duplicate field alias `#{aliasname}` for rule #{name} defined on line #{line_number}") + end + @aliases[aliasname] = i + c + else + component + end + end @rule_set_node_field_index_map = components.map {0} @code = code @ptypename = ptypename diff --git a/lib/propane/rule_set.rb b/lib/propane/rule_set.rb index dd20b3c..d8ef0f6 100644 --- a/lib/propane/rule_set.rb +++ b/lib/propane/rule_set.rb @@ -100,7 +100,9 @@ class Propane # Finalize a RuleSet after adding all Rules to it. def finalize(grammar) - build_ast_fields(grammar) + if grammar.ast + build_ast_fields(grammar) + end end private @@ -148,6 +150,18 @@ class Propane "#{grammar.ast_prefix}#{node_name}#{grammar.ast_suffix}" end end + # Now merge in the field aliases as given by the user in the + # grammar. + field_aliases = {} + @rules.each do |rule| + rule.aliases.each do |alias_name, index| + if field_aliases[alias_name] && field_aliases[alias_name] != index + raise Error.new("Error: conflicting AST node field positions for alias `#{alias_name}`") + end + field_aliases[alias_name] = index + @ast_fields[index][alias_name] = @ast_fields[index].first[1] + end + end end end diff --git a/spec/propane_spec.rb b/spec/propane_spec.rb index f5fd48d..42d712f 100644 --- a/spec/propane_spec.rb +++ b/spec/propane_spec.rb @@ -213,6 +213,42 @@ EOF expect(File.binread("spec/run/testparser.log")).to match %r{Shift/Reduce conflict \(state \d+\) between token b and rule As2\? \(defined on line 4\)} end + it "errors on duplicate field aliases in a rule" do + write_grammar < a:foo b:foo; +EOF + results = run_propane(extra_args: %w[-w], capture: true) + expect(results.stderr).to match %r{Error: duplicate field alias `foo` for rule Start defined on line 3} + expect(results.status).to_not eq 0 + end + + it "errors when an alias is in different positions for different rules in a rule set when AST mode is enabled" do + write_grammar < a:foo b; +Start -> b b:foo; +EOF + results = run_propane(extra_args: %w[-w], capture: true) + expect(results.stderr).to match %r{Error: conflicting AST node field positions for alias `foo`} + expect(results.status).to_not eq 0 + end + + it "does not error when an alias is in different positions for different rules in a rule set when AST mode is disabled" do + write_grammar < a:foo b; +Start -> b b:foo; +EOF + results = run_propane(extra_args: %w[-w], capture: true) + expect(results.stderr).to eq "" + expect(results.status).to eq 0 + end + %w[d c].each do |language| context "#{language.upcase} language" do @@ -1120,6 +1156,26 @@ EOF expect(results.stderr).to eq "" expect(results.status).to eq 0 end + + it "allows specifying field aliases" do + write_grammar < T:first T:second T:third; +T -> a; +T -> b; +T -> c; +EOF + run_propane(language: language) + compile("spec/test_field_aliases.#{language}", language: language) + results = run_test + expect(results.stderr).to eq "" + expect(results.status).to eq 0 + end end end end diff --git a/spec/test_ast_field_aliases.c b/spec/test_ast_field_aliases.c new file mode 100644 index 0000000..5b3716a --- /dev/null +++ b/spec/test_ast_field_aliases.c @@ -0,0 +1,19 @@ +#include "testparser.h" +#include +#include +#include "testutils.h" + +int main() +{ + char const * input = "\na\nb\nc"; + 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_eq(TOKEN_a, start->first->pToken->token); + assert_eq(TOKEN_b, start->second->pToken->token); + assert_eq(TOKEN_c, start->third->pToken->token); + + return 0; +} diff --git a/spec/test_ast_field_aliases.d b/spec/test_ast_field_aliases.d new file mode 100644 index 0000000..907946e --- /dev/null +++ b/spec/test_ast_field_aliases.d @@ -0,0 +1,21 @@ +import testparser; +import std.stdio; +import testutils; + +int main() +{ + return 0; +} + +unittest +{ + string input = "\na\nb\nc"; + p_context_t context; + p_context_init(&context, input); + assert(p_parse(&context) == P_SUCCESS); + Start * start = p_result(&context); + + assert_eq(TOKEN_a, start.first.pToken.token); + assert_eq(TOKEN_b, start.second.pToken.token); + assert_eq(TOKEN_c, start.third.pToken.token); +}