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,273 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Ibex
4
+ module Frontend
5
+ # Semantic support and token delivery for the generated grammar parser.
6
+ class GeneratedParserBase < Runtime::Parser
7
+ # @rbs @adapter: TokenAdapter
8
+ # @rbs @mode: Symbol
9
+
10
+ # @rbs (Array[Token] tokens, ?mode: Symbol) -> void
11
+ def initialize(tokens, mode: :racc)
12
+ raise ArgumentError, "mode must be :racc or :extended" unless %i[racc extended].include?(mode)
13
+
14
+ super()
15
+ @adapter = TokenAdapter.new(tokens)
16
+ @mode = mode
17
+ end
18
+
19
+ # @rbs () -> AST::Root
20
+ def parse
21
+ do_parse
22
+ end
23
+
24
+ # @rbs () -> ([external_token, Token] | false)
25
+ def next_token
26
+ @adapter.next_token
27
+ end
28
+
29
+ # @rbs (Integer? _token_id, Token? value, Array[untyped] _value_stack) -> bot
30
+ def on_error(_token_id, value, _value_stack)
31
+ token = value || @adapter.eof_token
32
+ raise_contextual_error(token)
33
+
34
+ received = token&.value || token&.type || :eof
35
+ location = token&.location || Location.new(file: "(grammar)", line: 1, column: 1)
36
+ expected = expected_description(token)
37
+ raise Ibex::Error, "#{location}: expected #{expected}, got #{received}"
38
+ end
39
+
40
+ private
41
+
42
+ # @rbs (Token? token) -> void
43
+ def raise_contextual_error(token)
44
+ raise_group_contextual_error(token)
45
+ raise_conversion_contextual_error
46
+ end
47
+
48
+ # @rbs (Token? token) -> void
49
+ def raise_group_contextual_error(token)
50
+ group_opening = @adapter.group_opening
51
+ if group_opening && token && token.type == :action
52
+ fail_at(token.location, "actions inside EBNF groups are not supported")
53
+ end
54
+ group_unterminated = @adapter.open_delimiter_kind == :group &&
55
+ (token.nil? || token.type == :eof || token.value == "end")
56
+ fail_at(group_opening.location, "unterminated EBNF group") if group_unterminated && group_opening
57
+ end
58
+
59
+ # @rbs () -> void
60
+ def raise_conversion_contextual_error
61
+ conversion_name = @adapter.conversion_name
62
+ return unless @adapter.declaration == :convert
63
+ return unless conversion_name
64
+
65
+ fail_at(conversion_name.location, "expected a quoted Ruby conversion expression")
66
+ end
67
+
68
+ # @rbs (Token? token) -> String
69
+ def expected_description(token)
70
+ expectation = @adapter.expectation(token)
71
+ return expectation if expectation
72
+ return ")" if @adapter.open_delimiter_kind == :separated
73
+
74
+ structural_expectation(token)
75
+ end
76
+
77
+ # @rbs (Token? token) -> String
78
+ def structural_expectation(token)
79
+ return "rule" if @adapter.section == :declarations
80
+ return "at least one rule" if @adapter.section == :user_code && token&.value == "end"
81
+ return "eof" if @adapter.section == :user_code
82
+ return "end" if @adapter.section == :rules && @adapter.eof_token
83
+
84
+ "grammar syntax"
85
+ end
86
+
87
+ # @rbs (Token class_token, Array[String] class_parts, Array[String]? superclass,
88
+ # Array[AST::declaration] declarations, Array[AST::Rule] rules, AST::user_code user_code) -> AST::Root
89
+ def build_root(class_token, class_parts, superclass, declarations, rules, user_code)
90
+ AST::Root.new(class_name: class_parts.join("::"), superclass: superclass&.join("::"),
91
+ declarations: declarations, rules: rules, user_code: user_code, loc: class_token.location)
92
+ end
93
+
94
+ # @rbs (Token keyword, Array[String] names) -> AST::Tokens
95
+ def build_tokens(keyword, names)
96
+ AST::Tokens.new(names: names, loc: keyword.location)
97
+ end
98
+
99
+ # @rbs (Token keyword, Symbol direction, Array[AST::PrecedenceLevel] levels) -> AST::Precedence
100
+ def build_precedence(keyword, direction, levels)
101
+ AST::Precedence.new(direction: direction, levels: levels, loc: keyword.location)
102
+ end
103
+
104
+ # @rbs (Token association, Array[String] symbols) -> AST::PrecedenceLevel
105
+ def build_precedence_level(association, symbols)
106
+ fail_at(association.location, "expected at least one precedence symbol") if symbols.empty?
107
+
108
+ AST::PrecedenceLevel.new(associativity: token_string(association).to_sym, symbols: symbols,
109
+ loc: association.location)
110
+ end
111
+
112
+ # @rbs (Token keyword, Array[String] names) -> AST::Options
113
+ def build_options(keyword, names)
114
+ AST::Options.new(names: names, loc: keyword.location)
115
+ end
116
+
117
+ # @rbs (Token keyword, Token integer) -> AST::Expect
118
+ def build_expect(keyword, integer)
119
+ AST::Expect.new(conflicts: token_integer(integer), loc: keyword.location)
120
+ end
121
+
122
+ # @rbs (Token keyword, String name) -> AST::Start
123
+ def build_start(keyword, name)
124
+ AST::Start.new(name: name, loc: keyword.location)
125
+ end
126
+
127
+ # @rbs (Token keyword, Array[AST::Conversion] pairs) -> AST::Convert
128
+ def build_convert(keyword, pairs)
129
+ AST::Convert.new(pairs: pairs, loc: keyword.location)
130
+ end
131
+
132
+ # @rbs (Token name_token, Token literal_token) -> AST::Conversion
133
+ def build_conversion(name_token, literal_token)
134
+ unless name_token.location.line == literal_token.location.line
135
+ fail_at(name_token.location, "expected a quoted Ruby conversion expression")
136
+ end
137
+
138
+ literal = token_string(literal_token)
139
+ expression = if literal.start_with?('"')
140
+ literal.undump
141
+ else
142
+ (literal[1...-1] || "").gsub("\\'", "'").gsub("\\\\", "\\")
143
+ end
144
+ AST::Conversion.new(name: token_string(name_token), expression: expression, loc: name_token.location)
145
+ rescue RuntimeError => e
146
+ fail_at(name_token.location, "invalid conversion expression: #{e.message}")
147
+ end
148
+
149
+ # @rbs (Token lhs, Array[AST::Alternative] alternatives) -> AST::Rule
150
+ def build_rule(lhs, alternatives)
151
+ AST::Rule.new(lhs: token_string(lhs), alternatives: alternatives, loc: lhs.location)
152
+ end
153
+
154
+ # @rbs (Array[AST::item] items, Token? precedence) -> AST::Alternative
155
+ def build_alternative(items, precedence)
156
+ last_token = @adapter.last_token
157
+ location = item_start_location(items.first) || precedence&.location || last_token&.location
158
+ raise Ibex::Error, "missing alternative location" unless location
159
+
160
+ action = nil #: AST::InlineAction?
161
+ last_item = items.last
162
+ if last_item.is_a?(AST::InlineAction)
163
+ items.pop
164
+ action = last_item
165
+ end
166
+ AST::Alternative.new(items: items, action: action,
167
+ precedence: precedence && token_string(precedence), loc: location)
168
+ end
169
+
170
+ # @rbs (AST::item? item) -> Location?
171
+ def item_start_location(item)
172
+ return unless item
173
+
174
+ if item.is_a?(AST::Optional) || item.is_a?(AST::Star) || item.is_a?(AST::Plus)
175
+ return item_start_location(item.item)
176
+ end
177
+
178
+ item.loc
179
+ end
180
+
181
+ # @rbs (Token token, [Token, Token]? named_reference, Array[Token] suffixes) -> AST::item
182
+ def build_symbol_reference(token, named_reference, suffixes)
183
+ if named_reference
184
+ colon, name = named_reference
185
+ extended_only!(colon.location, "named references")
186
+ named_reference = token_string(name)
187
+ end
188
+ item = AST::SymbolReference.new(name: token_string(token), named_reference: named_reference,
189
+ loc: token.location)
190
+ apply_suffixes(item, suffixes)
191
+ end
192
+
193
+ # @rbs (Token token) -> AST::InlineAction
194
+ def build_action(token)
195
+ AST::InlineAction.new(code: token_string(token), loc: token.location)
196
+ end
197
+
198
+ # @rbs (Token opening, Array[Array[AST::item]] alternatives, Array[Token] suffixes) -> AST::item
199
+ def build_group(opening, alternatives, suffixes)
200
+ extended_only!(opening.location, "EBNF groups")
201
+ apply_suffixes(AST::Group.new(alternatives: alternatives, loc: opening.location), suffixes)
202
+ end
203
+
204
+ # @rbs (Token function, AST::item item, AST::item separator) -> AST::SeparatedList
205
+ def build_separated_list(function, item, separator)
206
+ extended_only!(function.location, "separated lists")
207
+ AST::SeparatedList.new(item: item, separator: separator,
208
+ nonempty: function.value == "separated_nonempty_list", loc: function.location)
209
+ end
210
+
211
+ # @rbs (AST::item item, Array[Token] suffixes) -> AST::item
212
+ def apply_suffixes(item, suffixes)
213
+ suffixes.reduce(item) do |wrapped, suffix|
214
+ extended_only!(suffix.location, "EBNF suffixes")
215
+ case token_string(suffix)
216
+ when "?" then AST::Optional.new(item: wrapped, loc: suffix.location)
217
+ when "*" then AST::Star.new(item: wrapped, loc: suffix.location)
218
+ when "+" then AST::Plus.new(item: wrapped, loc: suffix.location)
219
+ else raise Ibex::Error, "#{suffix.location}: unknown EBNF suffix #{suffix.value.inspect}"
220
+ end
221
+ end
222
+ end
223
+
224
+ # @rbs (AST::user_code blocks, Token token) -> AST::user_code
225
+ def append_user_code(blocks, token)
226
+ value = token_user_code(token)
227
+ blocks[value[:name]] << AST::UserCode.new(name: value[:name], code: value[:code], loc: token.location)
228
+ blocks
229
+ end
230
+
231
+ # @rbs () -> AST::user_code
232
+ def empty_user_code
233
+ Hash.new { |hash, key| hash[key] = [] } #: AST::user_code
234
+ end
235
+
236
+ # @rbs (Location location, String feature) -> void
237
+ def extended_only!(location, feature)
238
+ return if @mode == :extended || @adapter.extended_pragma?
239
+
240
+ fail_at(location, "#{feature} require extended mode")
241
+ end
242
+
243
+ # @rbs (Location location, String message) -> bot
244
+ def fail_at(location, message)
245
+ raise Ibex::Error, "#{location}: #{message}"
246
+ end
247
+
248
+ # @rbs (Token token) -> String
249
+ def token_string(token)
250
+ value = token.value
251
+ return value if value.is_a?(String)
252
+
253
+ fail_at(token.location, "expected text token")
254
+ end
255
+
256
+ # @rbs (Token token) -> Integer
257
+ def token_integer(token)
258
+ value = token.value
259
+ return value if value.is_a?(Integer)
260
+
261
+ fail_at(token.location, "expected integer token")
262
+ end
263
+
264
+ # @rbs (Token token) -> user_code_token
265
+ def token_user_code(token)
266
+ value = token.value
267
+ return value if value.is_a?(Hash)
268
+
269
+ fail_at(token.location, "expected user-code token")
270
+ end
271
+ end
272
+ end
273
+ end
@@ -0,0 +1,156 @@
1
+ class Ibex::Frontend::GeneratedParser < Ibex::Frontend::GeneratedParserBase
2
+ token CLASS TOKEN PRECHIGH PRECLOW OPTIONS EXPECT START CONVERT PRAGMA RULE END
3
+ token LEFT RIGHT NONASSOC IDENTIFIER LITERAL INTEGER ACTION USER_CODE LHS
4
+ token SEPARATED_LIST SEPARATED_NONEMPTY_LIST
5
+ rule
6
+ grammar
7
+ : CLASS constant_path superclass pragmas declarations RULE rules END user_code
8
+ { result = build_root(val[0], val[1], val[2], val[4], val[6], val[8]) }
9
+
10
+ constant_path
11
+ : IDENTIFIER { result = [val[0].value] }
12
+ | constant_path '::' IDENTIFIER { result = val[0] + [val[2].value] }
13
+
14
+ superclass
15
+ : { result = nil }
16
+ | '<' constant_path { result = val[1] }
17
+
18
+ pragmas
19
+ :
20
+ | pragmas PRAGMA IDENTIFIER
21
+
22
+ declarations
23
+ : { result = Array.new(0) }
24
+ | declarations declaration { result = val[0] + [val[1]] }
25
+
26
+ declaration
27
+ : token_declaration { result = val[0] }
28
+ | precedence_declaration { result = val[0] }
29
+ | options_declaration { result = val[0] }
30
+ | expect_declaration { result = val[0] }
31
+ | start_declaration { result = val[0] }
32
+ | convert_declaration { result = val[0] }
33
+
34
+ token_declaration
35
+ : TOKEN symbols { result = build_tokens(val[0], val[1]) }
36
+
37
+ precedence_declaration
38
+ : PRECHIGH precedence_levels PRECLOW { result = build_precedence(val[0], :high_to_low, val[1]) }
39
+ | PRECLOW precedence_levels PRECHIGH { result = build_precedence(val[0], :low_to_high, val[1]) }
40
+
41
+ precedence_levels
42
+ : { result = Array.new(0) }
43
+ | precedence_levels precedence_level { result = val[0] + [val[1]] }
44
+
45
+ precedence_level
46
+ : association symbols { result = build_precedence_level(val[0], val[1]) }
47
+
48
+ association
49
+ : LEFT { result = val[0] }
50
+ | RIGHT { result = val[0] }
51
+ | NONASSOC { result = val[0] }
52
+
53
+ options_declaration
54
+ : OPTIONS identifiers { result = build_options(val[0], val[1]) }
55
+
56
+ expect_declaration
57
+ : EXPECT INTEGER { result = build_expect(val[0], val[1]) }
58
+
59
+ start_declaration
60
+ : START grammar_symbol { result = build_start(val[0], val[1].value) }
61
+
62
+ convert_declaration
63
+ : CONVERT conversions END { result = build_convert(val[0], val[1]) }
64
+
65
+ conversions
66
+ : { result = Array.new(0) }
67
+ | conversions conversion { result = val[0] + [val[1]] }
68
+
69
+ conversion
70
+ : grammar_symbol LITERAL { result = build_conversion(val[0], val[1]) }
71
+
72
+ identifiers
73
+ : { result = Array.new(0) }
74
+ | identifiers IDENTIFIER { result = val[0] + [val[1].value] }
75
+
76
+ symbols
77
+ : { result = Array.new(0) }
78
+ | symbols grammar_symbol { result = val[0] + [val[1].value] }
79
+
80
+ grammar_symbol
81
+ : IDENTIFIER { result = val[0] }
82
+ | LITERAL { result = val[0] }
83
+
84
+ rules
85
+ : rule_definition { result = [val[0]] }
86
+ | rules rule_definition { result = val[0] + [val[1]] }
87
+
88
+ rule_definition
89
+ : LHS ':' alternatives semicolon { result = build_rule(val[0], val[2]) }
90
+
91
+ semicolon
92
+ :
93
+ | ';'
94
+
95
+ alternatives
96
+ : alternative { result = [val[0]] }
97
+ | alternatives '|' alternative { result = val[0] + [val[2]] }
98
+
99
+ alternative
100
+ : items precedence_override { result = build_alternative(val[0], val[1]) }
101
+
102
+ precedence_override
103
+ : { result = nil }
104
+ | '=' grammar_symbol { result = val[1] }
105
+
106
+ items
107
+ : { result = Array.new(0) }
108
+ | items item { result = val[0] + [val[1]] }
109
+
110
+ item
111
+ : symbol_item { result = val[0] }
112
+ | ACTION { result = build_action(val[0]) }
113
+ | group_item { result = val[0] }
114
+ | separated_item { result = val[0] }
115
+
116
+ symbol_item
117
+ : grammar_symbol named_reference suffixes
118
+ { result = build_symbol_reference(val[0], val[1], val[2]) }
119
+
120
+ named_reference
121
+ : { result = nil }
122
+ | ':' IDENTIFIER { result = [val[0], val[1]] }
123
+
124
+ suffixes
125
+ : { result = Array.new(0) }
126
+ | suffixes '?' { result = val[0] + [val[1]] }
127
+ | suffixes '*' { result = val[0] + [val[1]] }
128
+ | suffixes '+' { result = val[0] + [val[1]] }
129
+
130
+ group_item
131
+ : '(' group_alternatives ')' suffixes
132
+ { result = build_group(val[0], val[1], val[3]) }
133
+
134
+ group_alternatives
135
+ : group_items { result = [val[0]] }
136
+ | group_alternatives '|' group_items { result = val[0] + [val[2]] }
137
+
138
+ group_items
139
+ : { result = Array.new(0) }
140
+ | group_items group_item_element { result = val[0] + [val[1]] }
141
+
142
+ group_item_element
143
+ : symbol_item { result = val[0] }
144
+ | group_item { result = val[0] }
145
+ | separated_item { result = val[0] }
146
+
147
+ separated_item
148
+ : SEPARATED_LIST '(' item ',' item ')'
149
+ { result = build_separated_list(val[0], val[2], val[4]) }
150
+ | SEPARATED_NONEMPTY_LIST '(' item ',' item ')'
151
+ { result = build_separated_list(val[0], val[2], val[4]) }
152
+
153
+ user_code
154
+ : { result = empty_user_code }
155
+ | user_code USER_CODE { result = append_user_code(val[0], val[1]) }
156
+ end
@@ -0,0 +1,165 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Ibex
4
+ module Frontend
5
+ # Tokenizes a racc-compatible grammar while preserving source locations.
6
+ class Lexer
7
+ PUNCTUATION = %w[: | ; = < > ? * + , ( )].to_h do |character|
8
+ [character, character.to_sym]
9
+ end.freeze #: Hash[String, Symbol]
10
+ USER_CODE = /\A----[ \t]+(header|inner|footer)[ \t]*(?:\r?\n|\z)/ #: Regexp
11
+
12
+ # @rbs @cursor: SourceCursor
13
+
14
+ # @rbs (String source, ?file: String) -> void
15
+ def initialize(source, file: "(grammar)")
16
+ @cursor = SourceCursor.new(source, file)
17
+ end
18
+
19
+ # @rbs () -> Array[Token]
20
+ def tokenize
21
+ tokens = [] #: Array[Token]
22
+ loop do
23
+ token = next_token
24
+ tokens << token
25
+ return tokens if token.type == :eof
26
+ end
27
+ end
28
+
29
+ # @rbs () -> Token
30
+ def next_token
31
+ skip_ignored
32
+ return token(:eof, nil) if @cursor.eof?
33
+
34
+ character = @cursor.peek || ""
35
+
36
+ return scan_user_code if line_start? && @cursor.rest.start_with?("----")
37
+ return ActionScanner.new(@cursor).scan if character == "{"
38
+ return scan_scope if @cursor.rest.start_with?("::")
39
+
40
+ scan_regular_token(character)
41
+ end
42
+
43
+ private
44
+
45
+ # @rbs (String character) -> Token
46
+ def scan_regular_token(character)
47
+ return scan_identifier if character.match?(/[A-Za-z_]/)
48
+ return scan_integer if character.match?(/\d/)
49
+ return scan_literal if ["'", '"'].include?(character)
50
+ return scan_punctuation if PUNCTUATION.key?(character)
51
+
52
+ raise Ibex::Error, "#{@cursor.location}: unexpected character #{character.inspect}"
53
+ end
54
+
55
+ # @rbs () -> void
56
+ def skip_ignored
57
+ loop do
58
+ @cursor.advance while @cursor.peek&.match?(/\s/)
59
+ if @cursor.peek == "#"
60
+ @cursor.advance until @cursor.eof? || @cursor.peek == "\n"
61
+ elsif @cursor.rest.start_with?("/*")
62
+ skip_block_comment
63
+ else
64
+ return
65
+ end
66
+ end
67
+ end
68
+
69
+ # @rbs () -> void
70
+ def skip_block_comment
71
+ location = @cursor.location
72
+ finish = @cursor.source.index("*/", @cursor.index + 2)
73
+ raise Ibex::Error, "#{location}: unterminated block comment" unless finish
74
+
75
+ @cursor.advance(finish + 2 - @cursor.index)
76
+ end
77
+
78
+ # @rbs () -> bool
79
+ def line_start?
80
+ @cursor.column == 1
81
+ end
82
+
83
+ # @rbs () -> Token
84
+ def scan_user_code
85
+ location = @cursor.location
86
+ match = @cursor.rest.match(USER_CODE)
87
+ raise Ibex::Error, "#{location}: expected ---- header, inner, or footer" unless match
88
+
89
+ name = match[1]
90
+ marker = match[0]
91
+ raise Ibex::Error, "#{location}: expected ---- header, inner, or footer" unless name && marker
92
+
93
+ @cursor.advance(marker.length)
94
+ start = @cursor.index
95
+ finish = @cursor.source.index(/^----/, start) || @cursor.source.length
96
+ @cursor.advance(finish - start)
97
+ code = @cursor.source[start...finish] || ""
98
+ token(:user_code, { name: name, code: code }, location)
99
+ end
100
+
101
+ # @rbs () -> Token
102
+ def scan_scope
103
+ location = @cursor.location
104
+ @cursor.advance(2)
105
+ token(:scope, "::", location)
106
+ end
107
+
108
+ # @rbs () -> Token
109
+ def scan_identifier
110
+ scan_match(:identifier, /\A[A-Za-z_][A-Za-z0-9_]*/)
111
+ end
112
+
113
+ # @rbs () -> Token
114
+ def scan_integer
115
+ scan_match(:integer, /\A\d+/) { |value| Integer(value, 10) }
116
+ end
117
+
118
+ # @rbs () -> Token
119
+ def scan_literal
120
+ location = @cursor.location
121
+ quote = @cursor.peek
122
+ start = @cursor.index
123
+ @cursor.advance
124
+ until @cursor.eof?
125
+ if @cursor.peek == "\\"
126
+ @cursor.advance(2)
127
+ elsif @cursor.peek == quote
128
+ @cursor.advance
129
+ value = @cursor.source[start...@cursor.index] || ""
130
+ return token(:literal, value, location)
131
+ else
132
+ @cursor.advance
133
+ end
134
+ end
135
+ raise Ibex::Error, "#{location}: unterminated quoted token"
136
+ end
137
+
138
+ # @rbs () -> Token
139
+ def scan_punctuation
140
+ location = @cursor.location
141
+ character = @cursor.peek
142
+ raise Ibex::Error, "#{location}: expected punctuation" unless character
143
+
144
+ @cursor.advance
145
+ token(PUNCTUATION.fetch(character), character, location)
146
+ end
147
+
148
+ # @rbs (Symbol type, Regexp pattern) ?{ (String) -> token_value } -> Token
149
+ def scan_match(type, pattern)
150
+ location = @cursor.location
151
+ value = @cursor.rest.match(pattern)&.[](0)
152
+ raise Ibex::Error, "#{location}: invalid #{type} token" unless value
153
+
154
+ @cursor.advance(value.length)
155
+ value = yield(value) if block_given?
156
+ token(type, value, location)
157
+ end
158
+
159
+ # @rbs (Symbol type, token_value value, ?Location location) -> Token
160
+ def token(type, value, location = @cursor.location)
161
+ Token.new(type: type, value: value, location: location)
162
+ end
163
+ end
164
+ end
165
+ end