ibex 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (118) hide show
  1. checksums.yaml +7 -0
  2. data/.rubocop.yml +43 -0
  3. data/CHANGELOG.md +30 -0
  4. data/LICENSE.txt +21 -0
  5. data/README.md +194 -0
  6. data/Rakefile +25 -0
  7. data/Steepfile +10 -0
  8. data/docs/architecture.md +99 -0
  9. data/docs/compat-notes.md +37 -0
  10. data/docs/grammar-reference.md +127 -0
  11. data/docs/lexer-coverage.md +14 -0
  12. data/docs/phase10-extensions.md +27 -0
  13. data/docs/racc-migration.md +47 -0
  14. data/exe/ibex +6 -0
  15. data/gemfiles/Gemfile +7 -0
  16. data/gemfiles/Gemfile.lock +98 -0
  17. data/lib/ibex/analysis/sets.rb +194 -0
  18. data/lib/ibex/analysis.rb +8 -0
  19. data/lib/ibex/cli/counterexample_options.rb +38 -0
  20. data/lib/ibex/cli/outputs.rb +88 -0
  21. data/lib/ibex/cli.rb +333 -0
  22. data/lib/ibex/codegen/dot.rb +52 -0
  23. data/lib/ibex/codegen/html.rb +110 -0
  24. data/lib/ibex/codegen/rbs.rb +54 -0
  25. data/lib/ibex/codegen/report.rb +149 -0
  26. data/lib/ibex/codegen/ruby.rb +235 -0
  27. data/lib/ibex/codegen/symbol_labels.rb +18 -0
  28. data/lib/ibex/error.rb +7 -0
  29. data/lib/ibex/frontend/action_scanner.rb +266 -0
  30. data/lib/ibex/frontend/ast.rb +146 -0
  31. data/lib/ibex/frontend/bootstrap_parser.rb +163 -0
  32. data/lib/ibex/frontend/dsl.rb +196 -0
  33. data/lib/ibex/frontend/generated_parser.rb +434 -0
  34. data/lib/ibex/frontend/generated_parser_base.rb +273 -0
  35. data/lib/ibex/frontend/grammar.y +156 -0
  36. data/lib/ibex/frontend/lexer.rb +165 -0
  37. data/lib/ibex/frontend/parser/declarations.rb +163 -0
  38. data/lib/ibex/frontend/parser/rules.rb +154 -0
  39. data/lib/ibex/frontend/parser.rb +23 -0
  40. data/lib/ibex/frontend/regenerator.rb +41 -0
  41. data/lib/ibex/frontend/source_cursor.rb +94 -0
  42. data/lib/ibex/frontend/token_adapter/declaration_state.rb +248 -0
  43. data/lib/ibex/frontend/token_adapter/delimiter_tracker.rb +37 -0
  44. data/lib/ibex/frontend/token_adapter/rule_state.rb +162 -0
  45. data/lib/ibex/frontend/token_adapter.rb +108 -0
  46. data/lib/ibex/frontend.rb +28 -0
  47. data/lib/ibex/ir/automaton_ir.rb +109 -0
  48. data/lib/ibex/ir/grammar_ir.rb +192 -0
  49. data/lib/ibex/ir/serialize.rb +154 -0
  50. data/lib/ibex/ir.rb +53 -0
  51. data/lib/ibex/lalr/builder.rb +270 -0
  52. data/lib/ibex/lalr/conflict.rb +120 -0
  53. data/lib/ibex/lalr/conflict_search.rb +332 -0
  54. data/lib/ibex/lalr/conflict_search_limits.rb +39 -0
  55. data/lib/ibex/lalr/counterexample.rb +138 -0
  56. data/lib/ibex/lalr/default_reductions.rb +67 -0
  57. data/lib/ibex/lalr.rb +27 -0
  58. data/lib/ibex/normalize/declarations.rb +60 -0
  59. data/lib/ibex/normalize/diagnostics.rb +99 -0
  60. data/lib/ibex/normalize/expander.rb +218 -0
  61. data/lib/ibex/normalize/expression.rb +54 -0
  62. data/lib/ibex/normalize.rb +166 -0
  63. data/lib/ibex/runtime/parser.rb +360 -0
  64. data/lib/ibex/runtime.rb +8 -0
  65. data/lib/ibex/tables.rb +125 -0
  66. data/lib/ibex/version.rb +6 -0
  67. data/lib/ibex.rb +23 -0
  68. data/sig/ibex/analysis/sets.rbs +72 -0
  69. data/sig/ibex/analysis.rbs +6 -0
  70. data/sig/ibex/cli/counterexample_options.rbs +16 -0
  71. data/sig/ibex/cli/outputs.rbs +31 -0
  72. data/sig/ibex/cli.rbs +108 -0
  73. data/sig/ibex/codegen/dot.rbs +19 -0
  74. data/sig/ibex/codegen/html.rbs +35 -0
  75. data/sig/ibex/codegen/rbs.rbs +31 -0
  76. data/sig/ibex/codegen/report.rbs +39 -0
  77. data/sig/ibex/codegen/ruby.rbs +92 -0
  78. data/sig/ibex/codegen/symbol_labels.rbs +11 -0
  79. data/sig/ibex/error.rbs +7 -0
  80. data/sig/ibex/frontend/action_scanner.rbs +74 -0
  81. data/sig/ibex/frontend/ast.rbs +223 -0
  82. data/sig/ibex/frontend/bootstrap_parser.rbs +80 -0
  83. data/sig/ibex/frontend/dsl.rbs +124 -0
  84. data/sig/ibex/frontend/generated_parser.rbs +163 -0
  85. data/sig/ibex/frontend/generated_parser_base.rbs +114 -0
  86. data/sig/ibex/frontend/lexer.rbs +61 -0
  87. data/sig/ibex/frontend/parser/declarations.rbs +56 -0
  88. data/sig/ibex/frontend/parser/rules.rbs +48 -0
  89. data/sig/ibex/frontend/parser.rbs +16 -0
  90. data/sig/ibex/frontend/regenerator.rbs +16 -0
  91. data/sig/ibex/frontend/source_cursor.rbs +73 -0
  92. data/sig/ibex/frontend/token_adapter/declaration_state.rbs +97 -0
  93. data/sig/ibex/frontend/token_adapter/delimiter_tracker.rbs +25 -0
  94. data/sig/ibex/frontend/token_adapter/rule_state.rbs +77 -0
  95. data/sig/ibex/frontend/token_adapter.rbs +67 -0
  96. data/sig/ibex/frontend.rbs +15 -0
  97. data/sig/ibex/ir/automaton_ir.rbs +77 -0
  98. data/sig/ibex/ir/grammar_ir.rbs +143 -0
  99. data/sig/ibex/ir/serialize.rbs +50 -0
  100. data/sig/ibex/ir.rbs +43 -0
  101. data/sig/ibex/lalr/builder.rbs +85 -0
  102. data/sig/ibex/lalr/conflict.rbs +47 -0
  103. data/sig/ibex/lalr/conflict_search.rbs +131 -0
  104. data/sig/ibex/lalr/conflict_search_limits.rbs +29 -0
  105. data/sig/ibex/lalr/counterexample.rbs +53 -0
  106. data/sig/ibex/lalr/default_reductions.rbs +20 -0
  107. data/sig/ibex/lalr.rbs +23 -0
  108. data/sig/ibex/normalize/declarations.rbs +20 -0
  109. data/sig/ibex/normalize/diagnostics.rbs +32 -0
  110. data/sig/ibex/normalize/expander.rbs +66 -0
  111. data/sig/ibex/normalize/expression.rbs +21 -0
  112. data/sig/ibex/normalize.rbs +96 -0
  113. data/sig/ibex/runtime/parser.rbs +167 -0
  114. data/sig/ibex/runtime.rbs +6 -0
  115. data/sig/ibex/tables.rbs +50 -0
  116. data/sig/ibex/version.rbs +6 -0
  117. data/sig/ibex.rbs +6 -0
  118. metadata +161 -0
