birman 0.2.1 → 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 +214 -0
- data/README.md +405 -15
- data/lib/birman/ast.rb +10 -1
- data/lib/birman/combinators.rb +105 -13
- 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 +151 -29
- data/lib/birman/error_formatter.rb +6 -0
- data/lib/birman/grammar.rb +139 -17
- data/lib/birman/left_recursion.rb +409 -0
- data/lib/birman/matchers.rb +310 -53
- data/lib/birman/nodes.rb +17 -0
- data/lib/birman/result.rb +41 -2
- data/lib/birman/rule_set.rb +38 -6
- data/lib/birman/rules.rb +27 -5
- data/lib/birman/standalone.rb +111 -0
- data/lib/birman/version.rb +1 -1
- data/lib/birman.rb +30 -0
- metadata +11 -1
data/lib/birman/combinators.rb
CHANGED
|
@@ -1,5 +1,10 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
# [file]
|
|
4
|
+
# purpose = "The combinators available inside every rule body"
|
|
5
|
+
# responsibility = "Building matchers: raw strings and patterns, literals, sequences, choices, lists, wrappers"
|
|
6
|
+
# dependencies = ["Birman::Matcher", "matchers.rb (Seq, Choice, Pick, SepBy, Infix, Lexeme)"]
|
|
7
|
+
# pattern = "Parser combinators (PEG)"
|
|
3
8
|
module Birman
|
|
4
9
|
# Marker for seq: everything after it is committed (see Matcher#>).
|
|
5
10
|
COMMIT = Object.new.tap { |o| def o.inspect = "Birman::COMMIT" }.freeze
|
|
@@ -15,36 +20,78 @@ module Birman
|
|
|
15
20
|
# a rule body, after >> or |) is a literal: it skips trailing spacing
|
|
16
21
|
# when the grammar declares `ignore`, and word-like ones are keywords.
|
|
17
22
|
# Use str and match for exact, raw matching.
|
|
23
|
+
# [module.Combinators]
|
|
24
|
+
# purpose = "Matcher-building methods mixed into every grammar (via Birman::Grammar)"
|
|
25
|
+
# input = "Strings (literals), Regexps (raw patterns) and matchers"
|
|
26
|
+
# output = "Immutable matcher objects; nothing is matched until Grammar#parse"
|
|
27
|
+
# notes = "Method names here are reserved: a rule can't share one"
|
|
18
28
|
module Combinators
|
|
19
29
|
# --- Raw matchers ---
|
|
20
30
|
|
|
21
|
-
|
|
31
|
+
# [method.str]
|
|
32
|
+
# params = ["s: String"]
|
|
33
|
+
# returns = "Matcher that matches exactly s; never skips spacing, never a keyword"
|
|
34
|
+
# value = "s"
|
|
35
|
+
def str(s) = Str.new(s)
|
|
36
|
+
|
|
37
|
+
# [method.match]
|
|
38
|
+
# params = ["re: Regexp (anchored at the current position; \\A, ^ and lookbehind see the whole input)"]
|
|
39
|
+
# returns = "Matcher for the regexp; raw, nothing skipped"
|
|
40
|
+
# value = "the matched text"
|
|
22
41
|
def match(re) = Pat.new(re)
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
42
|
+
|
|
43
|
+
# [method.any]
|
|
44
|
+
# returns = "Matcher for any one character, newlines included"
|
|
45
|
+
def any = Pat.new(/./m)
|
|
46
|
+
|
|
47
|
+
# [method.eof]
|
|
48
|
+
# returns = "Matcher that succeeds only at the end of the input, consuming nothing"
|
|
49
|
+
def eof = any.absent.label("end of input")
|
|
50
|
+
|
|
51
|
+
# [method.never]
|
|
52
|
+
# returns = "Matcher that always fails (a placeholder for an empty choice)"
|
|
53
|
+
def never = Pat.new(/(?!)/)
|
|
26
54
|
|
|
27
55
|
# A whole word: "if" matches in "if x" but not in "iffy".
|
|
28
|
-
|
|
56
|
+
# [method.keyword]
|
|
57
|
+
# params = ["word: String"]
|
|
58
|
+
# returns = "Matcher for word when not followed by a word character"
|
|
59
|
+
# value = "word"
|
|
60
|
+
def keyword(word) = Pick.new([str(word), match(/\w/).absent], 0).label(word.inspect)
|
|
29
61
|
|
|
30
62
|
# --- Literals and the readable forms ---
|
|
31
63
|
|
|
32
64
|
# Anything usable as a matcher: strings become literals, regexps
|
|
33
65
|
# raw patterns, and matchers pass through unchanged.
|
|
66
|
+
# [method.lit]
|
|
67
|
+
# params = ["x: String | Regexp | Matcher"]
|
|
68
|
+
# returns = "A matcher: String -> literal, Regexp -> raw pattern, matcher unchanged"
|
|
69
|
+
# raises = ["ArgumentError (anything else, or only(...) outside seq)"]
|
|
34
70
|
def lit(x) = Birman.coerce(x)
|
|
35
71
|
|
|
36
72
|
# The matcher a literal stands for: "let" is a keyword, "=" an exact
|
|
37
|
-
# string, either way followed by the
|
|
38
|
-
# once per grammar instance.
|
|
73
|
+
# string, either way followed by the ignore rule of the grammar
|
|
74
|
+
# running it. Built once per grammar instance.
|
|
75
|
+
# [method.literal_matcher]
|
|
76
|
+
# params = ["text: String"]
|
|
77
|
+
# returns = "The matcher a literal stands for: keyword or exact string, then the ignore rule"
|
|
78
|
+
# side_effects = ["Caches the matcher on the grammar instance"]
|
|
79
|
+
# notes = "Called by Lit at match time; rarely needed directly"
|
|
39
80
|
def literal_matcher(text)
|
|
40
81
|
(@literal_matchers ||= {})[text] ||= begin
|
|
41
82
|
base = text.match?(/\A\w+\z/) ? keyword(text) : str(text)
|
|
42
|
-
Lexeme.new(base,
|
|
83
|
+
Lexeme.new(base, text)
|
|
43
84
|
end
|
|
44
85
|
end
|
|
45
86
|
|
|
46
87
|
# Sequence. Put `commit` where the construct becomes unambiguous:
|
|
47
88
|
# seq("let", commit, name.as(:name), "=", expr.as(:value))
|
|
89
|
+
# [method.seq]
|
|
90
|
+
# params = ["*items: String | Regexp | Matcher | commit | only(...)"]
|
|
91
|
+
# returns = "Matcher for the items in order"
|
|
92
|
+
# value = "Array of item values; a Hash of the .as-named parts if any; the only(...) part's value if used"
|
|
93
|
+
# raises = ["ArgumentError (commit first; more than one only; only mixed with .as names)",
|
|
94
|
+
# "ArgumentError (a name used twice; a nested seq's names the value would drop)"]
|
|
48
95
|
def seq(*items)
|
|
49
96
|
commit_at = items.index(COMMIT)
|
|
50
97
|
raise ArgumentError, "commit can't come first in a seq: nothing has matched yet" if commit_at&.zero?
|
|
@@ -54,38 +101,76 @@ module Birman
|
|
|
54
101
|
Seq.new(parts.map { _1.is_a?(Only) ? _1.matcher : lit(_1) }, commit_at, only_at)
|
|
55
102
|
end
|
|
56
103
|
|
|
104
|
+
# [method.commit]
|
|
105
|
+
# returns = "Marker for seq: if anything after it fails, the whole parse fails there"
|
|
106
|
+
# notes = "Lookaheads contain a commit failure; rep and maybe don't"
|
|
57
107
|
def commit = COMMIT
|
|
58
108
|
|
|
59
109
|
# Inside seq, makes this part the seq's whole value; the rest still
|
|
60
110
|
# has to match but is dropped:
|
|
61
111
|
# seq("[", commit, only(sep_by(value, ",")), "]") # => [...]
|
|
112
|
+
# [method.only]
|
|
113
|
+
# params = ["x: String | Regexp | Matcher"]
|
|
114
|
+
# returns = "Marker for seq making x's value the seq's whole value"
|
|
115
|
+
# notes = "x's .as name, if any, passes to the enclosing seq"
|
|
62
116
|
def only(x) = Only.new(lit(x))
|
|
63
117
|
|
|
64
118
|
# Ordered choice: the first alternative that matches wins.
|
|
119
|
+
# [method.either]
|
|
120
|
+
# params = ["*alts: String | Regexp | Matcher"]
|
|
121
|
+
# returns = "Ordered choice: the first alternative that matches wins"
|
|
122
|
+
# value = "The winning alternative's value (a name all the alternatives share passes outward)"
|
|
65
123
|
def either(*alts) = alts.size == 1 ? lit(alts.first) : Choice.new(alts.map { lit(_1) })
|
|
66
124
|
|
|
67
125
|
# One of several literals. Given a hash, each literal produces its
|
|
68
126
|
# value: one_of("true" => true, "null" => nil). They're tried in
|
|
69
127
|
# order, so put longer ones first ("<=" before "<").
|
|
128
|
+
# [method.one_of]
|
|
129
|
+
# params = ["*choices: String literals", "or one Hash of literal => value"]
|
|
130
|
+
# returns = "Ordered choice of the literals"
|
|
131
|
+
# value = "The matched literal, or its Hash value"
|
|
70
132
|
def one_of(*choices)
|
|
71
133
|
return either(*choices) unless choices in [Hash => table]
|
|
72
134
|
|
|
73
|
-
either(*table.map { |text, value| lit(text).
|
|
135
|
+
either(*table.map { |text, value| lit(text).means(value) })
|
|
74
136
|
end
|
|
75
137
|
|
|
76
138
|
# Left-associative binary operators, folded into nodes:
|
|
77
139
|
# infix(product, "+", "-", node: BinOp) # BinOp = node(:op, :left, :right)
|
|
140
|
+
# [method.infix]
|
|
141
|
+
# params = ["operand: Matcher", "*ops: String literals", "node: node class with op, left, right fields"]
|
|
142
|
+
# returns = "Matcher for operand (op operand)*, folded left-associatively"
|
|
143
|
+
# value = "operand's value, or nested node instances with spans"
|
|
144
|
+
# raises = ["ArgumentError at parse time if node lacks op/left/right"]
|
|
78
145
|
def infix(operand, *ops, node:) = Infix.new(Birman.coerce(operand), either(*ops).label("operator"), node)
|
|
79
146
|
|
|
80
147
|
# One or more items separated by sep; value is an array of items.
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
#
|
|
84
|
-
|
|
148
|
+
# With trailing: true, one separator may follow the last item:
|
|
149
|
+
# seq("[", only(sep_by(value, ",", trailing: true)), "]") # [1, 2,]
|
|
150
|
+
# [method.sep_by1]
|
|
151
|
+
# params = ["item: String | Regexp | Matcher", "sep: String | Regexp | Matcher", "trailing: Boolean = false"]
|
|
152
|
+
# returns = "Matcher for one or more items separated by sep (and, if trailing, maybe one more sep)"
|
|
153
|
+
# value = "Array of item values (item's .as name passes outward)"
|
|
154
|
+
def sep_by1(item, sep, trailing: false) = SepBy.new(lit(item), lit(sep), 1, trailing)
|
|
155
|
+
|
|
156
|
+
# Zero or more items separated by sep. A trailing separator needs an
|
|
157
|
+
# item before it: "," alone isn't an empty list.
|
|
158
|
+
# [method.sep_by]
|
|
159
|
+
# params = ["item: String | Regexp | Matcher", "sep: String | Regexp | Matcher", "trailing: Boolean = false"]
|
|
160
|
+
# returns = "Matcher for zero or more items separated by sep (and, if trailing, maybe one more sep)"
|
|
161
|
+
# value = "Array of item values, [] when there are none"
|
|
162
|
+
def sep_by(item, sep, trailing: false) = SepBy.new(lit(item), lit(sep), 0, trailing)
|
|
85
163
|
|
|
86
164
|
# inner wrapped in open/close; value is inner's.
|
|
165
|
+
# [method.between]
|
|
166
|
+
# params = ["open: String | Matcher", "close: String | Matcher", "inner: Matcher"]
|
|
167
|
+
# returns = "Matcher for open inner close"
|
|
168
|
+
# value = "inner's value (inner's .as name passes outward)"
|
|
87
169
|
def between(open, close, inner) = Pick.new([lit(open), Birman.coerce(inner), lit(close)], 1)
|
|
88
170
|
|
|
171
|
+
# [method.parens]
|
|
172
|
+
# params = ["inner: Matcher"]
|
|
173
|
+
# returns = "between(\"(\", \")\", inner)"
|
|
89
174
|
def parens(inner) = between("(", ")", inner)
|
|
90
175
|
|
|
91
176
|
private
|
|
@@ -99,4 +184,11 @@ module Birman
|
|
|
99
184
|
|
|
100
185
|
def ignore_rule = self.class.respond_to?(:ignore) ? self.class.ignore : nil
|
|
101
186
|
end
|
|
187
|
+
|
|
188
|
+
# Birman.seq, Birman.str and the rest, for building matchers outside a
|
|
189
|
+
# grammar (see Birman.parse). literal_matcher stays private: it's what
|
|
190
|
+
# a grammar runs for a literal, and Birman isn't one.
|
|
191
|
+
extend Combinators
|
|
192
|
+
|
|
193
|
+
singleton_class.send(:private, :literal_matcher)
|
|
102
194
|
end
|
|
@@ -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
|