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/grammar.rb
CHANGED
|
@@ -1,5 +1,10 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
# [file]
|
|
4
|
+
# purpose = "Makes a class (or a module of shared rules) a grammar"
|
|
5
|
+
# responsibility = "Settings, parse entry points, rule lookup and introspection"
|
|
6
|
+
# dependencies = ["Birman::Combinators", "Birman::RuleSet", "Birman::Nodes", "Birman::Context"]
|
|
7
|
+
# pattern = "Mixin with class-level DSL"
|
|
3
8
|
module Birman
|
|
4
9
|
# Include this to make a class a grammar. The class body also gets
|
|
5
10
|
# `node` (see Nodes), so AST nodes can live beside their rules.
|
|
@@ -7,6 +12,10 @@ module Birman
|
|
|
7
12
|
# Including it in a module makes a module of shared rules: whatever
|
|
8
13
|
# includes that module (a class, or another such module) becomes a
|
|
9
14
|
# grammar too.
|
|
15
|
+
# [module.Grammar]
|
|
16
|
+
# purpose = "include this in a class to make it a grammar"
|
|
17
|
+
# provides = ["rule / lexeme (RuleSet)", "node (Nodes)", "combinators", "settings and parse"]
|
|
18
|
+
# collaborators = ["Context", "Success", "Failure", "ErrorFormatter"]
|
|
10
19
|
module Grammar
|
|
11
20
|
include Combinators
|
|
12
21
|
|
|
@@ -26,18 +35,30 @@ module Birman
|
|
|
26
35
|
|
|
27
36
|
# Settings are inherited by subclasses. Passing nil clears an
|
|
28
37
|
# inherited one: `ignore nil`, `keywords nil`.
|
|
38
|
+
# [module.Grammar.ClassMethods]
|
|
39
|
+
# purpose = "Settings and parse entry points on a grammar class"
|
|
40
|
+
# notes = "Settings are inherited by subclasses; pass nil to clear one"
|
|
29
41
|
module ClassMethods
|
|
30
42
|
UNSET = Object.new.freeze
|
|
31
43
|
|
|
32
44
|
# The rule a parse starts from.
|
|
45
|
+
# [method.root]
|
|
46
|
+
# params = ["name: Symbol | nil (omit to read)"]
|
|
47
|
+
# returns = "The start rule's name"
|
|
33
48
|
def root(name = UNSET) = setting(:root, name)
|
|
34
49
|
|
|
35
50
|
# A rule the parser matches and discards after every literal and
|
|
36
51
|
# lexeme, and at the start of input. Usually :spacing (from
|
|
37
52
|
# Birman::Rules::Whitespace or Comments).
|
|
53
|
+
# [method.ignore]
|
|
54
|
+
# params = ["name: Symbol | nil (omit to read)"]
|
|
55
|
+
# returns = "The ignore rule's name (matched and discarded after literals and lexemes)"
|
|
38
56
|
def ignore(name = UNSET) = setting(:ignore, name)
|
|
39
57
|
|
|
40
58
|
# Reserved words, which Birman::Rules::Identifiers won't accept.
|
|
59
|
+
# [method.keywords]
|
|
60
|
+
# params = ["*words: String (none to read; nil to clear)"]
|
|
61
|
+
# returns = "Array of reserved words (Rules::Identifiers rejects them as names)"
|
|
41
62
|
def keywords(*words)
|
|
42
63
|
return setting(:keywords) || [] if words.empty?
|
|
43
64
|
|
|
@@ -47,10 +68,40 @@ module Birman
|
|
|
47
68
|
# These share one instance, so each rule's matcher is built once
|
|
48
69
|
# rather than on every parse. Defining a rule or changing a
|
|
49
70
|
# setting starts a fresh one.
|
|
50
|
-
|
|
71
|
+
# [method."Grammar.parse"]
|
|
72
|
+
# params = ["input: String"]
|
|
73
|
+
# returns = "Success(value) or Failure; see Grammar#parse"
|
|
74
|
+
# notes = "Uses one cached instance; defining a rule or changing a setting resets it"
|
|
75
|
+
def parse(input) = instance.parse(input)
|
|
76
|
+
|
|
77
|
+
# [method."Grammar.parse!"]
|
|
78
|
+
# params = ["input: String", "formatter: #call(failure) = ErrorFormatter.new"]
|
|
79
|
+
# returns = "The parsed value"
|
|
80
|
+
# raises = ["Birman::ParseError"]
|
|
51
81
|
def parse!(input, **) = instance.parse!(input, **)
|
|
52
|
-
|
|
53
|
-
|
|
82
|
+
|
|
83
|
+
# [method."Grammar.to_ast"]
|
|
84
|
+
# returns = "Hash of rule name => AST, for every rule reachable from root and ignore"
|
|
85
|
+
def to_ast = instance.to_ast
|
|
86
|
+
|
|
87
|
+
# [method."Grammar.to_ebnf"]
|
|
88
|
+
# returns = "String: the grammar in PEG-flavored EBNF"
|
|
89
|
+
def to_ebnf = EBNF.call(to_ast)
|
|
90
|
+
|
|
91
|
+
# A standalone parser for this grammar, as Ruby source: the same
|
|
92
|
+
# results as parse, from code Spinel can compile to a native binary.
|
|
93
|
+
# [method."Grammar.to_ruby"]
|
|
94
|
+
# params = [
|
|
95
|
+
# "namespace: String, the module the parser is defined in (\"CalcParser\")",
|
|
96
|
+
# "lookahead_depth: Integer = 56, how deeply lookaheads may nest (see Compiler)"
|
|
97
|
+
# ]
|
|
98
|
+
# returns = "String: Ruby source defining <namespace>::Parser (new(input).parse => Success or Failure)"
|
|
99
|
+
# raises = [
|
|
100
|
+
# "ArgumentError for left-recursive cycles with no rule in common, or a cycle that can't grow, as parse",
|
|
101
|
+
# "Birman::Compiler::Error for what can't be compiled (.map blocks, custom matchers...)",
|
|
102
|
+
# "Birman::Compiler::Error for actions calling Integer() or to_i: use large_integer"
|
|
103
|
+
# ]
|
|
104
|
+
def to_ruby(namespace:, **) = Compiler.new(self, namespace:, **).to_ruby
|
|
54
105
|
|
|
55
106
|
private
|
|
56
107
|
|
|
@@ -71,21 +122,31 @@ module Birman
|
|
|
71
122
|
def keywords = self.class.keywords
|
|
72
123
|
|
|
73
124
|
# Returns Success(value) or Failure(...). Never raises on bad input.
|
|
125
|
+
# [method.parse]
|
|
126
|
+
# params = ["input: String"]
|
|
127
|
+
# returns = "Birman::Success(value) when all of input parses, else Birman::Failure"
|
|
128
|
+
# raises = [
|
|
129
|
+
# "ArgumentError on the first parse, whatever the input, for no root rule",
|
|
130
|
+
# "ArgumentError on the first parse for left-recursive cycles with no rule in common",
|
|
131
|
+
# "ArgumentError on the first parse for a left-recursive cycle with no base case, or a base case first",
|
|
132
|
+
# "ArgumentError on the first parse for a reachable rule whose matcher can't be built (unnamed captures...)",
|
|
133
|
+
# "ArgumentError for an undefined rule, or left recursion through a custom matcher, when the parse reaches it"
|
|
134
|
+
# ]
|
|
135
|
+
# notes = "Bad input never raises: invalid UTF-8 and too-deep nesting are Failures with reason set"
|
|
74
136
|
def parse(input)
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
137
|
+
left_recursion
|
|
138
|
+
if (bad = Birman.invalid_byte(input))
|
|
139
|
+
return build_failure(input, bad, [], reason: "invalid #{input.encoding}")
|
|
140
|
+
end
|
|
79
141
|
|
|
80
|
-
|
|
81
|
-
build_failure(input, ctx.furthest, ctx.expected)
|
|
82
|
-
rescue Committed => e
|
|
83
|
-
build_failure(input, e.pos, e.expected, committed: true)
|
|
84
|
-
rescue SystemStackError
|
|
85
|
-
build_failure(input, ctx.position, [], reason: "input is nested too deeply")
|
|
142
|
+
__match_all(input)
|
|
86
143
|
end
|
|
87
144
|
|
|
88
145
|
# Returns the value or raises ParseError, worded by formatter.
|
|
146
|
+
# [method."parse!"]
|
|
147
|
+
# params = ["input: String", "formatter: #call(failure) = ErrorFormatter.new"]
|
|
148
|
+
# returns = "The parsed value"
|
|
149
|
+
# raises = ["Birman::ParseError (message from formatter)"]
|
|
89
150
|
def parse!(input, formatter: ErrorFormatter.new) = parse(input).value!(formatter)
|
|
90
151
|
|
|
91
152
|
def matcher_for(name)
|
|
@@ -99,19 +160,80 @@ module Birman
|
|
|
99
160
|
|
|
100
161
|
# { rule_name => ast } for every rule reachable from the root (and
|
|
101
162
|
# the ignore rule), in first-use order.
|
|
102
|
-
def to_ast
|
|
163
|
+
def to_ast = __reachable_asts { matcher_for(_1).to_ast }
|
|
164
|
+
|
|
165
|
+
# The grammar's left-call analysis (see LeftRecursion), built on the
|
|
166
|
+
# first parse or to_ruby. It raises then, and every time after, if
|
|
167
|
+
# the grammar has left-recursive cycles with no rule in common, or a
|
|
168
|
+
# cycle that can't grow (see LeftRecursion#check!); later parses only
|
|
169
|
+
# read the ivar. Context reads its leaders, to grow them.
|
|
170
|
+
def left_recursion = @left_recursion ||= LeftRecursion.new(__analyzable_asts, ignore: ignore_rule).check!
|
|
171
|
+
|
|
172
|
+
private
|
|
173
|
+
|
|
174
|
+
def __match_all(input)
|
|
175
|
+
ctx = Context.new(input, self, ignore_rule)
|
|
176
|
+
result = Ref.new(root_rule).match(ctx, ctx.start)
|
|
177
|
+
return Success.new(result.value) if result && result.pos == input.bytesize
|
|
178
|
+
|
|
179
|
+
ctx.fail_at(result.pos, "end of input") if result
|
|
180
|
+
build_failure(input, ctx.furthest, ctx.expected)
|
|
181
|
+
rescue Committed => e
|
|
182
|
+
build_failure(input, e.pos, e.expected, committed: true)
|
|
183
|
+
rescue SystemStackError
|
|
184
|
+
build_failure(input, ctx.position, [], reason: "input is nested too deeply")
|
|
185
|
+
end
|
|
186
|
+
|
|
187
|
+
# As to_ast, leaving out what it can't describe: undefined rules
|
|
188
|
+
# (the parse reports them if it gets there) and rules using a
|
|
189
|
+
# matcher with no to_ast. The analysis takes those to consume input
|
|
190
|
+
# and to call nothing, so a cycle through one is left to
|
|
191
|
+
# Context#memoize, and rules only it refers to aren't analyzed.
|
|
192
|
+
def __analyzable_asts
|
|
193
|
+
__reachable_asts do |name|
|
|
194
|
+
next unless respond_to?(:"__rule_#{name}", true)
|
|
195
|
+
|
|
196
|
+
matcher_for(name).to_ast
|
|
197
|
+
rescue NoMethodError => e
|
|
198
|
+
raise unless e.name == :to_ast && e.receiver.is_a?(Matcher)
|
|
199
|
+
end
|
|
200
|
+
end
|
|
201
|
+
|
|
202
|
+
# { rule_name => yield(rule_name) } for every rule reachable from the
|
|
203
|
+
# root and the ignore rule, in first-use order, skipping nil results.
|
|
204
|
+
def __reachable_asts
|
|
103
205
|
rules = {}
|
|
104
206
|
queue = [root_rule, ignore_rule].compact
|
|
105
207
|
while (name = queue.shift)
|
|
106
208
|
next if rules.key?(name)
|
|
107
209
|
|
|
108
|
-
rules[name] =
|
|
210
|
+
rules[name] = yield(name)
|
|
109
211
|
queue.concat(AST.refs(rules[name]))
|
|
110
212
|
end
|
|
111
|
-
rules
|
|
213
|
+
rules.compact
|
|
112
214
|
end
|
|
113
215
|
|
|
114
|
-
|
|
216
|
+
# Kernel#Integer(value, base), for actions: use it instead of Integer
|
|
217
|
+
# or String#to_i in a grammar you'll generate a parser from (see
|
|
218
|
+
# to_ruby). Past 63 bits, a Spinel binary's Integer and to_i raise or
|
|
219
|
+
# give 2**63 - 1; the generated parser's large_integer gives Bignums,
|
|
220
|
+
# as Ruby's does. Bases: Integers, or Floats truncated (Rational and
|
|
221
|
+
# NaN bases differ from Kernel#Integer in a generated parser). A base
|
|
222
|
+
# that isn't a number raises TypeError, as in Ruby 3.3+; Ruby 3.2's
|
|
223
|
+
# Integer ignores it.
|
|
224
|
+
# [method.large_integer]
|
|
225
|
+
# params = ["value: String or Numeric", "base: Integer = 0 (from the prefix, as Integer's)"]
|
|
226
|
+
# notes = "In a Spinel binary too, Floats past 63 bits convert exactly; negative bases raise there"
|
|
227
|
+
# returns = "Integer(value, base)"
|
|
228
|
+
# raises = ["ArgumentError or TypeError, as Kernel#Integer in Ruby 3.3+"]
|
|
229
|
+
def large_integer(value, base = 0)
|
|
230
|
+
raise TypeError, "no implicit conversion from nil to integer" if base.nil?
|
|
231
|
+
unless base.respond_to?(:to_int)
|
|
232
|
+
raise TypeError, "no implicit conversion of #{[true, false].include?(base) ? base : base.class} into Integer"
|
|
233
|
+
end
|
|
234
|
+
|
|
235
|
+
Integer(value, base)
|
|
236
|
+
end
|
|
115
237
|
|
|
116
238
|
def build_failure(input, byte_pos, expected, committed: false, reason: nil)
|
|
117
239
|
Failure.new(input:, pos: Birman.char_offset(input, byte_pos), expected: expected.uniq, committed:, reason:)
|
|
@@ -0,0 +1,409 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Birman
|
|
4
|
+
# Finds left recursion from a grammar's ASTs ({ rule_name => ast },
|
|
5
|
+
# as Grammar#to_ast returns), without parsing anything: a rule that
|
|
6
|
+
# can call itself, directly or through other rules, before consuming
|
|
7
|
+
# input would loop forever on some input. Grammar#left_recursion runs
|
|
8
|
+
# it on the first parse or to_ruby, and keeps it once it passes.
|
|
9
|
+
# Left recursion is supported, direct (a rule that calls itself) or
|
|
10
|
+
# indirect (through other rules), the way pegen does it: each cycle
|
|
11
|
+
# has a leader, a rule that every way round it passes through, and
|
|
12
|
+
# Context#memoize grows the leader's match from a seed. A cycle with
|
|
13
|
+
# no such rule, two cycles that share none, raises in check!.
|
|
14
|
+
class LeftRecursion
|
|
15
|
+
FLAGS = { "i" => Regexp::IGNORECASE, "x" => Regexp::EXTENDED, "m" => Regexp::MULTILINE,
|
|
16
|
+
"n" => Regexp::NOENCODING }.freeze
|
|
17
|
+
|
|
18
|
+
# Each cycle's rules (a strongly connected component of the left-call
|
|
19
|
+
# graph), in the order rules has them: first-use order, from
|
|
20
|
+
# Grammar#to_ast.
|
|
21
|
+
attr_reader :cycles
|
|
22
|
+
|
|
23
|
+
# { rule => its cycle's leader } for every rule in a cycle that has
|
|
24
|
+
# one. The leader is the first of its cycle's rules that every way
|
|
25
|
+
# round the cycle passes through: take it out and the rest can't
|
|
26
|
+
# reach each other in a loop. Only it grows; see Context#memoize.
|
|
27
|
+
attr_reader :leaders
|
|
28
|
+
|
|
29
|
+
# ignore: the grammar's ignore rule, which a lexeme calls after its
|
|
30
|
+
# token; at its start, if the token was empty.
|
|
31
|
+
def initialize(rules, ignore: nil)
|
|
32
|
+
@rules = rules
|
|
33
|
+
@ignore = ignore
|
|
34
|
+
@patterns = {}
|
|
35
|
+
@nullable = nullable_rules
|
|
36
|
+
@calls = rules.to_h { |name, ast| [name, calls(ast).uniq] }
|
|
37
|
+
@cycles = strongly_connected.select { cyclic?(_1) }
|
|
38
|
+
@cycle_of = @cycles.each_with_object({}) { |cycle, h| cycle.each { h[_1] = cycle } }
|
|
39
|
+
@leaders = @cycles.each_with_object({}) do |cycle, h|
|
|
40
|
+
leader = cycle.find { acyclic?(cycle - [_1]) } or next
|
|
41
|
+
cycle.each { h[_1] = leader }
|
|
42
|
+
end.freeze
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
# Raises ArgumentError naming the cycles of every component with no
|
|
46
|
+
# leader, or for the first cycle that could never grow (see
|
|
47
|
+
# check_growth!); returns self otherwise.
|
|
48
|
+
def check!
|
|
49
|
+
leaderless = cycles.reject { @leaders.key?(_1.first) }
|
|
50
|
+
unless leaderless.empty?
|
|
51
|
+
loops = leaderless.flat_map { cycles_with_no_rule_in_common(_1) }
|
|
52
|
+
names = loops.map { _1.map(&:inspect).join(" -> ") }.join("; ")
|
|
53
|
+
raise ArgumentError, "left recursion through cycles with no rule in common isn't supported: #{names} " \
|
|
54
|
+
"(each rule can call the next without consuming input)"
|
|
55
|
+
end
|
|
56
|
+
cycles.each { check_growth!(_1) }
|
|
57
|
+
self
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
# Can the rule match without consuming input? Undefined rules can't.
|
|
61
|
+
def nullable?(name) = @nullable.key?(name)
|
|
62
|
+
|
|
63
|
+
# The rules name can call at the position it starts at, before it
|
|
64
|
+
# consumes anything, in the order its AST mentions them.
|
|
65
|
+
def left_calls(name) = @calls.fetch(name, [])
|
|
66
|
+
|
|
67
|
+
def left_recursive?(name) = @cycle_of.key?(name)
|
|
68
|
+
def cycle(name) = @cycle_of[name]
|
|
69
|
+
def leader(name) = @leaders[name]
|
|
70
|
+
|
|
71
|
+
# Can node match without consuming input? Every node type gets an
|
|
72
|
+
# answer; anything else (a custom matcher's own AST) is assumed to
|
|
73
|
+
# consume, which can only hide left recursion, never invent it.
|
|
74
|
+
def nullable_node?(node) # rubocop:disable Metrics
|
|
75
|
+
case node
|
|
76
|
+
in [:str, s] then s.empty? # a literal's ignored spacing comes after the literal
|
|
77
|
+
in [:pat, source] then nullable_pattern?(source)
|
|
78
|
+
in [:ref, name] then nullable?(name)
|
|
79
|
+
in [:seq, _, parts] then parts.all? { nullable_node?(_1) }
|
|
80
|
+
in [:choice, alts] then alts.any? { nullable_node?(_1) }
|
|
81
|
+
in [:rep, min, _, inner] then min.zero? || nullable_node?(inner)
|
|
82
|
+
in [:not, _] | [:and, _] then true
|
|
83
|
+
in [:both, _, right] then nullable_node?(right) # the guard consumes nothing
|
|
84
|
+
in [:action, inner] then nullable_node?(inner)
|
|
85
|
+
in [:quiet, inner] then nullable_node?(inner)
|
|
86
|
+
in [:label, _, inner] then nullable_node?(inner)
|
|
87
|
+
in [:check, _, inner] then nullable_node?(inner) # it may reject, but it can match empty
|
|
88
|
+
in [:lexeme, inner] then nullable_node?(inner)
|
|
89
|
+
else false
|
|
90
|
+
end
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
private
|
|
94
|
+
|
|
95
|
+
# Least fixpoint: start with no rule nullable and add rules until
|
|
96
|
+
# nothing changes, so a rule that only calls itself stays non-nullable.
|
|
97
|
+
def nullable_rules
|
|
98
|
+
@nullable = {}
|
|
99
|
+
loop do
|
|
100
|
+
grown = @rules.reject { |name, ast| @nullable.key?(name) || !nullable_node?(ast) }
|
|
101
|
+
return @nullable if grown.empty?
|
|
102
|
+
|
|
103
|
+
grown.each_key { @nullable[_1] = true }
|
|
104
|
+
end
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
# Rules node calls at its start position. A seq's parts run there
|
|
108
|
+
# until one consumes input; lookaheads count too, since re-entering
|
|
109
|
+
# a rule at the same position loops just the same.
|
|
110
|
+
def calls(node) # rubocop:disable Metrics
|
|
111
|
+
case node
|
|
112
|
+
in [:ref, name] then [name]
|
|
113
|
+
in [:seq, _, parts] then leading(parts).flat_map { calls(_1) }
|
|
114
|
+
in [:choice, alts] then alts.flat_map { calls(_1) }
|
|
115
|
+
in [:both, left, right] then calls(left) + calls(right)
|
|
116
|
+
in [:rep, _, 0, _] then [] # at most none: inner never runs
|
|
117
|
+
in [:rep, _, _, inner] then calls(inner)
|
|
118
|
+
in [:not, inner] then calls(inner)
|
|
119
|
+
in [:and, inner] then calls(inner)
|
|
120
|
+
in [:action, inner] then calls(inner)
|
|
121
|
+
in [:quiet, inner] then calls(inner)
|
|
122
|
+
in [:label, _, inner] then calls(inner)
|
|
123
|
+
in [:check, _, inner] then calls(inner)
|
|
124
|
+
in [:lexeme, inner] then calls(inner) + (@ignore && nullable_node?(inner) ? [@ignore] : [])
|
|
125
|
+
else []
|
|
126
|
+
end
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
# A cycle grows from a first match that didn't go round it (its
|
|
130
|
+
# seed), so one of its rules needs an alternative that doesn't start
|
|
131
|
+
# with any of them: without one it never matches. And in each of its
|
|
132
|
+
# rules, an alternative like that has to come after the ones that do
|
|
133
|
+
# start with one: tried first, it wins every round, so they never
|
|
134
|
+
# grow. Only each rule's own choice is checked, through the wrappers
|
|
135
|
+
# around it, not choices nested inside its parts.
|
|
136
|
+
def check_growth!(cycle) # rubocop:disable Metrics
|
|
137
|
+
raise ArgumentError, no_base_case(cycle) unless cycle.any? { seedable?(@rules[_1], cycle) }
|
|
138
|
+
|
|
139
|
+
cycle.each do |name|
|
|
140
|
+
alts = alternatives(@rules[name])
|
|
141
|
+
base = alts.index { !enters?(_1, cycle) } or next
|
|
142
|
+
grows = alts.drop(base).find { enters?(_1, cycle) } or next
|
|
143
|
+
first, later = [alts[base], grows].map { EBNF.expr(_1, EBNF::CHOICE) }
|
|
144
|
+
raise ArgumentError, "in left-recursive rule :#{name}, the alternative `#{first}` comes before `#{later}`, " \
|
|
145
|
+
"so the rule can never grow past it; " \
|
|
146
|
+
"put the alternatives that start with #{names(cycle, "or")} first"
|
|
147
|
+
end
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
def no_base_case(cycle)
|
|
151
|
+
if cycle.one?
|
|
152
|
+
name = cycle.first
|
|
153
|
+
"left-recursive rule :#{name} has no alternative that doesn't start with :#{name}, " \
|
|
154
|
+
"so it can never match; add one that doesn't, as a base case"
|
|
155
|
+
else
|
|
156
|
+
"left-recursive rules #{names(cycle, "and")}, which can each reach the others without consuming " \
|
|
157
|
+
"input, have no alternative that doesn't start with one of them, so they can never match; " \
|
|
158
|
+
"add one that doesn't, as a base case"
|
|
159
|
+
end
|
|
160
|
+
end
|
|
161
|
+
|
|
162
|
+
def enters?(node, cycle) = calls(node).intersect?(cycle)
|
|
163
|
+
|
|
164
|
+
# ":a, :b and :c"
|
|
165
|
+
def names(rules, conjunction)
|
|
166
|
+
[rules[0...-1].map(&:inspect).join(", "), rules.last.inspect].reject(&:empty?).join(" #{conjunction} ")
|
|
167
|
+
end
|
|
168
|
+
|
|
169
|
+
# Can node match while its calls to the cycle's rules at its start
|
|
170
|
+
# fail?
|
|
171
|
+
def seedable?(node, cycle) # rubocop:disable Metrics
|
|
172
|
+
case node
|
|
173
|
+
in [:ref, name] then !cycle.include?(name)
|
|
174
|
+
in [:choice, alts] then alts.any? { seedable?(_1, cycle) }
|
|
175
|
+
in [:seq, _, parts] then leading(parts).all? { seedable?(_1, cycle) }
|
|
176
|
+
in [:rep, 0, _, _] | [:rep, _, 0, _] | [:not, _] then true
|
|
177
|
+
in [:rep | :label | :check, *] | [:and | :action | :quiet | :lexeme, _]
|
|
178
|
+
seedable?(node.last, cycle) # a wrapper: its inner node is its last element
|
|
179
|
+
in [:both, left, right] then seedable?(left, cycle) && seedable?(right, cycle)
|
|
180
|
+
else true
|
|
181
|
+
end
|
|
182
|
+
end
|
|
183
|
+
|
|
184
|
+
# The alternatives of node's choice, looking through wrappers; node
|
|
185
|
+
# itself if it isn't one.
|
|
186
|
+
def alternatives(node)
|
|
187
|
+
case node
|
|
188
|
+
in [:choice, alts] then alts
|
|
189
|
+
in [:action | :quiet, _] | [:label | :check, _, _] then alternatives(node.last)
|
|
190
|
+
else [node]
|
|
191
|
+
end
|
|
192
|
+
end
|
|
193
|
+
|
|
194
|
+
# The parts up to and including the first that consumes input.
|
|
195
|
+
def leading(parts) = parts.take((parts.index { !nullable_node?(_1) } || parts.size) + 1)
|
|
196
|
+
|
|
197
|
+
# The regexp is rebuilt from its inspect form and tried on "" with
|
|
198
|
+
# every assertion (see Assertions) taken to hold, since each holds
|
|
199
|
+
# somewhere: /(?=a)/ matches empty before an "a", though not in "".
|
|
200
|
+
# Assertions that contradict each other side by side are caught;
|
|
201
|
+
# other combinations that can't hold, like /(?=a)(?=b)/, still count
|
|
202
|
+
# as nullable. A pattern that can't be rebuilt is assumed to consume.
|
|
203
|
+
def nullable_pattern?(source)
|
|
204
|
+
@patterns.fetch(source) do
|
|
205
|
+
body, flags = %r{\A/(.*)/([a-z]*)\z}m.match(source)&.captures
|
|
206
|
+
@patterns[source] = body ? empty_match?(body, flags.chars.sum { FLAGS.fetch(_1, 0) }) : false
|
|
207
|
+
end
|
|
208
|
+
end
|
|
209
|
+
|
|
210
|
+
# Falls back to the regexp as written when taking out its assertions
|
|
211
|
+
# breaks it, or could make a backreference name another group: taking
|
|
212
|
+
# out (?=(a)) renumbers the groups after it.
|
|
213
|
+
def empty_match?(body, options)
|
|
214
|
+
stripped = Assertions.strip(body, options.anybits?(Regexp::EXTENDED))
|
|
215
|
+
stripped = body if stripped != body && body.match?(/\\(?:[1-9]|k[<'])/)
|
|
216
|
+
[stripped, body].each do |source|
|
|
217
|
+
return Regexp.new(source, options).match?("")
|
|
218
|
+
rescue RegexpError
|
|
219
|
+
next
|
|
220
|
+
end
|
|
221
|
+
false
|
|
222
|
+
end
|
|
223
|
+
|
|
224
|
+
def cyclic?(rules) = rules.size > 1 || left_calls(rules.first).include?(rules.first)
|
|
225
|
+
|
|
226
|
+
# Tarjan's algorithm over the left-call graph. Each component's rules,
|
|
227
|
+
# and the components, come back in rule order.
|
|
228
|
+
def strongly_connected
|
|
229
|
+
@index = {}
|
|
230
|
+
@low = {}
|
|
231
|
+
@stack = []
|
|
232
|
+
@components = []
|
|
233
|
+
@rules.each_key { visit(_1) unless @index.key?(_1) }
|
|
234
|
+
order = @rules.keys.each_with_index.to_h
|
|
235
|
+
@components.map { |c| c.sort_by { order[_1] } }.sort_by { order[_1.first] }
|
|
236
|
+
end
|
|
237
|
+
|
|
238
|
+
def visit(name)
|
|
239
|
+
@index[name] = @low[name] = @index.size
|
|
240
|
+
@stack.push(name)
|
|
241
|
+
left_calls(name).each do |callee|
|
|
242
|
+
next unless @rules.key?(callee)
|
|
243
|
+
|
|
244
|
+
visit(callee) unless @index.key?(callee)
|
|
245
|
+
@low[name] = [@low[name], @low[callee]].min if @stack.include?(callee)
|
|
246
|
+
end
|
|
247
|
+
@components << pop_component(name) if @low[name] == @index[name]
|
|
248
|
+
end
|
|
249
|
+
|
|
250
|
+
def pop_component(name)
|
|
251
|
+
component = []
|
|
252
|
+
component << @stack.pop until component.last == name
|
|
253
|
+
component
|
|
254
|
+
end
|
|
255
|
+
|
|
256
|
+
# Can none of names reach itself through the others' left calls?
|
|
257
|
+
def acyclic?(names)
|
|
258
|
+
done = {}
|
|
259
|
+
names.none? { looping?(_1, names, done, {}) }
|
|
260
|
+
end
|
|
261
|
+
|
|
262
|
+
def looping?(name, names, done, on_path)
|
|
263
|
+
return true if on_path[name]
|
|
264
|
+
return false if done[name]
|
|
265
|
+
|
|
266
|
+
on_path[name] = done[name] = true
|
|
267
|
+
left_calls(name).any? { names.include?(_1) && looping?(_1, names, done, on_path) }
|
|
268
|
+
ensure
|
|
269
|
+
on_path.delete(name)
|
|
270
|
+
end
|
|
271
|
+
|
|
272
|
+
# For a component with no leader, cycles no one rule is in all of
|
|
273
|
+
# (though two of them may share one): for each rule, the first cycle
|
|
274
|
+
# through the others that leaves it out.
|
|
275
|
+
def cycles_with_no_rule_in_common(component)
|
|
276
|
+
order = @rules.keys.each_with_index.to_h
|
|
277
|
+
cycles = component.filter_map do |left_out|
|
|
278
|
+
rest = component - [left_out]
|
|
279
|
+
rest.lazy.filter_map { path(_1, rest) }.first
|
|
280
|
+
end
|
|
281
|
+
cycles.uniq(&:to_set).each_with_index.sort_by { |cycle, i| [order[cycle.first], i] }.map(&:first)
|
|
282
|
+
end
|
|
283
|
+
|
|
284
|
+
# The shortest way round from start back to it through names, through
|
|
285
|
+
# another rule if there is one (a leader may also call itself); nil
|
|
286
|
+
# if there's none.
|
|
287
|
+
def path(start, names) # rubocop:disable Metrics/MethodLength
|
|
288
|
+
from = {}
|
|
289
|
+
queue = [start]
|
|
290
|
+
while (name = queue.shift)
|
|
291
|
+
left_calls(name).each do |callee|
|
|
292
|
+
if callee == start
|
|
293
|
+
next if name == start
|
|
294
|
+
|
|
295
|
+
return [*trail(from, name), start]
|
|
296
|
+
end
|
|
297
|
+
next if !names.include?(callee) || from.key?(callee)
|
|
298
|
+
|
|
299
|
+
from[callee] = name
|
|
300
|
+
queue << callee
|
|
301
|
+
end
|
|
302
|
+
end
|
|
303
|
+
[start, start] if left_calls(start).include?(start)
|
|
304
|
+
end
|
|
305
|
+
|
|
306
|
+
def trail(from, name)
|
|
307
|
+
names = [name]
|
|
308
|
+
names.unshift(from[names.first]) while from.key?(names.first)
|
|
309
|
+
names
|
|
310
|
+
end
|
|
311
|
+
|
|
312
|
+
# A regexp's source with each assertion, which matches only where the
|
|
313
|
+
# text around it allows, replaced by (?:), which matches anywhere:
|
|
314
|
+
# lookarounds, anchors (\A, ^, $, \z, \Z, \G) and \b, \B. Assertions
|
|
315
|
+
# side by side hold at one position, so a pair there that can't both
|
|
316
|
+
# hold, (?=X) with (?!X), (?<=X) with (?<!X), or \b with \B, becomes
|
|
317
|
+
# (?!), which matches nowhere. Only that shape: (?=a)(?=b) can't hold
|
|
318
|
+
# either, but stays (?:)(?:). So does a negative lookaround of what
|
|
319
|
+
# always matches empty, as never's (?!) or (?!a*).
|
|
320
|
+
# Escapes, character classes and comments are copied as they are.
|
|
321
|
+
# Only /x makes # start a comment; an inline (?x) doesn't, so a "[" or
|
|
322
|
+
# "(" in such a comment can hide an assertion after it (it's then
|
|
323
|
+
# taken as written, as before this rewrite).
|
|
324
|
+
module Assertions
|
|
325
|
+
module_function
|
|
326
|
+
|
|
327
|
+
# kind: "=", "!", "<=" or "<!" for a lookaround (body: its source),
|
|
328
|
+
# or the anchor itself ("^", "\\b"...).
|
|
329
|
+
Assertion = Data.define(:kind, :body)
|
|
330
|
+
OPPOSITE = { "=" => "!", "<=" => "<!", "\\b" => "\\B" }.freeze
|
|
331
|
+
|
|
332
|
+
def strip(source, extended)
|
|
333
|
+
scanner = StringScanner.new(source)
|
|
334
|
+
out = +""
|
|
335
|
+
run = []
|
|
336
|
+
until scanner.eos?
|
|
337
|
+
item = piece(scanner, extended)
|
|
338
|
+
next run << item if item.is_a?(Assertion)
|
|
339
|
+
|
|
340
|
+
flush(run, out, extended, quantified: item.match?(/\A[?*+{]/))
|
|
341
|
+
out << item
|
|
342
|
+
end
|
|
343
|
+
flush(run, out, extended, quantified: false)
|
|
344
|
+
out
|
|
345
|
+
end
|
|
346
|
+
|
|
347
|
+
# Writes out a run of assertions side by side. A quantifier after the
|
|
348
|
+
# last one may skip it, so then it can't fail the rest.
|
|
349
|
+
def flush(run, out, extended, quantified:)
|
|
350
|
+
held = quantified ? run[0...-1] : run
|
|
351
|
+
never = held.any? { |a| held.include?(Assertion.new(OPPOSITE[a.kind], a.body)) || never?(a, extended) }
|
|
352
|
+
out << "(?!)" if never
|
|
353
|
+
out << ("(?:)" * run.size)
|
|
354
|
+
run.clear
|
|
355
|
+
end
|
|
356
|
+
|
|
357
|
+
# A negative lookaround of something with no assertions of its own
|
|
358
|
+
# that matches "" never holds: that matches empty everywhere.
|
|
359
|
+
def never?(assertion, extended)
|
|
360
|
+
return false unless %w[! <!].include?(assertion.kind) && strip(assertion.body, extended) == assertion.body
|
|
361
|
+
|
|
362
|
+
Regexp.new(assertion.body, extended ? Regexp::EXTENDED : 0).match?("")
|
|
363
|
+
rescue RegexpError
|
|
364
|
+
false
|
|
365
|
+
end
|
|
366
|
+
|
|
367
|
+
# The next piece of the source: an Assertion, or text to copy.
|
|
368
|
+
def piece(s, extended) # rubocop:disable Metrics
|
|
369
|
+
if s.scan(/\\[bBAzZG]|[\^$]/) then Assertion.new(s.matched, nil)
|
|
370
|
+
elsif s.scan(/\(\?(<?[=!])/) then lookaround(s, s[1], extended)
|
|
371
|
+
elsif s.scan(/\\./m) || s.scan(/\(\?#[^)]*\)/) then s.matched
|
|
372
|
+
elsif s.skip(/\[/) then character_class(s)
|
|
373
|
+
elsif extended && s.scan(/#[^\n]*/) then s.matched
|
|
374
|
+
else s.getch
|
|
375
|
+
end
|
|
376
|
+
end
|
|
377
|
+
|
|
378
|
+
# The rest of a lookaround, after its "(?=" (or "(?!", "(?<=", "(?<!").
|
|
379
|
+
def lookaround(s, kind, extended)
|
|
380
|
+
start = finish = s.pos
|
|
381
|
+
depth = 1
|
|
382
|
+
until depth.zero? || s.eos?
|
|
383
|
+
finish = s.pos
|
|
384
|
+
case piece(s, extended)
|
|
385
|
+
when "(" then depth += 1
|
|
386
|
+
when ")" then depth -= 1
|
|
387
|
+
end
|
|
388
|
+
end
|
|
389
|
+
Assertion.new(kind, s.string.byteslice(start, finish - start))
|
|
390
|
+
end
|
|
391
|
+
|
|
392
|
+
# A character class (nested ones included), after its "[". A "]"
|
|
393
|
+
# right after "[" or "[^" is a character, not the end.
|
|
394
|
+
def character_class(s)
|
|
395
|
+
start = s.pos - 1
|
|
396
|
+
s.skip(/\^?\]?/)
|
|
397
|
+
depth = 1
|
|
398
|
+
while depth.positive? && !s.eos?
|
|
399
|
+
if s.skip(/\\./m) then next
|
|
400
|
+
elsif s.skip(/\[/) then depth += 1
|
|
401
|
+
elsif s.skip(/\]/) then depth -= 1
|
|
402
|
+
else s.getch
|
|
403
|
+
end
|
|
404
|
+
end
|
|
405
|
+
s.string.byteslice(start, s.pos - start)
|
|
406
|
+
end
|
|
407
|
+
end
|
|
408
|
+
end
|
|
409
|
+
end
|