diff --git a/lib/lrama/grammar.rb b/lib/lrama/grammar.rb index 95a80bb01..cdba8fba7 100644 --- a/lib/lrama/grammar.rb +++ b/lib/lrama/grammar.rb @@ -440,6 +440,8 @@ def append_special_symbols # @rbs () -> void def resolve_inline_rules + validate_inline_rules! + while @rule_builders.any?(&:has_inline_rules?) do @rule_builders = @rule_builders.flat_map do |builder| if builder.has_inline_rules? @@ -451,6 +453,34 @@ def resolve_inline_rules end end + # @rbs () -> void + def validate_inline_rules! + visiting = {} #: Hash[Parameterized::Rule, bool] + visited = {} #: Hash[Parameterized::Rule, bool] + + parameterized_rules.select(&:inline?).each do |rule| + validate_inline_rule!(rule, visiting, visited) + end + end + + # @rbs (Parameterized::Rule rule, Hash[Parameterized::Rule, bool] visiting, Hash[Parameterized::Rule, bool] visited) -> void + def validate_inline_rule!(rule, visiting, visited) + return if visited[rule] + raise "Recursive inline rule: #{rule.name}" if visiting[rule] + + visiting[rule] = true + rule.rhs.each do |rhs| + rhs.symbols.each do |symbol| + next if rule.parameters.any? { |parameter| parameter.s_value == symbol.s_value } + + inline_rule = @parameterized_resolver.find_inline(symbol) + validate_inline_rule!(inline_rule, visiting, visited) if inline_rule + end + end + visiting.delete(rule) + visited[rule] = true + end + # @rbs () -> void def normalize_rules add_accept_rule diff --git a/sig/generated/lrama/grammar.rbs b/sig/generated/lrama/grammar.rbs index faab4f04d..a5f1e28a4 100644 --- a/sig/generated/lrama/grammar.rbs +++ b/sig/generated/lrama/grammar.rbs @@ -247,6 +247,12 @@ module Lrama # @rbs () -> void def resolve_inline_rules: () -> void + # @rbs () -> void + def validate_inline_rules!: () -> void + + # @rbs (Parameterized::Rule rule, Hash[Parameterized::Rule, bool] visiting, Hash[Parameterized::Rule, bool] visited) -> void + def validate_inline_rule!: (Parameterized::Rule rule, Hash[Parameterized::Rule, bool] visiting, Hash[Parameterized::Rule, bool] visited) -> void + # @rbs () -> void def normalize_rules: () -> void diff --git a/spec/lrama/parser_spec.rb b/spec/lrama/parser_spec.rb index 6478fef4d..3c99ab3b1 100644 --- a/spec/lrama/parser_spec.rb +++ b/spec/lrama/parser_spec.rb @@ -1,6 +1,7 @@ # frozen_string_literal: true require "tempfile" +require "timeout" RSpec.describe Lrama::Parser do T ||= Lrama::Lexer::Token @@ -3040,6 +3041,21 @@ end end + context 'when inline is recursive' do + let(:path) { "recursive_inline.y" } + let(:y) do + <<~GRAMMAR + %rule %inline a : 'x' a | 'y' ; + %% + program : a ; + GRAMMAR + end + + it "raises an error" do + expect { Timeout.timeout(1) { grammar } }.to raise_error(RuntimeError, "Recursive inline rule: a") + end + end + context 'when inline have rhs include parameterized rule' do let(:path) { "inlining/rhs_include_parameterized.y" }