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