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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: e91d16c77b4d59f8ce036475bb9f00d9d5c673b6c1d5ae83633189b76000ba68
4
- data.tar.gz: 0ee49bea1a6235fc2d0da6840c920122cb58e394cd575a2739ce96a2fd9532d7
3
+ metadata.gz: 323319e8a15dae0341e6c2c9def9238fb14587e5a90d65a4c2366ef6c6c67725
4
+ data.tar.gz: 6f3ec1d93cb1c702745f363bd586cbd85f18b9ec53c6d49b6fda2a856f63f86d
5
5
  SHA512:
6
- metadata.gz: 11eded3b70d584c53dc890f133b322e8485c43332c32c8694e40e91dd666bf64365014dc2b714ea94e5b0a25cee575f50479180829806b9a9fccbed4b084b5b6
7
- data.tar.gz: 1055a219d1c8645739a891e049e5e0d598bf4643674ddde27807dda3b6efedbd3ffae0a4bd494e5fa84af6e751f5a4ad81b8d5071428e59e2607766137d68b35
6
+ metadata.gz: b69786e8a9a448ab47a494a2388e608e46546bea33902e0c68ecac9598655e92a7b1cc3abb2214b39a874fc97b3322c32a05228b53894fc3b5404c4212a48fb1
7
+ data.tar.gz: 6fe207a4536aa12062681558cfdf340709c0a9cab191cf9dea65c6f865c4260e5593ebfcd4e41ef10db56f77957b8c3d9b3de53bcf7bf047a03865282231a949
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.2.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
@@ -1,9 +1,10 @@
1
1
  # Compser
2
2
 
3
- Compser is a parser builder 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) to get a glimpse of the syntax
5
- and available building blocks you can use to compose more complex and sophisticated parsers.
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 the native `JSON.parse`.
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 5000 times.
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 | Difference
254
+ Implementation | Time | Comparison to `JSON.parse`
218
255
  :---:|:---:|:---:
219
- `JSON.parse` | 0.019s | -
220
- `MyJson.parse` (with YJIT) | 9.9s | 526x slower
221
- `MyJSON.parse` | 12.3s | 654x slower
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))")
@@ -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,18 +1,21 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require_relative "../lib/compser"
2
4
 
3
- module MyJson
4
- extend Compser
5
+ module Compser::Json
5
6
  extend self
7
+ include Compser
6
8
 
7
- def parse(str)
8
- value.parse(str)
9
+ def parse(...)
10
+ value.parse(...)
9
11
  end
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
@@ -1,9 +1,9 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  class Compser::Step
4
- IsDigit = ->(char) do
5
- char =~ /\d/
6
- end
4
+ DIGIT = /\d/.freeze
5
+
6
+ IsDigit = ->(ch) { DIGIT.match?(ch) }
7
7
 
8
8
  NotFollowedByAlpha = ->(state) do
9
9
  return state if state.eof?
@@ -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,
@@ -15,9 +16,9 @@ class Compser::Step
15
16
  token: Token.curry
16
17
  }.freeze
17
18
 
18
- def initialize(mapper = nil)
19
- @mapper = mapper
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
- 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)
@@ -35,10 +36,10 @@ class Compser::Step
35
36
 
36
37
  results_after = state.result_stack.size
37
38
 
38
- if @mapper && state.good?
39
+ if @to_value && state.good?
39
40
  args = state.pop_results(results_after - results_before).map(&:value)
40
41
 
41
- state.good!(@mapper.call(*args))
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 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.1"
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"
@@ -23,12 +24,12 @@ module Compser
23
24
  Step.new
24
25
  end
25
26
 
26
- def map(mapper)
27
- Step.new(mapper)
27
+ def map(to_value)
28
+ Step.new(to_value)
28
29
  end
29
30
 
30
31
  def take(...)
31
- Step.new.and_then(...)
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.0
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-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 library for Ruby inspired by elm-parser.
74
76
  test_files: []