parsanol 1.3.64 → 1.3.66
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/lib/parsanol/atoms/state.rb +151 -0
- data/lib/parsanol/atoms.rb +1 -0
- data/lib/parsanol/native/serializer.rb +36 -0
- data/lib/parsanol/parg/artifact.rb +6 -1
- data/lib/parsanol/parg/compiler.rb +46 -1
- data/lib/parsanol/parg/document.rb +4 -1
- data/lib/parsanol/parg/lutaml.rb +4 -0
- data/lib/parsanol/parg/parser.rb +52 -1
- data/lib/parsanol/parser.rb +5 -0
- data/lib/parsanol/scope.rb +8 -0
- data/lib/parsanol/version.rb +1 -1
- data/lib/parsanol/vm.rb +25 -18
- metadata +2 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: da894739a391336f9b9f65a65abc87da1fe51b3f3268c0c2334638e53f5adb8f
|
|
4
|
+
data.tar.gz: 6777a602a63a37e599fdc1ed9c38f82eb8b01a30a12a23c24bbed7518f37e5cf
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 0fad1aa5febfc712a767578c4ca4d9c9c6cf3def48ed7f6fe97a66a95e45d0e628a4285660eb14b40e6d0b72bbd6422868b85ea901d84318382f23aa5744378b
|
|
7
|
+
data.tar.gz: 6ab9b3b4461592a3a5e91accd57d0341c59c7f878985b01fd89b73fe91c8abd71eacedd6246d0c79a2fd57547607415c0932e73cfb2c2e61b3adb2af5165a9a6
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Parsanol
|
|
4
|
+
module Atoms
|
|
5
|
+
# PARG runtime-state atoms (parsanol-ruby#129): per-parse mutable
|
|
6
|
+
# slots living in the parse context's capture store, so capture
|
|
7
|
+
# rollback, scope discipline and the dispatch semantics are the
|
|
8
|
+
# proven dynamic-callback ones. All four are pure data — they
|
|
9
|
+
# serialize as structured tags and rebuild identically in any
|
|
10
|
+
# runtime that implements the state machine.
|
|
11
|
+
class StateBase < Base
|
|
12
|
+
def cached?
|
|
13
|
+
false
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
private
|
|
17
|
+
|
|
18
|
+
# Slots and captures may live in a parent scope frame (an earlier
|
|
19
|
+
# element's frame); Scope#[] only reads the active one.
|
|
20
|
+
def lookup(context, key)
|
|
21
|
+
context.captures.fetch(key)
|
|
22
|
+
rescue Parsanol::Scope::UndefinedVariable
|
|
23
|
+
nil
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def mark_unsafe(context)
|
|
27
|
+
# The atom reads mutable parse state (captures), so no enclosing
|
|
28
|
+
# composite result may be memoized — replaying it would skip the
|
|
29
|
+
# re-evaluation (same contract as Atoms::Dynamic).
|
|
30
|
+
context.mark_cache_unsafe!
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
# Writes a state slot. Two forms: a literal value (always succeeds,
|
|
35
|
+
# consumes nothing) or an inline expression — the expression matches
|
|
36
|
+
# at the position and the slot stores its consumed text (the
|
|
37
|
+
# block_open pattern: match the delimiter, remember it).
|
|
38
|
+
class StateSet < StateBase
|
|
39
|
+
def initialize(slot, value: nil, atom: nil)
|
|
40
|
+
super()
|
|
41
|
+
@slot = slot
|
|
42
|
+
@value = value
|
|
43
|
+
@atom = atom
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
attr_reader :slot, :value, :atom
|
|
47
|
+
|
|
48
|
+
def try(source, context, _consume_all)
|
|
49
|
+
mark_unsafe(context)
|
|
50
|
+
if @atom.nil?
|
|
51
|
+
context.captures[@slot] = @value
|
|
52
|
+
return [true, ""]
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
start = source.bytepos
|
|
56
|
+
outcome = @atom.apply(source, context, false)
|
|
57
|
+
return outcome unless outcome.first
|
|
58
|
+
|
|
59
|
+
span = source.input.byteslice(start, source.bytepos - start)
|
|
60
|
+
context.captures[@slot] = span
|
|
61
|
+
outcome
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def to_s_inner(_prec)
|
|
65
|
+
"set #{@slot}=#{@value || '…'}"
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
# Matches the state slot's current value verbatim at the position —
|
|
70
|
+
# the block-delimiter comparison ("exactly what was captured above").
|
|
71
|
+
class StateMatch < StateBase
|
|
72
|
+
def initialize(slot)
|
|
73
|
+
super()
|
|
74
|
+
@slot = slot
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
attr_reader :slot
|
|
78
|
+
|
|
79
|
+
def try(source, context, _consume_all)
|
|
80
|
+
mark_unsafe(context)
|
|
81
|
+
expected = lookup(context, @slot)
|
|
82
|
+
return [false, nil] if expected.nil?
|
|
83
|
+
|
|
84
|
+
actual = source.peek(expected.length)
|
|
85
|
+
return [false, nil] if actual != expected
|
|
86
|
+
|
|
87
|
+
source.bytepos += expected.length
|
|
88
|
+
[true, actual]
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
def to_s_inner(_prec)
|
|
92
|
+
"state(#{@slot})"
|
|
93
|
+
end
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
# Dispatches on the slot's current value to a rule reference. The
|
|
97
|
+
# resolver maps a rule name to its atom lazily (rules may still be
|
|
98
|
+
# under construction when this atom is built). A nil arm fails the
|
|
99
|
+
# atom — the native dynamic-callback parity.
|
|
100
|
+
class StateSwitch < StateBase
|
|
101
|
+
def initialize(slot, arms, default, resolver)
|
|
102
|
+
super()
|
|
103
|
+
@slot = slot
|
|
104
|
+
@arms = arms
|
|
105
|
+
@default = default
|
|
106
|
+
@resolver = resolver
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
attr_reader :slot, :arms, :default
|
|
110
|
+
|
|
111
|
+
def try(source, context, consume_all)
|
|
112
|
+
mark_unsafe(context)
|
|
113
|
+
current = lookup(context, @slot)
|
|
114
|
+
rule_name = @arms[current] || @default
|
|
115
|
+
return [false, nil] if rule_name.nil?
|
|
116
|
+
|
|
117
|
+
@resolver.call(rule_name).apply(source, context, consume_all)
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
def to_s_inner(_prec)
|
|
121
|
+
"switch(#{@slot})"
|
|
122
|
+
end
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
# A foreign atom: a class resolved from the artifact's customs
|
|
126
|
+
# binding at compile time. The instance runs as-is on the Ruby
|
|
127
|
+
# engine; the serialized tag carries the binding so any runtime can
|
|
128
|
+
# decide support loudly.
|
|
129
|
+
class CustomRef < Base
|
|
130
|
+
def initialize(name, klass)
|
|
131
|
+
super()
|
|
132
|
+
@name = name
|
|
133
|
+
@atom = klass.new
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
attr_reader :name
|
|
137
|
+
|
|
138
|
+
def cached?
|
|
139
|
+
false
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
def try(source, context, consume_all)
|
|
143
|
+
@atom.try(source, context, consume_all)
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
def to_s_inner(_prec)
|
|
147
|
+
"custom(#{@name})"
|
|
148
|
+
end
|
|
149
|
+
end
|
|
150
|
+
end
|
|
151
|
+
end
|
data/lib/parsanol/atoms.rb
CHANGED
|
@@ -38,6 +38,7 @@ module Parsanol
|
|
|
38
38
|
require "parsanol/atoms/entity"
|
|
39
39
|
require "parsanol/atoms/capture"
|
|
40
40
|
require "parsanol/atoms/dynamic"
|
|
41
|
+
require "parsanol/atoms/state"
|
|
41
42
|
require "parsanol/atoms/scope"
|
|
42
43
|
require "parsanol/atoms/infix"
|
|
43
44
|
# Load visitor pattern (must be after all atom classes)
|
|
@@ -68,6 +68,14 @@ module Parsanol
|
|
|
68
68
|
serialize_capture(atom)
|
|
69
69
|
when Parsanol::Atoms::Scope
|
|
70
70
|
serialize_scope(atom)
|
|
71
|
+
when Parsanol::Atoms::StateSet
|
|
72
|
+
serialize_state_set(atom)
|
|
73
|
+
when Parsanol::Atoms::StateMatch
|
|
74
|
+
serialize_state_match(atom)
|
|
75
|
+
when Parsanol::Atoms::StateSwitch
|
|
76
|
+
serialize_state_switch(atom)
|
|
77
|
+
when Parsanol::Atoms::CustomRef
|
|
78
|
+
serialize_custom_ref(atom)
|
|
71
79
|
when Parsanol::Atoms::Dynamic
|
|
72
80
|
serialize_dynamic(atom)
|
|
73
81
|
when Parsanol::Atoms::Ignored
|
|
@@ -219,6 +227,34 @@ module Parsanol
|
|
|
219
227
|
}
|
|
220
228
|
end
|
|
221
229
|
|
|
230
|
+
def serialize_state_set(atom)
|
|
231
|
+
payload = { "slot" => atom.slot.to_s }
|
|
232
|
+
if atom.atom
|
|
233
|
+
payload["expr"] = serialize_atom(atom.atom)
|
|
234
|
+
else
|
|
235
|
+
payload["value"] = atom.value
|
|
236
|
+
end
|
|
237
|
+
{ "StateSet" => payload }
|
|
238
|
+
end
|
|
239
|
+
|
|
240
|
+
def serialize_state_match(atom)
|
|
241
|
+
{ "StateMatch" => { "slot" => atom.slot.to_s } }
|
|
242
|
+
end
|
|
243
|
+
|
|
244
|
+
def serialize_state_switch(atom)
|
|
245
|
+
{
|
|
246
|
+
"StateSwitch" => {
|
|
247
|
+
"slot" => atom.slot.to_s,
|
|
248
|
+
"arms" => atom.arms.transform_keys(&:to_s),
|
|
249
|
+
"default" => atom.default,
|
|
250
|
+
},
|
|
251
|
+
}
|
|
252
|
+
end
|
|
253
|
+
|
|
254
|
+
def serialize_custom_ref(atom)
|
|
255
|
+
{ "CustomRef" => { "name" => atom.name } }
|
|
256
|
+
end
|
|
257
|
+
|
|
222
258
|
def serialize_dynamic(atom)
|
|
223
259
|
# Dynamic evaluates a Ruby block at parse time.
|
|
224
260
|
# Register the block and get a callback ID for FFI.
|
|
@@ -57,10 +57,15 @@ module Parsanol
|
|
|
57
57
|
# path is a straight register-and-run — no Ruby recompilation. The
|
|
58
58
|
# Ruby atom runtime (mode: :ruby) remains available explicitly, and
|
|
59
59
|
# serves as the fallback on platforms without the extension.
|
|
60
|
+
#
|
|
61
|
+
# Dynamic artifacts (runtime state, custom atoms — #129) route to
|
|
62
|
+
# the Ruby engine: the envelope carries the PARG source precisely
|
|
63
|
+
# so a Ruby runtime can rebuild the live atom tree; the closures
|
|
64
|
+
# are not portable JSON.
|
|
60
65
|
def parse(entry_name, input, mode: :native)
|
|
61
66
|
case mode
|
|
62
67
|
when :native
|
|
63
|
-
if Native.available?
|
|
68
|
+
if !envelope["dynamic"] && Native.available?
|
|
64
69
|
return Native.parse(JSON.generate(entry(entry_name).fetch("grammar")), input)
|
|
65
70
|
end
|
|
66
71
|
|
|
@@ -50,6 +50,7 @@ module Parsanol
|
|
|
50
50
|
@atom_cache = {}
|
|
51
51
|
@first_cache = {}.compare_by_identity
|
|
52
52
|
@ref_stack = []
|
|
53
|
+
@dynamic = false
|
|
53
54
|
@tables = {}
|
|
54
55
|
@warnings = []
|
|
55
56
|
end
|
|
@@ -67,6 +68,16 @@ module Parsanol
|
|
|
67
68
|
Result.new(@atom_cache, @warnings, envelope)
|
|
68
69
|
end
|
|
69
70
|
|
|
71
|
+
def resolve_custom(name)
|
|
72
|
+
class_name = @document.customs.fetch(name)
|
|
73
|
+
klass = Object.const_get(class_name)
|
|
74
|
+
Atoms::CustomRef.new(name, klass)
|
|
75
|
+
rescue NameError
|
|
76
|
+
raise CompileError,
|
|
77
|
+
"custom atom #{name.inspect} cannot resolve #{class_name.inspect} " \
|
|
78
|
+
"(bind the class before compiling, or fix the custom declaration)"
|
|
79
|
+
end
|
|
80
|
+
|
|
70
81
|
def atom_for(name)
|
|
71
82
|
@atom_cache[name] ||= build_atom(fetch_rule(name))
|
|
72
83
|
end
|
|
@@ -157,6 +168,13 @@ module Parsanol
|
|
|
157
168
|
end
|
|
158
169
|
|
|
159
170
|
def fetch_rule(name)
|
|
171
|
+
# Customs are opaque terminals for the analysis passes (first
|
|
172
|
+
# sets, left-recursion): a literal stand-in keeps them
|
|
173
|
+
# non-nullable with a singleton first set.
|
|
174
|
+
if !@document.rules.key?(name) && @document.customs.key?(name)
|
|
175
|
+
return Node.new(:lit, "\u0001", false)
|
|
176
|
+
end
|
|
177
|
+
|
|
160
178
|
@document.rules[name] || raise(CompileError, "unknown rule #{name.inspect}")
|
|
161
179
|
end
|
|
162
180
|
|
|
@@ -194,7 +212,31 @@ module Parsanol
|
|
|
194
212
|
end
|
|
195
213
|
|
|
196
214
|
Atoms::Named.new(build_atom(node.b), node.a.to_sym)
|
|
197
|
-
when :
|
|
215
|
+
when :set
|
|
216
|
+
@dynamic = true
|
|
217
|
+
slot = node.a.to_sym
|
|
218
|
+
value = node.b
|
|
219
|
+
if value.is_a?(String)
|
|
220
|
+
Atoms::StateSet.new(slot, value: value)
|
|
221
|
+
else
|
|
222
|
+
Atoms::StateSet.new(slot, atom: build_atom(value))
|
|
223
|
+
end
|
|
224
|
+
when :state_match
|
|
225
|
+
@dynamic = true
|
|
226
|
+
Atoms::StateMatch.new(node.a.to_sym)
|
|
227
|
+
when :switch
|
|
228
|
+
@dynamic = true
|
|
229
|
+
slot = node.a.to_sym
|
|
230
|
+
arms = node.b.to_h { |match, rule| [match, rule] }
|
|
231
|
+
default = arms.delete(:default)
|
|
232
|
+
Atoms::StateSwitch.new(slot, arms, default, method(:atom_for))
|
|
233
|
+
when :ref
|
|
234
|
+
if @document.customs.key?(node.a)
|
|
235
|
+
@dynamic = true
|
|
236
|
+
resolve_custom(node.a)
|
|
237
|
+
else
|
|
238
|
+
Atoms::Entity.new(node.a) { atom_for(node.a) }
|
|
239
|
+
end
|
|
198
240
|
when :table
|
|
199
241
|
values = table_column(node.a, node.b)
|
|
200
242
|
Atoms::Alternative.new(*values.map { |value| Atoms::Str.new(value) })
|
|
@@ -430,6 +472,9 @@ module Parsanol
|
|
|
430
472
|
"docs" => @document.docs,
|
|
431
473
|
"source" => @document.source,
|
|
432
474
|
}
|
|
475
|
+
envelope["customs"] = @document.customs unless @document.customs.empty?
|
|
476
|
+
envelope["states"] = @document.states unless @document.states.empty?
|
|
477
|
+
envelope["dynamic"] = true if @dynamic
|
|
433
478
|
envelope["checksum"] = Compiler.checksum(envelope)
|
|
434
479
|
envelope
|
|
435
480
|
end
|
|
@@ -10,7 +10,8 @@ module Parsanol
|
|
|
10
10
|
Test = Struct.new(:entry, :kind, :input, :expect)
|
|
11
11
|
|
|
12
12
|
attr_accessor :grammar_name, :version, :source, :uses
|
|
13
|
-
attr_reader :rules, :bindings, :preprocess, :entries, :docs, :tests, :render, :derive
|
|
13
|
+
attr_reader :rules, :bindings, :preprocess, :entries, :docs, :tests, :render, :derive,
|
|
14
|
+
:customs, :states
|
|
14
15
|
# Entries authored in this file (pre-import); nil-entry tests resolve
|
|
15
16
|
# against these first.
|
|
16
17
|
attr_accessor :own_entries
|
|
@@ -29,6 +30,8 @@ module Parsanol
|
|
|
29
30
|
@tests = []
|
|
30
31
|
@uses = []
|
|
31
32
|
@own_entries = nil
|
|
33
|
+
@customs = {}
|
|
34
|
+
@states = {}
|
|
32
35
|
end
|
|
33
36
|
|
|
34
37
|
def validate!
|
data/lib/parsanol/parg/lutaml.rb
CHANGED
|
@@ -1,5 +1,9 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
# Standalone requires of this file bypass the parg autoload registry —
|
|
4
|
+
# declare the namespace dependency so Artifact resolves.
|
|
5
|
+
require "parsanol/parg"
|
|
6
|
+
|
|
3
7
|
module Parsanol
|
|
4
8
|
module PARG
|
|
5
9
|
# lutaml-model integration: register a PARG artifact as a lutaml-model
|
data/lib/parsanol/parg/parser.rb
CHANGED
|
@@ -18,8 +18,9 @@ module Parsanol
|
|
|
18
18
|
|
|
19
19
|
IN_BLOCK_SECTIONS = %w[entry bindings preprocess test].freeze
|
|
20
20
|
|
|
21
|
+
STATE_KEYWORDS = %w[custom state].freeze
|
|
21
22
|
KEYWORDS = %w[grammar as alt from_table column bindings render derive
|
|
22
|
-
preprocess entry table_lookup].freeze
|
|
23
|
+
preprocess entry table_lookup custom state set switch].freeze
|
|
23
24
|
|
|
24
25
|
def initialize(text)
|
|
25
26
|
@tokens = Lexer.new(text).tokens
|
|
@@ -160,6 +161,19 @@ module Parsanol
|
|
|
160
161
|
skip_newlines
|
|
161
162
|
next
|
|
162
163
|
end
|
|
164
|
+
if peek&.type == :ident && STATE_KEYWORDS.include?(peek.value)
|
|
165
|
+
keyword = advance.value
|
|
166
|
+
slot = ident.value
|
|
167
|
+
if keyword == "custom"
|
|
168
|
+
punct("=")
|
|
169
|
+
document.customs[slot] = unquote(expect(:str).value)
|
|
170
|
+
else
|
|
171
|
+
punct(":")
|
|
172
|
+
document.states[slot] = ident.value
|
|
173
|
+
end
|
|
174
|
+
skip_newlines
|
|
175
|
+
next
|
|
176
|
+
end
|
|
163
177
|
name = rule_name
|
|
164
178
|
document.docs[name] = docs.join("\n") unless docs.empty?
|
|
165
179
|
punct("=")
|
|
@@ -274,6 +288,43 @@ module Parsanol
|
|
|
274
288
|
|
|
275
289
|
def parse_ident(token)
|
|
276
290
|
case token.value
|
|
291
|
+
when "state"
|
|
292
|
+
Node.new(:state_match, ident.value)
|
|
293
|
+
when "set"
|
|
294
|
+
slot = ident.value
|
|
295
|
+
punct("=")
|
|
296
|
+
# A string writes the literal; a bare name copies the matched
|
|
297
|
+
# text of a capture (the block-delimiter pattern: set the slot
|
|
298
|
+
# from what the opening delimiter matched).
|
|
299
|
+
value = if peek&.type == :str
|
|
300
|
+
unquote(expect(:str).value)
|
|
301
|
+
else
|
|
302
|
+
punct("(")
|
|
303
|
+
inner = parse_choice
|
|
304
|
+
punct(")")
|
|
305
|
+
inner
|
|
306
|
+
end
|
|
307
|
+
Node.new(:set, slot, value)
|
|
308
|
+
when "switch"
|
|
309
|
+
slot = ident.value
|
|
310
|
+
punct("{")
|
|
311
|
+
skip_newlines
|
|
312
|
+
arms = []
|
|
313
|
+
until peek&.type == :punct && peek.value == "}"
|
|
314
|
+
match = if peek&.type == :ident && peek.value == "_"
|
|
315
|
+
advance
|
|
316
|
+
:default
|
|
317
|
+
else
|
|
318
|
+
unquote(expect(:str).value)
|
|
319
|
+
end
|
|
320
|
+
arrow = advance
|
|
321
|
+
raise ParseError, "expected \"->\", got #{arrow&.type}:#{arrow&.value}" unless arrow&.value == "->"
|
|
322
|
+
|
|
323
|
+
arms << [match, ident.value]
|
|
324
|
+
skip_newlines
|
|
325
|
+
end
|
|
326
|
+
punct("}")
|
|
327
|
+
Node.new(:switch, slot, arms)
|
|
277
328
|
when "alt"
|
|
278
329
|
ident("from_table")
|
|
279
330
|
table = unquote(expect(:str).value)
|
data/lib/parsanol/parser.rb
CHANGED
|
@@ -238,6 +238,11 @@ module Parsanol
|
|
|
238
238
|
# The degradation warning prints once per reason per process — not
|
|
239
239
|
# per parser instance and not per parse.
|
|
240
240
|
def native_expressible?
|
|
241
|
+
# The ext attaches its methods lazily (available? triggers the load);
|
|
242
|
+
# without this guard a first-touch call NoMethodErrors inside the
|
|
243
|
+
# serializer (Dynamic.register -> Native.register_callback).
|
|
244
|
+
return false unless Parsanol::Native.available?
|
|
245
|
+
|
|
241
246
|
Parsanol::Native::Parser.grammar_handle(root)
|
|
242
247
|
true
|
|
243
248
|
rescue Parsanol::Native::UnsupportedGrammar => e
|
data/lib/parsanol/scope.rb
CHANGED
|
@@ -84,6 +84,14 @@ module Parsanol
|
|
|
84
84
|
@active_frame.fetch(key)
|
|
85
85
|
end
|
|
86
86
|
|
|
87
|
+
# Chain-aware read: the active frame, then its parents ([] is
|
|
88
|
+
# deliberately active-frame-only for capture-scope discipline; state
|
|
89
|
+
# atoms read across frames — a slot may have been written inside an
|
|
90
|
+
# earlier sibling element's frame).
|
|
91
|
+
def fetch(key)
|
|
92
|
+
@active_frame.fetch(key)
|
|
93
|
+
end
|
|
94
|
+
|
|
87
95
|
# Chain-aware membership test, mirroring Parslet's captures.key?.
|
|
88
96
|
# Checks the current frame and all parents without raising.
|
|
89
97
|
#
|
data/lib/parsanol/version.rb
CHANGED
data/lib/parsanol/vm.rb
CHANGED
|
@@ -175,8 +175,10 @@ module Parsanol
|
|
|
175
175
|
compiler.to_program
|
|
176
176
|
end
|
|
177
177
|
|
|
178
|
+
# Returns violation descriptions for this atom's ordered-choice
|
|
179
|
+
# branches (empty-matchable non-last branches shadow later ones).
|
|
178
180
|
def validate_choice_branches(atom)
|
|
179
|
-
atom.alternatives.each_with_index do |branch, idx|
|
|
181
|
+
atom.alternatives.each_with_index.filter_map do |branch, idx|
|
|
180
182
|
walk_branch = branch
|
|
181
183
|
next if idx == atom.alternatives.size - 1
|
|
182
184
|
|
|
@@ -194,14 +196,13 @@ module Parsanol
|
|
|
194
196
|
next if walk_branch.nil?
|
|
195
197
|
next unless Compiler.new.nullable?(walk_branch)
|
|
196
198
|
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
"ones come last, or make the branch non-empty."
|
|
199
|
+
"alternative branch #{idx} (#{branch.inspect}) can match " \
|
|
200
|
+
"empty input and is not the last branch — ordered choice " \
|
|
201
|
+
"commits to its empty match, so " \
|
|
202
|
+
"#{atom.alternatives.size - idx - 1} later branch(es) can " \
|
|
203
|
+
"never match at positions where it matches empty. Reorder " \
|
|
204
|
+
"branches so empty-matchable ones come last, or make the " \
|
|
205
|
+
"branch non-empty."
|
|
205
206
|
end
|
|
206
207
|
end
|
|
207
208
|
|
|
@@ -211,6 +212,7 @@ module Parsanol
|
|
|
211
212
|
# the grammar is wrong, and inputs it would mis-parse fail with a
|
|
212
213
|
# baffling cause. Raise before any input is seen.
|
|
213
214
|
def validate_alternatives(root)
|
|
215
|
+
violations = []
|
|
214
216
|
visited = {}.compare_by_identity
|
|
215
217
|
walk = lambda do |atom, depth|
|
|
216
218
|
next if atom.nil? || depth > 20
|
|
@@ -219,26 +221,26 @@ module Parsanol
|
|
|
219
221
|
visited[atom] = true
|
|
220
222
|
case atom
|
|
221
223
|
when Parsanol::Atoms::Alternative
|
|
222
|
-
validate_choice_branches(atom)
|
|
224
|
+
violations.concat(validate_choice_branches(atom))
|
|
223
225
|
atom.alternatives.each { |c| walk.call(c, depth + 1) }
|
|
224
226
|
when Parsanol::Atoms::Sequence
|
|
225
227
|
atom.parslets.each { |c| walk.call(c, depth + 1) }
|
|
226
228
|
when Parsanol::Atoms::Repetition, Parsanol::Atoms::Named
|
|
227
229
|
walk.call(atom.parslet, depth + 1)
|
|
228
|
-
when Parsanol::Atoms::Lookahead
|
|
229
|
-
walk.call(atom.bound_parslet, depth + 1)
|
|
230
|
-
when Parsanol::Atoms::Ignored
|
|
231
|
-
walk.call(atom.wrapped_atom, depth + 1)
|
|
232
230
|
when Parsanol::Atoms::Entity
|
|
231
|
+
# Rule bodies hide behind the entity wrapper; without this
|
|
232
|
+
# descent the lint passes DSL grammars that fail the same
|
|
233
|
+
# grammar compiled from PARG (where bodies are inlined).
|
|
234
|
+
# Entities that cannot resolve outside a parse are skipped.
|
|
233
235
|
begin
|
|
234
236
|
walk.call(atom.parslet, depth + 1)
|
|
235
|
-
rescue Parsanol::GrammarError
|
|
236
|
-
raise
|
|
237
237
|
rescue StandardError
|
|
238
|
-
# Lazily resolved / recursive entity that cannot resolve
|
|
239
|
-
# outside a parse: skip this branch of the walk.
|
|
240
238
|
nil
|
|
241
239
|
end
|
|
240
|
+
when Parsanol::Atoms::Lookahead
|
|
241
|
+
walk.call(atom.bound_parslet, depth + 1)
|
|
242
|
+
when Parsanol::Atoms::Ignored
|
|
243
|
+
walk.call(atom.wrapped_atom, depth + 1)
|
|
242
244
|
when Parsanol::Atoms::Scope
|
|
243
245
|
begin
|
|
244
246
|
walk.call(atom.block.call, depth + 1)
|
|
@@ -248,6 +250,11 @@ module Parsanol
|
|
|
248
250
|
end
|
|
249
251
|
end
|
|
250
252
|
walk.call(root, 0)
|
|
253
|
+
return if violations.empty?
|
|
254
|
+
|
|
255
|
+
raise Parsanol::GrammarError,
|
|
256
|
+
"wrong grammar: #{violations.size} shadowed-alternative " \
|
|
257
|
+
"violation(s):\n - #{violations.join('\n - ')}"
|
|
251
258
|
end
|
|
252
259
|
|
|
253
260
|
# Executes a compiled program. Returns [true, value] on success;
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: parsanol
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 1.3.
|
|
4
|
+
version: 1.3.66
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Ribose Inc.
|
|
@@ -70,6 +70,7 @@ files:
|
|
|
70
70
|
- lib/parsanol/atoms/repetition.rb
|
|
71
71
|
- lib/parsanol/atoms/scope.rb
|
|
72
72
|
- lib/parsanol/atoms/sequence.rb
|
|
73
|
+
- lib/parsanol/atoms/state.rb
|
|
73
74
|
- lib/parsanol/atoms/str.rb
|
|
74
75
|
- lib/parsanol/atoms/visitor.rb
|
|
75
76
|
- lib/parsanol/buffer.rb
|