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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: e91d16c77b4d59f8ce036475bb9f00d9d5c673b6c1d5ae83633189b76000ba68
4
- data.tar.gz: 0ee49bea1a6235fc2d0da6840c920122cb58e394cd575a2739ce96a2fd9532d7
3
+ metadata.gz: 2772b2accc3f032d8ca438c0510c439d17bcd036339d33fbbb4c9aea8ac1292c
4
+ data.tar.gz: cc6ee194f1f60bf428057eda00d43d7119d264939bea9acd54925b746d4d339f
5
5
  SHA512:
6
- metadata.gz: 11eded3b70d584c53dc890f133b322e8485c43332c32c8694e40e91dd666bf64365014dc2b714ea94e5b0a25cee575f50479180829806b9a9fccbed4b084b5b6
7
- data.tar.gz: 1055a219d1c8645739a891e049e5e0d598bf4643674ddde27807dda3b6efedbd3ffae0a4bd494e5fa84af6e751f5a4ad81b8d5071428e59e2607766137d68b35
6
+ metadata.gz: 360a5566bf3d8fd5621662ec57d2a744e68a001ca72b66890a9b8095ab06e4ed7a26eae7bd965fff3f47c5eb35f8ad6c297db591de513fc7fdbf56ee0040d79d
7
+ data.tar.gz: 65eef3b42ac80f16fadfe202e9d46e3fc84c48ef95c0f2cb76c42a37e69995f62bf21976fd6fee59c7b55fd80ac837fc7e457a8447e99eabfd7fb15dcf3b9a7d
data/Gemfile.lock CHANGED
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- Compser (0.1.0)
4
+ compser (0.1.0)
5
5
 
6
6
  GEM
7
7
  remote: https://rubygems.org/
@@ -13,7 +13,7 @@ PLATFORMS
13
13
  x86_64-linux
14
14
 
15
15
  DEPENDENCIES
16
- Compser!
16
+ compser!
17
17
  minitest (~> 5.0)
18
18
  rake (~> 13.0)
19
19
 
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 the native `JSON.parse`.
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 5000 times.
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 | Difference
243
+ Implementation | Time | Comparison to `JSON.parse`
218
244
  :---:|:---:|:---:
219
- `JSON.parse` | 0.019s | -
220
- `MyJson.parse` (with YJIT) | 9.9s | 526x slower
221
- `MyJSON.parse` | 12.3s | 654x slower
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))")
@@ -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 = 5000
12
+ n = 100
8
13
 
9
- n.times do; MyJson.parse(json); end
14
+ 5000.times do; Compser::Json.parse(json); end
10
15
 
11
16
  Benchmark.bm do |x|
12
- x.report("MyJson.parse") { n.times do; MyJson.parse(json); end }
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 MyJson
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
- take(:double_quoted_string),
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),
@@ -1,12 +1,12 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Compser::Result
4
- Good = Data.define(:state, :value) do
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(:state, :message) do
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(self, nil)]
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(self, value)
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(self, message)
85
+ Compser::Result::Bad.new(message)
86
86
  end
87
87
  end
@@ -2,8 +2,8 @@
2
2
 
3
3
  class Compser::Step
4
4
  ChompWhile = ->(predicate, state) do
5
- state.chomp while !state.eof? && predicate.call(state.peek)
6
-
7
- state
5
+ while !state.eof? && predicate.call(state.peek)
6
+ state.chomp
7
+ end
8
8
  end
9
9
  end
@@ -7,7 +7,5 @@ class Compser::Step
7
7
  parser.call(state)
8
8
 
9
9
  savepoint.rollback_chomped_and_result_stack if state.good?
10
-
11
- state
12
10
  end.curry
13
11
  end
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ class Compser::Step
4
+ Lazy = ->(to_parser, state) do
5
+ to_parser.call.call(state)
6
+ end
7
+ end
@@ -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 state if state.good? || savepoint.has_changes?
13
- return state if index == branches.size - 1 # is last
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 state if state.bad?
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
- state = @steps.reduce(state) do |state, step|
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 then first
53
+ when Proc then first
53
54
  when Compser::Step then first
54
- when Symbol then STEPS.fetch(args.first).call(*rest)
55
- when nil then block ? block : raise(ArgumentError, "expected a callable")
56
- else raise ArgumentError, "expected a callable, got #{args.inspect}"
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 then Drop.(first)
72
+ when Proc then Drop.(first)
72
73
  when Compser::Step then Drop.(first)
73
- when Symbol then Drop.(STEPS.fetch(first).call(*rest))
74
- when nil then block ? Drop.(block) : raise(ArgumentError, "expected a callable")
75
- else raise ArgumentError, "expected a callable"
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
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Compser
4
- VERSION = "0.1.0"
4
+ VERSION = "0.2.0"
5
5
  end
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.1.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-14 00:00:00.000000000 Z
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: Parser combinator for Ruby.
75
+ summary: Compser is a parser builder library for Ruby.
74
76
  test_files: []