compser 0.1.0 → 0.2.1
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 +4 -4
- data/Gemfile.lock +2 -2
- data/README.md +49 -10
- data/examples/calculator.rb +76 -0
- data/examples/json-benchmark.rb +9 -3
- data/examples/json.rb +12 -5
- data/lib/compser/result.rb +2 -2
- data/lib/compser/state.rb +3 -3
- data/lib/compser/step/chomp_while.rb +3 -3
- data/lib/compser/step/drop.rb +0 -2
- data/lib/compser/step/integer.rb +3 -3
- data/lib/compser/step/lazy.rb +7 -0
- data/lib/compser/step/one_of.rb +2 -2
- data/lib/compser/step/sequence.rb +2 -5
- data/lib/compser/step.rb +15 -14
- data/lib/compser/version.rb +1 -1
- data/lib/compser.rb +4 -3
- metadata +5 -3
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 323319e8a15dae0341e6c2c9def9238fb14587e5a90d65a4c2366ef6c6c67725
|
4
|
+
data.tar.gz: 6f3ec1d93cb1c702745f363bd586cbd85f18b9ec53c6d49b6fda2a856f63f86d
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: b69786e8a9a448ab47a494a2388e608e46546bea33902e0c68ecac9598655e92a7b1cc3abb2214b39a874fc97b3322c32a05228b53894fc3b5404c4212a48fb1
|
7
|
+
data.tar.gz: 6fe207a4536aa12062681558cfdf340709c0a9cab191cf9dea65c6f865c4260e5593ebfcd4e41ef10db56f77957b8c3d9b3de53bcf7bf047a03865282231a949
|
data/Gemfile.lock
CHANGED
data/README.md
CHANGED
@@ -1,9 +1,10 @@
|
|
1
1
|
# Compser
|
2
2
|
|
3
|
-
Compser is a parser
|
4
|
-
Take a look at the [JSON parser](https://github.com/luizpvas/Compser/blob/main/examples/json.rb) to get a glimpse of the syntax
|
5
|
-
and
|
3
|
+
Compser is a parser library for Ruby inspired by [elm-parser](https://package.elm-lang.org/packages/elm/parser/latest/).
|
4
|
+
Take a look at the [JSON parser](https://github.com/luizpvas/Compser/blob/main/examples/json.rb) and [Calculator](https://github.com/luizpvas/compser/blob/main/examples/calculator.rb) to get a glimpse of the syntax
|
5
|
+
and the building blocks you can sue to compose more complex and sophisticated parsers.
|
6
6
|
|
7
|
+
* [Installation](#installation)
|
7
8
|
* Building blocks
|
8
9
|
* [`drop`](#drop)
|
9
10
|
* [`integer`](#integer)
|
@@ -14,11 +15,22 @@ and available building blocks you can use to compose more complex and sophistica
|
|
14
15
|
* [`map`](#map)
|
15
16
|
* [`one_of`](#one_of)
|
16
17
|
* [`sequence`](#sequence)
|
18
|
+
* [`lazy`](#lazy)
|
17
19
|
* [`spaces`](#spaces)
|
18
20
|
* [`chomp_if`](#chomp_if)
|
19
21
|
* [`chomp_while`](#chomp_while)
|
20
22
|
* [Benchmark](#benchmark)
|
21
23
|
|
24
|
+
## Installation
|
25
|
+
|
26
|
+
Add the following line to your Gemfile:
|
27
|
+
|
28
|
+
```ruby
|
29
|
+
gem 'compser', '~> 0.2'
|
30
|
+
```
|
31
|
+
|
32
|
+
See more details at [https://rubygems.org/gems/compser](https://rubygems.org/gems/compser).
|
33
|
+
|
22
34
|
#### `drop`
|
23
35
|
|
24
36
|
Discard any result or chomped string produced by the parser.
|
@@ -51,7 +63,7 @@ parser.parse('123a') # => Bad<...>
|
|
51
63
|
parser.parse('0x1A') # => Bad<...>
|
52
64
|
|
53
65
|
|
54
|
-
# negative integers with '-' prefix
|
66
|
+
# support negative integers with '-' prefix
|
55
67
|
def my_integer
|
56
68
|
take(:one_of, [
|
57
69
|
map(->(x) { x * -1 }).drop(:token, '-').take(:integer),
|
@@ -176,6 +188,31 @@ parser.parse('12,') # => Bad<...>
|
|
176
188
|
parser.parse(',12') # => Bad<...>
|
177
189
|
```
|
178
190
|
|
191
|
+
#### `lazy`
|
192
|
+
|
193
|
+
Wraps a parser in a lazy-evaluated proc. Use `lazy` to build recursive parsers.
|
194
|
+
|
195
|
+
```ruby
|
196
|
+
ToList = ->(*integers) { integers }
|
197
|
+
|
198
|
+
CommaSeparatedInteger = -> do
|
199
|
+
take(:integer)
|
200
|
+
.drop(:spaces)
|
201
|
+
.take(:one_of, [
|
202
|
+
drop(:token, ',').drop(:spaces).take(:lazy, CommaSeparatedInteger),
|
203
|
+
succeed
|
204
|
+
])
|
205
|
+
end
|
206
|
+
|
207
|
+
parser = map(ToList).take(CommaSeparatedInteger.call())
|
208
|
+
|
209
|
+
parser.parse('12, 23, 34') # => Good<[12, 23, 34]>
|
210
|
+
parser.parse('123') # => Good<[123]>
|
211
|
+
|
212
|
+
parser.parse('12,') # => Bad<...>
|
213
|
+
parser.parse(',12') # => Bad<...>
|
214
|
+
```
|
215
|
+
|
179
216
|
#### `spaces`
|
180
217
|
|
181
218
|
Chompes zero or more blankspaces, line breaks and tabs. Always succeeds.
|
@@ -210,12 +247,14 @@ parser.parse('cccdd').state # => State<good?: true, offset: 0, chomped: ''>
|
|
210
247
|
## Benchmark
|
211
248
|
|
212
249
|
The following result is a benchark of a [JSON parser](https://github.com/luizpvas/Compser/blob/main/examples/json.rb) I implemented
|
213
|
-
with this library. I ran the benchmark with and without YJIT, and compared the result against
|
250
|
+
with this library. I ran the benchmark with and without YJIT, and compared the result against `JSON.parse` (native C implementation) and [Parsby](https://github.com/jolmg/parsby).
|
214
251
|
|
215
|
-
The benchmark parses a 1,5kb payload
|
252
|
+
[The benchmark](https://github.com/luizpvas/compser/blob/main/examples/json-benchmark.rb) parses a 1,5kb payload 100 times.
|
216
253
|
|
217
|
-
Implementation | Time |
|
254
|
+
Implementation | Time | Comparison to `JSON.parse`
|
218
255
|
:---:|:---:|:---:
|
219
|
-
`JSON.parse`
|
220
|
-
`
|
221
|
-
`
|
256
|
+
`JSON.parse` | 0.00067s | -
|
257
|
+
`Compser::Json.parse` (with YJIT) | 0.216s | 322x slower
|
258
|
+
`Compser::Json.parse` | 0.268s | 400x slower
|
259
|
+
`Parsby::Example::JsonParser` (with YJIT) | 24.19s | 36100x slower
|
260
|
+
`Parsby::Example::JsonParser` | 27.22s | 40626x slower
|
@@ -0,0 +1,76 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative "../lib/compser"
|
4
|
+
|
5
|
+
module Compser::Calculator
|
6
|
+
extend self
|
7
|
+
include Compser
|
8
|
+
|
9
|
+
def evaluate(...)
|
10
|
+
expression.parse(...)
|
11
|
+
end
|
12
|
+
|
13
|
+
OPERATORS = {
|
14
|
+
"+" => ->(a, b) { a + b },
|
15
|
+
"-" => ->(a, b) { a - b },
|
16
|
+
"*" => ->(a, b) { a * b },
|
17
|
+
"/" => ->(a, b) { a / b }
|
18
|
+
}.freeze
|
19
|
+
|
20
|
+
Evaluate = ->(*args) {
|
21
|
+
result = args.shift
|
22
|
+
|
23
|
+
while args.any?
|
24
|
+
oprt = args.shift
|
25
|
+
expr = args.shift
|
26
|
+
|
27
|
+
result = OPERATORS.fetch(oprt).call(result, expr)
|
28
|
+
end
|
29
|
+
|
30
|
+
result
|
31
|
+
}
|
32
|
+
|
33
|
+
ExpressionHelper = ->(continue, done) do
|
34
|
+
take(:one_of, [
|
35
|
+
take(operator).drop(:spaces).take(value).drop(:spaces).and_then(continue),
|
36
|
+
done
|
37
|
+
])
|
38
|
+
end
|
39
|
+
|
40
|
+
def expression
|
41
|
+
map(Evaluate)
|
42
|
+
.take(value)
|
43
|
+
.drop(:spaces)
|
44
|
+
.take(:sequence, ExpressionHelper)
|
45
|
+
end
|
46
|
+
|
47
|
+
def value
|
48
|
+
take(:one_of, [
|
49
|
+
drop(:token, "(").drop(:spaces).take(:lazy, -> { expression }).drop(:spaces).drop(:token, ")"),
|
50
|
+
number
|
51
|
+
])
|
52
|
+
end
|
53
|
+
|
54
|
+
def operator
|
55
|
+
take(:chomp_if, ->(c) { OPERATORS.key?(c) })
|
56
|
+
.and_then { |state| state.good!(state.consume_chomped) }
|
57
|
+
end
|
58
|
+
|
59
|
+
def number
|
60
|
+
take(:one_of, [
|
61
|
+
map(->(n) { n * -1 }).drop(:token, "-").take(:decimal),
|
62
|
+
take(:decimal)
|
63
|
+
])
|
64
|
+
end
|
65
|
+
end
|
66
|
+
|
67
|
+
calc = ->(input) { input + " = " + sprintf("%.2f", Compser::Calculator.evaluate(input).value) }
|
68
|
+
|
69
|
+
puts calc.("1 + 1")
|
70
|
+
puts calc.("1 + 2 + 3")
|
71
|
+
puts calc.("3 * 3 / 2")
|
72
|
+
puts calc.("0.1 + 0.2")
|
73
|
+
puts calc.("2 + 2 * 4")
|
74
|
+
puts calc.("(2 + 2) * 4")
|
75
|
+
puts calc.("((((10))))")
|
76
|
+
puts calc.("((((10))+5))")
|
data/examples/json-benchmark.rb
CHANGED
@@ -1,16 +1,22 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
require_relative "json"
|
2
4
|
require "json"
|
3
5
|
require "benchmark"
|
6
|
+
require "stringio"
|
7
|
+
require "parsby"
|
8
|
+
require "parsby/example/json_parser"
|
4
9
|
|
5
10
|
json = File.read("./json-payload.json")
|
6
11
|
|
7
|
-
n =
|
12
|
+
n = 100
|
8
13
|
|
9
|
-
|
14
|
+
5000.times do; Compser::Json.parse(json); end
|
10
15
|
|
11
16
|
Benchmark.bm do |x|
|
12
|
-
x.report("
|
17
|
+
x.report("Compser::Json.parse") { n.times do; Compser::Json.parse(json); end }
|
13
18
|
x.report("JSON.parse") { n.times do; ::JSON.parse(json); end }
|
19
|
+
x.report("Parsby") { n.times do; Parsby::Example::JsonParser.parse(json); end }
|
14
20
|
end
|
15
21
|
|
16
22
|
puts RubyVM::YJIT.runtime_stats
|
data/examples/json.rb
CHANGED
@@ -1,18 +1,21 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
require_relative "../lib/compser"
|
2
4
|
|
3
|
-
module
|
4
|
-
extend Compser
|
5
|
+
module Compser::Json
|
5
6
|
extend self
|
7
|
+
include Compser
|
6
8
|
|
7
|
-
def parse(
|
8
|
-
value.parse(
|
9
|
+
def parse(...)
|
10
|
+
value.parse(...)
|
9
11
|
end
|
10
12
|
|
11
13
|
def value
|
12
14
|
take(:one_of, [
|
13
|
-
|
15
|
+
null,
|
14
16
|
number,
|
15
17
|
boolean,
|
18
|
+
take(:double_quoted_string),
|
16
19
|
array,
|
17
20
|
object
|
18
21
|
])
|
@@ -57,6 +60,10 @@ module MyJson
|
|
57
60
|
.drop(:token, "]")
|
58
61
|
end
|
59
62
|
|
63
|
+
def null
|
64
|
+
map(-> { nil }).drop(:token, "null")
|
65
|
+
end
|
66
|
+
|
60
67
|
def number
|
61
68
|
take(:one_of, [
|
62
69
|
map(->(x) { x * -1 }).drop(:token, "-").take(:decimal),
|
data/lib/compser/result.rb
CHANGED
@@ -1,12 +1,12 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
module Compser::Result
|
4
|
-
Good = Data.define(:
|
4
|
+
Good = Data.define(:value) do
|
5
5
|
def good? = true
|
6
6
|
def bad? = false
|
7
7
|
end
|
8
8
|
|
9
|
-
Bad = Data.define(:
|
9
|
+
Bad = Data.define(:message) do
|
10
10
|
def bad? = true
|
11
11
|
def good? = false
|
12
12
|
end
|
data/lib/compser/state.rb
CHANGED
@@ -9,7 +9,7 @@ class Compser::State
|
|
9
9
|
@line = 0
|
10
10
|
@column = 0
|
11
11
|
@chomped = ::String.new("")
|
12
|
-
@result_stack = [Compser::Result::Good.new(
|
12
|
+
@result_stack = [Compser::Result::Good.new(nil)]
|
13
13
|
end
|
14
14
|
|
15
15
|
def result
|
@@ -76,12 +76,12 @@ class Compser::State
|
|
76
76
|
def good(value)
|
77
77
|
return value if value.is_a?(Compser::Result::Good)
|
78
78
|
|
79
|
-
Compser::Result::Good.new(
|
79
|
+
Compser::Result::Good.new(value)
|
80
80
|
end
|
81
81
|
|
82
82
|
def bad(message)
|
83
83
|
return message if message.is_a?(Compser::Result::Bad)
|
84
84
|
|
85
|
-
Compser::Result::Bad.new(
|
85
|
+
Compser::Result::Bad.new(message)
|
86
86
|
end
|
87
87
|
end
|
data/lib/compser/step/drop.rb
CHANGED
data/lib/compser/step/integer.rb
CHANGED
data/lib/compser/step/one_of.rb
CHANGED
@@ -9,8 +9,8 @@ class Compser::Step
|
|
9
9
|
branches.each_with_index do |branch, index|
|
10
10
|
state = branch.call(state)
|
11
11
|
|
12
|
-
return
|
13
|
-
return
|
12
|
+
return if savepoint.has_changes?
|
13
|
+
return if index == branches.size - 1 # is last
|
14
14
|
|
15
15
|
savepoint.rollback
|
16
16
|
end
|
@@ -16,11 +16,8 @@ class Compser::Step
|
|
16
16
|
state = helper.call(continue, done).call(state)
|
17
17
|
end
|
18
18
|
|
19
|
-
return
|
20
|
-
|
21
|
-
if state.good? && state.__sequence__ == DONE
|
22
|
-
return state
|
23
|
-
end
|
19
|
+
return if state.bad?
|
20
|
+
return if state.__sequence__ == DONE
|
24
21
|
|
25
22
|
state.bad!("unbound sequence")
|
26
23
|
end
|
data/lib/compser/step.rb
CHANGED
@@ -8,6 +8,7 @@ class Compser::Step
|
|
8
8
|
double_quoted_string: DoubleQuotedString,
|
9
9
|
integer: Integer,
|
10
10
|
keyword: Keyword.curry,
|
11
|
+
lazy: Lazy.curry,
|
11
12
|
one_of: OneOf.curry,
|
12
13
|
problem: Problem.curry,
|
13
14
|
sequence: Sequence.curry,
|
@@ -15,9 +16,9 @@ class Compser::Step
|
|
15
16
|
token: Token.curry
|
16
17
|
}.freeze
|
17
18
|
|
18
|
-
def initialize(
|
19
|
-
@
|
20
|
-
@steps
|
19
|
+
def initialize(to_value = nil)
|
20
|
+
@to_value = to_value
|
21
|
+
@steps = []
|
21
22
|
end
|
22
23
|
|
23
24
|
def parse(src)
|
@@ -27,7 +28,7 @@ class Compser::Step
|
|
27
28
|
def call(state)
|
28
29
|
results_before = state.result_stack.size
|
29
30
|
|
30
|
-
|
31
|
+
@steps.each do |step|
|
31
32
|
return state if state.bad?
|
32
33
|
|
33
34
|
step.call(state)
|
@@ -35,10 +36,10 @@ class Compser::Step
|
|
35
36
|
|
36
37
|
results_after = state.result_stack.size
|
37
38
|
|
38
|
-
if @
|
39
|
+
if @to_value && state.good?
|
39
40
|
args = state.pop_results(results_after - results_before).map(&:value)
|
40
41
|
|
41
|
-
state.good!(@
|
42
|
+
state.good!(@to_value.call(*args))
|
42
43
|
end
|
43
44
|
|
44
45
|
state
|
@@ -49,11 +50,11 @@ class Compser::Step
|
|
49
50
|
|
50
51
|
step =
|
51
52
|
case first
|
52
|
-
when Proc
|
53
|
+
when Proc then first
|
53
54
|
when Compser::Step then first
|
54
|
-
when Symbol
|
55
|
-
when nil
|
56
|
-
else
|
55
|
+
when Symbol then STEPS.fetch(args.first).call(*rest)
|
56
|
+
when nil then block ? block : raise(ArgumentError, "expected a callable")
|
57
|
+
else raise ArgumentError, "expected a callable, got #{args.inspect}"
|
57
58
|
end
|
58
59
|
|
59
60
|
@steps << step
|
@@ -68,11 +69,11 @@ class Compser::Step
|
|
68
69
|
|
69
70
|
step =
|
70
71
|
case first
|
71
|
-
when Proc
|
72
|
+
when Proc then Drop.(first)
|
72
73
|
when Compser::Step then Drop.(first)
|
73
|
-
when Symbol
|
74
|
-
when nil
|
75
|
-
else
|
74
|
+
when Symbol then Drop.(STEPS.fetch(first).call(*rest))
|
75
|
+
when nil then block ? Drop.(block) : raise(ArgumentError, "expected a callable")
|
76
|
+
else raise ArgumentError, "expected a callable"
|
76
77
|
end
|
77
78
|
|
78
79
|
@steps << step
|
data/lib/compser/version.rb
CHANGED
data/lib/compser.rb
CHANGED
@@ -11,6 +11,7 @@ require_relative "compser/step/double_quoted_string"
|
|
11
11
|
require_relative "compser/step/drop"
|
12
12
|
require_relative "compser/step/integer"
|
13
13
|
require_relative "compser/step/keyword"
|
14
|
+
require_relative "compser/step/lazy"
|
14
15
|
require_relative "compser/step/one_of"
|
15
16
|
require_relative "compser/step/problem"
|
16
17
|
require_relative "compser/step/sequence"
|
@@ -23,12 +24,12 @@ module Compser
|
|
23
24
|
Step.new
|
24
25
|
end
|
25
26
|
|
26
|
-
def map(
|
27
|
-
Step.new(
|
27
|
+
def map(to_value)
|
28
|
+
Step.new(to_value)
|
28
29
|
end
|
29
30
|
|
30
31
|
def take(...)
|
31
|
-
Step.new.
|
32
|
+
Step.new.take(...)
|
32
33
|
end
|
33
34
|
|
34
35
|
def drop(...)
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: compser
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.1
|
4
|
+
version: 0.2.1
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Luiz Vasconcellos
|
8
8
|
autorequire:
|
9
9
|
bindir: exe
|
10
10
|
cert_chain: []
|
11
|
-
date: 2023-10-
|
11
|
+
date: 2023-10-15 00:00:00.000000000 Z
|
12
12
|
dependencies: []
|
13
13
|
description:
|
14
14
|
email:
|
@@ -24,6 +24,7 @@ files:
|
|
24
24
|
- LICENSE.txt
|
25
25
|
- README.md
|
26
26
|
- Rakefile
|
27
|
+
- examples/calculator.rb
|
27
28
|
- examples/json-benchmark.rb
|
28
29
|
- examples/json-payload.json
|
29
30
|
- examples/json.rb
|
@@ -39,6 +40,7 @@ files:
|
|
39
40
|
- lib/compser/step/drop.rb
|
40
41
|
- lib/compser/step/integer.rb
|
41
42
|
- lib/compser/step/keyword.rb
|
43
|
+
- lib/compser/step/lazy.rb
|
42
44
|
- lib/compser/step/one_of.rb
|
43
45
|
- lib/compser/step/problem.rb
|
44
46
|
- lib/compser/step/sequence.rb
|
@@ -70,5 +72,5 @@ requirements: []
|
|
70
72
|
rubygems_version: 3.4.10
|
71
73
|
signing_key:
|
72
74
|
specification_version: 4
|
73
|
-
summary:
|
75
|
+
summary: Compser is a parser library for Ruby inspired by elm-parser.
|
74
76
|
test_files: []
|