pestle 0.1.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.
Files changed (75) hide show
  1. checksums.yaml +7 -0
  2. checksums.yaml.gz.sig +0 -0
  3. data/.rubocop.yml +59 -0
  4. data/.ruby-version +1 -0
  5. data/CHANGELOG.md +3 -0
  6. data/LICENSE.txt +21 -0
  7. data/LICENSE_PEST.txt +23 -0
  8. data/README.md +124 -0
  9. data/Rakefile +23 -0
  10. data/Steepfile +19 -0
  11. data/benchmarks/jsonpath_ips.rb +33 -0
  12. data/examples/calculator_pratt.rb +157 -0
  13. data/examples/calculator_prec_climber.rb +225 -0
  14. data/examples/calculator_stack_vm.rb +291 -0
  15. data/examples/csv.rb +73 -0
  16. data/examples/ini.rb +90 -0
  17. data/examples/json_example.rb +141 -0
  18. data/examples/jsonpath/README.md +3 -0
  19. data/examples/jsonpath/jsonpath.pest +182 -0
  20. data/examples/jsonpath/lib/jsonpath/ast.rb +362 -0
  21. data/examples/jsonpath/lib/jsonpath/function_extensions.rb +201 -0
  22. data/examples/jsonpath/lib/jsonpath/node.rb +20 -0
  23. data/examples/jsonpath/lib/jsonpath/query.rb +25 -0
  24. data/examples/jsonpath/lib/jsonpath.rb +453 -0
  25. data/lib/pestle/errors.rb +98 -0
  26. data/lib/pestle/grammar/builtin_rules/ascii.rb +38 -0
  27. data/lib/pestle/grammar/builtin_rules/special.rb +63 -0
  28. data/lib/pestle/grammar/builtin_rules/unicode.rb +291 -0
  29. data/lib/pestle/grammar/errors.rb +62 -0
  30. data/lib/pestle/grammar/expression.rb +90 -0
  31. data/lib/pestle/grammar/expressions/choice.rb +36 -0
  32. data/lib/pestle/grammar/expressions/group.rb +27 -0
  33. data/lib/pestle/grammar/expressions/identifier.rb +26 -0
  34. data/lib/pestle/grammar/expressions/postfix.rb +272 -0
  35. data/lib/pestle/grammar/expressions/prefix.rb +51 -0
  36. data/lib/pestle/grammar/expressions/range.rb +26 -0
  37. data/lib/pestle/grammar/expressions/sequence.rb +38 -0
  38. data/lib/pestle/grammar/expressions/stack.rb +192 -0
  39. data/lib/pestle/grammar/expressions/string.rb +46 -0
  40. data/lib/pestle/grammar/lexer.rb +464 -0
  41. data/lib/pestle/grammar/parser.rb +340 -0
  42. data/lib/pestle/grammar/rule.rb +98 -0
  43. data/lib/pestle/pair.rb +325 -0
  44. data/lib/pestle/parser.rb +48 -0
  45. data/lib/pestle/pratt.rb +74 -0
  46. data/lib/pestle/state.rb +220 -0
  47. data/lib/pestle/version.rb +5 -0
  48. data/lib/pestle.rb +24 -0
  49. data/sig/errors.rbs +22 -0
  50. data/sig/grammar/ascii.rbs +9 -0
  51. data/sig/grammar/choice.rbs +14 -0
  52. data/sig/grammar/errors.rbs +22 -0
  53. data/sig/grammar/expression.rbs +39 -0
  54. data/sig/grammar/group.rbs +14 -0
  55. data/sig/grammar/identifier.rbs +11 -0
  56. data/sig/grammar/lexer.rbs +85 -0
  57. data/sig/grammar/parser.rbs +57 -0
  58. data/sig/grammar/postfix.rbs +112 -0
  59. data/sig/grammar/prefix.rbs +27 -0
  60. data/sig/grammar/range.rbs +20 -0
  61. data/sig/grammar/rule.rbs +40 -0
  62. data/sig/grammar/sequence.rbs +14 -0
  63. data/sig/grammar/special.rbs +39 -0
  64. data/sig/grammar/stack.rbs +57 -0
  65. data/sig/grammar/string.rbs +27 -0
  66. data/sig/grammar/unicode.rbs +15 -0
  67. data/sig/pair.rbs +168 -0
  68. data/sig/parser.rbs +16 -0
  69. data/sig/pestle.rbs +5 -0
  70. data/sig/pratt.rbs +27 -0
  71. data/sig/state.rbs +95 -0
  72. data/sig/stdlib/strscan.rbs +3 -0
  73. data.tar.gz.sig +0 -0
  74. metadata +141 -0
  75. metadata.gz.sig +0 -0
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 06fb88b355cc01ed9c3b17aa398c2b5c61246a875082246dd3f509406979e62c
4
+ data.tar.gz: 55e5b166a282d8d3aba0c8b6af8751d1f1b6098482a986079a21da1f5480d6e8
5
+ SHA512:
6
+ metadata.gz: b2ded82209106e7e7355ddb2605069e4f9c008ad35bfb51247cf94e48dff7e812ca15508a4a6bb83cc5f6c7704b137da5fb300dd705f8ca7a60f2b605c9ec8d4
7
+ data.tar.gz: 0d9228e4e56327ace972b15e707c85ff3f3d2f20680e8998436e9895924dcf5083e0f1b0259d58ac11f74d63f8bbb4376ee74970569dfda49bbfced147912676
checksums.yaml.gz.sig ADDED
Binary file
data/.rubocop.yml ADDED
@@ -0,0 +1,59 @@
1
+ plugins:
2
+ - rubocop-minitest
3
+ - rubocop-rake
4
+ - rubocop-performance
5
+
6
+ AllCops:
7
+ TargetRubyVersion: 3.3
8
+ NewCops: enable
9
+
10
+ Style/StringLiterals:
11
+ EnforcedStyle: double_quotes
12
+
13
+ Style/StringLiteralsInInterpolation:
14
+ EnforcedStyle: double_quotes
15
+
16
+ Style/ClassAndModuleChildren:
17
+ Enabled: false
18
+
19
+ Style/FormatStringToken:
20
+ Enabled: false
21
+
22
+ Layout/LineLength:
23
+ Max: 100
24
+
25
+ Metrics/ModuleLength:
26
+ Enabled: false
27
+
28
+ Metrics/MethodLength:
29
+ Enabled: false
30
+
31
+ Metrics/AbcSize:
32
+ Enabled: false
33
+
34
+ Metrics/PerceivedComplexity:
35
+ Max: 100
36
+
37
+ Metrics/CyclomaticComplexity:
38
+ Max: 50
39
+
40
+ Metrics/ParameterLists:
41
+ Enabled: false
42
+
43
+ Metrics/ClassLength:
44
+ Enabled: false
45
+
46
+ Metrics/BlockLength:
47
+ Enabled: false
48
+
49
+ Naming/PredicateMethod:
50
+ Enabled: false
51
+
52
+ Naming/MethodParameterName:
53
+ AllowedNames: [op, re]
54
+
55
+ Lint/UnusedMethodArgument:
56
+ AllowUnusedKeywordArguments: true
57
+
58
+ Minitest/AssertPredicate:
59
+ Enabled: false
data/.ruby-version ADDED
@@ -0,0 +1 @@
1
+ 3.4.8
data/CHANGELOG.md ADDED
@@ -0,0 +1,3 @@
1
+ ## [0.1.0] - 26-02-21
2
+
3
+ - Initial release
data/LICENSE.txt ADDED
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2025 James Prior
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in
13
+ all copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21
+ THE SOFTWARE.
data/LICENSE_PEST.txt ADDED
@@ -0,0 +1,23 @@
1
+ Permission is hereby granted, free of charge, to any
2
+ person obtaining a copy of this software and associated
3
+ documentation files (the "Software"), to deal in the
4
+ Software without restriction, including without
5
+ limitation the rights to use, copy, modify, merge,
6
+ publish, distribute, sublicense, and/or sell copies of
7
+ the Software, and to permit persons to whom the Software
8
+ is furnished to do so, subject to the following
9
+ conditions:
10
+
11
+ The above copyright notice and this permission notice
12
+ shall be included in all copies or substantial portions
13
+ of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF
16
+ ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED
17
+ TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
18
+ PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT
19
+ SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
20
+ CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
21
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR
22
+ IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
23
+ DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,124 @@
1
+ # Ruby Pestle
2
+
3
+ Pestle is a Ruby port of the [Rust pest](https://pest.rs/) parsing library - a PEG (Parsing Expression Grammar) parser generator.
4
+
5
+ We use the same grammar syntax as Pest v2. See the [Pest Book](https://pest.rs/book/).
6
+
7
+ Language grammars are parsed to an internal representation from which input text can be parsed into token pairs. Currently there is no code gen phase.
8
+
9
+ As of version 0.1.0, grammar optimization is unimplemented. Even with some optimization passes, Pest implemented in pure Ruby is never going to be as fast as a hand-crafted parser. It might still be useful for prototyping and/or testing during language design.
10
+
11
+ ## Links
12
+
13
+ - Change log: https://github.com/jg-rp/ruby-pestle/blob/main/CHANGELOG.md
14
+ - RubyGems: https://rubygems.org/gems/pestle
15
+ - Source code: https://github.com/jg-rp/ruby-pestle
16
+ - Issue tracker: https://github.com/jg-rp/ruby-pestle/issues
17
+
18
+ ## Usage
19
+
20
+ Please see [examples](https://github.com/jg-rp/ruby-pestle/tree/main/examples) and refer to [pair.rb](https://github.com/jg-rp/ruby-pestle/blob/main/lib/pestle/pair.rb) for the token API.
21
+
22
+ ### Debugging
23
+
24
+ Given this example grammar for a calculator with grammar-encoded operator precedence.
25
+
26
+ ```ruby
27
+ GRAMMAR = <<~GRAMMAR
28
+ program = { SOI ~ expr ~ EOI }
29
+ expr = { add_sub } // top-level expression
30
+
31
+ add_sub = { mul_div ~ (add_op ~ mul_div)* }
32
+ add_op = _{ add | sub }
33
+ add = { "+" }
34
+ sub = { "-" }
35
+
36
+ mul_div = { pow_expr ~ (mul_op ~ pow_expr)* }
37
+ mul_op = _{ mul | div }
38
+ mul = { "*" }
39
+ div = { "/" }
40
+
41
+ pow_expr = { prefix ~ (pow_op ~ pow_expr)? } // right-associative
42
+ pow_op = _{ pow }
43
+ pow = { "^" }
44
+
45
+ prefix = { (neg)* ~ postfix }
46
+ neg = { "-" }
47
+
48
+ postfix = { primary ~ (fac)* }
49
+ fac = { "!" }
50
+
51
+ primary = { int | ident | "(" ~ expr ~ ")" }
52
+ int = @{ (ASCII_NONZERO_DIGIT ~ ASCII_DIGIT* | "0") }
53
+ ident = @{ ASCII_ALPHA+ }
54
+
55
+ WHITESPACE = _{ " " | "\t" | NEWLINE }
56
+ GRAMMAR
57
+
58
+ START_RULE = :program
59
+
60
+ parser = Pestle::Parser.from_grammar(GRAMMAR)
61
+ ```
62
+
63
+ We can dump a tree view of the grammar.
64
+
65
+ ```ruby
66
+ puts parser.tree_view
67
+ ```
68
+
69
+ **Output** (we're just showing the first three rules here)
70
+
71
+ ```
72
+ Pestle::Grammar::Rule program = { SOI ~ expr ~ EOI }
73
+ └── Pestle::Grammar::Sequence SOI ~ expr ~ EOI
74
+ ├── Pestle::Grammar::Identifier SOI
75
+ ├── Pestle::Grammar::Identifier expr
76
+ └── Pestle::Grammar::Identifier EOI
77
+
78
+ Pestle::Grammar::Rule expr = { add_sub }
79
+ └── Pestle::Grammar::Identifier add_sub
80
+
81
+ Pestle::Grammar::Rule add_sub = { mul_div ~ (add_op ~ mul_div)* }
82
+ └── Pestle::Grammar::Sequence mul_div ~ (add_op ~ mul_div)*
83
+ ├── Pestle::Grammar::Identifier mul_div
84
+ └── Pestle::Grammar::Repeat (add_op ~ mul_div)*
85
+ └── Pestle::Grammar::Group (add_op ~ mul_div)
86
+ └── Pestle::Grammar::Sequence add_op ~ mul_div
87
+ ├── Pestle::Grammar::Identifier add_op
88
+ └── Pestle::Grammar::Identifier mul_div
89
+ ```
90
+
91
+ We can also dump arbitrary token pairs for inspection.
92
+
93
+ ```ruby
94
+ pairs = parser.parse(START_RULE, "1 + 2 * 3!")
95
+ puts pairs.dumps
96
+ ```
97
+
98
+ **Output**
99
+
100
+ ```
101
+ - program
102
+ - expr > add_sub
103
+ - mul_div > pow_expr > prefix > postfix > primary > int: "1"
104
+ - add: "+"
105
+ - mul_div
106
+ - pow_expr > prefix > postfix > primary > int: "2"
107
+ - mul: "*"
108
+ - pow_expr > prefix > postfix
109
+ - primary > int: "3"
110
+ - fac: "!"
111
+ - EOI: ""
112
+ ```
113
+
114
+ There's also `Pair#dump` and `Pairs#dump`, which return a more verbose, JSON-like representation of the generated token pairs.
115
+
116
+ ```ruby
117
+ puts JSON.pretty_generate(pairs.dump)
118
+ ```
119
+
120
+ ## License
121
+
122
+ The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
123
+
124
+ Ruby Pestle is a port of [Rust pest](https://pest.rs/). See `LICENSE_PEST.txt`.
data/Rakefile ADDED
@@ -0,0 +1,23 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "bundler/gem_tasks"
4
+ require "minitest/test_task"
5
+
6
+ Minitest::TestTask.create
7
+
8
+ require "rubocop/rake_task"
9
+
10
+ RuboCop::RakeTask.new do |task|
11
+ task.plugins << "rubocop-minitest"
12
+ task.plugins << "rubocop-rake"
13
+ task.plugins << "rubocop-performance"
14
+ end
15
+
16
+ require "steep/rake_task"
17
+
18
+ Steep::RakeTask.new do |t|
19
+ t.check.severity_level = :error
20
+ t.watch.verbose
21
+ end
22
+
23
+ task default: %i[test rubocop steep]
data/Steepfile ADDED
@@ -0,0 +1,19 @@
1
+ # frozen_string_literal: true
2
+
3
+ D = Steep::Diagnostic
4
+
5
+ target :lib do
6
+ signature "sig"
7
+ check "lib"
8
+
9
+ library "json"
10
+ library "strscan"
11
+
12
+ # configure_code_diagnostics(D::Ruby.default) # `default` diagnostics setting
13
+ # configure_code_diagnostics(D::Ruby.strict) # `strict` diagnostics setting
14
+ # configure_code_diagnostics(D::Ruby.lenient) # `lenient` diagnostics setting
15
+ # configure_code_diagnostics(D::Ruby.silent) # `silent` diagnostics setting
16
+ # configure_code_diagnostics do |hash| # You can setup everything yourself
17
+ # hash[D::Ruby::NoMethod] = :information
18
+ # end
19
+ end
@@ -0,0 +1,33 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "benchmark/ips"
4
+ require "json"
5
+ require_relative "../examples/jsonpath/lib/jsonpath"
6
+
7
+ CTS = JSON.parse(File.read("test/jsonpath-compliance-test-suite/cts.json"))
8
+ VALID_QUERIES = CTS["tests"].filter { |t| !t.key?("invalid_selector") }
9
+ COMPILED_QUERIES = VALID_QUERIES.map { |t| [JSONPathPest.compile(t["selector"]), t["document"]] }
10
+
11
+ puts "#{VALID_QUERIES.length} queries per iteration"
12
+
13
+ Benchmark.ips do |x|
14
+ # Configure the number of seconds used during
15
+ # the warmup phase (default 2) and calculation phase (default 5)
16
+ x.config(warmup: 2, time: 5)
17
+
18
+ x.report("compile and find:") do
19
+ VALID_QUERIES.map { |t| JSONPathPest.find(t["selector"], t["document"]) }
20
+ end
21
+
22
+ x.report("just compile:") do
23
+ VALID_QUERIES.map { |t| JSONPathPest.compile(t["selector"]) }
24
+ end
25
+
26
+ x.report("just find:") do
27
+ COMPILED_QUERIES.map { |p, d| p.find(d) }
28
+ end
29
+
30
+ x.report("just pest parse:") do
31
+ VALID_QUERIES.map { |t| JSONPathPest.pest_parse(t["selector"]) }
32
+ end
33
+ end
@@ -0,0 +1,157 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Based on the calculator example found in the pest book, with
4
+ # the addition of the `ident` rule.
5
+ # https://pest.rs/book/precedence.html.
6
+
7
+ # https://github.com/pest-parser/book/blob/master/LICENSE-MIT
8
+ #
9
+ # Permission is hereby granted, free of charge, to any
10
+ # person obtaining a copy of this software and associated
11
+ # documentation files (the "Software"), to deal in the
12
+ # Software without restriction, including without
13
+ # limitation the rights to use, copy, modify, merge,
14
+ # publish, distribute, sublicense, and/or sell copies of
15
+ # the Software, and to permit persons to whom the Software
16
+ # is furnished to do so, subject to the following
17
+ # conditions:
18
+ #
19
+ # The above copyright notice and this permission notice
20
+ # shall be included in all copies or substantial portions
21
+ # of the Software.
22
+ #
23
+ # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF
24
+ # ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED
25
+ # TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
26
+ # PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT
27
+ # SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
28
+ # CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
29
+ # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR
30
+ # IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
31
+ # DEALINGS IN THE SOFTWARE.
32
+
33
+ require_relative "../lib/pestle"
34
+
35
+ module PrattExample
36
+ GRAMMAR = <<~'GRAMMAR'
37
+ WHITESPACE = _{ " " | "\t" | NEWLINE }
38
+
39
+ program = { SOI ~ expr ~ EOI }
40
+ expr = { prefix* ~ primary ~ postfix* ~ (infix ~ prefix* ~ primary ~ postfix* )* }
41
+ infix = _{ add | sub | mul | div | pow }
42
+ add = { "+" } // Addition
43
+ sub = { "-" } // Subtraction
44
+ mul = { "*" } // Multiplication
45
+ div = { "/" } // Division
46
+ pow = { "^" } // Exponentiation
47
+ prefix = _{ neg }
48
+ neg = { "-" } // Negation
49
+ postfix = _{ fac }
50
+ fac = { "!" } // Factorial
51
+ primary = _{ int | "(" ~ expr ~ ")" | ident }
52
+ int = @{ (ASCII_NONZERO_DIGIT ~ ASCII_DIGIT+ | ASCII_DIGIT) }
53
+ ident = @{ ASCII_ALPHA+ }
54
+ GRAMMAR
55
+
56
+ PARSER = Pestle::Parser.from_grammar(GRAMMAR)
57
+
58
+ START_RULE = :program
59
+
60
+ # Very basic abstract syntax tree (AST) nodes.
61
+
62
+ VarExpr = Struct.new(:value) do
63
+ def evaluate(vars) = vars[value]
64
+ end
65
+
66
+ IntExpr = Struct.new(:value) do
67
+ def evaluate(vars) = value # rubocop: disable Lint/UnusedMethodArgument
68
+ end
69
+
70
+ PrefixExpr = Struct.new(:op, :expr) do
71
+ def evaluate(vars) = expr.evaluate(vars).send(op)
72
+ end
73
+
74
+ InfixExpr = Struct.new(:left, :op, :right) do
75
+ def evaluate(vars) = left.evaluate(vars).send(op, right.evaluate(vars))
76
+ end
77
+
78
+ PostfixExpr = Struct.new(:expr, :op) do
79
+ def evaluate(vars) = expr.evaluate(vars).send(op)
80
+ end
81
+
82
+ # Monkey patch Integer with a factorial method.
83
+ class ::Integer
84
+ remove_method(:fact) if method_defined?(:fact)
85
+ def fact
86
+ (1..self).reduce(1, :*)
87
+ end
88
+ end
89
+
90
+ # Example Pratt parser for a calculator grammar.
91
+ class CalculatorParser < Pestle::PrattParser
92
+ PREFIX_OPS = { neg: 6 }.freeze
93
+
94
+ INFIX_OPS = {
95
+ add: [3, LEFT_ASSOC],
96
+ sub: [3, LEFT_ASSOC],
97
+ mul: [4, LEFT_ASSOC],
98
+ div: [4, LEFT_ASSOC],
99
+ pow: [5, RIGHT_ASSOC]
100
+ }.freeze
101
+
102
+ POSTFIX_OPS = { fac: 7 }.freeze
103
+
104
+ def parse(program)
105
+ pairs = PARSER.parse(START_RULE, program)
106
+ parse_expr(pairs.first.inner.first.stream)
107
+ end
108
+
109
+ def parse_primary(pair)
110
+ case pair
111
+ in :int, _
112
+ IntExpr.new(pair.text.to_i)
113
+ in :ident, _
114
+ VarExpr.new(pair.text)
115
+ in :expr, _
116
+ parse_expr(pair.stream)
117
+ else
118
+ raise "unexpected #{pair.text.inspect}"
119
+ end
120
+ end
121
+
122
+ def parse_prefix(op, rhs)
123
+ raise "unknown prefix operator #{op.text.inspect}" unless op.rule == :neg
124
+
125
+ PrefixExpr.new(:-@, rhs)
126
+ end
127
+
128
+ def parse_postfix(lhs, op)
129
+ raise "unknown postfix operator #{op.text.inspect}" unless op.rule == :fac
130
+
131
+ PostfixExpr.new(lhs, :fact)
132
+ end
133
+
134
+ def parse_infix(lhs, op, rhs)
135
+ case op
136
+ in :add, _
137
+ InfixExpr.new(lhs, :+, rhs)
138
+ in :sub, _
139
+ InfixExpr.new(lhs, :-, rhs)
140
+ in :mul, _
141
+ InfixExpr.new(lhs, :*, rhs)
142
+ in :div, _
143
+ InfixExpr.new(lhs, :/, rhs)
144
+ in :pow, _
145
+ InfixExpr.new(lhs, :**, rhs)
146
+ else
147
+ raise "unknown infix operator #{op.text.inspect}"
148
+ end
149
+ end
150
+ end
151
+ end
152
+
153
+ if __FILE__ == $PROGRAM_NAME
154
+ parser = PrattExample::CalculatorParser.new
155
+ prog = parser.parse("1 + 2 + x")
156
+ puts prog.evaluate({ "x" => 39 })
157
+ end