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.
- checksums.yaml +7 -0
- data/.rubocop.yml +43 -0
- data/CHANGELOG.md +30 -0
- data/LICENSE.txt +21 -0
- data/README.md +194 -0
- data/Rakefile +25 -0
- data/Steepfile +10 -0
- data/docs/architecture.md +99 -0
- data/docs/compat-notes.md +37 -0
- data/docs/grammar-reference.md +127 -0
- data/docs/lexer-coverage.md +14 -0
- data/docs/phase10-extensions.md +27 -0
- data/docs/racc-migration.md +47 -0
- data/exe/ibex +6 -0
- data/gemfiles/Gemfile +7 -0
- data/gemfiles/Gemfile.lock +98 -0
- data/lib/ibex/analysis/sets.rb +194 -0
- data/lib/ibex/analysis.rb +8 -0
- data/lib/ibex/cli/counterexample_options.rb +38 -0
- data/lib/ibex/cli/outputs.rb +88 -0
- data/lib/ibex/cli.rb +333 -0
- data/lib/ibex/codegen/dot.rb +52 -0
- data/lib/ibex/codegen/html.rb +110 -0
- data/lib/ibex/codegen/rbs.rb +54 -0
- data/lib/ibex/codegen/report.rb +149 -0
- data/lib/ibex/codegen/ruby.rb +235 -0
- data/lib/ibex/codegen/symbol_labels.rb +18 -0
- data/lib/ibex/error.rb +7 -0
- data/lib/ibex/frontend/action_scanner.rb +266 -0
- data/lib/ibex/frontend/ast.rb +146 -0
- data/lib/ibex/frontend/bootstrap_parser.rb +163 -0
- data/lib/ibex/frontend/dsl.rb +196 -0
- data/lib/ibex/frontend/generated_parser.rb +434 -0
- data/lib/ibex/frontend/generated_parser_base.rb +273 -0
- data/lib/ibex/frontend/grammar.y +156 -0
- data/lib/ibex/frontend/lexer.rb +165 -0
- data/lib/ibex/frontend/parser/declarations.rb +163 -0
- data/lib/ibex/frontend/parser/rules.rb +154 -0
- data/lib/ibex/frontend/parser.rb +23 -0
- data/lib/ibex/frontend/regenerator.rb +41 -0
- data/lib/ibex/frontend/source_cursor.rb +94 -0
- data/lib/ibex/frontend/token_adapter/declaration_state.rb +248 -0
- data/lib/ibex/frontend/token_adapter/delimiter_tracker.rb +37 -0
- data/lib/ibex/frontend/token_adapter/rule_state.rb +162 -0
- data/lib/ibex/frontend/token_adapter.rb +108 -0
- data/lib/ibex/frontend.rb +28 -0
- data/lib/ibex/ir/automaton_ir.rb +109 -0
- data/lib/ibex/ir/grammar_ir.rb +192 -0
- data/lib/ibex/ir/serialize.rb +154 -0
- data/lib/ibex/ir.rb +53 -0
- data/lib/ibex/lalr/builder.rb +270 -0
- data/lib/ibex/lalr/conflict.rb +120 -0
- data/lib/ibex/lalr/conflict_search.rb +332 -0
- data/lib/ibex/lalr/conflict_search_limits.rb +39 -0
- data/lib/ibex/lalr/counterexample.rb +138 -0
- data/lib/ibex/lalr/default_reductions.rb +67 -0
- data/lib/ibex/lalr.rb +27 -0
- data/lib/ibex/normalize/declarations.rb +60 -0
- data/lib/ibex/normalize/diagnostics.rb +99 -0
- data/lib/ibex/normalize/expander.rb +218 -0
- data/lib/ibex/normalize/expression.rb +54 -0
- data/lib/ibex/normalize.rb +166 -0
- data/lib/ibex/runtime/parser.rb +360 -0
- data/lib/ibex/runtime.rb +8 -0
- data/lib/ibex/tables.rb +125 -0
- data/lib/ibex/version.rb +6 -0
- data/lib/ibex.rb +23 -0
- data/sig/ibex/analysis/sets.rbs +72 -0
- data/sig/ibex/analysis.rbs +6 -0
- data/sig/ibex/cli/counterexample_options.rbs +16 -0
- data/sig/ibex/cli/outputs.rbs +31 -0
- data/sig/ibex/cli.rbs +108 -0
- data/sig/ibex/codegen/dot.rbs +19 -0
- data/sig/ibex/codegen/html.rbs +35 -0
- data/sig/ibex/codegen/rbs.rbs +31 -0
- data/sig/ibex/codegen/report.rbs +39 -0
- data/sig/ibex/codegen/ruby.rbs +92 -0
- data/sig/ibex/codegen/symbol_labels.rbs +11 -0
- data/sig/ibex/error.rbs +7 -0
- data/sig/ibex/frontend/action_scanner.rbs +74 -0
- data/sig/ibex/frontend/ast.rbs +223 -0
- data/sig/ibex/frontend/bootstrap_parser.rbs +80 -0
- data/sig/ibex/frontend/dsl.rbs +124 -0
- data/sig/ibex/frontend/generated_parser.rbs +163 -0
- data/sig/ibex/frontend/generated_parser_base.rbs +114 -0
- data/sig/ibex/frontend/lexer.rbs +61 -0
- data/sig/ibex/frontend/parser/declarations.rbs +56 -0
- data/sig/ibex/frontend/parser/rules.rbs +48 -0
- data/sig/ibex/frontend/parser.rbs +16 -0
- data/sig/ibex/frontend/regenerator.rbs +16 -0
- data/sig/ibex/frontend/source_cursor.rbs +73 -0
- data/sig/ibex/frontend/token_adapter/declaration_state.rbs +97 -0
- data/sig/ibex/frontend/token_adapter/delimiter_tracker.rbs +25 -0
- data/sig/ibex/frontend/token_adapter/rule_state.rbs +77 -0
- data/sig/ibex/frontend/token_adapter.rbs +67 -0
- data/sig/ibex/frontend.rbs +15 -0
- data/sig/ibex/ir/automaton_ir.rbs +77 -0
- data/sig/ibex/ir/grammar_ir.rbs +143 -0
- data/sig/ibex/ir/serialize.rbs +50 -0
- data/sig/ibex/ir.rbs +43 -0
- data/sig/ibex/lalr/builder.rbs +85 -0
- data/sig/ibex/lalr/conflict.rbs +47 -0
- data/sig/ibex/lalr/conflict_search.rbs +131 -0
- data/sig/ibex/lalr/conflict_search_limits.rbs +29 -0
- data/sig/ibex/lalr/counterexample.rbs +53 -0
- data/sig/ibex/lalr/default_reductions.rbs +20 -0
- data/sig/ibex/lalr.rbs +23 -0
- data/sig/ibex/normalize/declarations.rbs +20 -0
- data/sig/ibex/normalize/diagnostics.rbs +32 -0
- data/sig/ibex/normalize/expander.rbs +66 -0
- data/sig/ibex/normalize/expression.rbs +21 -0
- data/sig/ibex/normalize.rbs +96 -0
- data/sig/ibex/runtime/parser.rbs +167 -0
- data/sig/ibex/runtime.rbs +6 -0
- data/sig/ibex/tables.rbs +50 -0
- data/sig/ibex/version.rbs +6 -0
- data/sig/ibex.rbs +6 -0
- 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
|