compser 0.1.0 → 0.2.0

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: 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: []