Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions lib/lrama/command.rb
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,7 @@ def merge_stdlib(grammar)
end

def prepare_grammar(grammar)
grammar.no_inline = @options.no_inline
grammar.prepare
grammar.validate!
end
Expand Down
13 changes: 12 additions & 1 deletion lib/lrama/grammar.rb
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,7 @@ class Grammar
attr_accessor :locations #: bool
attr_accessor :define #: Hash[String, String]
attr_accessor :required #: bool
attr_accessor :no_inline #: bool

def_delegators "@symbols_resolver", :symbols, :nterms, :terms, :add_nterm, :add_term, :find_term_by_s_value,
:find_symbol_by_number!, :find_symbol_by_id!, :token_to_symbol,
Expand Down Expand Up @@ -133,6 +134,7 @@ def initialize(rule_counter, locations, define = {})
@required = false
@precedences = []
@start_nterm = nil
@no_inline = false

append_special_symbols
end
Expand Down Expand Up @@ -254,7 +256,10 @@ def epilogue=(epilogue)

# @rbs () -> void
def prepare
resolve_inline_rules
unless @no_inline
validate_inline_rules
resolve_inline_rules
end
normalize_rules
collect_symbols
set_lhs_and_rhs
Expand Down Expand Up @@ -438,6 +443,12 @@ def append_special_symbols
@accept_symbol = term
end

# @rbs () -> void
def validate_inline_rules
validator = Inline::Validator.new(@parameterized_resolver, @start_nterm)
validator.validate!
end

# @rbs () -> void
def resolve_inline_rules
while @rule_builders.any?(&:has_inline_rules?) do
Expand Down
1 change: 1 addition & 0 deletions lib/lrama/grammar/inline.rb
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
# frozen_string_literal: true

require_relative 'inline/resolver'
require_relative 'inline/validator'
42 changes: 37 additions & 5 deletions lib/lrama/grammar/inline/resolver.rb
Original file line number Diff line number Diff line change
Expand Up @@ -66,13 +66,45 @@ def replace_user_code(rhs, index)
user_code = @rule_builder.user_code
return user_code if rhs.user_code.nil? || user_code.nil?

