paco 0.2.1 → 0.2.2

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