paco 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
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: []