oo_peg 0.1.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 +7 -0
- data/LICENSE +235 -0
- data/README.md +3 -0
- data/lib/enumerable.rb +28 -0
- data/lib/oo_peg/input.rb +34 -0
- data/lib/oo_peg/ostruct.rb +8 -0
- data/lib/oo_peg/parser/class_methods.rb +33 -0
- data/lib/oo_peg/parser/combinators/lazy.rb +17 -0
- data/lib/oo_peg/parser/combinators.rb +458 -0
- data/lib/oo_peg/parser.rb +72 -0
- data/lib/oo_peg/parsers/advanced/operator_parser.rb +22 -0
- data/lib/oo_peg/parsers/advanced/sexp_parser.rb +77 -0
- data/lib/oo_peg/parsers/advanced/string_parser.rb +40 -0
- data/lib/oo_peg/parsers/advanced/symbol_parser.rb +22 -0
- data/lib/oo_peg/parsers/advanced.rb +117 -0
- data/lib/oo_peg/parsers/base_parsers.rb +252 -0
- data/lib/oo_peg/parsers/common_parsers.rb +193 -0
- data/lib/oo_peg/parsers/lispy_parser.rb +18 -0
- data/lib/oo_peg/parsers/pseudo_parsers.rb +12 -0
- data/lib/oo_peg/parsers/true_set.rb +11 -0
- data/lib/oo_peg/parsers.rb +13 -0
- data/lib/oo_peg/result.rb +88 -0
- data/lib/oo_peg/version.rb +8 -0
- data/lib/oo_peg.rb +257 -0
- metadata +91 -0
@@ -0,0 +1,458 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative '../../enumerable'
|
4
|
+
require_relative 'combinators/lazy'
|
5
|
+
module OOPeg
|
6
|
+
class Parser
|
7
|
+
class InfiniteLoop < Exception; end
|
8
|
+
|
9
|
+
##
|
10
|
+
#
|
11
|
+
# === The raison d'être of a PEG Parser are in fact the combinators
|
12
|
+
#
|
13
|
+
# So, without further ado...
|
14
|
+
#
|
15
|
+
# ==== +many+
|
16
|
+
#
|
17
|
+
# If a parser +p+ parses a string, the parser +p.many+ parses, well
|
18
|
+
# many occurrences of such strings concatenated.
|
19
|
+
#
|
20
|
+
# *N.B.* By default +many+ parses the empty input (0 occurrences)
|
21
|
+
# and can therefore _never_ _fail_!
|
22
|
+
# but we can force a minimum of occurrences.
|
23
|
+
#
|
24
|
+
# As indicated below, +many+ returns a list of all parsed occurrences.
|
25
|
+
#
|
26
|
+
# # example: many, the combinator for lists
|
27
|
+
#
|
28
|
+
# vowel_parser = char_parser('aeiouy').many
|
29
|
+
#
|
30
|
+
# parse(vowel_parser, "").ast => []
|
31
|
+
# parse(vowel_parser, "ae").ast => %w[a e]
|
32
|
+
#
|
33
|
+
# And we can force a minimum of occurrences
|
34
|
+
#
|
35
|
+
# # example: manyer, more than many
|
36
|
+
#
|
37
|
+
# two_chars = char_parser.many(min: 2)
|
38
|
+
#
|
39
|
+
# parse(two_chars, 'ab').ast => %w[a b]
|
40
|
+
# parse(two_chars, 'a').error => '"many(char_parser(TrueSet))" did not succeed the required 2 times, but only 1'
|
41
|
+
#
|
42
|
+
# # We can see that the error message lacks some context, let's try again
|
43
|
+
#
|
44
|
+
# named = char_parser.many(min: 2, name: '2 chars')
|
45
|
+
# parse(named, '').error => '"2 chars" did not succeed the required 2 times, but only 0'
|
46
|
+
#
|
47
|
+
# It is very frequently not an array of parsed strings, that we want, but the whole string, enter the
|
48
|
+
#
|
49
|
+
# ==== +joined+ combinator
|
50
|
+
#
|
51
|
+
# # example: joining together
|
52
|
+
#
|
53
|
+
# all_parser = char_parser.many.joined
|
54
|
+
#
|
55
|
+
# parse(all_parser, "some text").ast => "some text"
|
56
|
+
#
|
57
|
+
# ==== +map+ modify the result's ast
|
58
|
+
#
|
59
|
+
# In the above example we have seen, that the +map+ combinator returns a list, oftentimes we want the
|
60
|
+
# parsed string to be returned...
|
61
|
+
#
|
62
|
+
# Enter +map+
|
63
|
+
#
|
64
|
+
# # example: mapping a list back to a string
|
65
|
+
#
|
66
|
+
# vowel_string_parser = char_parser('aeiouy').many.map(&:join)
|
67
|
+
# parse(vowel_string_parser, "yo").ast => "yo"
|
68
|
+
#
|
69
|
+
# ===== Tagging, a special form of map
|
70
|
+
#
|
71
|
+
# +tagged(tag)+ is a short, convenience method for <tt>map { [tag, it] }</tt>
|
72
|
+
#
|
73
|
+
# # example: tagging a result
|
74
|
+
#
|
75
|
+
# tagged_char_parser = char_parser.tagged(:char)
|
76
|
+
#
|
77
|
+
# parse(tagged_char_parser, 'q').ast => [:char, 'q']
|
78
|
+
#
|
79
|
+
#
|
80
|
+
# ==== +maybe+ (oftentimes called +option+ in other PEG Parsers)
|
81
|
+
#
|
82
|
+
# If a parser +p+ parses a string, the parser +p.maybe+ parses, well
|
83
|
+
# 0 or 1 occurrences of such a string.
|
84
|
+
#
|
85
|
+
# *N.B.* +maybe+ parses the empty input (0 occurrences)
|
86
|
+
# and can therefore _never_ _fail_!
|
87
|
+
#
|
88
|
+
# We could also implement maybe with map as follows, but there
|
89
|
+
# is a subtle difference between <tt>[], "", nil</tt> to be observed.
|
90
|
+
#
|
91
|
+
# # example: map and maybe
|
92
|
+
#
|
93
|
+
# optional_char = char_parser.many(max: 1).map(&:join)
|
94
|
+
# maybe_char = char_parser.maybe
|
95
|
+
#
|
96
|
+
# parse(optional_char, "abc").ast => "a"
|
97
|
+
# parse(optional_char, "").ast => ""
|
98
|
+
#
|
99
|
+
# parse(maybe_char, "abc").ast => "a"
|
100
|
+
# parse(maybe_char, "").ast => nil
|
101
|
+
#
|
102
|
+
# ==== +or+ (oftentimes called +select+ in other PEG Parsers)
|
103
|
+
#
|
104
|
+
# Can be an instance method
|
105
|
+
#
|
106
|
+
# # example: or as an instance method
|
107
|
+
#
|
108
|
+
# # yet another implementation of maybe :P
|
109
|
+
#
|
110
|
+
# maybe_parser = char_parser('a').or(char_parser('b'), true_parser)
|
111
|
+
#
|
112
|
+
# parse(maybe_parser, 'a').ast => 'a'
|
113
|
+
# parse(maybe_parser, 'b').ast => 'b'
|
114
|
+
# parse(maybe_parser, 'c').ast => nil
|
115
|
+
# parse(maybe_parser, 'c') is! ok
|
116
|
+
#
|
117
|
+
# Can be a class method
|
118
|
+
#
|
119
|
+
# # example: or as a class method
|
120
|
+
#
|
121
|
+
# int_or_number_parser = OOPeg::Parser.or(int_parser, set_parser('one', 'two'))
|
122
|
+
#
|
123
|
+
# parse(int_or_number_parser, '12').ast => 12
|
124
|
+
# parse(int_or_number_parser, 'one').ast => 'one'
|
125
|
+
# parse(int_or_number_parser, 'two').ast => 'two'
|
126
|
+
#
|
127
|
+
# parse(int_or_number_parser, 'three') not! ok
|
128
|
+
#
|
129
|
+
# ==== +and+ (oftentimes called +sequence+ in other PEG Parsers)
|
130
|
+
#
|
131
|
+
# Can be an instance method
|
132
|
+
#
|
133
|
+
# # example: and as an instance method
|
134
|
+
#
|
135
|
+
# # yet another implementation of set_parser("abc")
|
136
|
+
#
|
137
|
+
# abc_parser = char_parser('a').and(char_parser('b'), char_parser('c'))
|
138
|
+
#
|
139
|
+
# parse(abc_parser, 'abc').ast => %w[a b c]
|
140
|
+
# parse(abc_parser, 'bca') not! ok
|
141
|
+
#
|
142
|
+
# Can be a class method
|
143
|
+
#
|
144
|
+
# # example: and as a class method
|
145
|
+
#
|
146
|
+
# abc_parser = OOPeg::Parser.and(char_parser('a'), char_parser('b'), char_parser('c'))
|
147
|
+
#
|
148
|
+
# parse(abc_parser, 'abc').ast => %w[a b c]
|
149
|
+
# parse(abc_parser, 'bca') not! ok
|
150
|
+
#
|
151
|
+
# Sometimes we do not want some parsed text in our AST, therefore the +.and+ combinator
|
152
|
+
# _ignores_ +nil+ ASTs.
|
153
|
+
#
|
154
|
+
# # example: ignore a sign (at your own peril of course)
|
155
|
+
#
|
156
|
+
# parser = OOPeg::Parser.and( char_parser('+').map {nil}, char_class_parser(:digit).many.joined )
|
157
|
+
#
|
158
|
+
# parse(parser, "+42").ast => ['42']
|
159
|
+
#
|
160
|
+
# As it is a little bit cumbersome to write <tt>map { nil }</tt> we made a more idiomatic way to
|
161
|
+
# express this behavior:
|
162
|
+
#
|
163
|
+
# ==== +ignore+
|
164
|
+
#
|
165
|
+
# # example: a better way of ignorance
|
166
|
+
#
|
167
|
+
# parser = OOPeg::Parser.and( char_parser('+').ignore, char_class_parser(:digit).many.joined )
|
168
|
+
#
|
169
|
+
# parse(parser, "+42").ast => ['42']
|
170
|
+
#
|
171
|
+
#
|
172
|
+
# ==== +satifsfy+ can fail a successful result does not touch failing results
|
173
|
+
#
|
174
|
+
# This, e.g. is used in the +kwd_parser+ or +set_parser+
|
175
|
+
#
|
176
|
+
# # example: an odd parser
|
177
|
+
#
|
178
|
+
# odd_parser = int_parser.satisfy(name: "oddy", &:odd?)
|
179
|
+
#
|
180
|
+
# parse(odd_parser, '11').ast => 11
|
181
|
+
# parse(odd_parser, '10') not! ok
|
182
|
+
#
|
183
|
+
# parse(odd_parser, '10').error => 'oddy failed'
|
184
|
+
#
|
185
|
+
# **N.B.** it can also modify the result
|
186
|
+
#
|
187
|
+
# even_parser = int_parser.satisfy { |n|
|
188
|
+
# n.even? ? [:ok, n/2] : [:error, "#{n}'s odd"]
|
189
|
+
# }
|
190
|
+
#
|
191
|
+
# parse(even_parser, '84').ast => 42
|
192
|
+
#
|
193
|
+
# parse(even_parser, '73').error => "73's odd"
|
194
|
+
#
|
195
|
+
# ==== Change result for succeeding *and* failing parsers: +map_or_rename+
|
196
|
+
#
|
197
|
+
# This allows us to modify the name or error message in case of failure *and* still
|
198
|
+
# transforming the ast if a block is given.
|
199
|
+
#
|
200
|
+
# # example: A better odd parser?
|
201
|
+
#
|
202
|
+
# better = int_parser.satisfy(&:odd?).map_or_rename(error: "Oh no", &:succ)
|
203
|
+
#
|
204
|
+
# parse(better, '11').ast => 12
|
205
|
+
# parse(better, '12').error => "Oh no"
|
206
|
+
#
|
207
|
+
# # example: No need to provide a block
|
208
|
+
#
|
209
|
+
# parse(char_parser.map_or_rename(name: "My Parser"), '') not! ok
|
210
|
+
#
|
211
|
+
# ==== Lookahead, an important concept...
|
212
|
+
#
|
213
|
+
# We can just check on the future \\o/
|
214
|
+
#
|
215
|
+
# # example: Look ahead, do not advance
|
216
|
+
#
|
217
|
+
# result = parse(int_parser.lookahead, '42')
|
218
|
+
#
|
219
|
+
# result.ast is! nil
|
220
|
+
# result is! ok
|
221
|
+
# result.input.pos => 1
|
222
|
+
# result.input.content => %w[4 2]
|
223
|
+
#
|
224
|
+
# # example: Look ahead, be disappointed
|
225
|
+
#
|
226
|
+
# parse(int_parser.lookahead, 'alpha') not! ok
|
227
|
+
#
|
228
|
+
# === What About Recursive Parsers?
|
229
|
+
#
|
230
|
+
# Let us assume that we want to parse a language that is recursive, how can we create
|
231
|
+
# a recursive parser that does not loop up to a stack overflow?
|
232
|
+
#
|
233
|
+
# Here is a toy example to introduce the concept (but it will come in very handy later
|
234
|
+
# in our advanced OOPeg::Parsers::Advanced::SexpParser .
|
235
|
+
#
|
236
|
+
# ==== A parser for the language <tt>a^n • b^n</tt>, the naïve way.
|
237
|
+
#
|
238
|
+
#
|
239
|
+
# # example: Look Ma' a stack overflow
|
240
|
+
#
|
241
|
+
# $ab_count = 0
|
242
|
+
# def ab_parser
|
243
|
+
# $ab_count += 1
|
244
|
+
# return true_parser if $ab_count == 1_000
|
245
|
+
# char_parser('a').and(ab_parser, char_parser('b')).or(true_parser)
|
246
|
+
# end
|
247
|
+
#
|
248
|
+
# parse(ab_parser, 'aabb')
|
249
|
+
#
|
250
|
+
# $ab_count => 1_000
|
251
|
+
#
|
252
|
+
# This problem can be solved by delaying the recursively called parser, which is done,
|
253
|
+
# with the aptly named:
|
254
|
+
#
|
255
|
+
# ==== +delay+ combinator
|
256
|
+
#
|
257
|
+
# # example: Look Ma' it's working now
|
258
|
+
#
|
259
|
+
# $ab_count = 0
|
260
|
+
# def ab_parser
|
261
|
+
# $ab_count += 1
|
262
|
+
# return true_parser if $ab_count == 1_000
|
263
|
+
# char_parser('a').and(delay {ab_parser}, char_parser('b')).joined.or(true_parser)
|
264
|
+
# end
|
265
|
+
#
|
266
|
+
# parse(ab_parser, 'aabb').ast => "aabb"
|
267
|
+
#
|
268
|
+
# $ab_count => 3
|
269
|
+
#
|
270
|
+
module Combinators
|
271
|
+
|
272
|
+
private
|
273
|
+
def _debug(parser, name: nil)
|
274
|
+
Parser.new(parser.name) do |input|
|
275
|
+
puts "debugging #{name || parser.name}: #{input.inspect}"
|
276
|
+
result = Parser.parse(parser, input)
|
277
|
+
puts "debugging #{name || parser.name}: #{result}"
|
278
|
+
result
|
279
|
+
end
|
280
|
+
end
|
281
|
+
|
282
|
+
def _lookahead(parsers, name:)
|
283
|
+
parser = _select(parsers)
|
284
|
+
Parser.new(name || "lookahead(#{parser.name})") do |input, name|
|
285
|
+
case Parser.parse(parser, input)
|
286
|
+
in {ok: true}
|
287
|
+
{ok: true, ast: nil, input:}
|
288
|
+
in error
|
289
|
+
error
|
290
|
+
end
|
291
|
+
end
|
292
|
+
end
|
293
|
+
|
294
|
+
def _map(parser:, name:, &mapper)
|
295
|
+
raise ArgumentError, "missing mapper function" unless mapper
|
296
|
+
|
297
|
+
name ||= "map(#{parser.name})"
|
298
|
+
Parser.new(name) do |input|
|
299
|
+
result = Parser.parse(parser, input)
|
300
|
+
# require "debug"; binding.break
|
301
|
+
result.map(&mapper)
|
302
|
+
# require "debug"; binding.break
|
303
|
+
# case result
|
304
|
+
# in {ok: true, input: rest, ast:}
|
305
|
+
# Result.ok(ast: mapper.(ast), input: rest)
|
306
|
+
# in {error:}
|
307
|
+
# Result.nok(error:, input:, parser_name: name)
|
308
|
+
# end
|
309
|
+
end
|
310
|
+
end
|
311
|
+
|
312
|
+
def _many(parser, max:, min:, name:)
|
313
|
+
name ||= "many(#{parser.name})"
|
314
|
+
Parser.new(name) do |input|
|
315
|
+
total_ast = []
|
316
|
+
original_input = input
|
317
|
+
current_input = input
|
318
|
+
match_count = 0
|
319
|
+
loop do
|
320
|
+
if current_input.empty?
|
321
|
+
break Result.ok(ast: total_ast, input:) if match_count >= min
|
322
|
+
break Result.nok(
|
323
|
+
error: "#{name.inspect} did not succeed the required #{min} times, but only #{match_count}",
|
324
|
+
input: original_input,
|
325
|
+
parser_name: name)
|
326
|
+
end
|
327
|
+
|
328
|
+
case Parser.parse(parser, current_input)
|
329
|
+
in {ok: true, ast:, input:}
|
330
|
+
raise InfiniteLoop, "must not parse zero width inside many in parser: #{parser.name}" if input.pos == current_input.pos
|
331
|
+
current_input = input
|
332
|
+
total_ast = [*total_ast, ast]
|
333
|
+
match_count += 1
|
334
|
+
break Result.ok(ast: total_ast, input:) if max && match_count >= max
|
335
|
+
in _
|
336
|
+
break Result.ok(ast: total_ast, input:) if match_count >= min
|
337
|
+
break Result.nok(
|
338
|
+
error: "many #{name} did not succeed the required #{min} times, but only #{match_count}",
|
339
|
+
input: original_input,
|
340
|
+
parser_name: name)
|
341
|
+
end
|
342
|
+
end
|
343
|
+
end
|
344
|
+
end
|
345
|
+
|
346
|
+
def _map_or_rename(parser:, name: nil, error: nil, &mapper)
|
347
|
+
parser_name = name || parser.name
|
348
|
+
Parser.new(name) do |input|
|
349
|
+
result = Parser.parse(parser, input)
|
350
|
+
case result
|
351
|
+
in {ok: true, ast:}
|
352
|
+
mapper ? result.merge(ast: mapper.(ast)) : result
|
353
|
+
in _
|
354
|
+
result.merge(parser_name:, error:)
|
355
|
+
end
|
356
|
+
end
|
357
|
+
end
|
358
|
+
|
359
|
+
def _map_result(parser, name, mapper)
|
360
|
+
Parser.new(name) do |input, name|
|
361
|
+
result = Parser.parse(parser, input)
|
362
|
+
# require "debug"; binding.break
|
363
|
+
mapper.(result)
|
364
|
+
end
|
365
|
+
end
|
366
|
+
|
367
|
+
def _maybe(parser:, name: nil, replace_with: nil)
|
368
|
+
Parser.new(name || "maybe(#{parser.name})") do |input, name|
|
369
|
+
case Parser.parse(parser, input)
|
370
|
+
in {ok: false}
|
371
|
+
Result.ok(ast: replace_with, input:)
|
372
|
+
in success
|
373
|
+
success
|
374
|
+
end
|
375
|
+
end
|
376
|
+
end
|
377
|
+
|
378
|
+
def _satisfy(parser, name:, &satisfier)
|
379
|
+
name ||= "satisfy(#{parser.name})"
|
380
|
+
|
381
|
+
Parser.new(name) do |input|
|
382
|
+
original_input = input
|
383
|
+
case Parser.parse(parser, input)
|
384
|
+
in {ok: false} => error
|
385
|
+
error
|
386
|
+
in {ok: true, ast:, input:} => result
|
387
|
+
case satisfier.(ast)
|
388
|
+
in true
|
389
|
+
result
|
390
|
+
in [:ok, ast]
|
391
|
+
Result.ok(ast:, input:)
|
392
|
+
in [:error, error]
|
393
|
+
Result.nok(input: original_input, error:, parser_name: name)
|
394
|
+
in _
|
395
|
+
Result.nok(input: original_input, error: "#{name} failed", parser_name: name)
|
396
|
+
end
|
397
|
+
end
|
398
|
+
end
|
399
|
+
end
|
400
|
+
|
401
|
+
def _select(*parsers, name: nil)
|
402
|
+
parsers = parsers.flatten
|
403
|
+
name ||= "select(#{parsers.map(&:name).join(",")})"
|
404
|
+
raise ArgumentError, "all parsers must be instances of Parser" unless parsers.all? { Parser === it }
|
405
|
+
Parser.new(name || "select #{parsers.map(&:name).join(", ")}") do |input, name|
|
406
|
+
result = Result.nok(error: "No parser matched in select named #{name}", input:, parser_name: name)
|
407
|
+
parsers.each do |parser|
|
408
|
+
# case p(Parser.parse(parser, input))
|
409
|
+
this_result = Parser.parse(parser, input)
|
410
|
+
# require "debug"; binding.break
|
411
|
+
case this_result
|
412
|
+
in {ok: true} => result
|
413
|
+
break result
|
414
|
+
in _
|
415
|
+
nil
|
416
|
+
end
|
417
|
+
end
|
418
|
+
result
|
419
|
+
end
|
420
|
+
end
|
421
|
+
|
422
|
+
def _sequence(*parsers, name:)
|
423
|
+
parsers = parsers.flatten
|
424
|
+
raise ArgumentError, "all parsers must be instances of Parser" unless parsers.all? { Parser === it }
|
425
|
+
name ||= "seq(#{parsers.map(&:name).join(", ")})"
|
426
|
+
Parser.new(name) do |input|
|
427
|
+
original_input = input
|
428
|
+
result = parsers.reduce_while [input, []] do |(input, ast), parser|
|
429
|
+
# require "debug"; binding.break
|
430
|
+
parsed = Parser.parse(parser, input)
|
431
|
+
# p parsed
|
432
|
+
case parsed
|
433
|
+
in {ok: true, ast: nil, input:}
|
434
|
+
cont_reduce([input, ast])
|
435
|
+
in {ok: true, ast: ast_node, input:}
|
436
|
+
cont_reduce([input, [*ast, ast_node]])
|
437
|
+
in {ok: false, error:}
|
438
|
+
halt_reduce(Result.nok(input: original_input, error:, parser_name: name))
|
439
|
+
end
|
440
|
+
end
|
441
|
+
|
442
|
+
case result
|
443
|
+
in {ok: false} => error
|
444
|
+
result
|
445
|
+
in [input, ast]
|
446
|
+
Result.ok(ast:, input:)
|
447
|
+
end
|
448
|
+
end
|
449
|
+
end
|
450
|
+
|
451
|
+
def _tagged(tag, parser:, name:)
|
452
|
+
name ||= "tagged(#{parser.name} with #{tag.inspect})"
|
453
|
+
_map(parser:, name:) { [tag, it] }
|
454
|
+
end
|
455
|
+
end
|
456
|
+
end
|
457
|
+
end
|
458
|
+
# SPDX-License-Identifier: AGPL-3.0-or-later
|
@@ -0,0 +1,72 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative 'parser/class_methods'
|
4
|
+
require_relative 'parser/combinators'
|
5
|
+
require_relative 'input'
|
6
|
+
require_relative 'parsers'
|
7
|
+
require_relative 'result'
|
8
|
+
|
9
|
+
module OOPeg
|
10
|
+
##
|
11
|
+
#
|
12
|
+
# This class represents a, well, parser
|
13
|
+
#
|
14
|
+
# It exposes a class method +parse+ which takes an input or a string (making it to
|
15
|
+
# an instance of +OOPeg::Input+ if necessary) and returns a +OOPeg::Result+
|
16
|
+
#
|
17
|
+
# It also exposes an homonymous instance method which *must* be called with an
|
18
|
+
# instance of +OOPeg::Input+
|
19
|
+
#
|
20
|
+
# # example: the parse methods
|
21
|
+
#
|
22
|
+
# expect(OOPeg::Parser.parse(end_parser, "")).to be_a OOPeg::Result
|
23
|
+
#
|
24
|
+
# # but for efficeny there is no check or conversion in the instance
|
25
|
+
# # method
|
26
|
+
#
|
27
|
+
# expect { end_parser.parse("") }
|
28
|
+
# .to raise_error(NoMethodError, "undefined method 'content' for an instance of String")
|
29
|
+
#
|
30
|
+
class Parser
|
31
|
+
include Combinators
|
32
|
+
extend Combinators::Lazy
|
33
|
+
extend ClassMethods
|
34
|
+
|
35
|
+
attr_reader :name
|
36
|
+
|
37
|
+
|
38
|
+
def parse(input) = @parse_fn.(input)
|
39
|
+
|
40
|
+
# Combinators
|
41
|
+
#
|
42
|
+
def and(*parsers, name: nil) = _sequence([self, *parsers], name:)
|
43
|
+
|
44
|
+
def debug(name: nil) = _debug(self, name:)
|
45
|
+
|
46
|
+
def ignore = self.map { nil }
|
47
|
+
|
48
|
+
def joined(joiner: "") = self.map { |ast| ast.join(joiner) }
|
49
|
+
|
50
|
+
def lookahead(name: nil) = _lookahead(self, name:)
|
51
|
+
|
52
|
+
def many(name: nil, max: nil, min: 0) = _many(self, name:, max:, min:)
|
53
|
+
def map(name: nil, &blk) = _map(parser: self, name:, &blk)
|
54
|
+
def map_or_rename(name: nil, error: nil, &blk) = _map_or_rename(parser: self, name:, error:, &blk)
|
55
|
+
def maybe(name: nil, replace_with: nil) = _maybe(parser: self, name:, replace_with:)
|
56
|
+
|
57
|
+
def or(*parsers, name: nil) = _select(self, *parsers, name:)
|
58
|
+
|
59
|
+
def satisfy(name: nil, &satisfier) = _satisfy(self, name:, &satisfier)
|
60
|
+
|
61
|
+
def tagged(tag, name: nil) = _tagged(tag, parser: self, name:)
|
62
|
+
|
63
|
+
private
|
64
|
+
def initialize(name, &blk)
|
65
|
+
raise ArgumentError, "blk must be provided as a parse function" unless blk
|
66
|
+
@name = name
|
67
|
+
@parse_fn = blk
|
68
|
+
end
|
69
|
+
|
70
|
+
end
|
71
|
+
end
|
72
|
+
# SPDX-License-Identifier: AGPL-3.0-or-later
|
@@ -0,0 +1,22 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module OOPeg
|
4
|
+
module Parsers
|
5
|
+
module Advanced
|
6
|
+
module OperatorParser extend self
|
7
|
+
include OOPeg::Parsers
|
8
|
+
|
9
|
+
DEFAULT_OP_CHARS = "><+-*/=:.&|^~!"
|
10
|
+
|
11
|
+
def make(allowed: nil, min: 1, max: 3, name: nil)
|
12
|
+
allowed ||= DEFAULT_OP_CHARS
|
13
|
+
name ||= "OperatorParser"
|
14
|
+
char_parser(allowed)
|
15
|
+
.many(min:, max:, name:)
|
16
|
+
.joined(&:to_sym)
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
22
|
+
# SPDX-License-Identifier: AGPL-3.0-or-later
|
@@ -0,0 +1,77 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module OOPeg
|
4
|
+
module Parsers
|
5
|
+
module Advanced
|
6
|
+
module SexpParser extend self
|
7
|
+
include OOPeg::Parsers
|
8
|
+
include Advanced
|
9
|
+
|
10
|
+
def make(
|
11
|
+
parens: [:sexp, %w[( )], :map, %w[{ }], :arr, %w[[ ]]],
|
12
|
+
head_parser: nil,
|
13
|
+
tail_parser: nil,
|
14
|
+
sep_parser: ws_parser,
|
15
|
+
name: nil)
|
16
|
+
|
17
|
+
name ||= "SexpParser"
|
18
|
+
tail_parser ||= _default_tail_parser
|
19
|
+
head_parser ||= tail_parser
|
20
|
+
|
21
|
+
ws_parser(min: 0)
|
22
|
+
.and(
|
23
|
+
Parser.or(
|
24
|
+
parens
|
25
|
+
.each_slice(2)
|
26
|
+
.map(&_sexp_parser(head_parser:, tail_parser:, sep_parser:))
|
27
|
+
)).map(&:first)
|
28
|
+
end
|
29
|
+
|
30
|
+
private
|
31
|
+
def _default_tail_parser
|
32
|
+
Parser.or(
|
33
|
+
id_parser.tagged(:id),
|
34
|
+
int_parser.tagged(:int),
|
35
|
+
string_parser.tagged(:str),
|
36
|
+
symbol_parser.tagged(:sym),
|
37
|
+
operator_parser.tagged(:op),
|
38
|
+
delay {sexp_parser}
|
39
|
+
)
|
40
|
+
end
|
41
|
+
|
42
|
+
def _head_and_tail
|
43
|
+
-> ast do
|
44
|
+
ast => [h, t]
|
45
|
+
[h, *t]
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
def _inner_parser(head_parser:, tail_parser:, sep_parser:)
|
50
|
+
Parser.and(
|
51
|
+
head_parser,
|
52
|
+
sep_parser.and(tail_parser).many.map { it.map(&:first) },
|
53
|
+
name: 'inner s-exp parser'
|
54
|
+
)
|
55
|
+
.map(&_head_and_tail)
|
56
|
+
.maybe(replace_with: [])
|
57
|
+
end
|
58
|
+
|
59
|
+
def _sexp_parser(head_parser:, tail_parser:, sep_parser:)
|
60
|
+
-> parens do
|
61
|
+
parens => [tag, parsers]
|
62
|
+
parsers.map { char_parser(it).ignore } => [open, close]
|
63
|
+
|
64
|
+
Parser.and(
|
65
|
+
open.and(ws_parser(min: 0)).ignore,
|
66
|
+
_inner_parser(head_parser:, tail_parser:, sep_parser:)
|
67
|
+
.tagged(tag),
|
68
|
+
ws_parser(min: 0).and(close).ignore
|
69
|
+
)
|
70
|
+
# .debug
|
71
|
+
end
|
72
|
+
end
|
73
|
+
end
|
74
|
+
end
|
75
|
+
end
|
76
|
+
end
|
77
|
+
# SPDX-License-Identifier: AGPL-3.0-or-later
|
@@ -0,0 +1,40 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module OOPeg
|
4
|
+
module Parsers
|
5
|
+
module Advanced
|
6
|
+
module StringParser extend self
|
7
|
+
include OOPeg::Parsers
|
8
|
+
|
9
|
+
def make(delim: %{'"}, doubled_escape: nil, extra_parser: nil, escape_with: "\\", name: "StringParser")
|
10
|
+
parser =
|
11
|
+
Parser.or(
|
12
|
+
delim.grapheme_clusters.map { |quote| make_delim_parser(quote, doubled_escape:, extra_parser:, escape_with:) }
|
13
|
+
)
|
14
|
+
parser.map_or_rename(error: "Missing closing delimiter", name:) { |ast| ast[1].join }
|
15
|
+
end
|
16
|
+
|
17
|
+
private
|
18
|
+
|
19
|
+
def make_delim_parser(delim, doubled_escape:, extra_parser:, escape_with:)
|
20
|
+
char_parser(delim)
|
21
|
+
.and(
|
22
|
+
inner_parser(delim, doubled_escape:, extra_parser:, escape_with:),
|
23
|
+
char_parser(delim)
|
24
|
+
)
|
25
|
+
end
|
26
|
+
|
27
|
+
def inner_parser(delim, doubled_escape:, extra_parser:, escape_with:)
|
28
|
+
parsers = [
|
29
|
+
(doubled_escape && word_parser(delim + delim).map { delim }),
|
30
|
+
word_parser(escape_with + delim).map { delim },
|
31
|
+
word_parser(escape_with + escape_with).map { escape_with },
|
32
|
+
char_parser(delim, negate: true)
|
33
|
+
].compact
|
34
|
+
Parser.or(parsers).many
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
40
|
+
# SPDX-License-Identifier: AGPL-3.0-or-later
|
@@ -0,0 +1,22 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module OOPeg
|
4
|
+
module Parsers
|
5
|
+
module Advanced
|
6
|
+
module SymbolParser extend self
|
7
|
+
include OOPeg::Parsers
|
8
|
+
|
9
|
+
def make(prefix: %{:}, inner_class: [:alnum, '_'], lead_class: :alpha, name: nil)
|
10
|
+
name ||= "SymbolParser"
|
11
|
+
Parser
|
12
|
+
.and(
|
13
|
+
char_parser(prefix).ignore,
|
14
|
+
id_parser(inner_class:, lead_class:, name:)
|
15
|
+
)
|
16
|
+
.map { it.first.to_sym }
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
22
|
+
# SPDX-License-Identifier: AGPL-3.0-or-later
|