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 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,8 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+ IFS=$'\n\t'
4
+ set -vx
5
+
6
+ bundle install
7
+
8
+ # Do any other automated setup that you need to do here
@@ -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
@@ -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
@@ -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
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Paco
4
+ VERSION = "0.1.0"
5
+ end
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: []