paco 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/CHANGELOG.md +22 -0
- data/LICENSE.txt +21 -0
- data/README.md +42 -0
- data/bin/console +15 -0
- data/bin/setup +8 -0
- data/lib/paco/combinators/char.rb +206 -0
- data/lib/paco/combinators.rb +183 -0
- data/lib/paco/context.rb +40 -0
- data/lib/paco/parse_error.rb +29 -0
- data/lib/paco/parser.rb +168 -0
- data/lib/paco/version.rb +5 -0
- data/lib/paco.rb +17 -0
- metadata +60 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: 874b690b05bae4b9627aab0da0d248b020de705fbbbc88be1dc6e08afdeb8bc5
|
4
|
+
data.tar.gz: 4d69df9a10c651470acc62b297c0c628ab80afa2f508c3f0588c24e811b14000
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 6c5ed079f9a70a4eb11dc754ed24b59d1b50ee9e5a29baf36ab9f61cb040c6609284f8d4b9d6f3044d1ab26e8617a349b4b3811026c97b87c731d423422576d5
|
7
|
+
data.tar.gz: b1afa018f1646c3421e21d01be36cd052468cb824843dc66dd38cabd92a92aee734470a08fd5b169c5bbdadab196440543e1e14419322079e50cb0b31b82fd49
|
data/CHANGELOG.md
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
# Changelog
|
2
|
+
|
3
|
+
All notable changes to this project will be documented in this file.
|
4
|
+
|
5
|
+
The format is based on [Keep a Changelog],
|
6
|
+
and this project adheres to [Semantic Versioning].
|
7
|
+
|
8
|
+
## [Unreleased]
|
9
|
+
|
10
|
+
## [0.1.0]
|
11
|
+
|
12
|
+
### Added
|
13
|
+
|
14
|
+
- Initial implementation. ([@skryukov])
|
15
|
+
|
16
|
+
[@skryukov]: https://github.com/skryukov
|
17
|
+
|
18
|
+
[Unreleased]: https://github.com/skryukov/paco/compare/v0.1.0...HEAD
|
19
|
+
[0.1.0]: https://github.com/skryukov/paco/commits/v0.1.0
|
20
|
+
|
21
|
+
[Keep a Changelog]: https://keepachangelog.com/en/1.0.0/
|
22
|
+
[Semantic Versioning]: https://semver.org/spec/v2.0.0.html
|
data/LICENSE.txt
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
The MIT License (MIT)
|
2
|
+
|
3
|
+
Copyright (c) 2021 Svyatoslav Kryukov
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
7
|
+
in the Software without restriction, including without limitation the rights
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
10
|
+
furnished to do so, subject to the following conditions:
|
11
|
+
|
12
|
+
The above copyright notice and this permission notice shall be included in
|
13
|
+
all copies or substantial portions of the Software.
|
14
|
+
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
21
|
+
THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,42 @@
|
|
1
|
+
# Paco
|
2
|
+
|
3
|
+
Paco is a parser combinator library inspired by Haskell's [Parsec] and [Parsimmon].
|
4
|
+
|
5
|
+
## Installation
|
6
|
+
|
7
|
+
Add this line to your application's Gemfile:
|
8
|
+
|
9
|
+
```ruby
|
10
|
+
gem "paco"
|
11
|
+
```
|
12
|
+
|
13
|
+
And then execute:
|
14
|
+
|
15
|
+
$ bundle install
|
16
|
+
|
17
|
+
Or install it yourself as:
|
18
|
+
|
19
|
+
$ gem install paco
|
20
|
+
|
21
|
+
## Usage
|
22
|
+
|
23
|
+
See [API documentation](docs/paco.md), [examples](examples) and [specs](spec).
|
24
|
+
|
25
|
+
## Development
|
26
|
+
|
27
|
+
After checking out the repo, run `bin/setup` to install dependencies. Then, run `bundle exec rspec` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
|
28
|
+
|
29
|
+
To install this gem onto your local machine, run `bundle exec rake install`.
|
30
|
+
|
31
|
+
## Contributing
|
32
|
+
|
33
|
+
Bug reports and pull requests are welcome on GitHub at https://github.com/skryukov/paco.
|
34
|
+
|
35
|
+
## License
|
36
|
+
|
37
|
+
The gem is available as open source under the terms of the [MIT License].
|
38
|
+
|
39
|
+
[MIT License]: https://opensource.org/licenses/MIT
|
40
|
+
[Parsec]: https://github.com/haskell/parsec
|
41
|
+
[Parsimmon]: https://github.com/jneen/parsimmon
|
42
|
+
[parsby]: https://github.com/jolmg/parsby
|
data/bin/console
ADDED
@@ -0,0 +1,15 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
require "bundler/setup"
|
5
|
+
require "paco"
|
6
|
+
|
7
|
+
# You can add fixtures and/or initialization code here to make experimenting
|
8
|
+
# with your gem easier. You can also use a different console, if you like.
|
9
|
+
|
10
|
+
# (If you use this, don't forget to add pry to your Gemfile!)
|
11
|
+
# require "pry"
|
12
|
+
# Pry.start
|
13
|
+
|
14
|
+
require "irb"
|
15
|
+
IRB.start(__FILE__)
|
data/bin/setup
ADDED
@@ -0,0 +1,206 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Paco
|
4
|
+
module Combinators
|
5
|
+
module Char
|
6
|
+
# Returns a parser that returns a single character if passed block result is truthy:
|
7
|
+
#
|
8
|
+
# @example
|
9
|
+
# lower = Combinators.satisfy do |char|
|
10
|
+
# char == char.downcase
|
11
|
+
# end
|
12
|
+
#
|
13
|
+
# lower.parse("a") #=> "a"
|
14
|
+
# lower.parse("P") #=> ParseError
|
15
|
+
#
|
16
|
+
# @param [String] desc optional description for the parser
|
17
|
+
# @param [Proc] block proc with one argument – a next char of the input
|
18
|
+
# @return [Paco::Parser]
|
19
|
+
def satisfy(desc = "", &block)
|
20
|
+
Parser.new(desc) do |ctx, parser|
|
21
|
+
parser.failure(ctx) if ctx.eof?
|
22
|
+
|
23
|
+
char = ctx.read(1)
|
24
|
+
parser.failure(ctx) unless block.call(char)
|
25
|
+
|
26
|
+
ctx.pos += 1
|
27
|
+
char
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
# Returns a parser that looks for a passed `matcher` string and returns its value on success.
|
32
|
+
# @param [String] matcher
|
33
|
+
# @return [Paco::Parser]
|
34
|
+
def string(matcher)
|
35
|
+
Parser.new(matcher) do |ctx, parser|
|
36
|
+
src = ctx.read(matcher.length)
|
37
|
+
parser.failure(ctx) if src != matcher
|
38
|
+
|
39
|
+
ctx.pos += matcher.length
|
40
|
+
src
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
# Returns a parser that looks for a match to the regexp and returns the entire text matched.
|
45
|
+
# The regexp will always match starting at the current parse location.
|
46
|
+
# When `group` is specified, it returns only the text in the specific regexp match group.
|
47
|
+
# @param [Regexp] regexp
|
48
|
+
# @return [Paco::Parser]
|
49
|
+
def regexp(regexp, group: 0)
|
50
|
+
anchored_regexp = Regexp.new("^(?:#{regexp.source})", regexp.options)
|
51
|
+
Parser.new(regexp.inspect) do |ctx, parser|
|
52
|
+
match = anchored_regexp.match(ctx.read_all)
|
53
|
+
parser.failure(ctx) if match.nil?
|
54
|
+
|
55
|
+
ctx.pos += match[0].length
|
56
|
+
match[group]
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
# Returns a parser that checks current character against the passed `regexp`
|
61
|
+
# @param [Regexp] regexp
|
62
|
+
# @return [Paco::Parser]
|
63
|
+
def regexp_char(regexp)
|
64
|
+
satisfy(regexp.inspect) { |char| regexp.match?(char) }
|
65
|
+
end
|
66
|
+
|
67
|
+
# Returns a parser that looks for exactly one character from passed
|
68
|
+
# `matcher`, and returns its value on success.
|
69
|
+
# @param [String, Array<String>] matcher
|
70
|
+
# @return [Paco::Parser]
|
71
|
+
def one_of(matcher)
|
72
|
+
satisfy(matcher.to_s) { |char| matcher.include?(char) }
|
73
|
+
end
|
74
|
+
|
75
|
+
# Returns a parser that looks for exactly one character _NOT_ from passed
|
76
|
+
# `matcher`, and returns its value on success.
|
77
|
+
# @param [String, Array<String>] matcher
|
78
|
+
# @return [Paco::Parser]
|
79
|
+
def none_of(matcher)
|
80
|
+
satisfy("not #{matcher}") { |char| !matcher.include?(char) }
|
81
|
+
end
|
82
|
+
|
83
|
+
# Returns a parser that consumes and returns the next character of the input.
|
84
|
+
# @return [Paco::Parser]
|
85
|
+
def any_char
|
86
|
+
memoize { satisfy("any_char") { |ch| ch.length > 0 } }
|
87
|
+
end
|
88
|
+
|
89
|
+
# Returns a parser that consumes and returns the entire remainder of the input.
|
90
|
+
# @return [Paco::Parser]
|
91
|
+
def remainder
|
92
|
+
memoize do
|
93
|
+
Parser.new("remainder of the input") do |ctx, parser|
|
94
|
+
result = ctx.read_all
|
95
|
+
ctx.pos += result.length
|
96
|
+
result
|
97
|
+
end
|
98
|
+
end
|
99
|
+
end
|
100
|
+
|
101
|
+
# Returns a parser that returns a string containing all the next
|
102
|
+
# characters that are truthy for the passed block.
|
103
|
+
# @param [Proc] block proc with one argument – a next char of the input
|
104
|
+
# @return [Paco::Parser]
|
105
|
+
def take_while(&block)
|
106
|
+
satisfy(&block).many.join
|
107
|
+
end
|
108
|
+
|
109
|
+
# Returns a parser that matches end of file and returns nil.
|
110
|
+
# @return [Paco::Parser]
|
111
|
+
def eof
|
112
|
+
memoize do
|
113
|
+
Parser.new("end of file") do |ctx, parser|
|
114
|
+
parser.failure(ctx) unless ctx.eof?
|
115
|
+
nil
|
116
|
+
end
|
117
|
+
end
|
118
|
+
end
|
119
|
+
|
120
|
+
# Returns a parser that checks for the "carriage return" (`\r`) character.
|
121
|
+
# @return [Paco::Parser]
|
122
|
+
def cr
|
123
|
+
memoize { string("\r") }
|
124
|
+
end
|
125
|
+
|
126
|
+
# Returns a parser that checks for the "line feed" (`\n`) character.
|
127
|
+
# @return [Paco::Parser]
|
128
|
+
def lf
|
129
|
+
memoize { string("\n") }
|
130
|
+
end
|
131
|
+
|
132
|
+
# Returns a parser that checks for the "carriage return" character followed by the "line feed" character (`\r\n`).
|
133
|
+
# @return [Paco::Parser]
|
134
|
+
def crlf
|
135
|
+
memoize { string("\r\n") }
|
136
|
+
end
|
137
|
+
|
138
|
+
# Returns a parser that will match any kind of line ending.
|
139
|
+
# @return [Paco::Parser]
|
140
|
+
def newline
|
141
|
+
memoize { alt(crlf, lf, cr) }
|
142
|
+
end
|
143
|
+
|
144
|
+
# Returns a parser that will match any kind of line ending *including* end of file.
|
145
|
+
# @return [Paco::Parser]
|
146
|
+
def end_of_line
|
147
|
+
memoize { alt(newline, eof) }
|
148
|
+
end
|
149
|
+
|
150
|
+
# Alias for `Paco::Combinators.regexp(/[a-z]/i)`.
|
151
|
+
# @return [Paco::Parser]
|
152
|
+
def letter
|
153
|
+
memoize { regexp_char(/[a-z]/i) }
|
154
|
+
end
|
155
|
+
|
156
|
+
# Alias for `Paco::Combinators.regexp(/[a-z]+/i)`.
|
157
|
+
# @return [Paco::Parser]
|
158
|
+
def letters
|
159
|
+
memoize { seq(letter, letter.many).fmap { |x| x.flatten.join } }
|
160
|
+
end
|
161
|
+
|
162
|
+
# Alias for `Paco::Combinators.regexp(/[a-z]*/i)`.
|
163
|
+
# @return [Paco::Parser]
|
164
|
+
def opt_letters
|
165
|
+
memoize { letters | succeed("") }
|
166
|
+
end
|
167
|
+
|
168
|
+
# Alias for `Paco::Combinators.regexp(/[0-9]/)`.
|
169
|
+
# @return [Paco::Parser]
|
170
|
+
def digit
|
171
|
+
memoize { regexp_char(/[0-9]/) }
|
172
|
+
end
|
173
|
+
|
174
|
+
# Alias for `Paco::Combinators.regexp(/[0-9]+/)`.
|
175
|
+
# @return [Paco::Parser]
|
176
|
+
def digits
|
177
|
+
memoize { seq(digit, digit.many).fmap { |x| x.flatten.join } }
|
178
|
+
end
|
179
|
+
|
180
|
+
# Alias for `Paco::Combinators.regexp(/[0-9]*/)`.
|
181
|
+
# @return [Paco::Parser]
|
182
|
+
def opt_digits
|
183
|
+
memoize { digits | succeed("") }
|
184
|
+
end
|
185
|
+
|
186
|
+
# Alias for `Paco::Combinators.regexp(/\s+/)`.
|
187
|
+
# @return [Paco::Parser]
|
188
|
+
def ws
|
189
|
+
memoize { regexp(/\s+/) }
|
190
|
+
end
|
191
|
+
|
192
|
+
# Alias for `Paco::Combinators.regexp(/\s*/)`.
|
193
|
+
# @return [Paco::Parser]
|
194
|
+
def opt_ws
|
195
|
+
memoize { regexp(/\s*/) }
|
196
|
+
end
|
197
|
+
|
198
|
+
# Alias for `parser.trim(Paco::Combinators.opt_ws)`.
|
199
|
+
# @param [Paco::Parser] parser
|
200
|
+
# @return [Paco::Parser]
|
201
|
+
def spaced(parser)
|
202
|
+
parser.trim(opt_ws)
|
203
|
+
end
|
204
|
+
end
|
205
|
+
end
|
206
|
+
end
|
@@ -0,0 +1,183 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "monitor"
|
4
|
+
|
5
|
+
require "paco/combinators/char"
|
6
|
+
|
7
|
+
module Paco
|
8
|
+
module Combinators
|
9
|
+
def self.extended(base)
|
10
|
+
base.extend MonitorMixin
|
11
|
+
base.extend Char
|
12
|
+
end
|
13
|
+
|
14
|
+
def self.included(base)
|
15
|
+
base.include MonitorMixin
|
16
|
+
base.include Char
|
17
|
+
end
|
18
|
+
|
19
|
+
extend self
|
20
|
+
|
21
|
+
# Returns a parser that runs the passed `parser` without consuming the input, and
|
22
|
+
# returns `null` if the passed `parser` _does not match_ the input. Fails otherwise.
|
23
|
+
# @param [Paco::Parser] parser
|
24
|
+
# @return [Paco::Parser]
|
25
|
+
def not_followed_by(parser)
|
26
|
+
Parser.new("not #{parser.desc}") do |ctx, pars|
|
27
|
+
start_pos = ctx.pos
|
28
|
+
begin
|
29
|
+
parser._parse(ctx)
|
30
|
+
rescue ParseError
|
31
|
+
ctx.pos = start_pos
|
32
|
+
nil
|
33
|
+
else
|
34
|
+
pars.failure(ctx)
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
# Returns a parser that doesn't consume any input and always returns `result`.
|
40
|
+
# @return [Paco::Parser]
|
41
|
+
def succeed(result)
|
42
|
+
Parser.new { result }
|
43
|
+
end
|
44
|
+
|
45
|
+
# Returns a parser that doesn't consume any input and always fails with passed `message`.
|
46
|
+
# @param [String] message
|
47
|
+
# @return [Paco::Parser]
|
48
|
+
def failed(message)
|
49
|
+
Parser.new(message) { |ctx, parser| parser.failure(ctx) }
|
50
|
+
end
|
51
|
+
|
52
|
+
# Returns a parser that runs the passed `parser` without consuming the input,
|
53
|
+
# and returns empty string.
|
54
|
+
# @param [Paco::Parser] parser
|
55
|
+
# @return [Paco::Parser]
|
56
|
+
def lookahead(parser)
|
57
|
+
Parser.new do |ctx|
|
58
|
+
start_pos = ctx.pos
|
59
|
+
parser._parse(ctx)
|
60
|
+
ctx.pos = start_pos
|
61
|
+
""
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
65
|
+
# Accepts any number of parsers, and returns a parser that returns the value of the first parser that succeeds, backtracking in between.
|
66
|
+
# @param [Array<Paco::Parser>] parsers
|
67
|
+
# @return [Paco::Parser]
|
68
|
+
def alt(*parsers)
|
69
|
+
raise ArgumentError, "no parsers specified" if parsers.empty?
|
70
|
+
|
71
|
+
Parser.new do |ctx|
|
72
|
+
result = nil
|
73
|
+
last_error = nil
|
74
|
+
start_pos = ctx.pos
|
75
|
+
parsers.each do |pars|
|
76
|
+
break result = {value: pars._parse(ctx)}
|
77
|
+
rescue ParseError => e
|
78
|
+
last_error = e
|
79
|
+
ctx.pos = start_pos
|
80
|
+
next
|
81
|
+
end
|
82
|
+
raise last_error unless result
|
83
|
+
result[:value]
|
84
|
+
end
|
85
|
+
end
|
86
|
+
|
87
|
+
# Accepts one or more parsers, and returns a parser that expects them
|
88
|
+
# to match in order, returns an array of all their results.
|
89
|
+
# @param [Array<Paco::Parser>] parsers
|
90
|
+
# @return [Paco::Parser]
|
91
|
+
def seq(*parsers)
|
92
|
+
raise ArgumentError, "no parsers specified" if parsers.empty?
|
93
|
+
|
94
|
+
Parser.new do |ctx|
|
95
|
+
parsers.map { |parser| parser._parse(ctx) }
|
96
|
+
end
|
97
|
+
end
|
98
|
+
|
99
|
+
# Returns a parser that matches all `parsers` sequentially, and passes
|
100
|
+
# their results as an arguments to a `block`, and at the end returns its result.
|
101
|
+
# @param [Array<Paco::Parser>] parsers
|
102
|
+
# @return [Paco::Parser]
|
103
|
+
def seq_map(*parsers, &block)
|
104
|
+
raise ArgumentError, "no parsers specified" if parsers.empty?
|
105
|
+
|
106
|
+
seq(*parsers).fmap do |results|
|
107
|
+
block.call(*results)
|
108
|
+
end
|
109
|
+
end
|
110
|
+
|
111
|
+
# Accepts a block that returns a parser, which is evaluated the first time the parser is used.
|
112
|
+
# This is useful for referencing parsers that haven't yet been defined, and for implementing recursive parsers.
|
113
|
+
# @return [Paco::Parser]
|
114
|
+
def lazy(desc = "", &block)
|
115
|
+
Parser.new(desc) { |ctx| block.call._parse(ctx) }
|
116
|
+
end
|
117
|
+
|
118
|
+
# Returns a parser that expects zero or more matches for `parser`,
|
119
|
+
# separated by the parser `separator`. Returns an array of `parser` results.
|
120
|
+
# @param [Paco::Parser] parser
|
121
|
+
# @param [Paco::Parser] separator
|
122
|
+
# @return [Paco::Parser]
|
123
|
+
def sep_by(parser, separator)
|
124
|
+
alt(sep_by_1(parser, separator), succeed([]))
|
125
|
+
end
|
126
|
+
|
127
|
+
# Returns a parser that expects one or more matches for `parser`,
|
128
|
+
# separated by the parser `separator`. Returns an array of `parser` results.
|
129
|
+
# @param [Paco::Parser] parser
|
130
|
+
# @param [Paco::Parser] separator
|
131
|
+
# @return [Paco::Parser]
|
132
|
+
def sep_by_1(parser, separator)
|
133
|
+
seq_map(parser, many(separator.next(parser))) do |first, arr|
|
134
|
+
[first] + arr
|
135
|
+
end
|
136
|
+
end
|
137
|
+
|
138
|
+
# Expects the parser `before` before `parser` and `after` after `parser. Returns the result of the parser.
|
139
|
+
# @param [Paco::Parser] before
|
140
|
+
# @param [Paco::Parser] after
|
141
|
+
# @param [Paco::Parser] parser
|
142
|
+
# @return [Paco::Parser]
|
143
|
+
def wrap(before, after, parser)
|
144
|
+
before.next(parser).skip(after)
|
145
|
+
end
|
146
|
+
|
147
|
+
# Expects `parser` zero or more times, and returns an array of the results.
|
148
|
+
# @param [Paco::Parser] parser
|
149
|
+
# @return [Paco::Parser]
|
150
|
+
def many(parser)
|
151
|
+
Parser.new do |ctx|
|
152
|
+
results = []
|
153
|
+
# last_pos = ctx.pos
|
154
|
+
loop do
|
155
|
+
results << parser._parse(ctx)
|
156
|
+
# raise ArgumentError, "smth wrong" if last_pos == ctx.pos
|
157
|
+
# last_pos = ctx.pos
|
158
|
+
rescue ParseError
|
159
|
+
break
|
160
|
+
end
|
161
|
+
results
|
162
|
+
end
|
163
|
+
end
|
164
|
+
|
165
|
+
# Returns parser that returns result of the passed `parser` or nil if `parser` fails.
|
166
|
+
# @param [Paco::Parser] parser
|
167
|
+
# @return [Paco::Parser]
|
168
|
+
def optional(parser)
|
169
|
+
alt(parser, succeed(nil))
|
170
|
+
end
|
171
|
+
|
172
|
+
# Helper used for memoization
|
173
|
+
def memoize(&block)
|
174
|
+
key = block.source_location
|
175
|
+
synchronize do
|
176
|
+
@_paco_memoized ||= {}
|
177
|
+
return @_paco_memoized[key] if @_paco_memoized.key?(key)
|
178
|
+
|
179
|
+
@_paco_memoized[key] = block.call
|
180
|
+
end
|
181
|
+
end
|
182
|
+
end
|
183
|
+
end
|
data/lib/paco/context.rb
ADDED
@@ -0,0 +1,40 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
module Paco
|
3
|
+
class Context
|
4
|
+
attr_reader :input, :last_pos, :pos
|
5
|
+
|
6
|
+
def pos=(np)
|
7
|
+
# TODO: is that needed?
|
8
|
+
@last_pos = @pos
|
9
|
+
@pos = np
|
10
|
+
end
|
11
|
+
|
12
|
+
def initialize(input, pos = 0)
|
13
|
+
@input = input
|
14
|
+
@pos = pos
|
15
|
+
end
|
16
|
+
|
17
|
+
def read(n)
|
18
|
+
input[pos, n]
|
19
|
+
end
|
20
|
+
|
21
|
+
def read_all
|
22
|
+
input[pos..-1]
|
23
|
+
end
|
24
|
+
|
25
|
+
def eof?
|
26
|
+
pos >= input.length
|
27
|
+
end
|
28
|
+
|
29
|
+
def index(from = nil)
|
30
|
+
from ||= pos
|
31
|
+
lines = input[0..from].lines
|
32
|
+
|
33
|
+
{
|
34
|
+
line: lines.length,
|
35
|
+
column: lines[-1]&.length || 0,
|
36
|
+
pos: from
|
37
|
+
}
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
@@ -0,0 +1,29 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
module Paco
|
3
|
+
class Error < StandardError; end
|
4
|
+
|
5
|
+
class ParseError < Error
|
6
|
+
# @param [Paco::Context] ctx
|
7
|
+
def initialize(ctx, expected)
|
8
|
+
@ctx = ctx
|
9
|
+
@pos = ctx.pos
|
10
|
+
@expected = expected
|
11
|
+
|
12
|
+
# TODO: make this possible to show every parsing message? or last n?
|
13
|
+
# puts ""
|
14
|
+
# puts "#{ctx.pos}/#{ctx.input.length}: #{ctx.input[ctx.last_pos..ctx.pos]}"
|
15
|
+
# puts "expected: #{expected}"
|
16
|
+
# puts ""
|
17
|
+
end
|
18
|
+
|
19
|
+
def message
|
20
|
+
index = @ctx.index(@pos)
|
21
|
+
<<~MSG
|
22
|
+
Parsing error
|
23
|
+
line #{index[:line]}, column #{index[:column]}:
|
24
|
+
unexpected #{@ctx.input[@pos] || "end of file"}
|
25
|
+
expecting #{@expected}
|
26
|
+
MSG
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
data/lib/paco/parser.rb
ADDED
@@ -0,0 +1,168 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "paco/combinators"
|
4
|
+
|
5
|
+
module Paco
|
6
|
+
class Parser
|
7
|
+
attr_reader :desc
|
8
|
+
|
9
|
+
def initialize(desc = "", &block)
|
10
|
+
@desc = desc
|
11
|
+
@block = block
|
12
|
+
end
|
13
|
+
|
14
|
+
def parse(input)
|
15
|
+
ctx = input.is_a?(Context) ? input : Context.new(input)
|
16
|
+
skip(Paco::Combinators.eof)._parse(ctx)
|
17
|
+
end
|
18
|
+
|
19
|
+
def _parse(ctx)
|
20
|
+
@block.call(ctx, self)
|
21
|
+
# TODO: add ability for debugging
|
22
|
+
# puts ""
|
23
|
+
# puts "#{@block.source_location} succeed."
|
24
|
+
# puts "#{ctx.input.length}/#{ctx.pos}: " + ctx.input[ctx.last_pos..ctx.pos].inspect
|
25
|
+
# puts ""
|
26
|
+
# res
|
27
|
+
end
|
28
|
+
|
29
|
+
# Raises ParseError
|
30
|
+
# @param [Paco::Context] ctx
|
31
|
+
# @raise [Paco::ParseError]
|
32
|
+
def failure(ctx)
|
33
|
+
raise ParseError.new(ctx, desc), "", []
|
34
|
+
end
|
35
|
+
|
36
|
+
# Returns a new parser which tries `parser`, and if it fails uses `other`.
|
37
|
+
def or(other)
|
38
|
+
Parser.new do |ctx|
|
39
|
+
_parse(ctx)
|
40
|
+
rescue ParseError
|
41
|
+
other._parse(ctx)
|
42
|
+
end
|
43
|
+
end
|
44
|
+
alias_method :|, :or
|
45
|
+
|
46
|
+
# Expects `other` parser to follow `parser`, but returns only the value of `parser`.
|
47
|
+
# @param [Poco::Parser] other
|
48
|
+
# @return [Paco::Parser]
|
49
|
+
def skip(other)
|
50
|
+
Paco::Combinators.seq(self, other).fmap { |results| results[0] }
|
51
|
+
end
|
52
|
+
alias_method :<, :skip
|
53
|
+
|
54
|
+
# Expects `other` parser to follow `parser`, but returns only the value of `other` parser.
|
55
|
+
# @param [Poco::Parser] other
|
56
|
+
# @return [Paco::Parser]
|
57
|
+
def next(other)
|
58
|
+
bind { other }
|
59
|
+
end
|
60
|
+
alias_method :>, :next
|
61
|
+
|
62
|
+
# Transforms the output of `parser` with the given block.
|
63
|
+
# @return [Paco::Parser]
|
64
|
+
def fmap(&block)
|
65
|
+
Parser.new do |ctx|
|
66
|
+
block.call(_parse(ctx))
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
70
|
+
# Returns a new parser which tries `parser`, and on success
|
71
|
+
# calls the `block` with the result of the parse, which is expected
|
72
|
+
# to return another parser, which will be tried next. This allows you
|
73
|
+
# to dynamically decide how to continue the parse, which is impossible
|
74
|
+
# with the other Paco::Combinators.
|
75
|
+
# @return [Paco::Parser]
|
76
|
+
def bind(&block)
|
77
|
+
Parser.new do |ctx|
|
78
|
+
block.call(_parse(ctx))._parse(ctx)
|
79
|
+
end
|
80
|
+
end
|
81
|
+
alias_method :chain, :bind
|
82
|
+
|
83
|
+
# Expects `parser` zero or more times, and returns an array of the results.
|
84
|
+
# @return [Paco::Parser]
|
85
|
+
def many
|
86
|
+
Paco::Combinators.many(self)
|
87
|
+
end
|
88
|
+
|
89
|
+
# Returns a new parser with the same behavior, but which returns passed `value`.
|
90
|
+
# @return [Paco::Parser]
|
91
|
+
def result(value)
|
92
|
+
fmap { value }
|
93
|
+
end
|
94
|
+
|
95
|
+
# Returns a new parser which tries `parser` and, if it fails, returns `value` without consuming any input.
|
96
|
+
# @return [Paco::Parser]
|
97
|
+
def fallback(value)
|
98
|
+
self.or(Paco::Combinators.succeed(value))
|
99
|
+
end
|
100
|
+
|
101
|
+
# Expects `other` parser before and after `parser`, and returns the result of the parser.
|
102
|
+
# @param [Paco::Parser] other
|
103
|
+
# @return [Paco::Parser]
|
104
|
+
def trim(other)
|
105
|
+
other.next(self).skip(other)
|
106
|
+
end
|
107
|
+
|
108
|
+
# Expects the parser `before` before `parser` and `after` after `parser. Returns the result of the parser.
|
109
|
+
# @param [Paco::Parser] before
|
110
|
+
# @param [Paco::Parser] after
|
111
|
+
# @return [Paco::Parser]
|
112
|
+
def wrap(before, after)
|
113
|
+
Paco::Combinators.wrap(before, after, self)
|
114
|
+
end
|
115
|
+
|
116
|
+
# Returns a parser that runs passed `other` parser without consuming the input, and
|
117
|
+
# returns result of the `parser` if the passed one _does not match_ the input. Fails otherwise.
|
118
|
+
# @param [Paco::Parser] other
|
119
|
+
# @return [Paco::Parser]
|
120
|
+
def not_followed_by(other)
|
121
|
+
skip(Paco::Combinators.not_followed_by(other))
|
122
|
+
end
|
123
|
+
|
124
|
+
# Returns a parser that runs `parser` and concatenate it results with the `separator`.
|
125
|
+
# @param [String] separator
|
126
|
+
# @return [Paco::Parser]
|
127
|
+
def join(separator = "")
|
128
|
+
fmap { |result| result.join(separator) }
|
129
|
+
end
|
130
|
+
|
131
|
+
# Returns a parser that runs `parser` between `min` and `max` times,
|
132
|
+
# and returns an array of the results. When `max` is not specified, `max` = `min`.
|
133
|
+
# @param [Integer] min
|
134
|
+
# @param [Integer] max
|
135
|
+
# @return [Paco::Parser]
|
136
|
+
def times(min, max = nil)
|
137
|
+
max ||= min
|
138
|
+
if min < 0 || max < min
|
139
|
+
raise ArgumentError, "invalid attributes: min `#{min}`, max `#{max}`"
|
140
|
+
end
|
141
|
+
|
142
|
+
Parser.new do |ctx|
|
143
|
+
results = min.times.map { _parse(ctx) }
|
144
|
+
(max - min).times.each do
|
145
|
+
results << _parse(ctx)
|
146
|
+
rescue ParseError
|
147
|
+
break
|
148
|
+
end
|
149
|
+
|
150
|
+
results
|
151
|
+
end
|
152
|
+
end
|
153
|
+
|
154
|
+
# Returns a parser that runs `parser` at least `num` times,
|
155
|
+
# and returns an array of the results.
|
156
|
+
def at_least(num)
|
157
|
+
Paco::Combinators.seq_map(times(num), many) do |head, rest|
|
158
|
+
head + rest
|
159
|
+
end
|
160
|
+
end
|
161
|
+
|
162
|
+
# Returns a parser that runs `parser` at most `num` times,
|
163
|
+
# and returns an array of the results.
|
164
|
+
def at_most(num)
|
165
|
+
times(0, num)
|
166
|
+
end
|
167
|
+
end
|
168
|
+
end
|
data/lib/paco/version.rb
ADDED
data/lib/paco.rb
ADDED
@@ -0,0 +1,17 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "paco/version"
|
4
|
+
require "paco/parse_error"
|
5
|
+
require "paco/context"
|
6
|
+
require "paco/parser"
|
7
|
+
require "paco/combinators"
|
8
|
+
|
9
|
+
module Paco
|
10
|
+
def self.extended(base)
|
11
|
+
base.extend Combinators
|
12
|
+
end
|
13
|
+
|
14
|
+
def self.included(base)
|
15
|
+
base.include Combinators
|
16
|
+
end
|
17
|
+
end
|
metadata
ADDED
@@ -0,0 +1,60 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: paco
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.1.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Svyatoslav Kryukov
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2021-12-12 00:00:00.000000000 Z
|
12
|
+
dependencies: []
|
13
|
+
description: Paco is a parser combinator library.
|
14
|
+
email:
|
15
|
+
- s.g.kryukov@yandex.ru
|
16
|
+
executables: []
|
17
|
+
extensions: []
|
18
|
+
extra_rdoc_files: []
|
19
|
+
files:
|
20
|
+
- CHANGELOG.md
|
21
|
+
- LICENSE.txt
|
22
|
+
- README.md
|
23
|
+
- bin/console
|
24
|
+
- bin/setup
|
25
|
+
- lib/paco.rb
|
26
|
+
- lib/paco/combinators.rb
|
27
|
+
- lib/paco/combinators/char.rb
|
28
|
+
- lib/paco/context.rb
|
29
|
+
- lib/paco/parse_error.rb
|
30
|
+
- lib/paco/parser.rb
|
31
|
+
- lib/paco/version.rb
|
32
|
+
homepage: https://github.com/skryukov/paco
|
33
|
+
licenses:
|
34
|
+
- MIT
|
35
|
+
metadata:
|
36
|
+
bug_tracker_uri: https://github.com/skryukov/paco/issues
|
37
|
+
changelog_uri: https://github.com/skryukov/paco/blob/master/CHANGELOG.md
|
38
|
+
documentation_uri: https://github.com/skryukov/paco/blob/master/README.md
|
39
|
+
homepage_uri: https://github.com/skryukov/paco
|
40
|
+
source_code_uri: https://github.com/skryukov/paco
|
41
|
+
post_install_message:
|
42
|
+
rdoc_options: []
|
43
|
+
require_paths:
|
44
|
+
- lib
|
45
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
46
|
+
requirements:
|
47
|
+
- - ">="
|
48
|
+
- !ruby/object:Gem::Version
|
49
|
+
version: 2.6.0
|
50
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - ">="
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: '0'
|
55
|
+
requirements: []
|
56
|
+
rubygems_version: 3.2.15
|
57
|
+
signing_key:
|
58
|
+
specification_version: 4
|
59
|
+
summary: Parser combinator library
|
60
|
+
test_files: []
|