code = user_code.s_value.gsub(/\$#{index + 1}/, rhs.user_code.s_value)
inline_action = rhs.user_code.s_value
inline_var = "_inline_#{index + 1}"

# Replace $$ or $<tag>$ in inline action with the temporary variable
# $$ -> _inline_n, $<tag>$ -> _inline_n.tag
inline_action_with_var = inline_action.gsub(/\$(<(\w+)>)?\$/) do |_match|
if $2
"#{inline_var}.#{$2}"
else
inline_var
end
end

# Build the merged action with variable binding
# First, adjust $n references in the outer action for the expanded RHS
# index is 0-indexed position, ref.index is 1-indexed ($1, $2, etc.)
# We need to adjust references AFTER the inline position (index + 1 in 1-indexed terms)
# So we skip: nil ($$), and positions <= index + 1 (the inline position itself)
outer_code = user_code.s_value
user_code.references.each do |ref|
next if ref.index.nil? || ref.index <= index # nil は $$ の場合
code = code.gsub(/\$#{ref.index}/, "$#{ref.index + (rhs.symbols.count - 1)}")
code = code.gsub(/@#{ref.index}/, "@#{ref.index + (rhs.symbols.count - 1)}")
next if ref.index.nil? || ref.index <= index + 1 # nil は $$、index + 1 は inline 位置
outer_code = outer_code.gsub(/\$#{ref.index}/, "$#{ref.index + (rhs.symbols.count - 1)}")
outer_code = outer_code.gsub(/@#{ref.index}/, "@#{ref.index + (rhs.symbols.count - 1)}")
end
Lrama::Lexer::Token::UserCode.new(s_value: code, location: user_code.location)

# Replace $n or $<tag>n (the inline symbol reference) with the temporary variable
# $n -> _inline_n, $<tag>n -> _inline_n.tag
outer_code = outer_code.gsub(/\$(<(\w+)>)?#{index + 1}/) do |_match|
if $2
"#{inline_var}.#{$2}"
else
inline_var
end
end

# Combine: declare temp var, execute inline action, then outer action
merged_code = " YYSTYPE #{inline_var}; { #{inline_action_with_var} } #{outer_code}"

Lrama::Lexer::Token::UserCode.new(s_value: merged_code, location: user_code.location)
end
end
end
Expand Down
83 changes: 83 additions & 0 deletions lib/lrama/grammar/inline/validator.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
# rbs_inline: enabled
# frozen_string_literal: true

module Lrama
class Grammar
class Inline
# Validates inline rules according to Menhir specification.
# Detects:
# - Direct recursion (inline rule references itself)
# - Mutual recursion (inline rules reference each other in a cycle)
# - Start symbol declared as inline
class Validator
class RecursiveInlineError < StandardError; end
class StartSymbolInlineError < StandardError; end

# @rbs (Lrama::Grammar::Parameterized::Resolver parameterized_resolver, Lexer::Token::Base? start_nterm) -> void
def initialize(parameterized_resolver, start_nterm = nil)
@parameterized_resolver = parameterized_resolver
@start_nterm = start_nterm
end

# @rbs () -> void
def validate!
inline_rules = collect_inline_rules
return if inline_rules.empty?

validate_no_start_symbol_inline(inline_rules)
validate_no_recursion(inline_rules)
end

private

# @rbs () -> Array[Lrama::Grammar::Parameterized::Rule]
def collect_inline_rules
@parameterized_resolver.rules.select(&:inline?)
end

# @rbs (Array[Lrama::Grammar::Parameterized::Rule] inline_rules) -> void
def validate_no_start_symbol_inline(inline_rules)
return unless @start_nterm

start_symbol_name = @start_nterm.s_value
inline_names = inline_rules.map(&:name)

if inline_names.include?(start_symbol_name)
raise StartSymbolInlineError, "Start symbol '#{start_symbol_name}' cannot be declared as inline."
end
end

# @rbs (Array[Lrama::Grammar::Parameterized::Rule] inline_rules) -> void
def validate_no_recursion(inline_rules)
inline_names = inline_rules.map(&:name).to_set

inline_rules.each do |rule|
check_recursion(rule, inline_names, Set.new)
end
end

# @rbs (Lrama::Grammar::Parameterized::Rule rule, Set[String] inline_names, Set[String] visited) -> void
def check_recursion(rule, inline_names, visited)
if visited.include?(rule.name)
raise RecursiveInlineError, "Recursive inline definition detected: #{visited.to_a.join(' -> ')} -> #{rule.name}. Inline rules cannot reference themselves directly or indirectly."
end

new_visited = visited + [rule.name]

rule.rhs.each do |rhs|
rhs.symbols.each do |symbol|
symbol_name = symbol.s_value

if inline_names.include?(symbol_name)
referenced_rule = @parameterized_resolver.rules.find { |r| r.name == symbol_name && r.inline? }
if referenced_rule
check_recursion(referenced_rule, inline_names, new_visited)
end
end
end
end
end
end
end
end
end
3 changes: 3 additions & 0 deletions lib/lrama/option_parser.rb
Original file line number Diff line number Diff line change
Expand Up @@ -132,6 +132,9 @@ def parse_by_option_parser(argv)
o.separator 'Error Recovery:'
o.on('-e', 'enable error recovery') {|v| @options.error_recovery = true }
o.separator ''
o.separator 'Grammar Processing:'
o.on('--no-inline', 'ignore all %inline keywords') {|v| @options.no_inline = true }
o.separator ''
o.separator 'Other options:'
o.on('-V', '--version', "output version information and exit") {|v| puts "lrama #{Lrama::VERSION}"; exit 0 }
o.on('-h', '--help', "display this help and exit") {|v| puts o; exit 0 }
Expand Down
2 changes: 2 additions & 0 deletions lib/lrama/options.rb
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ class Options
attr_accessor :diagram #: bool
attr_accessor :diagram_file #: String
attr_accessor :profile_opts #: Hash[Symbol, bool]?
attr_accessor :no_inline #: bool

# @rbs () -> void
def initialize
Expand All @@ -41,6 +42,7 @@ def initialize
@diagram = false
@diagram_file = "diagram.html"
@profile_opts = nil
@no_inline = false
end
end
end
Loading