compser 0.1.0 → 0.2.0
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 +35 -7
- data/examples/calculator.rb +76 -0
- data/examples/json-benchmark.rb +9 -3
- data/examples/json.rb +9 -2
- 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/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 +10 -9
- data/lib/compser/version.rb +1 -1
- data/lib/compser.rb +1 -0
- 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: 2772b2accc3f032d8ca438c0510c439d17bcd036339d33fbbb4c9aea8ac1292c
|
4
|
+
data.tar.gz: cc6ee194f1f60bf428057eda00d43d7119d264939bea9acd54925b746d4d339f
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 360a5566bf3d8fd5621662ec57d2a744e68a001ca72b66890a9b8095ab06e4ed7a26eae7bd965fff3f47c5eb35f8ad6c297db591de513fc7fdbf56ee0040d79d
|
7
|
+
data.tar.gz: 65eef3b42ac80f16fadfe202e9d46e3fc84c48ef95c0f2cb76c42a37e69995f62bf21976fd6fee59c7b55fd80ac837fc7e457a8447e99eabfd7fb15dcf3b9a7d
|
data/Gemfile.lock
CHANGED
data/README.md
CHANGED
@@ -14,6 +14,7 @@ and available building blocks you can use to compose more complex and sophistica
|
|
14
14
|
* [`map`](#map)
|
15
15
|
* [`one_of`](#one_of)
|
16
16
|
* [`sequence`](#sequence)
|
17
|
+
* [`lazy`](#lazy)
|
17
18
|
* [`spaces`](#spaces)
|
18
19
|
* [`chomp_if`](#chomp_if)
|
19
20
|
* [`chomp_while`](#chomp_while)
|
@@ -51,7 +52,7 @@ parser.parse('123a') # => Bad<...>
|
|
51
52
|
parser.parse('0x1A') # => Bad<...>
|
52
53
|
|
53
54
|
|
54
|
-
# negative integers with '-' prefix
|
55
|
+
# support negative integers with '-' prefix
|
55
56
|
def my_integer
|
56
57
|
take(:one_of, [
|
57
58
|
map(->(x) { x * -1 }).drop(:token, '-').take(:integer),
|
@@ -176,6 +177,31 @@ parser.parse('12,') # => Bad<...>
|
|
176
177
|
parser.parse(',12') # => Bad<...>
|
177
178
|
```
|
178
179
|
|
180
|
+
#### `lazy`
|
181
|
+
|
182
|
+
Wraps a parser in a lazy-evaluated proc. Use `lazy` to build recursive parsers.
|
183
|
+
|
184
|
+
```ruby
|
185
|
+
ToList = ->(*integers) { integers }
|
186
|
+
|
187
|
+
CommaSeparatedInteger = -> do
|
188
|
+
take(:integer)
|
189
|
+
.drop(:spaces)
|
190
|
+
.take(:one_of, [
|
191
|
+
drop(:token, ',').drop(:spaces).take(:lazy, CommaSeparatedInteger),
|
192
|
+
succeed
|
193
|
+
])
|
194
|
+
end
|
195
|
+
|
196
|
+
parser = map(ToList).take(CommaSeparatedInteger.call())
|
197
|
+
|
198
|
+
parser.parse('12, 23, 34') # => Good<[12, 23, 34]>
|
199
|
+
parser.parse('123') # => Good<[123]>
|
200
|
+
|
201
|
+
parser.parse('12,') # => Bad<...>
|
202
|
+
parser.parse(',12') # => Bad<...>
|
203
|
+
```
|
204
|
+
|
179
205
|
#### `spaces`
|
180
206
|
|
181
207
|
Chompes zero or more blankspaces, line breaks and tabs. Always succeeds.
|
@@ -210,12 +236,14 @@ parser.parse('cccdd').state # => State<good?: true, offset: 0, chomped: ''>
|
|
210
236
|
## Benchmark
|
211
237
|
|
212
238
|
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
|
239
|
+
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
240
|
|
215
|
-
The benchmark parses a 1,5kb payload
|
241
|
+
[The benchmark](https://github.com/luizpvas/compser/blob/main/examples/json-benchmark.rb) parses a 1,5kb payload 100 times.
|
216
242
|
|
217
|
-
Implementation | Time |
|
243
|
+
Implementation | Time | Comparison to `JSON.parse`
|
218
244
|
:---:|:---:|:---:
|
219
|
-
`JSON.parse`
|
220
|
-
`
|
221
|
-
`
|
245
|
+
`JSON.parse` | 0.00067s | -
|
246
|
+
`Compser::Json.parse` (with YJIT) | 0.216s | 322x slower
|
247
|
+
`Compser::Json.parse` | 0.268s | 400x slower
|
248
|
+
`Parsby::Example::JsonParser` (with YJIT) | 24.19s | 36100x slower
|
249
|
+
`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 Compser
|
7
|
+
extend self
|
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,6 +1,8 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
require_relative "../lib/compser"
|
2
4
|
|
3
|
-
module
|
5
|
+
module Compser::Json
|
4
6
|
extend Compser
|
5
7
|
extend self
|
6
8
|
|
@@ -10,9 +12,10 @@ module MyJson
|
|
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/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,
|
@@ -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)
|
@@ -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"
|
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.
|
4
|
+
version: 0.2.0
|
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 builder library for Ruby.
|
74
76
|
test_files: []
|