Skip to content

Commit 575c775

Browse files
committed
Implement %inline feature compliant with Menhir specification
This PR makes Lrama's inline feature compliant with the [Menhir](https://gallium.inria.fr/~fpottier/menhir/) specification. Changed from macro-like substitution (simple string replacement of `$n`) to a temporary variable binding approach. Before: ```c // Simple substitution of $2 with inline_action_string $$ = $1 inline_action_string $3; ``` After: ```c // Variable binding ensures single evaluation YYSTYPE _inline_2; { _inline_2 = '+'; } $$ = $1 _inline_2 $3; ``` This resolves the issue where inline actions with side effects could be evaluated multiple times. Added a Validator that detects the following errors per Menhir specification: - Direct recursion (inline rule references itself) - Mutual recursion (multiple inline rules form a reference cycle) - Start symbol declared as inline Added an option to ignore all `%inline` keywords in the grammar specification. Useful for verifying whether inlining contributes to conflict resolution. Added samples demonstrating the core use case of `%inline` (resolving precedence conflicts): - `sample/calc_inline.y`: Resolves precedence issue with `%inline` - `sample/calc_no_inline.y`: Shows conflicts without `%inline` - [Menhir Reference Manual - Inlining](https://gallium.inria.fr/~fpottier/menhir/manual.html#sec%3Ainlining) - [Menhir GitHub Repository](https://github.com/LexiFi/menhir)
1 parent 92dd5dd commit 575c775

29 files changed

+1860
-45
lines changed

lib/lrama/command.rb

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,7 @@ def merge_stdlib(grammar)
7777
end
7878

7979
def prepare_grammar(grammar)
80+
grammar.no_inline = @options.no_inline
8081
grammar.prepare
8182
grammar.validate!
8283
end

lib/lrama/grammar.rb

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -100,6 +100,7 @@ class Grammar
100100
attr_accessor :locations #: bool
101101
attr_accessor :define #: Hash[String, String]
102102
attr_accessor :required #: bool
103+
attr_accessor :no_inline #: bool
103104

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

137139
append_special_symbols
138140
end
@@ -254,7 +256,10 @@ def epilogue=(epilogue)
254256

255257
# @rbs () -> void
256258
def prepare
257-
resolve_inline_rules
259+
unless @no_inline
260+
validate_inline_rules
261+
resolve_inline_rules
262+
end
258263
normalize_rules
259264
collect_symbols
260265
set_lhs_and_rhs
@@ -438,6 +443,12 @@ def append_special_symbols
438443
@accept_symbol = term
439444
end
440445

446+
# @rbs () -> void
447+
def validate_inline_rules
448+
validator = Inline::Validator.new(@parameterized_resolver, @start_nterm)
449+
validator.validate!
450+
end
451+
441452
# @rbs () -> void
442453
def resolve_inline_rules
443454
while @rule_builders.any?(&:has_inline_rules?) do

lib/lrama/grammar/inline.rb

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
11
# frozen_string_literal: true
22

33
require_relative 'inline/resolver'
4+
require_relative 'inline/validator'

lib/lrama/grammar/inline/resolver.rb

Lines changed: 37 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -66,13 +66,45 @@ def replace_user_code(rhs, index)
6666
user_code = @rule_builder.user_code
6767
return user_code if rhs.user_code.nil? || user_code.nil?
6868

