paco 0.2.1 → 0.2.2

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 422192f0a277e1d7c6361db58ce97310db6e5fadfe06fed8e76a3978c1f65492
4
- data.tar.gz: ed3e9e6c0ad0e4d11d7cf18afffcebcea83ba4b47928c98d72714eae0b3031af
3
+ metadata.gz: 8d72f440d9727dc302366b842d7a7bf3d3d2ac84f4e13b41ce8954dad4b971f0
4
+ data.tar.gz: 81b0e2b377c35060db0416472115754cd62d45efda5f302265003b1340e639b4
5
5
  SHA512:
6
- metadata.gz: b96441b63edaf507c709c3f39db7a1a69c9805ef514eb8057d2ff04b1c63a97f8bac43d3a66221d3d86fb073c0337912b8ea85d95b4b81f8fd7ebf553e482b95
7
- data.tar.gz: 7aba8df2a852b468e09c4b70478336eb74f667c189505ba8ef1985d7370c9c5c9df7c18f409d311f0f0ac521721ddacf04841eff22815a44cff1c928f27eed93
6
+ metadata.gz: 3aaa2ce601f6da71e1d24a0a13bee121abc3c6d52b9120e6795b57e5880cefbb2fae9c24b69f7e6284a54913b293e25c6d3fcbe7e503105e782fd84817e13413
7
+ data.tar.gz: fed4af90111069f4c39f4ce6ed85b72480aaf6bf10bcf4f06e8701a60f037095638647b423282a7dfe00ae0f1e8c17e94b907368879394b0f664ae06a812a47e
data/CHANGELOG.md CHANGED
@@ -7,6 +7,12 @@ and this project adheres to [Semantic Versioning].
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [0.2.2] - 2023-08-23
11
+
12
+ ### Fixed
13
+
14
+ - Ship gem with pre-transpiled files (`lib/.rbnext`). ([@palkan][])
15
+
10
16
  ## [0.2.1] - 2022-05-17
11
17
 
12
18
  ### Added
