Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
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 位置

@yui-knk yui-knk Jan 22, 2026

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could you rewrite this comment with English?

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