parsanol 1.3.4 → 1.3.7
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/Cargo.lock +0 -2
- data/Rakefile +48 -48
- data/ext/parsanol_native/Cargo.toml +1 -2
- data/ext/parsanol_native/extconf.rb +4 -4
- data/lib/parsanol/ast_visitor.rb +1 -1
- data/lib/parsanol/atoms/alternative.rb +3 -2
- data/lib/parsanol/atoms/base.rb +12 -6
- data/lib/parsanol/atoms/can_flatten.rb +8 -8
- data/lib/parsanol/atoms/context.rb +23 -16
- data/lib/parsanol/atoms/custom.rb +2 -2
- data/lib/parsanol/atoms/dynamic.rb +1 -1
- data/lib/parsanol/atoms/infix.rb +10 -5
- data/lib/parsanol/atoms/lookahead.rb +7 -4
- data/lib/parsanol/atoms/re.rb +1 -1
- data/lib/parsanol/atoms/repetition.rb +29 -11
- data/lib/parsanol/atoms/sequence.rb +3 -2
- data/lib/parsanol/atoms/str.rb +9 -3
- data/lib/parsanol/atoms.rb +20 -20
- data/lib/parsanol/builder_callbacks.rb +2 -2
- data/lib/parsanol/cause.rb +2 -2
- data/lib/parsanol/context.rb +2 -2
- data/lib/parsanol/error_reporter.rb +5 -5
- data/lib/parsanol/expression/treetop.rb +17 -17
- data/lib/parsanol/expression.rb +1 -1
- data/lib/parsanol/fast_mode.rb +50 -12
- data/lib/parsanol/first_set.rb +1 -1
- data/lib/parsanol/grammar_builder.rb +10 -8
- data/lib/parsanol/incremental_parser.rb +13 -8
- data/lib/parsanol/interval_tree.rb +12 -3
- data/lib/parsanol/lazy_result.rb +2 -2
- data/lib/parsanol/mermaid.rb +12 -9
- data/lib/parsanol/native/batch_decoder.rb +13 -9
- data/lib/parsanol/native/dynamic.rb +7 -6
- data/lib/parsanol/native/parser.rb +12 -4
- data/lib/parsanol/native/serializer.rb +42 -42
- data/lib/parsanol/native/transformer.rb +55 -28
- data/lib/parsanol/native/types.rb +3 -3
- data/lib/parsanol/native.rb +60 -21
- data/lib/parsanol/optimizer.rb +6 -6
- data/lib/parsanol/optimizers/choice_optimizer.rb +1 -1
- data/lib/parsanol/optimizers/cut_inserter.rb +5 -2
- data/lib/parsanol/optimizers/lookahead_optimizer.rb +9 -3
- data/lib/parsanol/optimizers/quantifier_optimizer.rb +5 -5
- data/lib/parsanol/optimizers/sequence_optimizer.rb +1 -1
- data/lib/parsanol/options/zero_copy.rb +1 -1
- data/lib/parsanol/options.rb +1 -1
- data/lib/parsanol/parallel.rb +8 -13
- data/lib/parsanol/parser.rb +51 -13
- data/lib/parsanol/parslet.rb +7 -7
- data/lib/parsanol/pattern/binding.rb +1 -1
- data/lib/parsanol/pattern.rb +4 -1
- data/lib/parsanol/pool.rb +3 -3
- data/lib/parsanol/pools/buffer_pool.rb +2 -2
- data/lib/parsanol/pools/position_pool.rb +2 -2
- data/lib/parsanol/position.rb +1 -1
- data/lib/parsanol/result_builder.rb +4 -4
- data/lib/parsanol/result_stream.rb +10 -5
- data/lib/parsanol/slice.rb +11 -8
- data/lib/parsanol/source.rb +14 -9
- data/lib/parsanol/source_location.rb +1 -1
- data/lib/parsanol/streaming_parser.rb +3 -3
- data/lib/parsanol/string_view.rb +4 -1
- data/lib/parsanol/transform.rb +2 -2
- data/lib/parsanol/version.rb +1 -1
- data/lib/parsanol/wasm_parser.rb +1 -1
- data/lib/parsanol.rb +37 -39
- data/parsanol.gemspec +30 -30
- metadata +1 -1
|
@@ -36,7 +36,7 @@ module Parsanol
|
|
|
36
36
|
super()
|
|
37
37
|
|
|
38
38
|
# Handle nil max_count (unbounded repetition)
|
|
39
|
-
if max_count
|
|
39
|
+
if max_count&.zero?
|
|
40
40
|
raise ArgumentError, "Cannot repeat zero times: #{parser.inspect}"
|
|
41
41
|
end
|
|
42
42
|
|
|
@@ -50,7 +50,7 @@ module Parsanol
|
|
|
50
50
|
|
|
51
51
|
# Pre-built error messages
|
|
52
52
|
@min_error = "Expected at least #{min_count} of #{parser.inspect}"
|
|
53
|
-
@extra_error =
|
|
53
|
+
@extra_error = "Extra input after last repetition"
|
|
54
54
|
end
|
|
55
55
|
|
|
56
56
|
# Error messages hash (for compatibility)
|
|
@@ -71,10 +71,16 @@ module Parsanol
|
|
|
71
71
|
end
|
|
72
72
|
|
|
73
73
|
# Maybe (0 or 1) - very common, optimize
|
|
74
|
-
|
|
74
|
+
if @min.zero? && @max == 1
|
|
75
|
+
return try_maybe(source, context,
|
|
76
|
+
consume_all)
|
|
77
|
+
end
|
|
75
78
|
|
|
76
79
|
# Exact count optimization
|
|
77
|
-
|
|
80
|
+
if @min == @max && @max && @max <= 3
|
|
81
|
+
return try_exact(source, context,
|
|
82
|
+
consume_all)
|
|
83
|
+
end
|
|
78
84
|
|
|
79
85
|
# General case
|
|
80
86
|
try_general(source, context, consume_all)
|
|
@@ -88,7 +94,7 @@ module Parsanol
|
|
|
88
94
|
# @return [String]
|
|
89
95
|
def to_s_inner(prec)
|
|
90
96
|
suffix = if @min.zero? && @max == 1
|
|
91
|
-
|
|
97
|
+
"?"
|
|
92
98
|
else
|
|
93
99
|
"{#{@min}, #{@max}}"
|
|
94
100
|
end
|
|
@@ -135,7 +141,10 @@ module Parsanol
|
|
|
135
141
|
|
|
136
142
|
def double_match(source, context, consume_all)
|
|
137
143
|
success, v1 = @parslet.apply(source, context, false)
|
|
138
|
-
|
|
144
|
+
unless success
|
|
145
|
+
return context.err_at(self, source, @min_error, source.bytepos,
|
|
146
|
+
[v1])
|
|
147
|
+
end
|
|
139
148
|
|
|
140
149
|
success, v2 = @parslet.apply(source, context, consume_all)
|
|
141
150
|
return ok([@result_tag, v1, v2]) if success
|
|
@@ -145,10 +154,16 @@ module Parsanol
|
|
|
145
154
|
|
|
146
155
|
def triple_match(source, context, consume_all)
|
|
147
156
|
success, v1 = @parslet.apply(source, context, false)
|
|
148
|
-
|
|
157
|
+
unless success
|
|
158
|
+
return context.err_at(self, source, @min_error, source.bytepos,
|
|
159
|
+
[v1])
|
|
160
|
+
end
|
|
149
161
|
|
|
150
162
|
success, v2 = @parslet.apply(source, context, false)
|
|
151
|
-
|
|
163
|
+
unless success
|
|
164
|
+
return context.err_at(self, source, @min_error, source.bytepos,
|
|
165
|
+
[v2])
|
|
166
|
+
end
|
|
152
167
|
|
|
153
168
|
success, v3 = @parslet.apply(source, context, consume_all)
|
|
154
169
|
return ok([@result_tag, v1, v2, v3]) if success
|
|
@@ -184,7 +199,8 @@ module Parsanol
|
|
|
184
199
|
if occurrence < @min
|
|
185
200
|
context.release_buffer(buffer)
|
|
186
201
|
source.bytepos = start_pos
|
|
187
|
-
return context.err_at(self, source, @min_error, start_pos,
|
|
202
|
+
return context.err_at(self, source, @min_error, start_pos,
|
|
203
|
+
[last_error])
|
|
188
204
|
end
|
|
189
205
|
|
|
190
206
|
# Check complete consumption
|
|
@@ -236,14 +252,16 @@ module Parsanol
|
|
|
236
252
|
# Cache successful prefix
|
|
237
253
|
if occurrence.positive?
|
|
238
254
|
end_pos = positions[occurrence]
|
|
239
|
-
context.store_tree_memo(cache_key, start_pos, buffer.to_a[1..],
|
|
255
|
+
context.store_tree_memo(cache_key, start_pos, buffer.to_a[1..],
|
|
256
|
+
end_pos)
|
|
240
257
|
end
|
|
241
258
|
|
|
242
259
|
# Check minimum
|
|
243
260
|
if occurrence < @min
|
|
244
261
|
context.release_buffer(buffer)
|
|
245
262
|
source.bytepos = start_pos
|
|
246
|
-
return context.err_at(self, source, @min_error, start_pos,
|
|
263
|
+
return context.err_at(self, source, @min_error, start_pos,
|
|
264
|
+
[last_error])
|
|
247
265
|
end
|
|
248
266
|
|
|
249
267
|
# Check consumption
|
|
@@ -63,7 +63,8 @@ module Parsanol
|
|
|
63
63
|
when 2
|
|
64
64
|
match_pair(components[0], components[1], source, context, consume_all)
|
|
65
65
|
when 3
|
|
66
|
-
match_triple(components[0], components[1], components[2], source,
|
|
66
|
+
match_triple(components[0], components[1], components[2], source,
|
|
67
|
+
context, consume_all)
|
|
67
68
|
else
|
|
68
69
|
match_general(components, source, context, consume_all)
|
|
69
70
|
end
|
|
@@ -76,7 +77,7 @@ module Parsanol
|
|
|
76
77
|
# @param prec [Integer] precedence
|
|
77
78
|
# @return [String]
|
|
78
79
|
def to_s_inner(prec)
|
|
79
|
-
@parslets.map { |p| p.to_s(prec) }.join(
|
|
80
|
+
@parslets.map { |p| p.to_s(prec) }.join(" ")
|
|
80
81
|
end
|
|
81
82
|
|
|
82
83
|
# FIRST set is first element's FIRST set (with epsilon propagation).
|
data/lib/parsanol/atoms/str.rb
CHANGED
|
@@ -21,7 +21,7 @@ module Parsanol
|
|
|
21
21
|
@char_count = @str.length
|
|
22
22
|
|
|
23
23
|
# Pre-built error messages (frozen)
|
|
24
|
-
@early_eof_msg =
|
|
24
|
+
@early_eof_msg = "Unexpected end of input"
|
|
25
25
|
@mismatch_msg = "Expected #{@str.inspect}, but got "
|
|
26
26
|
|
|
27
27
|
# Optimization: single-char fast path
|
|
@@ -75,7 +75,10 @@ module Parsanol
|
|
|
75
75
|
|
|
76
76
|
# Fast path for single-character strings.
|
|
77
77
|
def single_char_match(source, context)
|
|
78
|
-
|
|
78
|
+
if source.chars_left < 1
|
|
79
|
+
return context.err(self, source,
|
|
80
|
+
@early_eof_msg)
|
|
81
|
+
end
|
|
79
82
|
|
|
80
83
|
pos = source.pos
|
|
81
84
|
slice = source.consume(1)
|
|
@@ -88,7 +91,10 @@ module Parsanol
|
|
|
88
91
|
|
|
89
92
|
# Standard path for multi-character strings.
|
|
90
93
|
def multi_char_match(source, context)
|
|
91
|
-
|
|
94
|
+
if source.chars_left < @char_count
|
|
95
|
+
return context.err(self, source,
|
|
96
|
+
@early_eof_msg)
|
|
97
|
+
end
|
|
92
98
|
|
|
93
99
|
pos = source.pos
|
|
94
100
|
slice = source.consume(@char_count)
|
data/lib/parsanol/atoms.rb
CHANGED
|
@@ -21,26 +21,26 @@ module Parsanol
|
|
|
21
21
|
end
|
|
22
22
|
|
|
23
23
|
# Load atom implementations
|
|
24
|
-
require
|
|
25
|
-
require
|
|
26
|
-
require
|
|
27
|
-
require
|
|
28
|
-
require
|
|
29
|
-
require
|
|
30
|
-
require
|
|
31
|
-
require
|
|
32
|
-
require
|
|
33
|
-
require
|
|
34
|
-
require
|
|
35
|
-
require
|
|
36
|
-
require
|
|
37
|
-
require
|
|
38
|
-
require
|
|
39
|
-
require
|
|
40
|
-
require
|
|
41
|
-
require
|
|
42
|
-
require
|
|
24
|
+
require "parsanol/atoms/can_flatten"
|
|
25
|
+
require "parsanol/atoms/context"
|
|
26
|
+
require "parsanol/atoms/dsl"
|
|
27
|
+
require "parsanol/atoms/base"
|
|
28
|
+
require "parsanol/atoms/custom"
|
|
29
|
+
require "parsanol/atoms/ignored"
|
|
30
|
+
require "parsanol/atoms/named"
|
|
31
|
+
require "parsanol/atoms/lookahead"
|
|
32
|
+
require "parsanol/atoms/cut"
|
|
33
|
+
require "parsanol/atoms/alternative"
|
|
34
|
+
require "parsanol/atoms/sequence"
|
|
35
|
+
require "parsanol/atoms/repetition"
|
|
36
|
+
require "parsanol/atoms/re"
|
|
37
|
+
require "parsanol/atoms/str"
|
|
38
|
+
require "parsanol/atoms/entity"
|
|
39
|
+
require "parsanol/atoms/capture"
|
|
40
|
+
require "parsanol/atoms/dynamic"
|
|
41
|
+
require "parsanol/atoms/scope"
|
|
42
|
+
require "parsanol/atoms/infix"
|
|
43
43
|
# Load visitor pattern (must be after all atom classes)
|
|
44
|
-
require
|
|
44
|
+
require "parsanol/atoms/visitor"
|
|
45
45
|
end
|
|
46
46
|
end
|
|
@@ -204,7 +204,7 @@ module Parsanol
|
|
|
204
204
|
end
|
|
205
205
|
|
|
206
206
|
def on_success
|
|
207
|
-
@events <<
|
|
207
|
+
@events << "success"
|
|
208
208
|
end
|
|
209
209
|
|
|
210
210
|
def on_error(message)
|
|
@@ -228,7 +228,7 @@ module Parsanol
|
|
|
228
228
|
end
|
|
229
229
|
|
|
230
230
|
def on_nil
|
|
231
|
-
@events <<
|
|
231
|
+
@events << "nil"
|
|
232
232
|
end
|
|
233
233
|
|
|
234
234
|
def on_hash_start(size = nil)
|
data/lib/parsanol/cause.rb
CHANGED
|
@@ -113,10 +113,10 @@ module Parsanol
|
|
|
113
113
|
return if prefix_flags.size < 2
|
|
114
114
|
|
|
115
115
|
prefix_flags[1..-2].each do |is_last|
|
|
116
|
-
stream.print is_last ?
|
|
116
|
+
stream.print is_last ? " " : "| "
|
|
117
117
|
end
|
|
118
118
|
|
|
119
|
-
stream.print prefix_flags.last ?
|
|
119
|
+
stream.print prefix_flags.last ? "`- " : "|- "
|
|
120
120
|
end
|
|
121
121
|
end
|
|
122
122
|
end
|
data/lib/parsanol/context.rb
CHANGED
|
@@ -32,8 +32,8 @@ module Parsanol
|
|
|
32
32
|
#
|
|
33
33
|
# @param name [Symbol, String] method name
|
|
34
34
|
# @yield block to execute when method is called
|
|
35
|
-
def define_singleton_method(name, &
|
|
36
|
-
singleton_class.define_method(name, &
|
|
35
|
+
def define_singleton_method(name, &)
|
|
36
|
+
singleton_class.define_method(name, &)
|
|
37
37
|
end
|
|
38
38
|
end
|
|
39
39
|
end
|
|
@@ -50,7 +50,7 @@ module Parsanol
|
|
|
50
50
|
#
|
|
51
51
|
def err(atom, source, message, children = nil)
|
|
52
52
|
raise NotImplementedError,
|
|
53
|
-
|
|
53
|
+
"Error reporters must implement #err(atom, source, message, children)"
|
|
54
54
|
end
|
|
55
55
|
|
|
56
56
|
# Report an error at a specific position.
|
|
@@ -66,7 +66,7 @@ module Parsanol
|
|
|
66
66
|
#
|
|
67
67
|
def err_at(atom, source, message, pos, children = nil)
|
|
68
68
|
raise NotImplementedError,
|
|
69
|
-
|
|
69
|
+
"Error reporters must implement #err_at(atom, source, message, pos, children)"
|
|
70
70
|
end
|
|
71
71
|
|
|
72
72
|
# Called when an expression successfully parses.
|
|
@@ -95,6 +95,6 @@ module Parsanol
|
|
|
95
95
|
end
|
|
96
96
|
end
|
|
97
97
|
|
|
98
|
-
require
|
|
99
|
-
require
|
|
100
|
-
require
|
|
98
|
+
require "parsanol/error_reporter/tree"
|
|
99
|
+
require "parsanol/error_reporter/deepest"
|
|
100
|
+
require "parsanol/error_reporter/contextual"
|
|
@@ -44,7 +44,7 @@ module Parsanol
|
|
|
44
44
|
|
|
45
45
|
# Alternative: 'a' / 'b'
|
|
46
46
|
rule(:alternatives) do
|
|
47
|
-
(simple >> (spaced(
|
|
47
|
+
(simple >> (spaced("/") >> simple).repeat).as(:alt)
|
|
48
48
|
end
|
|
49
49
|
|
|
50
50
|
# Sequence by concatenation: 'a' 'b'
|
|
@@ -52,15 +52,15 @@ module Parsanol
|
|
|
52
52
|
|
|
53
53
|
# Occurrence modifiers: ?, *, +, {min,max}
|
|
54
54
|
rule(:occurrence) do
|
|
55
|
-
(atom.as(:repetition) >> spaced(
|
|
56
|
-
(atom.as(:repetition) >> spaced(
|
|
55
|
+
(atom.as(:repetition) >> spaced("*").as(:sign)) |
|
|
56
|
+
(atom.as(:repetition) >> spaced("+").as(:sign)) |
|
|
57
57
|
(atom.as(:repetition) >> repetition_spec) |
|
|
58
|
-
(atom.as(:maybe) >> spaced(
|
|
58
|
+
(atom.as(:maybe) >> spaced("?")) |
|
|
59
59
|
atom
|
|
60
60
|
end
|
|
61
61
|
|
|
62
62
|
rule(:atom) do
|
|
63
|
-
(spaced(
|
|
63
|
+
(spaced("(") >> expression.as(:unwrap) >> spaced(")")) |
|
|
64
64
|
dot |
|
|
65
65
|
string |
|
|
66
66
|
char_class
|
|
@@ -68,30 +68,30 @@ module Parsanol
|
|
|
68
68
|
|
|
69
69
|
# Character class: [a-z], [0-9], etc.
|
|
70
70
|
rule(:char_class) do
|
|
71
|
-
(str(
|
|
72
|
-
((str(
|
|
73
|
-
str(
|
|
71
|
+
(str("[") >>
|
|
72
|
+
((str("\\") >> any) | (str("]").absent? >> any)).repeat(1) >>
|
|
73
|
+
str("]")).as(:match) >> space?
|
|
74
74
|
end
|
|
75
75
|
|
|
76
76
|
# Any character: .
|
|
77
|
-
rule(:dot) { spaced(
|
|
77
|
+
rule(:dot) { spaced(".").as(:any) }
|
|
78
78
|
|
|
79
79
|
# String literal: 'hello'
|
|
80
80
|
rule(:string) do
|
|
81
81
|
str("'") >>
|
|
82
|
-
((str(
|
|
82
|
+
((str("\\") >> any) | (str("'").absent? >> any)).repeat.as(:string) >>
|
|
83
83
|
str("'") >> space?
|
|
84
84
|
end
|
|
85
85
|
|
|
86
86
|
# Repetition specification: {1,3}, {2,}, {,5}
|
|
87
87
|
rule(:repetition_spec) do
|
|
88
|
-
spaced(
|
|
89
|
-
integer.maybe.as(:min) >> spaced(
|
|
90
|
-
integer.maybe.as(:max) >> spaced(
|
|
88
|
+
spaced("{") >>
|
|
89
|
+
integer.maybe.as(:min) >> spaced(",") >>
|
|
90
|
+
integer.maybe.as(:max) >> spaced("}")
|
|
91
91
|
end
|
|
92
92
|
|
|
93
93
|
rule(:integer) do
|
|
94
|
-
match[
|
|
94
|
+
match["0-9"].repeat(1)
|
|
95
95
|
end
|
|
96
96
|
|
|
97
97
|
# Whitespace handling
|
|
@@ -115,7 +115,7 @@ module Parsanol
|
|
|
115
115
|
class Transform < Parsanol::Transform
|
|
116
116
|
# Repetition with sign: * (zero+) or + (one+)
|
|
117
117
|
rule(repetition: simple(:rep), sign: simple(:sign)) do
|
|
118
|
-
min = sign ==
|
|
118
|
+
min = sign == "+" ? 1 : 0
|
|
119
119
|
Parsanol::Atoms::Repetition.new(rep, min, nil)
|
|
120
120
|
end
|
|
121
121
|
|
|
@@ -124,7 +124,7 @@ module Parsanol
|
|
|
124
124
|
Parsanol::Atoms::Repetition.new(
|
|
125
125
|
rep,
|
|
126
126
|
Integer(min || 0),
|
|
127
|
-
(max && Integer(max)) || nil
|
|
127
|
+
(max && Integer(max)) || nil,
|
|
128
128
|
)
|
|
129
129
|
end
|
|
130
130
|
|
|
@@ -147,7 +147,7 @@ module Parsanol
|
|
|
147
147
|
rule(match: simple(:m)) { Parsanol::Atoms::Re.new("[#{m}]") }
|
|
148
148
|
|
|
149
149
|
# Any character: .
|
|
150
|
-
rule(any: simple(:_a)) { Parsanol::Atoms::Re.new(
|
|
150
|
+
rule(any: simple(:_a)) { Parsanol::Atoms::Re.new(".") }
|
|
151
151
|
end
|
|
152
152
|
end
|
|
153
153
|
end
|
data/lib/parsanol/expression.rb
CHANGED
data/lib/parsanol/fast_mode.rb
CHANGED
|
@@ -25,7 +25,10 @@ module Parsanol
|
|
|
25
25
|
unless (entry = @cache[beg]&.[](obj.object_id))
|
|
26
26
|
result = obj.try(source, self, consume_all)
|
|
27
27
|
|
|
28
|
-
|
|
28
|
+
if obj.cached?
|
|
29
|
+
(@cache[beg] ||= {})[obj.object_id] =
|
|
30
|
+
[result, source.bytepos - beg]
|
|
31
|
+
end
|
|
29
32
|
|
|
30
33
|
return result
|
|
31
34
|
end
|
|
@@ -45,13 +48,24 @@ module Parsanol
|
|
|
45
48
|
case parslets.size
|
|
46
49
|
when 1
|
|
47
50
|
success, value = parslets[0].apply(source, context, consume_all)
|
|
48
|
-
success
|
|
51
|
+
if success
|
|
52
|
+
succ([:sequence,
|
|
53
|
+
value])
|
|
54
|
+
else
|
|
55
|
+
context.err(self, source, @error_msg,
|
|
56
|
+
[value])
|
|
57
|
+
end
|
|
49
58
|
when 2
|
|
50
59
|
success, v1 = parslets[0].apply(source, context, false)
|
|
51
60
|
return context.err(self, source, @error_msg, [v1]) unless success
|
|
52
61
|
|
|
53
62
|
success, v2 = parslets[1].apply(source, context, consume_all)
|
|
54
|
-
success
|
|
63
|
+
if success
|
|
64
|
+
succ([:sequence, v1,
|
|
65
|
+
v2])
|
|
66
|
+
else
|
|
67
|
+
context.err(self, source, @error_msg, [v2])
|
|
68
|
+
end
|
|
55
69
|
when 3
|
|
56
70
|
success, v1 = parslets[0].apply(source, context, false)
|
|
57
71
|
return context.err(self, source, @error_msg, [v1]) unless success
|
|
@@ -60,13 +74,19 @@ module Parsanol
|
|
|
60
74
|
return context.err(self, source, @error_msg, [v2]) unless success
|
|
61
75
|
|
|
62
76
|
success, v3 = parslets[2].apply(source, context, consume_all)
|
|
63
|
-
success
|
|
77
|
+
if success
|
|
78
|
+
succ([:sequence, v1, v2,
|
|
79
|
+
v3])
|
|
80
|
+
else
|
|
81
|
+
context.err(self, source, @error_msg, [v3])
|
|
82
|
+
end
|
|
64
83
|
else
|
|
65
84
|
result = [:sequence]
|
|
66
85
|
last_idx = parslets.size - 1
|
|
67
86
|
i = 0
|
|
68
87
|
while i <= last_idx
|
|
69
|
-
success, value = parslets[i].apply(source, context,
|
|
88
|
+
success, value = parslets[i].apply(source, context,
|
|
89
|
+
consume_all && i == last_idx)
|
|
70
90
|
return context.err(self, source, @error_msg, [value]) unless success
|
|
71
91
|
|
|
72
92
|
result << value
|
|
@@ -100,22 +120,37 @@ module Parsanol
|
|
|
100
120
|
case max
|
|
101
121
|
when 1
|
|
102
122
|
success, value = parslet.apply(source, context, consume_all)
|
|
103
|
-
return success ? succ([tag,
|
|
123
|
+
return success ? succ([tag,
|
|
124
|
+
value]) : context.err_at(self, source,
|
|
125
|
+
@error_msg, source.bytepos, [value])
|
|
104
126
|
when 2
|
|
105
127
|
success, v1 = parslet.apply(source, context, false)
|
|
106
|
-
|
|
128
|
+
unless success
|
|
129
|
+
return context.err_at(self, source, @error_msg, source.bytepos,
|
|
130
|
+
[v1])
|
|
131
|
+
end
|
|
107
132
|
|
|
108
133
|
success, v2 = parslet.apply(source, context, consume_all)
|
|
109
|
-
return success ? succ([tag, v1,
|
|
134
|
+
return success ? succ([tag, v1,
|
|
135
|
+
v2]) : context.err_at(self, source,
|
|
136
|
+
@error_msg, source.bytepos, [v2])
|
|
110
137
|
when 3
|
|
111
138
|
success, v1 = parslet.apply(source, context, false)
|
|
112
|
-
|
|
139
|
+
unless success
|
|
140
|
+
return context.err_at(self, source, @error_msg, source.bytepos,
|
|
141
|
+
[v1])
|
|
142
|
+
end
|
|
113
143
|
|
|
114
144
|
success, v2 = parslet.apply(source, context, false)
|
|
115
|
-
|
|
145
|
+
unless success
|
|
146
|
+
return context.err_at(self, source, @error_msg, source.bytepos,
|
|
147
|
+
[v2])
|
|
148
|
+
end
|
|
116
149
|
|
|
117
150
|
success, v3 = parslet.apply(source, context, consume_all)
|
|
118
|
-
return success ? succ([tag, v1, v2,
|
|
151
|
+
return success ? succ([tag, v1, v2,
|
|
152
|
+
v3]) : context.err_at(self, source,
|
|
153
|
+
@error_msg, source.bytepos, [v3])
|
|
119
154
|
end
|
|
120
155
|
end
|
|
121
156
|
|
|
@@ -140,7 +175,10 @@ module Parsanol
|
|
|
140
175
|
return context.err_at(self, source, @error_msg, start_pos, [break_on])
|
|
141
176
|
end
|
|
142
177
|
|
|
143
|
-
|
|
178
|
+
if consume_all && source.chars_left.positive?
|
|
179
|
+
return context.err(self, source, @unconsumed_msg,
|
|
180
|
+
[break_on])
|
|
181
|
+
end
|
|
144
182
|
|
|
145
183
|
succ(result)
|
|
146
184
|
end
|
data/lib/parsanol/first_set.rb
CHANGED
|
@@ -58,7 +58,7 @@ module Parsanol
|
|
|
58
58
|
return true if real_set1.empty? || real_set2.empty?
|
|
59
59
|
|
|
60
60
|
# Check if intersection is empty (using to_a for Opal compatibility)
|
|
61
|
-
|
|
61
|
+
!real_set1.to_a.intersect?(real_set2.to_a)
|
|
62
62
|
end
|
|
63
63
|
|
|
64
64
|
# Check if all FIRST sets in a collection are mutually disjoint
|
|
@@ -72,7 +72,8 @@ module Parsanol
|
|
|
72
72
|
when Hash
|
|
73
73
|
grammar
|
|
74
74
|
else
|
|
75
|
-
raise ArgumentError,
|
|
75
|
+
raise ArgumentError,
|
|
76
|
+
"Expected GrammarBuilder or Hash, got #{grammar.class}"
|
|
76
77
|
end
|
|
77
78
|
|
|
78
79
|
@imports << { grammar: grammar_data, prefix: prefix }
|
|
@@ -92,7 +93,8 @@ module Parsanol
|
|
|
92
93
|
when Hash
|
|
93
94
|
grammar
|
|
94
95
|
else
|
|
95
|
-
raise ArgumentError,
|
|
96
|
+
raise ArgumentError,
|
|
97
|
+
"Expected GrammarBuilder or Hash, got #{grammar.class}"
|
|
96
98
|
end
|
|
97
99
|
|
|
98
100
|
@imports << { grammar: grammar_data, prefix: prefix, rules: rules }
|
|
@@ -106,7 +108,7 @@ module Parsanol
|
|
|
106
108
|
{
|
|
107
109
|
rules: @rules,
|
|
108
110
|
root: @root,
|
|
109
|
-
imports: @imports
|
|
111
|
+
imports: @imports,
|
|
110
112
|
}
|
|
111
113
|
end
|
|
112
114
|
|
|
@@ -140,7 +142,7 @@ module Parsanol
|
|
|
140
142
|
if grammar_name
|
|
141
143
|
ref("#{grammar_name}:root")
|
|
142
144
|
else
|
|
143
|
-
ref(
|
|
145
|
+
ref("root")
|
|
144
146
|
end
|
|
145
147
|
end
|
|
146
148
|
|
|
@@ -149,9 +151,9 @@ module Parsanol
|
|
|
149
151
|
#
|
|
150
152
|
# @yield [GrammarBuilder] Builder to configure
|
|
151
153
|
# @return [Hash] Built grammar
|
|
152
|
-
def build(&
|
|
154
|
+
def build(&)
|
|
153
155
|
builder = new
|
|
154
|
-
builder.instance_eval(&
|
|
156
|
+
builder.instance_eval(&)
|
|
155
157
|
builder.build
|
|
156
158
|
end
|
|
157
159
|
|
|
@@ -170,8 +172,8 @@ module Parsanol
|
|
|
170
172
|
# Create a new grammar builder
|
|
171
173
|
#
|
|
172
174
|
# @return [GrammarBuilder] New builder
|
|
173
|
-
def grammar(&
|
|
174
|
-
GrammarBuilder.build(&
|
|
175
|
+
def grammar(&)
|
|
176
|
+
GrammarBuilder.build(&)
|
|
175
177
|
end
|
|
176
178
|
end
|
|
177
179
|
end
|
|
@@ -19,7 +19,7 @@ module Parsanol
|
|
|
19
19
|
class Edit
|
|
20
20
|
attr_reader :start, :deleted, :inserted
|
|
21
21
|
|
|
22
|
-
def initialize(start:, deleted:, inserted:
|
|
22
|
+
def initialize(start:, deleted:, inserted: "")
|
|
23
23
|
@start = start
|
|
24
24
|
@deleted = deleted
|
|
25
25
|
@inserted = inserted
|
|
@@ -61,13 +61,14 @@ module Parsanol
|
|
|
61
61
|
#
|
|
62
62
|
# @param grammar [Parsanol::Parser, Parsanol::Atoms::Base] Grammar to use
|
|
63
63
|
# @param initial_input [String] Initial input string
|
|
64
|
-
def initialize(grammar, initial_input =
|
|
64
|
+
def initialize(grammar, initial_input = "")
|
|
65
65
|
@grammar = grammar
|
|
66
66
|
@input = initial_input
|
|
67
67
|
|
|
68
68
|
if Parsanol::Native.available?
|
|
69
69
|
grammar_json = Parsanol::Native.serialize_grammar(grammar.root)
|
|
70
|
-
@native_parser = Parsanol::Native.incremental_parser_new(grammar_json,
|
|
70
|
+
@native_parser = Parsanol::Native.incremental_parser_new(grammar_json,
|
|
71
|
+
initial_input)
|
|
71
72
|
else
|
|
72
73
|
@native_parser = nil
|
|
73
74
|
end
|
|
@@ -81,7 +82,7 @@ module Parsanol
|
|
|
81
82
|
# @param start [Integer] Start position of edit
|
|
82
83
|
# @param deleted [Integer] Number of characters deleted
|
|
83
84
|
# @param inserted [String] Text to insert
|
|
84
|
-
def apply_edit(start:, deleted:, inserted:
|
|
85
|
+
def apply_edit(start:, deleted:, inserted: "")
|
|
85
86
|
edit = Edit.new(start: start, deleted: deleted, inserted: inserted)
|
|
86
87
|
@edits << edit
|
|
87
88
|
|
|
@@ -93,7 +94,8 @@ module Parsanol
|
|
|
93
94
|
|
|
94
95
|
return unless @native_parser
|
|
95
96
|
|
|
96
|
-
Parsanol::Native.incremental_parser_apply_edit(@native_parser, start,
|
|
97
|
+
Parsanol::Native.incremental_parser_apply_edit(@native_parser, start,
|
|
98
|
+
deleted, inserted)
|
|
97
99
|
end
|
|
98
100
|
|
|
99
101
|
# Convenience method to apply multiple edits
|
|
@@ -119,7 +121,9 @@ module Parsanol
|
|
|
119
121
|
return @cached_result if @cached_result
|
|
120
122
|
|
|
121
123
|
if @native_parser
|
|
122
|
-
@cached_result = Parsanol::Native.incremental_parser_reparse(
|
|
124
|
+
@cached_result = Parsanol::Native.incremental_parser_reparse(
|
|
125
|
+
@native_parser, @input
|
|
126
|
+
)
|
|
123
127
|
else
|
|
124
128
|
# Pure Ruby fallback - reparse from scratch
|
|
125
129
|
root = @grammar.root
|
|
@@ -164,14 +168,15 @@ module Parsanol
|
|
|
164
168
|
#
|
|
165
169
|
# @param new_input [String, nil] Optional new initial input
|
|
166
170
|
def reset(new_input = nil)
|
|
167
|
-
@input = new_input ||
|
|
171
|
+
@input = new_input || ""
|
|
168
172
|
@edits.clear
|
|
169
173
|
@cached_result = nil
|
|
170
174
|
|
|
171
175
|
return unless @native_parser && new_input
|
|
172
176
|
|
|
173
177
|
grammar_json = Parsanol::Native.serialize_grammar(@grammar.root)
|
|
174
|
-
@native_parser = Parsanol::Native.incremental_parser_new(grammar_json,
|
|
178
|
+
@native_parser = Parsanol::Native.incremental_parser_new(grammar_json,
|
|
179
|
+
@input)
|
|
175
180
|
end
|
|
176
181
|
end
|
|
177
182
|
end
|