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.
@@ -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