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
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Birman
|
|
4
|
+
class Compiler
|
|
5
|
+
# A coarse static type for each matcher's value, so every value
|
|
6
|
+
# instance variable starts as a literal of the type it will hold
|
|
7
|
+
# (Spinel boxes a variable whose writes disagree). :poly means it
|
|
8
|
+
# varies, or isn't known; nil, nodes and :poly all start as nil.
|
|
9
|
+
# [class.Compiler.Types]
|
|
10
|
+
# purpose = "Infers the type a matcher's value has, for initializers"
|
|
11
|
+
class Types
|
|
12
|
+
INITIAL = { str: '""', int: "0", float: "0.0", sym: ':""', array: "[]", hash: "{}" }.freeze
|
|
13
|
+
|
|
14
|
+
SENDS = { to_i: :int, to_f: :float, to_sym: :sym, to_s: :str, join: :str, to_h: :hash,
|
|
15
|
+
upcase: :str, downcase: :str, strip: :str, to_a: :array }.freeze
|
|
16
|
+
|
|
17
|
+
def initialize(grammar)
|
|
18
|
+
@grammar = grammar
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
# The initializer literal for a value of matcher's type.
|
|
22
|
+
def initial(matcher) = INITIAL.fetch(of(matcher), "nil")
|
|
23
|
+
|
|
24
|
+
def of(matcher) # rubocop:disable Metrics
|
|
25
|
+
case matcher
|
|
26
|
+
when Str, Pat then :str
|
|
27
|
+
when Lit then of(@grammar.literal_matcher(matcher.text))
|
|
28
|
+
when Seq then seq(matcher)
|
|
29
|
+
when Pick then of(matcher.parts[matcher.index])
|
|
30
|
+
when SepBy, Rep then :array
|
|
31
|
+
when Captures then :hash
|
|
32
|
+
when Choice then matcher.alts.map { of(_1) }.reduce { |a, b| join(a, b) }
|
|
33
|
+
when Not, And then :nil
|
|
34
|
+
when Both then of(matcher.right)
|
|
35
|
+
when Const then constant(matcher.constant)
|
|
36
|
+
when Send then SENDS.fetch(matcher.method_name, :poly)
|
|
37
|
+
when To then :node
|
|
38
|
+
when Infix then infix(of(matcher.operand))
|
|
39
|
+
when Ref then rules.fetch(matcher.name, :poly)
|
|
40
|
+
when Label, Check, Quiet, Named, Lexeme then of(matcher.inner)
|
|
41
|
+
else :poly
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
# A String, or nil where it's optional: str.maybe.
|
|
46
|
+
def str_or_nil?(matcher, seen = {})
|
|
47
|
+
case matcher
|
|
48
|
+
when Maybe then of(matcher.inner) == :str
|
|
49
|
+
when Label, Check, Quiet, Named, Lexeme then str_or_nil?(matcher.inner, seen)
|
|
50
|
+
when Ref
|
|
51
|
+
!seen.key?(matcher.name) && (seen[matcher.name] = true) &&
|
|
52
|
+
str_or_nil?(@grammar.matcher_for(matcher.name), seen)
|
|
53
|
+
else false
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
private
|
|
58
|
+
|
|
59
|
+
def rules = @rules ||= solve
|
|
60
|
+
|
|
61
|
+
def seq(matcher)
|
|
62
|
+
return of(matcher.parts[matcher.only_at]) if matcher.only_at
|
|
63
|
+
|
|
64
|
+
matcher.parts.any?(&:capture_key) ? :hash : :array
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def constant(value)
|
|
68
|
+
{ String => :str, Integer => :int, Float => :float, Symbol => :sym, NilClass => :nil }
|
|
69
|
+
.fetch(value.class, :poly)
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
# Every rule's type at once, so none depends on which was asked about
|
|
73
|
+
# first: starting from nothing known (nil) for each, a rule's type is
|
|
74
|
+
# widened by what its body gives until nothing changes. Rules refer
|
|
75
|
+
# to each other in cycles (expr -> atom -> expr); one that still
|
|
76
|
+
# gives nothing (it only recurses) is :poly.
|
|
77
|
+
def solve
|
|
78
|
+
names = rule_names
|
|
79
|
+
@rules = names.to_h { [_1, nil] }
|
|
80
|
+
changed = true
|
|
81
|
+
while changed
|
|
82
|
+
changed = false
|
|
83
|
+
names.each do |name|
|
|
84
|
+
type = join(@rules[name], of(@grammar.matcher_for(name)))
|
|
85
|
+
changed ||= type != @rules[name]
|
|
86
|
+
@rules[name] = type
|
|
87
|
+
end
|
|
88
|
+
end
|
|
89
|
+
@rules.transform_values { _1 || :poly }
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
# The rules reachable from the root and the ignore rule.
|
|
93
|
+
def rule_names
|
|
94
|
+
names = []
|
|
95
|
+
queue = [@grammar.class.root, @grammar.class.ignore].compact
|
|
96
|
+
while (name = queue.shift)
|
|
97
|
+
next if names.include?(name)
|
|
98
|
+
|
|
99
|
+
names << name
|
|
100
|
+
queue.concat(refs(@grammar.matcher_for(name)))
|
|
101
|
+
end
|
|
102
|
+
names
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
# Rule names a matcher refers to (custom matchers, which the emitter
|
|
106
|
+
# refuses, refer to none).
|
|
107
|
+
def refs(value)
|
|
108
|
+
case value
|
|
109
|
+
when Ref then [value.name]
|
|
110
|
+
when Lit then refs(@grammar.literal_matcher(value.text))
|
|
111
|
+
when Data then value.to_h.values.flat_map { refs(_1) }
|
|
112
|
+
when Array then value.flat_map { refs(_1) }
|
|
113
|
+
else []
|
|
114
|
+
end
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
# The type of a value that is either: nil is nothing known yet.
|
|
118
|
+
def join(one, other)
|
|
119
|
+
return other if one.nil?
|
|
120
|
+
return one if other.nil? || one == other
|
|
121
|
+
|
|
122
|
+
:poly
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
def infix(operand) = operand.nil? || operand == :node ? operand : :poly
|
|
126
|
+
end
|
|
127
|
+
end
|
|
128
|
+
end
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# [file]
|
|
4
|
+
# purpose = "Turns a grammar into a standalone Ruby parser (Grammar.to_ruby)"
|
|
5
|
+
# responsibility = "Walking the rules' matchers and assembling the generated source"
|
|
6
|
+
# dependencies = ["Birman::Compiler::Emitter", "Birman::Compiler::Actions", "compiler/runtime.rb"]
|
|
7
|
+
# pattern = "Code generation from the immutable matcher tree"
|
|
8
|
+
module Birman
|
|
9
|
+
# Generates Ruby source for a parser that gives exactly the results
|
|
10
|
+
# Grammar.parse gives, values, spans, errors and all, without Birman.
|
|
11
|
+
# The output is written in the Ruby subset Spinel compiles to a native
|
|
12
|
+
# binary: the runtime (compiler/runtime.rb), a class per node, and a
|
|
13
|
+
# Parser with one method per matcher.
|
|
14
|
+
#
|
|
15
|
+
# source = Calc::Grammar.to_ruby(namespace: "CalcParser")
|
|
16
|
+
# CalcParser::Parser.new(input).parse # => Success or Failure
|
|
17
|
+
#
|
|
18
|
+
# Every matcher method takes a byte position and returns where its
|
|
19
|
+
# match ended, or FAIL; its value, if anything reads it, goes in an
|
|
20
|
+
# instance variable. Grammars using .map with a block, or a custom
|
|
21
|
+
# matcher class, can't be compiled: Error names the rule.
|
|
22
|
+
# [class.Compiler]
|
|
23
|
+
# purpose = "Generates a standalone parser from a grammar class"
|
|
24
|
+
# collaborators = ["Emitter (matcher templates)", "Actions (copies .action methods)", "Nodes"]
|
|
25
|
+
class Compiler
|
|
26
|
+
# Raised for a grammar the compiler can't translate faithfully.
|
|
27
|
+
class Error < StandardError; end
|
|
28
|
+
|
|
29
|
+
RUNTIME = File.join(__dir__, "compiler", "runtime.rb")
|
|
30
|
+
|
|
31
|
+
# Constants the generated code and its runtime refer to. A node class
|
|
32
|
+
# or copied constant with one of these names would sit in the same
|
|
33
|
+
# lexical scope and shadow it, so it's refused.
|
|
34
|
+
RESERVED_CONSTANTS = %w[
|
|
35
|
+
BirmanRT Parser Node Span Success Failure Committed TooDeep FAIL IN_PROGRESS ABSENT NONE TEN19 TWO64
|
|
36
|
+
CHUNK_LIMIT FLOAT_CHUNK LOOKAHEAD_DEPTH
|
|
37
|
+
StringScanner Hash Array String Integer Float Symbol Regexp Kernel Encoding
|
|
38
|
+
StandardError ArgumentError RangeError TypeError FloatDomainError SystemStackError
|
|
39
|
+
].freeze
|
|
40
|
+
|
|
41
|
+
# Spinel allows 64 begin blocks active at once and exits the process
|
|
42
|
+
# past that. Each lookahead (absent, present, &) holds one while its
|
|
43
|
+
# inner matcher runs, so nesting deeper than this is a Failure ("input
|
|
44
|
+
# is nested too deeply") instead. A binary built with a bigger
|
|
45
|
+
# -DSP_EXC_STACK_MAX can take a bigger depth.
|
|
46
|
+
LOOKAHEAD_DEPTH = 56
|
|
47
|
+
|
|
48
|
+
def initialize(grammar_class, namespace:, lookahead_depth: LOOKAHEAD_DEPTH)
|
|
49
|
+
unless namespace.match?(/\A[A-Z]\w*(::[A-Z]\w*)*\z/)
|
|
50
|
+
raise ArgumentError, "namespace must be a constant path, like \"CalcParser\""
|
|
51
|
+
end
|
|
52
|
+
unless lookahead_depth.is_a?(Integer) && lookahead_depth.positive?
|
|
53
|
+
raise ArgumentError, "lookahead_depth must be a positive Integer"
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
@lookahead_depth = lookahead_depth
|
|
57
|
+
|
|
58
|
+
@grammar_class = grammar_class
|
|
59
|
+
@grammar = grammar_class.new
|
|
60
|
+
@namespace = namespace
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
# [method."Compiler#to_ruby"]
|
|
64
|
+
# returns = "String: Ruby source defining <namespace>::Parser, its node classes and BirmanRT"
|
|
65
|
+
# raises = [
|
|
66
|
+
# "ArgumentError for left-recursive cycles with no rule in common, or a cycle that can't grow, as Grammar#parse",
|
|
67
|
+
# "Compiler::Error for what can't be compiled (named by rule)"
|
|
68
|
+
# ]
|
|
69
|
+
def to_ruby
|
|
70
|
+
@grammar.left_recursion
|
|
71
|
+
emitter = emit
|
|
72
|
+
check_namespace(emitter.node_names)
|
|
73
|
+
body = [*emitter.node_classes, parser_class(emitter)].join("\n")
|
|
74
|
+
wrap_in_namespace("#{runtime}\n#{body}")
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
private
|
|
78
|
+
|
|
79
|
+
# Values nobody reads (spacing, keywords) are never kept. Which those
|
|
80
|
+
# are is found by compiling: start with only the root's value, and
|
|
81
|
+
# compile again with every rule found to be read, until none is new.
|
|
82
|
+
def emit
|
|
83
|
+
valued = [@grammar_class.root]
|
|
84
|
+
loop do
|
|
85
|
+
emitter = Emitter.new(@grammar, valued)
|
|
86
|
+
emitter.compile
|
|
87
|
+
return emitter if (emitter.read_rules - valued).empty?
|
|
88
|
+
|
|
89
|
+
valued |= emitter.read_rules
|
|
90
|
+
end
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
# Generating into a module that already holds other classes of the
|
|
94
|
+
# same names (the grammar's own nodes, say) would reopen them with a
|
|
95
|
+
# different superclass.
|
|
96
|
+
def check_namespace(node_names)
|
|
97
|
+
return unless Object.const_defined?(@namespace)
|
|
98
|
+
|
|
99
|
+
existing = Object.const_get(@namespace)
|
|
100
|
+
node_names.each do |name|
|
|
101
|
+
next unless existing.is_a?(Module) && existing.const_defined?(name, false)
|
|
102
|
+
|
|
103
|
+
found = existing.const_get(name, false)
|
|
104
|
+
next if found.is_a?(Class) && found.superclass.name == "#{@namespace}::BirmanRT::Node"
|
|
105
|
+
|
|
106
|
+
raise Error, "#{@namespace} already defines #{name}; generate the parser into another namespace"
|
|
107
|
+
end
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
def header
|
|
111
|
+
<<~RUBY
|
|
112
|
+
# Generated by Birman #{VERSION} from #{@grammar_class.name || "an anonymous grammar"}
|
|
113
|
+
# (Grammar.to_ruby). Regenerate it rather than editing it.
|
|
114
|
+
# Parse with #{@namespace}::Parser.new(input).parse, which returns
|
|
115
|
+
# a Success (value) or a Failure (pos, expected, message).
|
|
116
|
+
require "strscan"
|
|
117
|
+
RUBY
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
# The runtime file, after its require.
|
|
121
|
+
def runtime = File.read(RUNTIME).split(/^require "strscan"\n\n/, 2).last
|
|
122
|
+
|
|
123
|
+
def parser_class(emitter)
|
|
124
|
+
<<~RUBY
|
|
125
|
+
class Parser < BirmanRT::Parser
|
|
126
|
+
include BirmanRT
|
|
127
|
+
|
|
128
|
+
# How deeply lookaheads may nest (Grammar.to_ruby's lookahead_depth).
|
|
129
|
+
LOOKAHEAD_DEPTH = #{@lookahead_depth}
|
|
130
|
+
|
|
131
|
+
#{indent(emitter.parser_body, 1)}
|
|
132
|
+
end
|
|
133
|
+
RUBY
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
def wrap_in_namespace(body)
|
|
137
|
+
modules = @namespace.split("::")
|
|
138
|
+
opening = modules.each_with_index.map { |name, depth| indent("module #{name}", depth) }
|
|
139
|
+
closing = modules.each_index.reverse_each.map { |depth| indent("end", depth) }
|
|
140
|
+
[header, *opening, indent(body.chomp, modules.size), *closing].join("\n") << "\n"
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
def indent(text, depth)
|
|
144
|
+
pad = " " * depth
|
|
145
|
+
text.lines.map { |line| line.strip.empty? ? "\n" : "#{pad}#{line}" }.join.chomp
|
|
146
|
+
end
|
|
147
|
+
end
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
require_relative "compiler/literals"
|
|
151
|
+
require_relative "compiler/regexps"
|
|
152
|
+
require_relative "compiler/types"
|
|
153
|
+
require_relative "compiler/actions"
|
|
154
|
+
require_relative "compiler/nodes"
|
|
155
|
+
require_relative "compiler/emitter"
|
data/lib/birman/context.rb
CHANGED
|
@@ -8,23 +8,39 @@ module Birman
|
|
|
8
8
|
# non-ASCII string by character offset costs time proportional to
|
|
9
9
|
# the offset, which made parsing quadratic. Failure and Span convert
|
|
10
10
|
# back to characters for anything public.
|
|
11
|
-
class Context
|
|
11
|
+
class Context # rubocop:disable Metrics/ClassLength
|
|
12
12
|
# Marks a memo entry whose rule is still being matched.
|
|
13
13
|
IN_PROGRESS = Object.new.freeze
|
|
14
14
|
|
|
15
|
+
# The expectations of a frame that hasn't failed anywhere yet. Shared
|
|
16
|
+
# and frozen; fail_at replaces it rather than appending.
|
|
17
|
+
NOTHING_EXPECTED = [].freeze
|
|
18
|
+
|
|
15
19
|
# position: where the most recently entered rule started. When the
|
|
16
20
|
# input nests too deeply for Ruby's stack, that's where it happened.
|
|
17
|
-
|
|
21
|
+
# ignore_ref: a Ref to the grammar's ignore rule, or nil. Lexemes
|
|
22
|
+
# read it here, so the grammar running them decides what they skip.
|
|
23
|
+
attr_reader :input, :grammar, :ignore_ref, :furthest, :expected, :position, :isolated_furthest, :isolated_expected
|
|
18
24
|
|
|
19
|
-
def initialize(input, grammar)
|
|
25
|
+
def initialize(input, grammar, ignore_rule)
|
|
20
26
|
@input = input
|
|
21
27
|
@grammar = grammar
|
|
28
|
+
@ignore_ref = ignore_rule && Ref.new(ignore_rule) # built once per parse, not per token
|
|
22
29
|
@memo = {}
|
|
30
|
+
@growing = grammar.left_recursion.leaders.then { _1 unless _1.empty? } # nil: nothing to look up
|
|
31
|
+
@seeds = {}
|
|
32
|
+
# { leader of other rules => { pos it grows at => this round's memo
|
|
33
|
+
# for the other rules } }
|
|
34
|
+
@rounds = grammar.left_recursion.leaders.values.tally.filter_map { |leader, n| [leader, {}] if n > 1 }.to_h
|
|
23
35
|
@furthest = 0
|
|
24
|
-
@expected =
|
|
36
|
+
@expected = NOTHING_EXPECTED
|
|
25
37
|
@token_ends = {}
|
|
26
38
|
end
|
|
27
39
|
|
|
40
|
+
# Where the parse begins: past whatever the ignore rule (if any)
|
|
41
|
+
# matches at the start of the input.
|
|
42
|
+
def start = @ignore_ref&.match(self, 0)&.pos || 0
|
|
43
|
+
|
|
28
44
|
# A lexeme ended at token_end, then the ignore rule consumed spacing
|
|
29
45
|
# (whitespace, comments) up to pos. Spans use this to stop at the
|
|
30
46
|
# token instead of including what was ignored after it.
|
|
@@ -46,25 +62,40 @@ module Birman
|
|
|
46
62
|
@furthest = pos
|
|
47
63
|
@expected = [what]
|
|
48
64
|
elsif pos == @furthest
|
|
49
|
-
@expected << what
|
|
65
|
+
@expected = @expected.frozen? ? [what] : @expected << what
|
|
50
66
|
end
|
|
51
67
|
nil
|
|
52
68
|
end
|
|
53
69
|
|
|
54
|
-
|
|
70
|
+
# Record a finished frame's expectations (a memo hit, a label, a
|
|
71
|
+
# check) at pos. Ones already recorded there are skipped: a rule
|
|
72
|
+
# reached from several alternatives at one position is merged in
|
|
73
|
+
# once per alternative, and copying its whole list each time grew
|
|
74
|
+
# it exponentially. Failure#expected is uniq'd anyway, so this
|
|
75
|
+
# changes no error, only the work behind it. fail_at itself doesn't
|
|
76
|
+
# check: it runs far more often, and repeats there grow only
|
|
77
|
+
# linearly.
|
|
78
|
+
def merge(pos, expected)
|
|
79
|
+
expected.each { fail_at(pos, _1) unless pos == @furthest && @expected.include?(_1) }
|
|
80
|
+
end
|
|
55
81
|
|
|
56
82
|
# Run the block with fresh failure tracking starting at pos, then
|
|
57
|
-
# restore the outer state
|
|
58
|
-
#
|
|
59
|
-
#
|
|
83
|
+
# restore the outer state, and return the block's result. What the
|
|
84
|
+
# block recorded is left in isolated_furthest and isolated_expected
|
|
85
|
+
# for the caller to keep or drop: Ref memoizes it, Label rewrites
|
|
86
|
+
# it, Not throws it away. No arrays: this runs for every rule call.
|
|
60
87
|
def isolate(pos)
|
|
61
|
-
|
|
88
|
+
outer_furthest = @furthest
|
|
89
|
+
outer_expected = @expected
|
|
62
90
|
@furthest = pos
|
|
63
|
-
@expected =
|
|
91
|
+
@expected = NOTHING_EXPECTED
|
|
64
92
|
result = yield
|
|
65
|
-
|
|
93
|
+
@isolated_furthest = @furthest
|
|
94
|
+
@isolated_expected = @expected
|
|
95
|
+
result
|
|
66
96
|
ensure
|
|
67
|
-
@furthest
|
|
97
|
+
@furthest = outer_furthest
|
|
98
|
+
@expected = outer_expected
|
|
68
99
|
end
|
|
69
100
|
|
|
70
101
|
def commit_failure! = raise(Committed.new(@furthest, @expected.dup))
|
|
@@ -74,9 +105,12 @@ module Birman
|
|
|
74
105
|
# moves past, so they're dropped. If it fails, that's recorded at
|
|
75
106
|
# pos, as what it expected there or as the lookahead itself.
|
|
76
107
|
def peek?(matcher, pos)
|
|
77
|
-
result
|
|
108
|
+
result = isolate(pos) { lookahead { matcher.match(self, pos) } }
|
|
78
109
|
return true if result
|
|
79
110
|
|
|
111
|
+
furthest = isolated_furthest
|
|
112
|
+
expected = isolated_expected
|
|
113
|
+
|
|
80
114
|
if furthest == pos && expected.any?
|
|
81
115
|
merge(pos, expected)
|
|
82
116
|
else
|
|
@@ -86,38 +120,126 @@ module Birman
|
|
|
86
120
|
end
|
|
87
121
|
|
|
88
122
|
# Rule name's memo entry at pos, computed by the block the first
|
|
89
|
-
# time.
|
|
90
|
-
#
|
|
91
|
-
#
|
|
92
|
-
#
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
123
|
+
# time. The block returns [result, furthest, expected].
|
|
124
|
+
#
|
|
125
|
+
# A cycle of rules that call each other before consuming input (left
|
|
126
|
+
# recursion) is grown from a seed at its leader (see
|
|
127
|
+
# LeftRecursion#leaders and grow). While the leader grows at pos, the
|
|
128
|
+
# cycle's other rules are memoized there in a table of the round's
|
|
129
|
+
# own, not the memo: what they match there depends on the seed, which
|
|
130
|
+
# changes every round. (A match of theirs already in the memo there is
|
|
131
|
+
# final: it was made after the leader finished, or without it.)
|
|
132
|
+
# Every way back to a rule of the
|
|
133
|
+
# cycle at pos passes through the leader's seed, so meeting any rule
|
|
134
|
+
# again at the same position while it is still being matched means
|
|
135
|
+
# it reaches itself without consuming input in a way the grammar's
|
|
136
|
+
# ASTs don't show (through a custom matcher): that would never
|
|
137
|
+
# terminate, and it's a bug in the grammar, not the input, so it
|
|
138
|
+
# raises.
|
|
139
|
+
def memoize(name, pos, &) # rubocop:disable Metrics
|
|
140
|
+
table = @memo[name] ||= {}
|
|
141
|
+
entry = table[pos]
|
|
96
142
|
return entry if entry && !entry.equal?(IN_PROGRESS)
|
|
97
|
-
|
|
143
|
+
|
|
144
|
+
leader = @growing&.[](name) # nil without left recursion: no further lookups
|
|
145
|
+
round = @rounds[leader]&.[](pos) if leader
|
|
146
|
+
return in_round(round, name, pos, &) if round && !leader.equal?(name)
|
|
147
|
+
return seed(name, pos) if entry
|
|
98
148
|
|
|
99
149
|
@position = pos
|
|
100
|
-
|
|
150
|
+
return grow(name, table, pos, &) if leader.equal?(name)
|
|
151
|
+
|
|
152
|
+
compute(table, pos, &)
|
|
101
153
|
end
|
|
102
154
|
|
|
103
155
|
# Run a lookahead's inner match. A lookahead only asks whether
|
|
104
156
|
# something would match here, so a commit failure inside it is an
|
|
105
|
-
# ordinary failure (nil) rather than the end of the parse.
|
|
157
|
+
# ordinary failure (nil) rather than the end of the parse. What it
|
|
158
|
+
# expected is kept, as generated parsers keep it: the frames it
|
|
159
|
+
# unwound through restored what they started with, so without this a
|
|
160
|
+
# failure where the lookahead began would only be named, not said.
|
|
106
161
|
def lookahead
|
|
107
162
|
yield
|
|
108
|
-
rescue Committed
|
|
163
|
+
rescue Committed => e
|
|
164
|
+
@furthest = e.pos
|
|
165
|
+
@expected = e.expected
|
|
109
166
|
nil
|
|
110
167
|
end
|
|
111
168
|
|
|
112
169
|
private
|
|
113
170
|
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
171
|
+
# The memo is a Hash per rule, keyed by position, so no key arrays.
|
|
172
|
+
# (A round's table is keyed by rule instead.)
|
|
173
|
+
def compute(table, key)
|
|
174
|
+
table[key] = IN_PROGRESS
|
|
175
|
+
table[key] = yield
|
|
117
176
|
ensure
|
|
118
177
|
# A commit failure caught by a lookahead unwinds through here; the
|
|
119
178
|
# rule may be tried again at this position afterwards.
|
|
120
|
-
|
|
179
|
+
table.delete(key) if table[key].equal?(IN_PROGRESS)
|
|
180
|
+
end
|
|
181
|
+
|
|
182
|
+
# A rule of a cycle whose leader grows at pos, memoized in the round's
|
|
183
|
+
# table (keyed by rule): once a round, however many alternatives call
|
|
184
|
+
# it, or a cycle of n rules with k alternatives each would take
|
|
185
|
+
# k**(n - 1) matches a round. Back at itself without the leader's seed
|
|
186
|
+
# between, it raises as memoize does.
|
|
187
|
+
def in_round(round, name, pos, &)
|
|
188
|
+
entry = round[name]
|
|
189
|
+
return entry if entry && !entry.equal?(IN_PROGRESS)
|
|
190
|
+
return seed(name, pos) if entry
|
|
191
|
+
|
|
192
|
+
@position = pos
|
|
193
|
+
compute(round, name, &)
|
|
194
|
+
end
|
|
195
|
+
|
|
196
|
+
# Seed-growing (Warth et al. 2008), at a cycle's leader only, as pegen
|
|
197
|
+
# does. The rule is matched with its call to itself here failing,
|
|
198
|
+
# then again with that call returning the last match (the seed), for
|
|
199
|
+
# as long as the match gets further. It's a loop, so a longer chain
|
|
200
|
+
# uses no more stack. The memo entry stays IN_PROGRESS until the end;
|
|
201
|
+
# every way back to it passes through its seed. A leader of other
|
|
202
|
+
# rules starts each round with an empty table for them at pos.
|
|
203
|
+
#
|
|
204
|
+
# Each attempt's diagnostics are kept, not just the best one's: the
|
|
205
|
+
# last attempt, which got no further, is usually the one that knows
|
|
206
|
+
# what was missing ("1 +" expected a number after the "+").
|
|
207
|
+
def grow(name, table, pos) # rubocop:disable Metrics
|
|
208
|
+
seeds = @seeds[name] ||= {}
|
|
209
|
+
seeds[pos] = best = [nil, pos, NOTHING_EXPECTED]
|
|
210
|
+
table[pos] = IN_PROGRESS
|
|
211
|
+
rounds = @rounds[name]
|
|
212
|
+
diagnostics = [pos, NOTHING_EXPECTED]
|
|
213
|
+
# Not loop: its block adds frames to every level of nesting.
|
|
214
|
+
while true # rubocop:disable Style/InfiniteLoop
|
|
215
|
+
rounds[pos] = {} if rounds
|
|
216
|
+
result, furthest, expected = yield
|
|
217
|
+
diagnostics = further(diagnostics, furthest, expected)
|
|
218
|
+
break unless result && result.pos > (best[0]&.pos || -1)
|
|
219
|
+
|
|
220
|
+
seeds[pos] = best = [result, pos, NOTHING_EXPECTED]
|
|
221
|
+
end
|
|
222
|
+
table[pos] = [best[0], *diagnostics]
|
|
223
|
+
ensure
|
|
224
|
+
seeds&.delete(pos) # nil if the stack ran out before it was set
|
|
225
|
+
rounds&.delete(pos)
|
|
226
|
+
table.delete(pos) if table[pos].equal?(IN_PROGRESS)
|
|
227
|
+
end
|
|
228
|
+
|
|
229
|
+
# Two frames' diagnostics as one, as fail_at would: the further
|
|
230
|
+
# position wins, and at the same one the expectations are merged.
|
|
231
|
+
def further(diagnostics, furthest, expected)
|
|
232
|
+
return [furthest, expected] if furthest > diagnostics[0]
|
|
233
|
+
return diagnostics if furthest < diagnostics[0] || expected.empty?
|
|
234
|
+
|
|
235
|
+
[furthest, diagnostics[1] | expected]
|
|
236
|
+
end
|
|
237
|
+
|
|
238
|
+
# The rule's match so far at pos, while it grows there. Its
|
|
239
|
+
# diagnostics are grow's to keep, so it carries none.
|
|
240
|
+
def seed(name, pos)
|
|
241
|
+
@seeds[name]&.[](pos) || raise(ArgumentError,
|
|
242
|
+
"left recursion in rule :#{name}: it can reach itself without consuming input")
|
|
121
243
|
end
|
|
122
244
|
end
|
|
123
245
|
end
|
|
@@ -1,11 +1,17 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
# [file]
|
|
4
|
+
# purpose = "Default wording for parse errors"
|
|
5
|
+
# pattern = "Strategy: anything with #call(failure) can replace it"
|
|
3
6
|
module Birman
|
|
4
7
|
# Turns a Failure into readable text. Anything that responds to
|
|
5
8
|
# #call(failure) works in its place, for custom wording or i18n:
|
|
6
9
|
#
|
|
7
10
|
# failure.message(MyFormatter.new)
|
|
8
11
|
class ErrorFormatter
|
|
12
|
+
# [method.call]
|
|
13
|
+
# params = ["failure: Birman::Failure"]
|
|
14
|
+
# returns = "String: \"line L, column C: expected ..., found ...\" plus the source line and a caret"
|
|
9
15
|
def call(failure)
|
|
10
16
|
source = source_line(failure)
|
|
11
17
|
<<~MSG.chomp
|