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