birman 0.2.2 → 0.3.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 +4 -4
- data/CHANGELOG.md +204 -0
- data/README.md +381 -14
- data/lib/birman/ast.rb +10 -1
- data/lib/birman/combinators.rb +25 -14
- data/lib/birman/compiler/actions.rb +309 -0
- data/lib/birman/compiler/emitter.rb +940 -0
- data/lib/birman/compiler/literals.rb +29 -0
- data/lib/birman/compiler/nodes.rb +158 -0
- data/lib/birman/compiler/regexps.rb +182 -0
- data/lib/birman/compiler/runtime.rb +758 -0
- data/lib/birman/compiler/types.rb +128 -0
- data/lib/birman/compiler.rb +155 -0
- data/lib/birman/context.rb +124 -15
- data/lib/birman/grammar.rb +93 -16
- data/lib/birman/left_recursion.rb +409 -0
- data/lib/birman/matchers.rb +224 -38
- data/lib/birman/result.rb +10 -1
- data/lib/birman/rule_set.rb +19 -6
- data/lib/birman/rules.rb +10 -5
- data/lib/birman/standalone.rb +111 -0
- data/lib/birman/version.rb +1 -1
- data/lib/birman.rb +22 -0
- metadata +11 -1
|
@@ -0,0 +1,309 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Birman
|
|
4
|
+
class Compiler
|
|
5
|
+
# Copies the grammar methods that .action(:name) and .check(:name) call into the
|
|
6
|
+
# generated Parser, read from their source with Prism, together with
|
|
7
|
+
# the grammar methods they call and the constants they read (as
|
|
8
|
+
# literals, looked up lexically from where the method was written).
|
|
9
|
+
#
|
|
10
|
+
# Copied as written, so Integer(), String#to_i, #hex and #oct, which
|
|
11
|
+
# in a Spinel binary raise past 63 bits (or give 2**63 - 1, when the
|
|
12
|
+
# receiver's type isn't known), are refused (see Overflows):
|
|
13
|
+
# large_integer (Grammar's, and the generated parser's) is the way to
|
|
14
|
+
# read a number.
|
|
15
|
+
# [class.Compiler.Actions]
|
|
16
|
+
# purpose = "Copies .action and .check methods, their helpers and constants, into the generated parser"
|
|
17
|
+
# dependencies = ["prism (bundled from Ruby 3.3; a gem on 3.2), loaded on first use"]
|
|
18
|
+
class Actions
|
|
19
|
+
# Methods of the generated Parser and its runtime base class that a
|
|
20
|
+
# copied method must not replace.
|
|
21
|
+
RESERVED = /\A(?:rule_|ref_|lit_|init_|skip_|str_)/
|
|
22
|
+
RUNTIME_METHODS = %i[
|
|
23
|
+
initialize input parse reset skip_ignored fail_at merge commit_failure ignored_after token_end span_end
|
|
24
|
+
span failure failure_because invalid_input label_end check_end peek_end bytes_at? abandon left_recursion
|
|
25
|
+
large_integer
|
|
26
|
+
].freeze
|
|
27
|
+
|
|
28
|
+
# Methods the generated parser's runtime provides as Birman::Grammar
|
|
29
|
+
# does, which copied methods may call and actions may name.
|
|
30
|
+
PROVIDED = %i[large_integer].freeze
|
|
31
|
+
|
|
32
|
+
# The constant paths copied methods may read: Float::INFINITY and
|
|
33
|
+
# Float::NAN, which have no literal.
|
|
34
|
+
FLOAT_CONSTANTS = %i[INFINITY NAN].freeze
|
|
35
|
+
|
|
36
|
+
def initialize(grammar)
|
|
37
|
+
@grammar = grammar
|
|
38
|
+
@methods = {}
|
|
39
|
+
@constants = CopiedConstants.new
|
|
40
|
+
@trees = {}
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
# Copies grammar method name (and what it needs), once.
|
|
44
|
+
# form: "action" or "check", for the error if there's no such method.
|
|
45
|
+
def copy(name, rule, form = "action")
|
|
46
|
+
return if @methods.key?(name) || provided?(name)
|
|
47
|
+
|
|
48
|
+
@methods[name] = nil # reserved first: methods can call each other
|
|
49
|
+
@methods[name] = method_source(find(name, rule, form), rule)
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def any? = !@methods.empty?
|
|
53
|
+
|
|
54
|
+
def constants = @constants.sources
|
|
55
|
+
|
|
56
|
+
def constant_names = @constants.names
|
|
57
|
+
|
|
58
|
+
def methods = @methods.values
|
|
59
|
+
|
|
60
|
+
def names = @methods.keys
|
|
61
|
+
|
|
62
|
+
private
|
|
63
|
+
|
|
64
|
+
def find(name, rule, form)
|
|
65
|
+
unless @grammar.respond_to?(name, true)
|
|
66
|
+
raise Error, "rule :#{rule} uses .#{form}(:#{name}), but the grammar has no method #{name}"
|
|
67
|
+
end
|
|
68
|
+
if RESERVED.match?(name) || RUNTIME_METHODS.include?(name)
|
|
69
|
+
raise Error, "the grammar method #{name} would clash with the generated parser's own #{name}"
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
method = @grammar.method(name)
|
|
73
|
+
file, line = method.source_location
|
|
74
|
+
raise Error, "rule :#{rule}: can't find the source of #{name}" unless file && File.exist?(file)
|
|
75
|
+
|
|
76
|
+
MethodFinder.new(name, line).find(tree(file)) ||
|
|
77
|
+
raise(Error, "rule :#{rule}: #{name} isn't written with def, so it can't be copied")
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
def tree(file) = @trees[file] ||= MethodFinder.parse(file)
|
|
81
|
+
|
|
82
|
+
def method_source(found, rule)
|
|
83
|
+
node, scopes = found
|
|
84
|
+
walk(node) { visit(_1, scopes, rule) }
|
|
85
|
+
MethodFinder.source(node)
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
def visit(node, scopes, rule)
|
|
89
|
+
case node
|
|
90
|
+
when Prism::ConstantReadNode then @constants.read(node.name, scopes, rule)
|
|
91
|
+
when Prism::ConstantPathNode then constant_path(node, rule)
|
|
92
|
+
when Prism::SuperNode, Prism::ForwardingSuperNode
|
|
93
|
+
raise Error, "rule :#{rule}: copied methods can't call super"
|
|
94
|
+
when Prism::BlockArgumentNode then Overflows.check_block(node, rule)
|
|
95
|
+
when Prism::CallNode then call(node, rule)
|
|
96
|
+
end
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
# Float::INFINITY and Float::NAN, which the generated parser reads
|
|
100
|
+
# as written (its Float is checked like any core class's).
|
|
101
|
+
def constant_path(node, rule)
|
|
102
|
+
float = node.parent.is_a?(Prism::ConstantReadNode) && node.parent.name == :Float
|
|
103
|
+
return if float && FLOAT_CONSTANTS.include?(node.name)
|
|
104
|
+
|
|
105
|
+
raise Error, "rule :#{rule}: copied methods can't read #{node.slice}; use a constant of the grammar"
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
def call(node, rule)
|
|
109
|
+
if Overflows::DISPATCH.include?(node.name) then dispatch(node, rule)
|
|
110
|
+
elsif self_call?(node) then own_call(node.name, rule)
|
|
111
|
+
else Overflows.check_call(node, rule)
|
|
112
|
+
end
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
def self_call?(node) = node.receiver.nil? || node.receiver.is_a?(Prism::SelfNode)
|
|
116
|
+
|
|
117
|
+
# A call on self (written or not): a grammar method is copied too;
|
|
118
|
+
# Birman's own API (rules, keywords...) isn't there to call, and
|
|
119
|
+
# Kernel's Integer overflows.
|
|
120
|
+
def own_call(name, rule)
|
|
121
|
+
return if provided?(name)
|
|
122
|
+
raise Error, "rule :#{rule}: copied methods can't call the grammar's #{name}" if birman_api?(name)
|
|
123
|
+
return copy(name, rule) if grammar_method?(name)
|
|
124
|
+
|
|
125
|
+
Overflows.check(name, rule)
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
# send(:name), method(:name)...: checked as a call of name would be.
|
|
129
|
+
def dispatch(node, rule)
|
|
130
|
+
name = Overflows.dispatched(node, rule)
|
|
131
|
+
self_call?(node) ? own_call(name, rule) : Overflows.check(name, rule)
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
# Birman::Grammar's own, not one the grammar defines over it.
|
|
135
|
+
def provided?(name)
|
|
136
|
+
PROVIDED.include?(name) && @grammar.method(name).owner == Birman::Grammar
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
# A method of the grammar's, or of a rule module's (Rules::Strings'
|
|
140
|
+
# decode_escape): not Ruby's, and not part of Birman's grammar API.
|
|
141
|
+
def grammar_method?(name)
|
|
142
|
+
return false unless @grammar.respond_to?(name, true)
|
|
143
|
+
|
|
144
|
+
![Kernel, Object, BasicObject].include?(@grammar.method(name).owner)
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
# Rules (which return matchers) and the methods Birman::Grammar and
|
|
148
|
+
# Combinators give every grammar.
|
|
149
|
+
def birman_api?(name)
|
|
150
|
+
return false unless @grammar.respond_to?(name, true)
|
|
151
|
+
return true if @grammar.respond_to?(:"__rule_#{name}", true)
|
|
152
|
+
|
|
153
|
+
owner = @grammar.method(name).owner
|
|
154
|
+
[Birman::Grammar, Birman::Combinators].include?(owner)
|
|
155
|
+
end
|
|
156
|
+
|
|
157
|
+
def walk(node, &)
|
|
158
|
+
yield node
|
|
159
|
+
node.compact_child_nodes.each { walk(_1, &) }
|
|
160
|
+
end
|
|
161
|
+
end
|
|
162
|
+
|
|
163
|
+
# Methods that raise past 63 bits in a Spinel binary (or, on a receiver
|
|
164
|
+
# Spinel can't type, silently give 2**63 - 1), refused in copied
|
|
165
|
+
# methods, by call or by name (map(&:to_i), send(:hex), method(:Integer)).
|
|
166
|
+
# Whether a receiver is a String can't be told, so it's taken to be one;
|
|
167
|
+
# only to_i on an Integer literal is let through. A net for the common
|
|
168
|
+
# forms, not a proof: Float(t).round overflows too, maybe silently.
|
|
169
|
+
module Overflows
|
|
170
|
+
INTEGER = "Integer()/to_i overflow past 63 bits in a Spinel binary; use large_integer(...)"
|
|
171
|
+
MESSAGES = {
|
|
172
|
+
Integer: INTEGER, to_i: INTEGER,
|
|
173
|
+
hex: "hex overflows past 63 bits in a Spinel binary; use large_integer(text, 16)",
|
|
174
|
+
oct: "oct overflows past 63 bits in a Spinel binary; use large_integer(text, 8)"
|
|
175
|
+
}.freeze
|
|
176
|
+
|
|
177
|
+
def self.check(name, rule)
|
|
178
|
+
message = MESSAGES[name]
|
|
179
|
+
raise Error, "rule :#{rule}: #{message}" if message
|
|
180
|
+
end
|
|
181
|
+
|
|
182
|
+
def self.check_call(node, rule)
|
|
183
|
+
check(node.name, rule) unless node.name == :to_i && node.receiver.is_a?(Prism::IntegerNode)
|
|
184
|
+
end
|
|
185
|
+
|
|
186
|
+
# map(&:to_i) calls to_i on each element.
|
|
187
|
+
def self.check_block(node, rule)
|
|
188
|
+
check(node.expression.unescaped.to_sym, rule) if node.expression.is_a?(Prism::SymbolNode)
|
|
189
|
+
end
|
|
190
|
+
|
|
191
|
+
# Methods that call a method named by their first argument.
|
|
192
|
+
DISPATCH = %i[send public_send __send__ method public_method].freeze
|
|
193
|
+
|
|
194
|
+
# The method a DISPATCH call names, which must be written as a
|
|
195
|
+
# symbol to be checked.
|
|
196
|
+
def self.dispatched(node, rule)
|
|
197
|
+
target = node.arguments&.arguments&.first
|
|
198
|
+
return target.unescaped.to_sym if target.is_a?(Prism::SymbolNode)
|
|
199
|
+
|
|
200
|
+
raise Error, "rule :#{rule}: copied methods can't #{node.name} a computed name (#{node.slice}); " \
|
|
201
|
+
"call the method by name"
|
|
202
|
+
end
|
|
203
|
+
end
|
|
204
|
+
|
|
205
|
+
# The constants copied methods read, each written as a literal. Looked
|
|
206
|
+
# up lexically from where the method was written; core classes are
|
|
207
|
+
# left as they are, and anything else is refused.
|
|
208
|
+
class CopiedConstants
|
|
209
|
+
def initialize
|
|
210
|
+
@constants = {}
|
|
211
|
+
end
|
|
212
|
+
|
|
213
|
+
def sources = @constants.map { |name, (_owner, source)| "#{name} = #{source}" }
|
|
214
|
+
|
|
215
|
+
def names = @constants.keys.map(&:to_s)
|
|
216
|
+
|
|
217
|
+
# Classes Ruby (or the runtime) provides, which copied code may name.
|
|
218
|
+
CORE_CLASSES = %i[
|
|
219
|
+
Integer Float String Symbol Array Hash Regexp Comparable Math Kernel Encoding NilClass
|
|
220
|
+
StandardError ArgumentError KeyError RangeError TypeError FloatDomainError
|
|
221
|
+
].freeze
|
|
222
|
+
|
|
223
|
+
def read(name, scopes, rule)
|
|
224
|
+
owner = scopes.find { _1.const_defined?(name) } or
|
|
225
|
+
raise Error, "rule :#{rule}: copied methods read #{name}, which can't be found"
|
|
226
|
+
value = owner.const_get(name)
|
|
227
|
+
return core_class(name, value, rule) if value.is_a?(Module)
|
|
228
|
+
|
|
229
|
+
check_constant(name, owner, value, rule)
|
|
230
|
+
@constants[name] = [owner, Literals.source(value)]
|
|
231
|
+
end
|
|
232
|
+
|
|
233
|
+
def core_class(name, value, rule)
|
|
234
|
+
return if CORE_CLASSES.include?(name) && Object.const_get(name).equal?(value)
|
|
235
|
+
|
|
236
|
+
raise Error, "rule :#{rule}: copied methods use #{value.inspect}, which the generated parser won't have"
|
|
237
|
+
end
|
|
238
|
+
|
|
239
|
+
def check_constant(name, owner, value, rule)
|
|
240
|
+
if RESERVED_CONSTANTS.include?(name.to_s)
|
|
241
|
+
raise Error, "rule :#{rule}: copied methods read #{owner}::#{name}, a name the generated parser needs"
|
|
242
|
+
end
|
|
243
|
+
unless Literals.literal?(value)
|
|
244
|
+
raise Error, "rule :#{rule}: #{name} must be a literal (strings, numbers, symbols, arrays, hashes)"
|
|
245
|
+
end
|
|
246
|
+
|
|
247
|
+
seen, = @constants[name]
|
|
248
|
+
return if seen.nil? || seen == owner
|
|
249
|
+
|
|
250
|
+
raise Error, "copied methods read both #{seen}::#{name} and #{owner}::#{name}; rename one"
|
|
251
|
+
end
|
|
252
|
+
end
|
|
253
|
+
|
|
254
|
+
# The def of name at line, with the modules it was written in
|
|
255
|
+
# (innermost first, then Object), for looking up its constants.
|
|
256
|
+
class MethodFinder
|
|
257
|
+
def initialize(name, line)
|
|
258
|
+
@name = name
|
|
259
|
+
@line = line
|
|
260
|
+
end
|
|
261
|
+
|
|
262
|
+
# file's syntax tree, read with Prism.
|
|
263
|
+
def self.parse(file)
|
|
264
|
+
load_prism
|
|
265
|
+
Prism.parse_file(file).value
|
|
266
|
+
end
|
|
267
|
+
|
|
268
|
+
def self.load_prism
|
|
269
|
+
require "prism"
|
|
270
|
+
rescue LoadError
|
|
271
|
+
raise Error, "copying .action and .check methods and node initializers needs the prism gem " \
|
|
272
|
+
"(bundled from Ruby 3.3): add it to your Gemfile"
|
|
273
|
+
end
|
|
274
|
+
|
|
275
|
+
# The def's source, its lines indented as if written at the left
|
|
276
|
+
# margin.
|
|
277
|
+
def self.source(node)
|
|
278
|
+
"#{reindent(node.slice, node.location.start_column)}\n"
|
|
279
|
+
end
|
|
280
|
+
|
|
281
|
+
# The method's later lines are indented for where it was written.
|
|
282
|
+
def self.reindent(source, column)
|
|
283
|
+
first, *rest = source.lines
|
|
284
|
+
[first, *rest.map { _1.sub(/\A {0,#{column}}/, "") }].join
|
|
285
|
+
end
|
|
286
|
+
|
|
287
|
+
def find(node, nesting = [])
|
|
288
|
+
case node
|
|
289
|
+
when Prism::DefNode
|
|
290
|
+
return [node, scopes(nesting)] if node.name == @name && node.location.start_line == @line
|
|
291
|
+
when Prism::ClassNode, Prism::ModuleNode
|
|
292
|
+
nesting = [*nesting, node.constant_path.slice]
|
|
293
|
+
end
|
|
294
|
+
node.compact_child_nodes.each do |child|
|
|
295
|
+
found = find(child, nesting)
|
|
296
|
+
return found if found
|
|
297
|
+
end
|
|
298
|
+
nil
|
|
299
|
+
end
|
|
300
|
+
|
|
301
|
+
private
|
|
302
|
+
|
|
303
|
+
def scopes(nesting)
|
|
304
|
+
paths = nesting.each_index.map { nesting[0.._1].join("::") }.reverse
|
|
305
|
+
[*paths.filter_map { Object.const_get(_1) if Object.const_defined?(_1) }, Object]
|
|
306
|
+
end
|
|
307
|
+
end
|
|
308
|
+
end
|
|
309
|
+
end
|