@@ -0,0 +1,149 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "symbol_labels"
4
+
5
+ module Ibex
6
+ module Codegen
7
+ # Renders a human-readable state and conflict report from Automaton IR.
8
+ module Report
9
+ # @rbs!
10
+ # private def append_state: (
11
+ # Array[String] lines,
12
+ # IR::AutomatonState state,
13
+ # IR::Grammar grammar,
14
+ # Array[IR::counterexample] examples,
15
+ # Hash[Integer, String] labels
16
+ # ) -> void
17
+ # private def self.append_state: (
18
+ # Array[String] lines,
19
+ # IR::AutomatonState state,
20
+ # IR::Grammar grammar,
21
+ # Array[IR::counterexample] examples,
22
+ # Hash[Integer, String] labels
23
+ # ) -> void
24
+ # private def append_counterexample: (Array[String] lines, IR::counterexample example, IR::Grammar grammar,
25
+ # Hash[Integer, String] labels) -> void
26
+ # private def self.append_counterexample: (Array[String] lines, IR::counterexample example, IR::Grammar grammar,
27
+ # Hash[Integer, String] labels) -> void
28
+ # private def append_tree: (Array[String] lines, untyped tree, String indentation, IR::Grammar grammar,
29
+ # Hash[Integer, String] labels) -> void
30
+ # private def self.append_tree: (Array[String] lines, untyped tree, String indentation, IR::Grammar grammar,
31
+ # Hash[Integer, String] labels) -> void
32
+ # private def format_item: (IR::AutomatonItem item, IR::Grammar grammar, Hash[Integer, String] labels) -> String
33
+ # private def self.format_item: (IR::AutomatonItem item, IR::Grammar grammar,
34
+ # Hash[Integer, String] labels) -> String
35
+ # private def format_action: (IR::parser_action action) -> String
36
+ # private def self.format_action: (IR::parser_action action) -> String
37
+ # private def symbol_name: (Hash[Integer, String] labels, Integer id) -> String
38
+ # private def self.symbol_name: (Hash[Integer, String] labels, Integer id) -> String
39
+ # private def tree_label: (IR::Grammar grammar, Hash[Integer, String] labels, untyped value) -> untyped
40
+ # private def self.tree_label: (IR::Grammar grammar, Hash[Integer, String] labels, untyped value) -> untyped
41
+
42
+ # @rbs (IR::Automaton automaton, ?max_tokens: Integer, ?max_configurations: Integer) -> String
43
+ def render(automaton, max_tokens: LALR::Counterexample::DEFAULT_MAX_TOKENS,
44
+ max_configurations: LALR::Counterexample::DEFAULT_MAX_CONFIGURATIONS)
45
+ grammar = automaton.grammar
46
+ labels = SymbolLabels.build(grammar)
47
+ lines = ["Algorithm: #{automaton.algorithm}", "States: #{automaton.states.length}", ""]
48
+ examples = LALR::Counterexample.new(
49
+ automaton, max_tokens: max_tokens, max_configurations: max_configurations
50
+ ).all.group_by { |example| example[:state] }
51
+ automaton.states.each do |state|
52
+ append_state(lines, state, grammar, examples.fetch(state.id, Array.new(0)), labels)
53
+ end
54
+ summary = automaton.conflict_summary
55
+ lines << "Conflicts: #{summary[:sr]} shift/reduce, #{summary[:rr]} reduce/reduce"
56
+ "#{lines.join("\n")}\n"
57
+ end
58
+ module_function :render
59
+
60
+ # @rbs skip
61
+ private
62
+
63
+ # @rbs skip
64
+ def append_state(lines, state, grammar, examples, labels)
65
+ lines << "State #{state.id}"
66
+ state.items.each { |item| lines << " #{format_item(item, grammar, labels)}" }
67
+ state.actions.each do |token_id, action|
68
+ lines << " on #{symbol_name(labels, token_id)}: #{format_action(action)}"
69
+ end
70
+ lines << " default: #{format_action(state.default_action)}" if state.default_action
71
+ state.gotos.each { |symbol_id, target| lines << " goto #{symbol_name(labels, symbol_id)}: #{target}" }
72
+ state.conflicts.each { |conflict| lines << " conflict: #{conflict.inspect}" }
73
+ examples.each { |example| append_counterexample(lines, example, grammar, labels) }
74
+ lines << ""
75
+ end
76
+
77
+ # @rbs skip
78
+ def append_counterexample(lines, example, grammar, labels)
79
+ label = example[:unifying] ? "unifying counterexample" : "nonunifying witness"
80
+ sentence = example[:sentence].dup.insert(example[:lookahead_index], "•").join(" ")
81
+ lines << " #{label}: #{sentence}"
82
+ example[:interpretations].each do |interpretation|
83
+ lines << " #{interpretation[:kind]} derivation:"
84
+ append_tree(lines, interpretation[:tree], " ", grammar, labels)
85
+ end
86
+ end
87
+
88
+ # @rbs skip
89
+ def append_tree(lines, tree, indentation, grammar, labels)
90
+ unless tree.is_a?(Hash)
91
+ lines << "#{indentation}#{tree_label(grammar, labels, tree)}"
92
+ return
93
+ end
94
+
95
+ symbol = tree_label(grammar, labels, tree[:symbol] || tree[:token])
96
+ unless tree[:children]
97
+ lines << "#{indentation}#{symbol}"
98
+ return
99
+ end
100
+
101
+ production = tree[:production] ? " (production #{tree[:production]})" : ""
102
+ lines << "#{indentation}#{symbol}#{production}"
103
+ tree[:children].each { |child| append_tree(lines, child, "#{indentation} ", grammar, labels) }
104
+ end
105
+
106
+ # @rbs skip
107
+ def format_item(item, grammar, labels)
108
+ if item.production == LALR::Builder::AUGMENTED_PRODUCTION
109
+ rhs = [grammar.start]
110
+ lhs = "$accept"
111
+ else
112
+ production = grammar.productions.fetch(item.production)
113
+ rhs = production.rhs.map { |id| symbol_name(labels, id) }
114
+ lhs = symbol_name(labels, production.lhs)
115
+ end
116
+ rhs = rhs.dup.insert(item.dot, "•")
117
+ lookaheads = item.lookaheads.map { |id| symbol_name(labels, id) }.join(", ")
118
+ "#{lhs} -> #{rhs.join(' ')} [#{lookaheads}]"
119
+ end
120
+
121
+ # @rbs skip
122
+ def format_action(action)
123
+ case action[:type]
124
+ when :shift then "shift #{action[:state]}"
125
+ when :reduce then "reduce #{action[:production]}"
126
+ else action[:type].to_s
127
+ end
128
+ end
129
+
130
+ # @rbs skip
131
+ def symbol_name(labels, id)
132
+ labels.fetch(id) { raise Ibex::Error, "missing grammar symbol id #{id}" }
133
+ end
134
+
135
+ # @rbs skip
136
+ def tree_label(grammar, labels, value)
137
+ symbol = grammar.symbol(value.to_s)
138
+ symbol ? symbol_name(labels, symbol.id) : value
139
+ end
140
+ module_function :append_state, :append_counterexample, :append_tree, :format_item, :format_action, :symbol_name,
141
+ :tree_label
142
+
143
+ class << self
144
+ private :append_state, :append_counterexample, :append_tree, :format_item, :format_action, :symbol_name,
145
+ :tree_label
146
+ end
147
+ end
148
+ end
149
+ end
@@ -0,0 +1,235 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../runtime/parser"
4
+
5
+ module Ibex
6
+ module Codegen
7
+ # Generates a standalone Ruby parser class from Automaton IR.
8
+ class Ruby
9
+ # @rbs @automaton: IR::Automaton
10
+ # @rbs @grammar: IR::Grammar
11
+ # @rbs @table_format: Symbol
12
+ # @rbs @embedded: bool
13
+ # @rbs @line_convert: bool
14
+ # @rbs @line_convert_all: bool
15
+ # @rbs @debug: bool
16
+ # @rbs @omit_action_call: bool
17
+ # @rbs @superclass: String
18
+ # @rbs @executable: String?
19
+
20
+ # @rbs (IR::Automaton automaton, ?table: Symbol | String, ?embedded: bool, ?line_convert: bool,
21
+ # ?line_convert_all: bool, ?debug: bool, ?omit_action_call: bool?, ?superclass: String?,
22
+ # ?executable: String?) -> void
23
+ def initialize(automaton, table: :compact, embedded: false, line_convert: true, debug: false,
24
+ line_convert_all: false, omit_action_call: nil, superclass: nil, executable: nil)
25
+ @automaton = automaton
26
+ @grammar = automaton.grammar
27
+ @table_format = table.to_sym
28
+ @embedded = embedded
29
+ @line_convert = line_convert
30
+ @line_convert_all = line_convert_all
31
+ @debug = debug
32
+ @omit_action_call = omit_action_call.nil? ? @grammar.options[:omit_action_call] : omit_action_call
33
+ @superclass = superclass || @grammar.superclass || "Ibex::Runtime::Parser"
34
+ @executable = executable
35
+ end
36
+
37
+ # @rbs () -> String
38
+ def generate
39
+ lines = [] #: Array[String]
40
+ lines << "#!#{@executable}" if @executable
41
+ lines.push("# frozen_string_literal: true", "# Generated by Ibex #{grammar_digest_comment}", "")
42
+ append_user_code(lines, "header")
43
+ append_runtime(lines)
44
+ modules, class_name = class_parts
45
+ modules.each { |name| lines << "module #{name}" }
46
+ lines << "class #{class_name} < #{@superclass}"
47
+ append_tables(lines)
48
+ append_actions(lines)
49
+ append_user_code(lines, "inner", indent: 2)
50
+ lines << "end"
51
+ modules.reverse_each { lines << "end" }
52
+ append_user_code(lines, "footer")
53
+ "#{lines.join("\n")}\n"
54
+ end
55
+
56
+ private
57
+
58
+ # @rbs (Array[String] lines) -> void
59
+ def append_runtime(lines)
60
+ if @embedded
61
+ lines << embedded_source("../runtime/parser.rb")
62
+ lines << embedded_source("../../ibex/tables.rb")
63
+ else
64
+ lines << 'require "ibex/runtime"'
65
+ lines << 'require "ibex/tables"' if @table_format == :compact
66
+ end
67
+ lines << ""
68
+ end
69
+
70
+ # @rbs (String relative_path) -> String
71
+ def embedded_source(relative_path)
72
+ path = File.expand_path(relative_path, File.dirname(__FILE__))
73
+ File.read(path).lines.reject { |line| line.start_with?("# frozen_string_literal:") }.join.rstrip
74
+ end
75
+
76
+ # @rbs (Array[String] lines) -> void
77
+ def append_tables(lines)
78
+ table_set = Tables.build(@automaton, format: @table_format)
79
+ indent = " "
80
+ lines << "#{indent}PARSER_TABLE_FORMAT_VERSION = #{Runtime::PARSER_TABLE_FORMAT_VERSION}"
81
+ lines << "#{indent}TOKEN_IDS = #{token_ids_literal}.freeze"
82
+ lines << "#{indent}TOKEN_NAMES = #{token_names_literal}.freeze"
83
+ lines << "#{indent}ACTIONS = #{table_literal(table_set.actions)}"
84
+ lines << "#{indent}GOTOS = #{table_literal(table_set.gotos)}"
85
+ lines << "#{indent}DEFAULT_ACTIONS = #{table_set.default_actions.inspect}.freeze"
86
+ lines << "#{indent}PRODUCTIONS = #{productions_literal}.freeze"
87
+ lines << "#{indent}PARSER_TABLES = { format_version: PARSER_TABLE_FORMAT_VERSION,"
88
+ lines << "#{indent} tokens: TOKEN_IDS, token_names: TOKEN_NAMES, actions: ACTIONS,"
89
+ lines << "#{indent} gotos: GOTOS, default_actions: DEFAULT_ACTIONS,"
90
+ lines << "#{indent} productions: PRODUCTIONS }.freeze"
91
+ lines << "#{indent}def self.parser_tables = PARSER_TABLES"
92
+ lines << "#{indent}DEBUG_ENABLED = #{@debug}"
93
+ lines << ""
94
+ end
95
+
96
+ # @rbs (untyped table) -> String
97
+ def table_literal(table)
98
+ return "#{table.inspect}.freeze" unless table.is_a?(Tables::Compact)
99
+
100
+ "Ibex::Tables::Compact.new(offsets: #{table.offsets.inspect}, values: #{table.values.inspect}, " \
101
+ "checks: #{table.checks.inspect}, row_count: #{table.row_count})"
102
+ end
103
+
104
+ # @rbs () -> String
105
+ def token_ids_literal
106
+ pairs = @grammar.terminals.filter_map do |terminal|
107
+ next if terminal.id.zero? || terminal.name == "error"
108
+
109
+ "#{external_token_expression(terminal)} => #{terminal.id}"
110
+ end
111
+ "{ #{pairs.join(', ')} }"
112
+ end
113
+
114
+ # @rbs (IR::GrammarSymbol terminal) -> String
115
+ def external_token_expression(terminal)
116
+ conversion = @grammar.conversions[terminal.name]
117
+ return "(#{conversion})" if conversion
118
+ return terminal.name if terminal.name.start_with?("'", '"')
119
+
120
+ ":#{terminal.name}"
121
+ end
122
+
123
+ # @rbs () -> String
124
+ def token_names_literal
125
+ entries = @grammar.terminals.map { |terminal| "#{terminal.id} => #{terminal.name.inspect}" }
126
+ "{ #{entries.join(', ')} }"
127
+ end
128
+
129
+ # @rbs () -> String
130
+ def productions_literal
131
+ entries = @grammar.productions.map do |production|
132
+ action = action_method?(production) ? ":_ibex_action_#{production.id}" : "nil"
133
+ "{ lhs: #{production.lhs}, length: #{production.rhs.length}, action: #{action} }"
134
+ end
135
+ "[#{entries.join(', ')}]"
136
+ end
137
+
138
+ # @rbs (Array[String] lines) -> void
139
+ def append_actions(lines)
140
+ @grammar.productions.each do |production|
141
+ next unless action_method?(production)
142
+
143
+ append_action_constant(lines, production)
144
+ append_action_method(lines, production)
145
+ end
146
+ end
147
+
148
+ # @rbs (Array[String] lines, IR::Production production) -> void
149
+ def append_action_constant(lines, production)
150
+ return unless production.action && @line_convert
151
+
152
+ lines << " ACTION_CODE_#{production.id} = #{production.action.code.inspect}.freeze"
153
+ end
154
+
155
+ # @rbs (Array[String] lines, IR::Production production) -> void
156
+ def append_action_method(lines, production)
157
+ action = production.action
158
+ lines << " private def _ibex_action_#{production.id}(val, _values)"
159
+ lines << " val = _values.last(#{action.context_length})" if action&.context_length&.positive?
160
+ append_named_bindings(lines, action)
161
+ if action
162
+ append_semantic_code(lines, production)
163
+ else
164
+ lines << " val[0]"
165
+ end
166
+ lines << " end"
167
+ lines << ""
168
+ end
169
+
170
+ # @rbs (Array[String] lines, IR::Action? action) -> void
171
+ def append_named_bindings(lines, action)
172
+ return unless action&.named_refs&.any?
173
+
174
+ action.named_refs.each { |reference| lines << " #{reference[:name]} = val[#{reference[:index]}]" }
175
+ names = action.named_refs.map { |reference| reference[:name] }
176
+ lines << " _ibex_named_values = [#{names.join(', ')}]"
177
+ end
178
+
179
+ # @rbs (Array[String] lines, IR::Production production) -> void
180
+ def append_semantic_code(lines, production)
181
+ action = production.action || raise(Ibex::Error, "missing semantic action")
182
+ lines << " result = val[0]" if @grammar.options[:result_var]
183
+ if @line_convert
184
+ evaluation = "eval(ACTION_CODE_#{production.id}, binding, #{action.location[:file].inspect}, " \
185
+ "#{action.location[:line]})"
186
+ lines << " #{@grammar.options[:result_var] ? evaluation : "return #{evaluation}"}"
187
+ else
188
+ action.code.lines.each { |line| lines << " #{line.rstrip}" }
189
+ end
190
+ lines << " result" if @grammar.options[:result_var]
191
+ end
192
+
193
+ # @rbs (IR::Production production) -> bool
194
+ def action_method?(production)
195
+ !!(production.action || !@omit_action_call)
196
+ end
197
+
198
+ # @rbs (Array[String] lines, String name, ?indent: Integer) -> void
199
+ def append_user_code(lines, name, indent: 0)
200
+ code = @grammar.user_code[name]
201
+ return if code.nil? || code.empty?
202
+
203
+ prefix = " " * indent
204
+ chunks = @grammar.user_code_chunks.fetch(name, Array.new(0))
205
+ if map_user_code?(name) && !chunks.empty?
206
+ chunks.each do |chunk|
207
+ location = chunk.location
208
+ lines << "#{prefix}eval(#{chunk.code.dump}, binding, #{location[:file].inspect}, #{location[:line]})"
209
+ end
210
+ elsif @line_convert_all
211
+ raise Ibex::Error, "(codegen):1:1: source locations are required to convert #{name} user code"
212
+ else
213
+ code.lines.each { |line| lines << "#{prefix}#{line.rstrip}" }
214
+ end
215
+ lines << ""
216
+ end
217
+
218
+ # @rbs (String name) -> bool
219
+ def map_user_code?(name)
220
+ @line_convert_all || (@line_convert && name == "inner")
221
+ end
222
+
223
+ # @rbs () -> [Array[String], String]
224
+ def class_parts
225
+ parts = @grammar.class_name.split("::")
226
+ [parts.take(parts.length - 1), parts.fetch(-1)]
227
+ end
228
+
229
+ # @rbs () -> String
230
+ def grammar_digest_comment
231
+ @automaton.grammar_digest.delete_prefix("sha256:").chars.first(12).join
232
+ end
233
+ end
234
+ end
235
+ end
@@ -0,0 +1,18 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Ibex
4
+ module Codegen
5
+ # Builds human-facing symbol labels without changing Grammar IR identities.
6
+ module SymbolLabels
7
+ # @rbs (IR::Grammar grammar) -> Hash[Integer, String]
8
+ def self.build(grammar)
9
+ labels = grammar.symbols.to_h { |symbol| [symbol.id, symbol.name] }
10
+ grammar.productions.each do |production|
11
+ expression = production.origin[:expression]
12
+ labels[production.lhs] = expression if expression.is_a?(String)
13
+ end
14
+ labels
15
+ end
16
+ end
17
+ end
18
+ end
data/lib/ibex/error.rb ADDED
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+ # rbs_inline: enabled
3
+
4
+ module Ibex
5
+ # Base error raised for invalid grammars and command failures.
6
+ class Error < StandardError; end
7
+ end
@@ -0,0 +1,266 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Ibex
4
+ module Frontend
5
+ # Extracts a balanced Ruby action while ignoring braces inside literals.
6
+ class ActionScanner
7
+ # @rbs!
8
+ # type heredoc = {
9
+ # identifier: String,
10
+ # indented: bool,
11
+ # length: Integer,
12
+ # location: Location
13
+ # }
14
+
15
+ PAIRED_DELIMITERS = { "(" => ")", "[" => "]", "{" => "}", "<" => ">" }.freeze #: Hash[String, String]
16
+ REGEX_PREFIXES = "=([{!,:;?&|+-*%^~<>" #: String
17
+
18
+ # @rbs @cursor: SourceCursor
19
+ # @rbs @pending_heredocs: Array[heredoc]
20
+
21
+ # @rbs (SourceCursor cursor) -> void
22
+ def initialize(cursor)
23
+ @cursor = cursor
24
+ @pending_heredocs = [] #: Array[heredoc]
25
+ end
26
+
27
+ # @rbs () -> Token
28
+ def scan
29
+ location = @cursor.location
30
+ @cursor.advance
31
+ start = @cursor.index
32
+ scan_code(1)
33
+ finish = @cursor.index
34
+ @cursor.advance
35
+ Token.new(type: :action, value: @cursor.source[start...finish], location: location)
36
+ rescue Ibex::Error
37
+ raise
38
+ rescue StandardError => e
39
+ raise Ibex::Error, "#{location}: unable to scan action: #{e.message}"
40
+ end
41
+
42
+ private
43
+
44
+ # @rbs (Integer depth) -> void
45
+ def scan_code(depth)
46
+ until @cursor.eof?
47
+ if @cursor.peek == "\n" && @pending_heredocs.any?
48
+ @cursor.advance
49
+ scan_pending_heredocs
50
+ next
51
+ end
52
+
53
+ index = @cursor.index
54
+ scan_special_character
55
+ next if @cursor.index != index
56
+
57
+ depth += 1 if @cursor.peek == "{"
58
+ depth -= 1 if @cursor.peek == "}"
59
+ return if depth.zero?
60
+
61
+ @cursor.advance
62
+ end
63
+ raise Ibex::Error, "#{@cursor.location}: unterminated action"
64
+ end
65
+
66
+ # @rbs () -> void
67
+ def scan_special_character
68
+ character = @cursor.peek
69
+ case character
70
+ when "'", '"', "`" then scan_quoted(character)
71
+ when "%" then scan_percent_literal
72
+ when "/" then scan_regexp if regexp_start?
73
+ when "#" then scan_comment
74
+ when "?" then scan_character_literal
75
+ when "<" then scan_heredoc if @cursor.peek(1) == "<"
76
+ end
77
+ end
78
+
79
+ # @rbs (String quote) -> void
80
+ def scan_quoted(quote)
81
+ start = @cursor.location
82
+ @cursor.advance
83
+ until @cursor.eof?
84
+ if @cursor.peek == "\\"
85
+ @cursor.advance(2)
86
+ elsif quote != "'" && @cursor.rest.start_with?("\#{")
87
+ @cursor.advance(2)
88
+ scan_interpolation
89
+ elsif @cursor.peek == quote
90
+ @cursor.advance
91
+ return
92
+ else
93
+ @cursor.advance
94
+ end
95
+ end
96
+ raise Ibex::Error, "#{start}: unterminated #{quote} string in action"
97
+ end
98
+
99
+ # @rbs () -> void
100
+ def scan_interpolation
101
+ depth = 1
102
+ until @cursor.eof?
103
+ character = @cursor.peek
104
+ raise Ibex::Error, "#{@cursor.location}: unterminated string interpolation" unless character
105
+
106
+ if ["'", '"', "`"].include?(character)
107
+ scan_quoted(character)
108
+ next
109
+ end
110
+ if character == "{"
111
+ depth += 1
112
+ elsif character == "}"
113
+ depth -= 1
114
+ @cursor.advance
115
+ return if depth.zero?
116
+
117
+ next
118
+ end
119
+ @cursor.advance
120
+ end
121
+ raise Ibex::Error, "#{@cursor.location}: unterminated string interpolation"
122
+ end
123
+
124
+ # @rbs () -> void
125
+ def scan_percent_literal
126
+ match = @cursor.rest.match(/\A%(?:[qQwWiIxrs])?([^\w\s])/)
127
+ return unless match
128
+
129
+ literal_prefix = match[0]
130
+ opener = match[1]
131
+ return unless literal_prefix && opener
132
+
133
+ closer = PAIRED_DELIMITERS.fetch(opener, opener)
134
+ @cursor.advance(literal_prefix.length)
135
+ scan_delimited(opener, closer)
136
+ end
137
+
138
+ # @rbs (String opener, String closer) -> void
139
+ def scan_delimited(opener, closer)
140
+ start = @cursor.location
141
+ depth = 1
142
+ until @cursor.eof?
143
+ if @cursor.peek == "\\"
144
+ @cursor.advance(2)
145
+ next
146
+ end
147
+ depth += 1 if opener != closer && @cursor.peek == opener
148
+ depth -= 1 if @cursor.peek == closer
149
+ @cursor.advance
150
+ return if depth.zero?
151
+ end
152
+ raise Ibex::Error, "#{start}: unterminated percent literal"
153
+ end
154
+
155
+ # @rbs () -> bool
156
+ def regexp_start?
157
+ prefix = @cursor.source.chars.take(@cursor.index).join.rstrip[-1]
158
+ prefix.nil? || REGEX_PREFIXES.include?(prefix)
159
+ end
160
+
161
+ # @rbs () -> void
162
+ def scan_regexp
163
+ start = @cursor.location
164
+ @cursor.advance
165
+ in_class = false
166
+ until @cursor.eof?
167
+ if @cursor.peek == "\\"
168
+ @cursor.advance(2)
169
+ next
170
+ end
171
+ in_class = true if @cursor.peek == "["
172
+ in_class = false if @cursor.peek == "]"
173
+ if @cursor.peek == "/" && !in_class
174
+ @cursor.advance
175
+ @cursor.advance while @cursor.peek&.match?(/[a-z]/i)
176
+ return
177
+ end
178
+ @cursor.advance
179
+ end
180
+ raise Ibex::Error, "#{start}: unterminated regular expression"
181
+ end
182
+
183
+ # @rbs () -> void
184
+ def scan_comment
185
+ @cursor.advance until @cursor.eof? || @cursor.peek == "\n"
186
+ end
187
+
188
+ # @rbs () -> void
189
+ def scan_character_literal
190
+ @cursor.advance
191
+ @cursor.advance if @cursor.peek == "\\"
192
+ @cursor.advance unless @cursor.eof?
193
+ end
194
+
195
+ # @rbs () -> void
196
+ def scan_heredoc
197
+ opener = heredoc_opener
198
+ return unless opener
199
+
200
+ @cursor.advance(opener[:length])
201
+ @pending_heredocs << opener
202
+ end
203
+
204
+ # @rbs () -> heredoc?
205
+ def heredoc_opener
206
+ prefix = @cursor.rest.match(/\A<<([~-]?)/)
207
+ return unless prefix
208
+
209
+ marker = prefix[0]
210
+ return unless marker
211
+
212
+ quote = @cursor.rest[marker.length]
213
+ identifier, length = if quote && ["'", '"', "`"].include?(quote)
214
+ quoted_heredoc_identifier(marker, quote)
215
+ else
216
+ bare_heredoc_identifier(marker)
217
+ end
218
+ return unless identifier
219
+
220
+ modifier = prefix[1] || ""
221
+ { identifier: identifier, indented: !modifier.empty?, length: length,
222
+ location: @cursor.location } #: heredoc
223
+ end
224
+
225
+ # @rbs (String prefix, String quote) -> [String, Integer]?
226
+ def quoted_heredoc_identifier(prefix, quote)
227
+ start = prefix.length + 1
228
+ finish = @cursor.rest.index(quote, start)
229
+ return unless finish
230
+
231
+ identifier = @cursor.rest[start...finish]
232
+ return unless identifier
233
+ return if identifier.match?(/[\r\n]/)
234
+
235
+ [identifier, finish + 1]
236
+ end
237
+
238
+ # @rbs (String prefix) -> [String?, Integer]
239
+ def bare_heredoc_identifier(prefix)
240
+ suffix = @cursor.rest[prefix.length..] || ""
241
+ identifier = suffix.match(/\A[A-Za-z_]\w*/)&.[](0)
242
+ [identifier, prefix.length + identifier.to_s.length]
243
+ end
244
+
245
+ # @rbs () -> void
246
+ def scan_pending_heredocs
247
+ @pending_heredocs.shift.then { |heredoc| scan_heredoc_body(heredoc) } until @pending_heredocs.empty?
248
+ end
249
+
250
+ # @rbs (heredoc heredoc) -> void
251
+ def scan_heredoc_body(heredoc)
252
+ identifier = heredoc[:identifier]
253
+ escaped_identifier = Regexp.escape(identifier)
254
+ prefix = heredoc[:indented] ? "[ \\t]*" : ""
255
+ terminator = /\A#{prefix}#{escaped_identifier}\r?\z/
256
+ until @cursor.eof?
257
+ line = @cursor.rest[/\A[^\n]*(?:\n|\z)/] || ""
258
+ content = line.delete_suffix("\n")
259
+ @cursor.advance(line.length)
260
+ return if content.match?(terminator)
261
+ end
262
+ raise Ibex::Error, "#{heredoc[:location]}: unterminated heredoc #{identifier}"
263
+ end
264
+ end
265
+ end
266
+ end