69-
code = user_code.s_value.gsub(/\$#{index + 1}/, rhs.user_code.s_value)
69+
inline_action = rhs.user_code.s_value
70+
inline_var = "_inline_#{index + 1}"
71+
72+
# Replace $$ or $<tag>$ in inline action with the temporary variable
73+
# $$ -> _inline_n, $<tag>$ -> _inline_n.tag
74+
inline_action_with_var = inline_action.gsub(/\$(<(\w+)>)?\$/) do |_match|
75+
if $2
76+
"#{inline_var}.#{$2}"
77+
else
78+
inline_var
79+
end
80+
end
81+
82+
# Build the merged action with variable binding
83+
# First, adjust $n references in the outer action for the expanded RHS
84+
# index is 0-indexed position, ref.index is 1-indexed ($1, $2, etc.)
85+
# We need to adjust references AFTER the inline position (index + 1 in 1-indexed terms)
86+
# So we skip: nil ($$), and positions <= index + 1 (the inline position itself)
87+
outer_code = user_code.s_value
7088
user_code.references.each do |ref|
71-
next if ref.index.nil? || ref.index <= index # nil は $$ の場合
72-
code = code.gsub(/\$#{ref.index}/, "$#{ref.index + (rhs.symbols.count - 1)}")
73-
code = code.gsub(/@#{ref.index}/, "@#{ref.index + (rhs.symbols.count - 1)}")
89+
next if ref.index.nil? || ref.index <= index + 1 # nil は $$、index + 1 は inline 位置
90+
outer_code = outer_code.gsub(/\$#{ref.index}/, "$#{ref.index + (rhs.symbols.count - 1)}")
91+
outer_code = outer_code.gsub(/@#{ref.index}/, "@#{ref.index + (rhs.symbols.count - 1)}")
7492
end
75-
Lrama::Lexer::Token::UserCode.new(s_value: code, location: user_code.location)
93+
94+
# Replace $n or $<tag>n (the inline symbol reference) with the temporary variable
95+
# $n -> _inline_n, $<tag>n -> _inline_n.tag
96+
outer_code = outer_code.gsub(/\$(<(\w+)>)?#{index + 1}/) do |_match|
97+
if $2
98+
"#{inline_var}.#{$2}"
99+
else
100+
inline_var
101+
end
102+
end
103+
104+
# Combine: declare temp var, execute inline action, then outer action
105+
merged_code = " YYSTYPE #{inline_var}; { #{inline_action_with_var} } #{outer_code}"
106+
107+
Lrama::Lexer::Token::UserCode.new(s_value: merged_code, location: user_code.location)
76108
end
77109
end
78110
end
Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
# rbs_inline: enabled
2+
# frozen_string_literal: true
3+
4+
module Lrama
5+
class Grammar
6+
class Inline
7+
# Validates inline rules according to Menhir specification.
8+
# Detects:
9+
# - Direct recursion (inline rule references itself)
10+
# - Mutual recursion (inline rules reference each other in a cycle)
11+
# - Start symbol declared as inline
12+
class Validator
13+
class RecursiveInlineError < StandardError; end
14+
class StartSymbolInlineError < StandardError; end
15+
16+
# @rbs (Lrama::Grammar::Parameterized::Resolver parameterized_resolver, Lexer::Token::Base? start_nterm) -> void
17+
def initialize(parameterized_resolver, start_nterm = nil)
18+
@parameterized_resolver = parameterized_resolver
19+
@start_nterm = start_nterm
20+
end
21+
22+
# @rbs () -> void
23+
def validate!
24+
inline_rules = collect_inline_rules
25+
return if inline_rules.empty?
26+
27+
validate_no_start_symbol_inline(inline_rules)
28+
validate_no_recursion(inline_rules)
29+
end
30+
31+
private
32+
33+
# @rbs () -> Array[Lrama::Grammar::Parameterized::Rule]
34+
def collect_inline_rules
35+
@parameterized_resolver.rules.select(&:inline?)
36+
end
37+
38+
# @rbs (Array[Lrama::Grammar::Parameterized::Rule] inline_rules) -> void
39+
def validate_no_start_symbol_inline(inline_rules)
40+
return unless @start_nterm
41+
42+
start_symbol_name = @start_nterm.s_value
43+
inline_names = inline_rules.map(&:name)
44+
45+
if inline_names.include?(start_symbol_name)
46+
raise StartSymbolInlineError, "Start symbol '#{start_symbol_name}' cannot be declared as inline."
47+
end
48+
end
49+
50+
# @rbs (Array[Lrama::Grammar::Parameterized::Rule] inline_rules) -> void
51+
def validate_no_recursion(inline_rules)
52+
inline_names = inline_rules.map(&:name).to_set
53+
54+
inline_rules.each do |rule|
55+
check_recursion(rule, inline_names, Set.new)
56+
end
57+
end
58+
59+
# @rbs (Lrama::Grammar::Parameterized::Rule rule, Set[String] inline_names, Set[String] visited) -> void
60+
def check_recursion(rule, inline_names, visited)
61+
if visited.include?(rule.name)
62+
raise RecursiveInlineError, "Recursive inline definition detected: #{visited.to_a.join(' -> ')} -> #{rule.name}. Inline rules cannot reference themselves directly or indirectly."
63+
end
64+
65+
new_visited = visited + [rule.name]
66+
67+
rule.rhs.each do |rhs|
68+
rhs.symbols.each do |symbol|
69+
symbol_name = symbol.s_value
70+
71+
if inline_names.include?(symbol_name)
72+
referenced_rule = @parameterized_resolver.rules.find { |r| r.name == symbol_name && r.inline? }
73+
if referenced_rule
74+
check_recursion(referenced_rule, inline_names, new_visited)
75+
end
76+
end
77+
end
78+
end
79+
end
80+
end
81+
end
82+
end
83+
end

lib/lrama/option_parser.rb

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -132,6 +132,9 @@ def parse_by_option_parser(argv)
132132
o.separator 'Error Recovery:'
133133
o.on('-e', 'enable error recovery') {|v| @options.error_recovery = true }
134134
o.separator ''
135+
o.separator 'Grammar Processing:'
136+
o.on('--no-inline', 'ignore all %inline keywords') {|v| @options.no_inline = true }
137+
o.separator ''
135138
o.separator 'Other options:'
136139
o.on('-V', '--version', "output version information and exit") {|v| puts "lrama #{Lrama::VERSION}"; exit 0 }
137140
o.on('-h', '--help', "display this help and exit") {|v| puts o; exit 0 }

lib/lrama/options.rb

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ class Options
2121
attr_accessor :diagram #: bool
2222
attr_accessor :diagram_file #: String
2323
attr_accessor :profile_opts #: Hash[Symbol, bool]?
24+
attr_accessor :no_inline #: bool
2425

2526
# @rbs () -> void
2627
def initialize
@@ -41,6 +42,7 @@ def initialize
4142
@diagram = false
4243
@diagram_file = "diagram.html"
4344
@profile_opts = nil
45+
@no_inline = false
4446
end
4547
end
4648
end

0 commit comments

Comments
 (0)