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,29 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Birman
|
|
4
|
+
class Compiler
|
|
5
|
+
# Values written into the generated parser as Ruby literals: the
|
|
6
|
+
# constants copied actions read, and .means values.
|
|
7
|
+
# [module.Compiler.Literals]
|
|
8
|
+
# purpose = "Which values can be written as literals, and how"
|
|
9
|
+
module Literals
|
|
10
|
+
module_function
|
|
11
|
+
|
|
12
|
+
def literal?(value)
|
|
13
|
+
case value
|
|
14
|
+
when String, Symbol, Integer, true, false, nil then true
|
|
15
|
+
when Float then value.finite?
|
|
16
|
+
when Array then value.all? { literal?(_1) }
|
|
17
|
+
when Hash then value.all? { |k, v| literal?(k) && literal?(v) }
|
|
18
|
+
else false
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
# A frozen Array or Hash stays frozen.
|
|
23
|
+
def source(value)
|
|
24
|
+
frozen = value.frozen? && (value.is_a?(Array) || value.is_a?(Hash))
|
|
25
|
+
frozen ? "#{value.inspect}.freeze" : value.inspect
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Birman
|
|
4
|
+
class Compiler
|
|
5
|
+
# The generated counterpart of each node(...) class a grammar builds:
|
|
6
|
+
# a plain class under BirmanRT::Node (so a rule whose value is one of
|
|
7
|
+
# several node classes keeps a single type), with readers, a span,
|
|
8
|
+
# == ignoring the span, and Birman::Node's s-expression inspect. A
|
|
9
|
+
# node's own initialize can give fields defaults (see Initializer),
|
|
10
|
+
# which are written where each node is built.
|
|
11
|
+
# [class.Compiler.Nodes]
|
|
12
|
+
# purpose = "Collects the grammar's node classes and writes their generated versions"
|
|
13
|
+
class Nodes
|
|
14
|
+
def initialize
|
|
15
|
+
@classes = {}
|
|
16
|
+
@defaults = {}
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
# The generated class's name for node, registering it.
|
|
20
|
+
def name_for(node, rule)
|
|
21
|
+
unless node.is_a?(Class) && node.include?(Birman::Node)
|
|
22
|
+
raise Error, "rule :#{rule} builds #{node.inspect}, which isn't a node(...) class"
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
name = node.name&.split("::")&.last or raise Error, "rule :#{rule} builds an anonymous node class"
|
|
26
|
+
check(node, name, rule)
|
|
27
|
+
other = @classes[name]
|
|
28
|
+
raise Error, "#{other.name} and #{node.name} would both be generated as #{name}" if other && other != node
|
|
29
|
+
|
|
30
|
+
@classes[name] = node
|
|
31
|
+
name
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def sources = @classes.map { |name, node| source(name, node.members) }
|
|
35
|
+
|
|
36
|
+
def names = @classes.keys
|
|
37
|
+
|
|
38
|
+
# The source of the default for each field node's initialize gives
|
|
39
|
+
# one, by field.
|
|
40
|
+
def defaults(node) = @defaults.fetch(node, {})
|
|
41
|
+
|
|
42
|
+
private
|
|
43
|
+
|
|
44
|
+
def check(node, name, rule)
|
|
45
|
+
if RESERVED_CONSTANTS.include?(name)
|
|
46
|
+
raise Error, "rule :#{rule} builds #{node.name}, but #{name} is a name the generated parser needs"
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
own = node.instance_methods(false) - node.members + node.private_instance_methods(false)
|
|
50
|
+
return if own.empty?
|
|
51
|
+
return @defaults[node] ||= Initializer.new(node, rule).defaults if own == [:initialize]
|
|
52
|
+
|
|
53
|
+
raise Error, "rule :#{rule} builds #{node.name}, whose methods (#{own.join(", ")}) can't be compiled yet"
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def source(name, fields)
|
|
57
|
+
<<~RUBY
|
|
58
|
+
class #{name} < BirmanRT::Node
|
|
59
|
+
#{"attr_reader #{fields.map(&:inspect).join(", ")}" unless fields.empty?}
|
|
60
|
+
|
|
61
|
+
def initialize(#{[*fields, :span].join(", ")})
|
|
62
|
+
#{[*fields, :span].map { " @#{_1} = #{_1}" }.join("\n")}
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def ==(other)
|
|
66
|
+
#{["other.is_a?(#{name})", *fields.map { "@#{_1} == other.#{_1}" }].join(" && ")}
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def inspect
|
|
70
|
+
"(#{[name, *fields.map { "\#{@#{_1}.inspect}" }].join(" ")})"
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
def dump
|
|
74
|
+
"(#{["#{name}[\#{@span.byte_from},\#{@span.byte_to}]", *fields.map { "\#{BirmanRT.dump(@#{_1})}" }].join(" ")})"
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
RUBY
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
# A node's own initialize, read from its source with Prism. One that
|
|
82
|
+
# takes exactly its fields as keywords, some with literal defaults,
|
|
83
|
+
# and only calls super, as in
|
|
84
|
+
#
|
|
85
|
+
# node(:name, :args) { def initialize(name:, args: []) = super }
|
|
86
|
+
#
|
|
87
|
+
# gives the defaults' sources: they're written where each node is
|
|
88
|
+
# built, so a [] is a new Array every time, as in Ruby. Anything
|
|
89
|
+
# more is refused.
|
|
90
|
+
class Initializer
|
|
91
|
+
def initialize(node, rule)
|
|
92
|
+
@node = node
|
|
93
|
+
@rule = rule
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
def defaults
|
|
97
|
+
found = find
|
|
98
|
+
check_parameters(found.parameters)
|
|
99
|
+
refuse("does more than call super, so it can't be compiled") unless bare_super?(found.body)
|
|
100
|
+
|
|
101
|
+
(found.parameters&.keywords || []).each_with_object({}) do |keyword, defaults|
|
|
102
|
+
next unless keyword.is_a?(Prism::OptionalKeywordParameterNode)
|
|
103
|
+
|
|
104
|
+
default = keyword.value
|
|
105
|
+
unless literal?(default)
|
|
106
|
+
refuse("gives #{keyword.name} a default (#{default.slice}) that isn't a literal " \
|
|
107
|
+
"(nil, true, false, numbers, strings, symbols, and arrays and hashes of them)")
|
|
108
|
+
end
|
|
109
|
+
defaults[keyword.name] = default.slice
|
|
110
|
+
end
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
private
|
|
114
|
+
|
|
115
|
+
def find
|
|
116
|
+
file, line = @node.instance_method(:initialize).source_location
|
|
117
|
+
found = (MethodFinder.new(:initialize, line).find(MethodFinder.parse(file)) if file && File.exist?(file))
|
|
118
|
+
found&.first or refuse("isn't written with def, so it can't be compiled")
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
def check_parameters(parameters)
|
|
122
|
+
keywords = parameters&.keywords || []
|
|
123
|
+
return if others(parameters).empty? && keywords.map(&:name).sort == @node.members.sort
|
|
124
|
+
|
|
125
|
+
refuse("must take exactly its fields (#{@node.members.join(", ")}) as keywords, and nothing else, " \
|
|
126
|
+
"to be compiled")
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
# Every parameter but the keywords.
|
|
130
|
+
def others(parameters)
|
|
131
|
+
return [] unless parameters
|
|
132
|
+
|
|
133
|
+
[*parameters.requireds, *parameters.optionals, parameters.rest, *parameters.posts,
|
|
134
|
+
parameters.keyword_rest, parameters.block].compact
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
def bare_super?(body)
|
|
138
|
+
body.is_a?(Prism::StatementsNode) && body.body.size == 1 &&
|
|
139
|
+
body.body.first.is_a?(Prism::ForwardingSuperNode) && body.body.first.block.nil?
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
def literal?(value)
|
|
143
|
+
case value
|
|
144
|
+
when Prism::NilNode, Prism::TrueNode, Prism::FalseNode, Prism::IntegerNode, Prism::FloatNode,
|
|
145
|
+
Prism::SymbolNode
|
|
146
|
+
true
|
|
147
|
+
when Prism::StringNode then !value.opening&.start_with?("<<") # a heredoc's slice is only its opening
|
|
148
|
+
when Prism::ArrayNode then value.elements.all? { literal?(_1) }
|
|
149
|
+
when Prism::HashNode
|
|
150
|
+
value.elements.all? { _1.is_a?(Prism::AssocNode) && literal?(_1.key) && literal?(_1.value) }
|
|
151
|
+
else false
|
|
152
|
+
end
|
|
153
|
+
end
|
|
154
|
+
|
|
155
|
+
def refuse(problem) = raise(Error, "rule :#{@rule} builds #{@node.name}, whose initialize #{problem}")
|
|
156
|
+
end
|
|
157
|
+
end
|
|
158
|
+
end
|
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Birman
|
|
4
|
+
class Compiler
|
|
5
|
+
# Regexps for StringScanner#skip at the scan position. The interpreter
|
|
6
|
+
# matches with fixed_anchor: true, so \A, ^, lookbehind, and a \b or
|
|
7
|
+
# \B before anything is matched see the input before the position;
|
|
8
|
+
# the generated parser's scanner starts its subject at the position,
|
|
9
|
+
# where they would silently mean something else. Those are refused.
|
|
10
|
+
# Everything else gets a leading \A: in Spinel an unanchored miss
|
|
11
|
+
# searches the rest of the input.
|
|
12
|
+
# [module.Compiler.Regexps]
|
|
13
|
+
# purpose = "Checking and anchoring the grammar's regexps"
|
|
14
|
+
module Regexps
|
|
15
|
+
module_function
|
|
16
|
+
|
|
17
|
+
# The regexp literal to emit for re.
|
|
18
|
+
def anchored(re)
|
|
19
|
+
extended = re.options.anybits?(Regexp::EXTENDED)
|
|
20
|
+
problem = Check.new(re.source, extended).problem
|
|
21
|
+
raise Error, "#{re.inspect} can't be compiled: it uses #{problem}" if problem
|
|
22
|
+
|
|
23
|
+
# With /x, a trailing comment would swallow the closing paren.
|
|
24
|
+
Regexp.new("\\A(?:#{re.source}#{"\n" if extended})", re.options).inspect
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
# Walks a regexp's source, tracking whether something is certain to
|
|
28
|
+
# have been matched (consumed) at each point. A \b or \B is allowed
|
|
29
|
+
# only where it is. Conservative: what it can't follow, it refuses.
|
|
30
|
+
class Check
|
|
31
|
+
# One open group: whether input was consumed when it opened, in
|
|
32
|
+
# every alternative finished so far, and in the current one; and,
|
|
33
|
+
# for spotting (?>x{m,n}), how many atoms it holds and the wide
|
|
34
|
+
# interval among them.
|
|
35
|
+
Frame = Struct.new(:start, :all, :now, :lookaround, :atomic, :atoms, :wide, :alternated)
|
|
36
|
+
|
|
37
|
+
# A {m,n} that lets more than WIDE lengths be tried.
|
|
38
|
+
Wide = Struct.new(:text, :safe)
|
|
39
|
+
WIDE = 16
|
|
40
|
+
|
|
41
|
+
SPINEL_BACKTRACKING = "%s with a lookaround, atomic group, backreference or lazy interval: Spinel's " \
|
|
42
|
+
"regexp engine tries every length it allows (exponential; it raises past about 18). " \
|
|
43
|
+
"Make the repeat atomic on its own, (?>x%s), if nothing after it needs a shorter " \
|
|
44
|
+
"match, or use + with a separate length check"
|
|
45
|
+
|
|
46
|
+
OPTIONAL = /[?*]|\{,|\{0+[,}]/
|
|
47
|
+
QUANTIFIER = /(?:[?*+]|\{\d*,?\d*\})[?+]?/
|
|
48
|
+
|
|
49
|
+
def initialize(source, extended)
|
|
50
|
+
@scanner = StringScanner.new(source)
|
|
51
|
+
@extended = extended
|
|
52
|
+
@frames = [Frame.new(false, true, false, false, false, 0)]
|
|
53
|
+
@backtracks = false
|
|
54
|
+
@wide = []
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def problem
|
|
58
|
+
until @scanner.eos?
|
|
59
|
+
problem = step
|
|
60
|
+
return problem if problem
|
|
61
|
+
end
|
|
62
|
+
spinel_problem
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
private
|
|
66
|
+
|
|
67
|
+
def frame = @frames.last
|
|
68
|
+
|
|
69
|
+
# Spinel runs a regexp on its backtracking engine if it has any of
|
|
70
|
+
# these, and there a wide {m,n} goes exponential when every length
|
|
71
|
+
# fails. Only a group that is (?>x{m,n}) and nothing else is safe.
|
|
72
|
+
def spinel_problem
|
|
73
|
+
wide = @wide.find { !_1.safe }
|
|
74
|
+
format(SPINEL_BACKTRACKING, wide.text, wide.text) if @backtracks && wide
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
def step # rubocop:disable Metrics
|
|
78
|
+
s = @scanner
|
|
79
|
+
if s.skip(/\\A/) then "\\A"
|
|
80
|
+
elsif s.skip(/\\[bB]/) then frame.now ? nil : "\\b or \\B before anything is matched"
|
|
81
|
+
elsif s.skip(/\\[zZG]/) then nil
|
|
82
|
+
elsif s.skip(/\\(?:\d|k<\w+>|k'\w+')/) then backreference
|
|
83
|
+
elsif s.skip(/\\[pPu]\{[^}]*\}|\\./m) then atom
|
|
84
|
+
elsif s.skip(/\(\?<[=!]/) then "lookbehind"
|
|
85
|
+
elsif s.skip(/\(\?#[^)]*\)/) then nil
|
|
86
|
+
elsif s.skip(/\(\?[=!]/) then open_group(lookaround: true)
|
|
87
|
+
elsif s.skip(/\(\?>/) then open_group(lookaround: false, atomic: true)
|
|
88
|
+
elsif s.skip(/\(\?[imx]*(?:-[imx]*)?\)/) then inline_options(s.matched)
|
|
89
|
+
elsif s.skip(/\((?:\?(?:[imx]*(?:-[imx]*)?:|>|<\w+>|'\w+'))?/) then open_group(lookaround: false)
|
|
90
|
+
elsif s.skip(/\|/) then alternative
|
|
91
|
+
elsif s.skip(/\)/) then close
|
|
92
|
+
elsif s.skip(/\^/) then "^"
|
|
93
|
+
elsif s.skip(/\[/) then character_class
|
|
94
|
+
elsif @extended && s.skip(/\s+|#[^\n]*/) then nil
|
|
95
|
+
else
|
|
96
|
+
s.getch
|
|
97
|
+
atom
|
|
98
|
+
end
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
# Something that matches at least one character, unless a
|
|
102
|
+
# quantifier after it makes it optional.
|
|
103
|
+
def atom = quantified(true)
|
|
104
|
+
|
|
105
|
+
def quantified(consumes)
|
|
106
|
+
optional = @scanner.match?(OPTIONAL)
|
|
107
|
+
quantifier(@scanner.scan(QUANTIFIER))
|
|
108
|
+
frame.now ||= consumes && !optional
|
|
109
|
+
nil
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
def backreference
|
|
113
|
+
@backtracks = true
|
|
114
|
+
quantified(false)
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
# Counts the atom just read, and notes its quantifier if it's a
|
|
118
|
+
# wide or lazy interval.
|
|
119
|
+
def quantifier(text)
|
|
120
|
+
frame.atoms += 1
|
|
121
|
+
frame.wide = nil
|
|
122
|
+
bounds = text&.match(/\A\{(\d*),(\d+)\}(\?)?/) or return
|
|
123
|
+
@backtracks ||= !bounds[3].nil?
|
|
124
|
+
return unless bounds[2].to_i - bounds[1].to_i > WIDE
|
|
125
|
+
|
|
126
|
+
@wide << (frame.wide = Wide.new(text, false))
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
def open_group(lookaround:, atomic: false)
|
|
130
|
+
@backtracks ||= lookaround || atomic
|
|
131
|
+
@frames << Frame.new(frame.now, true, frame.now, lookaround, atomic, 0)
|
|
132
|
+
nil
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
def alternative
|
|
136
|
+
frame.all &&= frame.now
|
|
137
|
+
frame.now = frame.start
|
|
138
|
+
frame.alternated = true
|
|
139
|
+
nil
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
def close
|
|
143
|
+
return "an unmatched )" if @frames.size == 1
|
|
144
|
+
|
|
145
|
+
group = @frames.pop
|
|
146
|
+
group.wide.safe = true if atomic_interval?(group)
|
|
147
|
+
consumed = !group.lookaround && group.all && group.now
|
|
148
|
+
optional = @scanner.match?(OPTIONAL)
|
|
149
|
+
quantifier(@scanner.scan(QUANTIFIER))
|
|
150
|
+
frame.now ||= consumed && !optional
|
|
151
|
+
nil
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
# (?>x{m,n}) and nothing else in the group.
|
|
155
|
+
def atomic_interval?(group)
|
|
156
|
+
group.atomic && group.wide && group.atoms == 1 && !group.alternated
|
|
157
|
+
end
|
|
158
|
+
|
|
159
|
+
# (?x) turns on extended mode for the rest (conservatively, even
|
|
160
|
+
# past the end of its group).
|
|
161
|
+
def inline_options(text)
|
|
162
|
+
@extended ||= text[/\A\(\?([imx]*)/, 1].include?("x")
|
|
163
|
+
nil
|
|
164
|
+
end
|
|
165
|
+
|
|
166
|
+
# A character class (nested ones included), after its "[".
|
|
167
|
+
def character_class
|
|
168
|
+
depth = 1
|
|
169
|
+
@scanner.skip(/\^?\]?/)
|
|
170
|
+
while depth.positive? && !@scanner.eos?
|
|
171
|
+
if @scanner.skip(/\\./m) then next
|
|
172
|
+
elsif @scanner.skip(/\[/) then depth += 1
|
|
173
|
+
elsif @scanner.skip(/\]/) then depth -= 1
|
|
174
|
+
else @scanner.getch
|
|
175
|
+
end
|
|
176
|
+
end
|
|
177
|
+
atom
|
|
178
|
+
end
|
|
179
|
+
end
|
|
180
|
+
end
|
|
181
|
+
end
|
|
182
|
+
end
|