@@ -0,0 +1,51 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "paco/callstack"
4
+ require "paco/index"
5
+
6
+ module Paco
7
+ class Context
8
+ attr_reader :input, :last_pos, :callstack
9
+ attr_accessor :pos
10
+
11
+ def initialize(input, pos: 0, with_callstack: false)
12
+ @input = input
13
+ @pos = pos
14
+ @callstack = Callstack.new if with_callstack
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
+ # @param [Integer] from
30
+ # @return [Paco::Index]
31
+ def index(from = nil)
32
+ Index.calculate(input: input, pos: from || pos)
33
+ end
34
+
35
+ # @param [Paco::Parser] parser
36
+ def failure_parse(parser)
37
+ ((((__safe_lvar__ = @callstack) || true) && (!__safe_lvar__.nil? || nil)) && __safe_lvar__.failure(pos: pos, parser: parser.desc))
38
+ end
39
+
40
+ # @param [Paco::Parser] parser
41
+ def start_parse(parser)
42
+ ((((__safe_lvar__ = @callstack) || true) && (!__safe_lvar__.nil? || nil)) && __safe_lvar__.start(pos: pos, parser: parser.desc))
43
+ end
44
+
45
+ # @param [Object] result
46
+ # @param [Paco::Parser] parser
47
+ def success_parse(result, parser)
48
+ ((((__safe_lvar__ = @callstack) || true) && (!__safe_lvar__.nil? || nil)) && __safe_lvar__.success(pos: pos, result: result, parser: parser.desc))
49
+ end
50
+ end
51
+ end
@@ -0,0 +1,15 @@
1
+ module Paco
2
+ Index = Struct.new(:pos, :line, :column) do
3
+ # @param [String] input
4
+ # @param [Integer] pos
5
+ def self.calculate(input:, pos:)
6
+ raise ArgumentError, "`pos` must be a non-negative integer" if pos < 0
7
+ raise ArgumentError, "`pos` is grater then input length" if pos > input.length
8
+
9
+ lines = input[0..pos].lines
10
+ line = lines.empty? ? 1 : lines.length
11
+ column = ((((__safe_lvar__ = lines.last) || true) && (!__safe_lvar__.nil? || nil)) && __safe_lvar__.length) || 1
12
+ new(pos, line, column)
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,30 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Paco
4
+ class Error < StandardError; end
5
+
6
+ class ParseError < Error
7
+ attr_reader :ctx, :pos, :expected
8
+
9
+ # @param [Paco::Context] ctx
10
+ def initialize(ctx, expected)
11
+ @ctx = ctx
12
+ @pos = ctx.pos
13
+ @expected = expected
14
+ end
15
+
16
+ def callstack
17
+ ctx.callstack
18
+ end
19
+
20
+ def message
21
+ index = ctx.index(pos)
22
+ <<-MSG
23
+ \nParsing error
24
+ line #{index.line}, column #{index.column}:
25
+ unexpected #{ctx.eof? ? "end of file" : ctx.input[pos].inspect}
26
+ expecting #{expected}
27
+ MSG
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,174 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "paco/combinators/char"
4
+ require "paco/memoizer"
5
+
6
+ module Paco
7
+ module Combinators
8
+ include Char
9
+ extend Char
10
+
11
+ module_function
12
+
13
+ # Returns a parser that runs the passed `parser` without consuming the input, and
14
+ # returns `null` if the passed `parser` _does not match_ the input. Fails otherwise.
15
+ # @param [Paco::Parser] parser
16
+ # @return [Paco::Parser]
17
+ def not_followed_by(parser)
18
+ Parser.new("not #{parser.desc}") do |ctx, pars|
19
+ start_pos = ctx.pos
20
+ begin
21
+ parser._parse(ctx)
22
+ rescue ParseError
23
+ nil
24
+ else
25
+ pars.failure(ctx)
26
+ ensure
27
+ ctx.pos = start_pos
28
+ end
29
+ end
30
+ end
31
+
32
+ # Returns a parser that doesn't consume any input and always returns `result`.
33
+ # @return [Paco::Parser]
34
+ def succeed(result)
35
+ Parser.new("succeed(#{result})") { result }
36
+ end
37
+
38
+ # Returns a parser that doesn't consume any input and always fails with passed `message`.
39
+ # @param [String] message
40
+ # @return [Paco::Parser]
41
+ def failed(message)
42
+ Parser.new(message) { |ctx, parser| parser.failure(ctx) }
43
+ end
44
+
45
+ # Returns a parser that runs the passed `parser` without consuming the input,
46
+ # and returns empty string.
47
+ # @param [Paco::Parser] parser
48
+ # @return [Paco::Parser]
49
+ def lookahead(parser)
50
+ Parser.new("lookahead(#{parser.desc})") do |ctx|
51
+ start_pos = ctx.pos
52
+ parser._parse(ctx)
53
+ ctx.pos = start_pos
54
+ ""
55
+ end
56
+ end
57
+
58
+ # Accepts any number of parsers, and returns a parser that returns the value of the first parser that succeeds, backtracking in between.
59
+ # @param [Array<Paco::Parser>] parsers
60
+ # @return [Paco::Parser]
61
+ def alt(*parsers)
62
+ raise ArgumentError, "no parsers specified" if parsers.empty?
63
+
64
+ Parser.new("alt(#{parsers.map(&:desc).join(", ")})") do |ctx|
65
+ result = nil
66
+ last_error = nil
67
+ start_pos = ctx.pos
68
+ parsers.each do |pars|
69
+ begin;break result = {value: pars._parse(ctx)}
70
+ rescue ParseError => e
71
+ last_error = e
72
+ ctx.pos = start_pos
73
+ next;end
74
+ end
75
+ raise last_error unless result
76
+ result[:value]
77
+ end
78
+ end
79
+
80
+ # Accepts one or more parsers, and returns a parser that expects them
81
+ # to match in order, returns an array of all their results.
82
+ # If `block` specified, passes results of the `parses` as an arguments
83
+ # to a `block`, and at the end returns its result.
84
+ # @param [Array<Paco::Parser>] parsers
85
+ # @return [Paco::Parser]
86
+ def seq(*parsers)
87
+ raise ArgumentError, "no parsers specified" if parsers.empty?
88
+
89
+ result = Parser.new("seq(#{parsers.map(&:desc).join(", ")})") do |ctx|
90
+ start_pos = ctx.pos
91
+ begin
92
+ parsers.map { |parser| parser._parse(ctx) }
93
+ rescue ParseError => e
94
+ ctx.pos = start_pos
95
+ raise e
96
+ end
97
+ end
98
+ return result unless block_given?
99
+
100
+ result.fmap { |results| yield(*results) }
101
+ end
102
+
103
+ # Accepts a block that returns a parser, which is evaluated the first time the parser is used.
104
+ # This is useful for referencing parsers that haven't yet been defined, and for implementing recursive parsers.
105
+ # @return [Paco::Parser]
106
+ def lazy(desc = "", &block)
107
+ Parser.new(desc) { |ctx| block.call._parse(ctx) }
108
+ end
109
+
110
+ # Returns a parser that expects zero or more matches for `parser`,
111
+ # separated by the parser `separator`. Returns an array of `parser` results.
112
+ # @param [Paco::Parser] parser
113
+ # @param [Paco::Parser] separator
114
+ # @return [Paco::Parser]
115
+ def sep_by(parser, separator)
116
+ alt(sep_by!(parser, separator), succeed([]))
117
+ .with_desc("sep_by(#{parser.desc}, #{separator.desc})")
118
+ end
119
+
120
+ # Returns a parser that expects one or more matches for `parser`,
121
+ # separated by the parser `separator`. Returns an array of `parser` results.
122
+ # @param [Paco::Parser] parser
123
+ # @param [Paco::Parser] separator
124
+ # @return [Paco::Parser]
125
+ def sep_by!(parser, separator)
126
+ seq(parser, many(separator.next(parser))) { |first, arr| [first] + arr }
127
+ .with_desc("sep_by!(#{parser.desc}, #{separator.desc})")
128
+ end
129
+ alias_method :sep_by_1, :sep_by!
130
+
131
+ # Expects the parser `before` before `parser` and `after` after `parser. Returns the result of the parser.
132
+ # @param [Paco::Parser] before
133
+ # @param [Paco::Parser] after
134
+ # @param [Paco::Parser] parser
135
+ # @return [Paco::Parser]
136
+ def wrap(before, after, parser)
137
+ before.next(parser).skip(after)
138
+ end
139
+
140
+ # Expects `parser` zero or more times, and returns an array of the results.
141
+ # @param [Paco::Parser] parser
142
+ # @return [Paco::Parser]
143
+ def many(parser)
144
+ Parser.new("many(#{parser.desc})") do |ctx|
145
+ results = []
146
+ loop do
147
+ begin;results << parser._parse(ctx)
148
+ rescue ParseError
149
+ break;end
150
+ end
151
+ results
152
+ end
153
+ end
154
+
155
+ # Returns parser that returns result of the passed `parser` or nil if `parser` fails.
156
+ # @param [Paco::Parser] parser
157
+ # @return [Paco::Parser]
158
+ def optional(parser)
159
+ alt(parser, succeed(nil))
160
+ end
161
+
162
+ # Returns parser that returns `Paco::Index` representing
163
+ # the current offset into the parse without consuming the input.
164
+ # @return [Paco::Parser]
165
+ def index
166
+ Parser.new { |ctx| ctx.index }
167
+ end
168
+
169
+ # Helper used for memoization
170
+ def memoize(&block)
171
+ Memoizer.memoize(block.source_location, &block)
172
+ end
173
+ end
174
+ end
@@ -0,0 +1,180 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "paco/combinators"
4
+
5
+ module Paco
6
+ class Parser
7
+ attr_reader :desc
8
+
9
+ # @param [String] desc
10
+ def initialize(desc = "", &block)
11
+ @desc = desc
12
+ @block = block
13
+ end
14
+
15
+ # @param [String] desc
16
+ # @return [Paco::Parser]
17
+ def with_desc(desc)
18
+ @desc = desc
19
+ self
20
+ end
21
+
22
+ # @param [String, Paco::Context] input
23
+ # @param [true, false] with_callstack
24
+ def parse(input, with_callstack: false)
25
+ ctx = input.is_a?(Context) ? input : Context.new(input, with_callstack: with_callstack)
26
+ skip(Paco::Combinators.eof)._parse(ctx)
27
+ end
28
+
29
+ # @param [Paco::Context] ctx
30
+ def _parse(ctx)
31
+ ctx.start_parse(self)
32
+ res = @block.call(ctx, self)
33
+ ctx.success_parse(res, self)
34
+ res
35
+ end
36
+
37
+ # Raises ParseError
38
+ # @param [Paco::Context] ctx
39
+ # @raise [Paco::ParseError]
40
+ def failure(ctx)
41
+ ctx.failure_parse(self)
42
+ raise ParseError.new(ctx, desc), "", []
43
+ end
44
+
45
+ # Returns a new parser which tries `parser`, and if it fails uses `other`.
46
+ # @param [Paco::Parser] other
47
+ # @return [Paco::Parser]
48
+ def or(other)
49
+ Parser.new("or(#{desc}, #{other.desc})") do |ctx|
50
+ begin;_parse(ctx)
51
+ rescue ParseError
52
+ other._parse(ctx);end
53
+ end
54
+ end
55
+ alias_method :|, :or
56
+
57
+ # Expects `other` parser to follow `parser`, but returns only the value of `parser`.
58
+ # @param [Poco::Parser] other
59
+ # @return [Paco::Parser]
60
+ def skip(other)
61
+ Paco::Combinators.seq(self, other).fmap { |results| results[0] }.with_desc("#{desc}.skip(#{other.desc})")
62
+ end
63
+ alias_method :<, :skip
64
+
65
+ # Expects `other` parser to follow `parser`, but returns only the value of `other` parser.
66
+ # @param [Poco::Parser] other
67
+ # @return [Paco::Parser]
68
+ def next(other)
69
+ Paco::Combinators.seq(self, other).fmap { |results| results[1] }
70
+ .with_desc("#{desc}.next(#{other.desc})")
71
+ end
72
+ alias_method :>, :next
73
+
74
+ # Transforms the output of `parser` with the given block.
75
+ # @return [Paco::Parser]
76
+ def fmap(&block)
77
+ Parser.new("#{desc}.fmap") do |ctx|
78
+ block.call(_parse(ctx))
79
+ end
80
+ end
81
+
82
+ # Returns a new parser which tries `parser`, and on success
83
+ # calls the `block` with the result of the parse, which is expected
84
+ # to return another parser, which will be tried next. This allows you
85
+ # to dynamically decide how to continue the parse, which is impossible
86
+ # with the other Paco::Combinators.
87
+ # @return [Paco::Parser]
88
+ def bind(&block)
89
+ Parser.new("#{desc}.bind") do |ctx|
90
+ block.call(_parse(ctx))._parse(ctx)
91
+ end
92
+ end
93
+ alias_method :chain, :bind
94
+
95
+ # Expects `parser` zero or more times, and returns an array of the results.
96
+ # @return [Paco::Parser]
97
+ def many
98
+ Paco::Combinators.many(self)
99
+ end
100
+
101
+ # Returns a new parser with the same behavior, but which returns passed `value`.
102
+ # @return [Paco::Parser]
103
+ def result(value)
104
+ fmap { value }
105
+ end
106
+
107
+ # Returns a new parser which tries `parser` and, if it fails, returns `value` without consuming any input.
108
+ # @return [Paco::Parser]
109
+ def fallback(value)
110
+ self.or(Paco::Combinators.succeed(value))
111
+ end
112
+
113
+ # Expects `other` parser before and after `parser`, and returns the result of the parser.
114
+ # @param [Paco::Parser] other
115
+ # @return [Paco::Parser]
116
+ def trim(other)
117
+ other.next(self).skip(other)
118
+ end
119
+
120
+ # Expects the parser `before` before `parser` and `after` after `parser. Returns the result of the parser.
121
+ # @param [Paco::Parser] before
122
+ # @param [Paco::Parser] after
123
+ # @return [Paco::Parser]
124
+ def wrap(before, after)
125
+ Paco::Combinators.wrap(before, after, self)
126
+ end
127
+
128
+ # Returns a parser that runs passed `other` parser without consuming the input, and
129
+ # returns result of the `parser` if the passed one _does not match_ the input. Fails otherwise.
130
+ # @param [Paco::Parser] other
131
+ # @return [Paco::Parser]
132
+ def not_followed_by(other)
133
+ skip(Paco::Combinators.not_followed_by(other))
134
+ end
135
+
136
+ # Returns a parser that runs `parser` and concatenate it results with the `separator`.
137
+ # @param [String] separator
138
+ # @return [Paco::Parser]
139
+ def join(separator = "")
140
+ fmap { |result| result.join(separator) }
141
+ end
142
+
143
+ # Returns a parser that runs `parser` between `min` and `max` times,
144
+ # and returns an array of the results. When `max` is not specified, `max` = `min`.
145
+ # @param [Integer] min
146
+ # @param [Integer] max
147
+ # @return [Paco::Parser]
148
+ def times(min, max = nil)
149
+ max ||= min
150
+ if min < 0 || max < min
151
+ raise ArgumentError, "invalid attributes: min `#{min}`, max `#{max}`"
152
+ end
153
+
154
+ Parser.new("#{desc}.times(#{min}, #{max})") do |ctx|
155
+ results = min.times.map { _parse(ctx) }
156
+ (max - min).times.each do
157
+ begin;results << _parse(ctx)
158
+ rescue ParseError
159
+ break;end
160
+ end
161
+
162
+ results
163
+ end
164
+ end
165
+
166
+ # Returns a parser that runs `parser` at least `num` times,
167
+ # and returns an array of the results.
168
+ def at_least(num)
169
+ Paco::Combinators.seq(times(num), many) do |head, rest|
170
+ head + rest
171
+ end
172
+ end
173
+
174
+ # Returns a parser that runs `parser` at most `num` times,
175
+ # and returns an array of the results.
176
+ def at_most(num)
177
+ times(0, num)
178
+ end
179
+ end
180
+ end
@@ -0,0 +1,47 @@
1
+ # frozen_string_literal: true
2
+
3
+ RSpec::Matchers.define(:parse) do |input|
4
+ chain :as do |expected_output = nil, &block|
5
+ @expected = expected_output
6
+ @block = block
7
+ end
8
+
9
+ chain :fully do
10
+ @expected = input
11
+ end
12
+
13
+ match do |parser|
14
+ begin;@result = parser.parse(input)
15
+ return @block.call(@result) if @block
16
+ return @expected == @result if defined?(@expected)
17
+
18
+ true
19
+ rescue Paco::ParseError => e
20
+ @error_message = e.message
21
+ false;end
22
+ end
23
+
24
+ failure_message do |subject|
25
+ msg = "expected output of parsing #{input.inspect} with #{subject.inspect} to"
26
+ was = (@result ? "was #{@result.inspect}" : "raised an error #{@error_message}")
27
+ return "#{msg} meet block conditions, but it didn't. It #{was}" if @block
28
+ return "#{msg} equal #{@expected.inspect}, but it #{was}" if defined?(@expected)
29
+
30
+ "expected #{subject.inspect} to successfully parse #{input.inspect}, but it #{was}"
31
+ end
32
+
33
+ failure_message_when_negated do |subject|
34
+ msg = "expected output of parsing #{input.inspect} with #{subject.inspect} not to"
35
+ return "#{msg} meet block conditions, but it did" if @block
36
+ return "#{msg} equal #{@expected.inspect}" if defined?(@expected)
37
+
38
+ "expected #{subject.inspect} to not parse #{input.inspect}, but it did"
39
+ end
40
+
41
+ description do
42
+ return "parse #{input.inspect} with block conditions" if @block
43
+ return "parse #{input.inspect} as #{@expected.inspect}" if defined?(@expected)
44
+
45
+ "parse #{input.inspect}"
46
+ end
47
+ end
@@ -0,0 +1,51 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "paco/callstack"
4
+ require "paco/index"
5
+
6
+ module Paco
7
+ class Context
8
+ attr_reader :input, :last_pos, :callstack
9
+ attr_accessor :pos
10
+
11
+ def initialize(input, pos: 0, with_callstack: false)
12
+ @input = input
13
+ @pos = pos
14
+ @callstack = Callstack.new if with_callstack
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
+ # @param [Integer] from
30
+ # @return [Paco::Index]
31
+ def index(from = nil)
32
+ Index.calculate(input: input, pos: from || pos)
33
+ end
34
+
35
+ # @param [Paco::Parser] parser
36
+ def failure_parse(parser)
37
+ @callstack&.failure(pos: pos, parser: parser.desc)
38
+ end
39
+
40
+ # @param [Paco::Parser] parser
41
+ def start_parse(parser)
42
+ @callstack&.start(pos: pos, parser: parser.desc)
43
+ end
44
+
45
+ # @param [Object] result
46
+ # @param [Paco::Parser] parser
47
+ def success_parse(result, parser)
48
+ @callstack&.success(pos: pos, result: result, parser: parser.desc)
49
+ end
50
+ end
51
+ end
data/lib/paco/version.rb CHANGED
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Paco
4
- VERSION = "0.2.1"
4
+ VERSION = "0.2.2"
5
5
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: paco
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.1
4
+ version: 0.2.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - Svyatoslav Kryukov
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2022-05-17 00:00:00.000000000 Z
11
+ date: 2023-08-23 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: ruby-next-core
@@ -34,6 +34,13 @@ files:
34
34
  - CHANGELOG.md
35
35
  - LICENSE.txt
36
36
  - README.md
37
+ - lib/.rbnext/2.3/paco/context.rb
38
+ - lib/.rbnext/2.3/paco/index.rb
39
+ - lib/.rbnext/2.3/paco/parse_error.rb
40
+ - lib/.rbnext/2.5/paco/combinators.rb
41
+ - lib/.rbnext/2.5/paco/parser.rb
42
+ - lib/.rbnext/2.5/paco/rspec/parse_matcher.rb
43
+ - lib/.rbnext/2.6/paco/context.rb
37
44
  - lib/paco.rb
38
45
  - lib/paco/callstack.rb
39
46
  - lib/paco/combinators.rb
@@ -70,7 +77,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
70
77
  - !ruby/object:Gem::Version
71
78
  version: '0'
72
79
  requirements: []
73
- rubygems_version: 3.2.15
80
+ rubygems_version: 3.4.8
74
81
  signing_key:
75
82
  specification_version: 4
76
83
  summary: Parser combinator library