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
@@ -0,0 +1,225 @@
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
+ # Example calculator parser using a precedence climbing technique to handle operator precedence.
36
+ module PrecClimberExample
37
+ GRAMMAR = <<~'GRAMMAR'
38
+ WHITESPACE = _{ " " | "\t" | NEWLINE }
39
+
40
+ program = { SOI ~ expr ~ EOI }
41
+ expr = { prefix* ~ primary ~ postfix* ~ (infix ~ prefix* ~ primary ~ postfix* )* }
42
+ infix = _{ add | sub | mul | div | pow }
43
+ add = { "+" } // Addition
44
+ sub = { "-" } // Subtraction
45
+ mul = { "*" } // Multiplication
46
+ div = { "/" } // Division
47
+ pow = { "^" } // Exponentiation
48
+ prefix = _{ neg }
49
+ neg = { "-" } // Negation
50
+ postfix = _{ fac }
51
+ fac = { "!" } // Factorial
52
+ primary = _{ int | "(" ~ expr ~ ")" | ident }
53
+ int = @{ (ASCII_NONZERO_DIGIT ~ ASCII_DIGIT+ | ASCII_DIGIT) }
54
+ ident = @{ ASCII_ALPHA+ }
55
+ GRAMMAR
56
+
57
+ PARSER = Pestle::Parser.from_grammar(GRAMMAR)
58
+
59
+ START_RULE = :program
60
+
61
+ # Very basic abstract syntax tree (AST) nodes.
62
+
63
+ VarExpr = Struct.new(:value) do
64
+ def evaluate(vars) = vars[value]
65
+ end
66
+
67
+ IntExpr = Struct.new(:value) do
68
+ def evaluate(vars) = value # rubocop: disable Lint/UnusedMethodArgument
69
+ end
70
+
71
+ PrefixExpr = Struct.new(:op, :expr) do
72
+ def evaluate(vars) = expr.evaluate(vars).send(op)
73
+ end
74
+
75
+ InfixExpr = Struct.new(:left, :op, :right) do
76
+ def evaluate(vars) = left.evaluate(vars).send(op, right.evaluate(vars))
77
+ end
78
+
79
+ PostfixExpr = Struct.new(:expr, :op) do
80
+ def evaluate(vars) = expr.evaluate(vars).send(op)
81
+ end
82
+
83
+ # Monkey patch Integer with a factorial method.
84
+ class ::Integer
85
+ remove_method(:fact) if method_defined?(:fact)
86
+ def fact
87
+ (1..self).reduce(1, :*)
88
+ end
89
+ end
90
+
91
+ # Operator precedence
92
+ PREC_LOWEST = 1
93
+ PREC_ADD = 3
94
+ PREC_SUB = 3
95
+ PREC_MUL = 4
96
+ PREC_DIV = 4
97
+ PREC_POW = 5
98
+ PREC_FAC = 7
99
+ PREC_PRE = 8
100
+
101
+ PRECEDENCES = {
102
+ add: PREC_ADD,
103
+ sub: PREC_SUB,
104
+ mul: PREC_MUL,
105
+ div: PREC_DIV,
106
+ pow: PREC_POW,
107
+ fac: PREC_FAC
108
+ }.freeze
109
+
110
+ INFIX_OPS = Set.new(%i[add sub mul div pow]).freeze
111
+
112
+ PREFIX_OPS = Set.new(%i[neg]).freeze
113
+
114
+ POSTFIX_OPS = Set.new(%i[fac]).freeze
115
+
116
+ # Calculator expression parser entry point.
117
+ # @param text [String]
118
+ # @return [Expr] A tree structure built from expression structs defined above.
119
+ def self.parse_program(text)
120
+ program = PARSER.parse(START_RULE, text)
121
+
122
+ # A successful parse is guaranteed to give us a root "program" pair with a
123
+ # single child. Unwrap it and get it's children as a Pestle::Pairs instance.
124
+ pairs = program.first.inner
125
+
126
+ # For this grammar, the `program` token is guaranteed to have exactly two
127
+ # children, an `expr` and `EOI`. Turn the expression into a stream and
128
+ # pass it to `parse_expr`.
129
+ parse_expr(pairs.first.stream)
130
+ end
131
+
132
+ # @param pairs [Pestle::Stream]
133
+ # @param precedence [Integer]
134
+ def self.parse_expr(pairs, precedence = PREC_LOWEST)
135
+ pair = pairs.next
136
+ raise "unexpected end of expression" if pair.nil?
137
+
138
+ left = if PREFIX_OPS.include?(pair.rule)
139
+ parse_prefix_expr(pair, pairs)
140
+ else
141
+ parse_basic_expr(pair)
142
+ end
143
+
144
+ pair = pairs.next
145
+
146
+ # Handle infix operators.
147
+ while !pair.nil? && INFIX_OPS.include?(pair.rule)
148
+ if (PRECEDENCES[pair.rule] || PREC_LOWEST) >= precedence
149
+ left = parse_infix_expr(pair, pairs, left)
150
+ pair = pairs.next
151
+ else
152
+ pairs.backup
153
+ return left
154
+ end
155
+ end
156
+
157
+ # Handle postfix operators
158
+ while !pair.nil? && POSTFIX_OPS.include?(pair.rule)
159
+ left = parse_postfix_expr(pair, left)
160
+ pair = pairs.next
161
+ end
162
+
163
+ raise "unexpected #{pair.text.inspect}" unless pair.nil? || pair.rule == :EOI
164
+
165
+ left
166
+ end
167
+
168
+ # @param pair [Pestle::Pair]
169
+ # @param pairs [Pestle::Stream]
170
+ def self.parse_prefix_expr(pair, pairs)
171
+ raise "unknown prefix operator #{pair.text.inspect}" unless PREFIX_OPS.include?(pair.rule)
172
+
173
+ PrefixExpr.new(:-@, parse_expr(pairs, PREC_PRE))
174
+ end
175
+
176
+ # @param op [Pestle::Pair]
177
+ # @param pairs [Pestle::Stream]
178
+ # @param left [Pestle::Pair]
179
+ def self.parse_infix_expr(op, pairs, left)
180
+ precedence = PRECEDENCES[op.rule] || PREC_LOWEST
181
+ right = parse_expr(pairs, precedence)
182
+
183
+ case op
184
+ in :add, _
185
+ InfixExpr.new(left, :+, right)
186
+ in :sub, _
187
+ InfixExpr.new(left, :-, right)
188
+ in :mul, _
189
+ InfixExpr.new(left, :*, right)
190
+ in :div, _
191
+ InfixExpr.new(left, :/, right)
192
+ in :pow, _
193
+ InfixExpr.new(left, :**, right)
194
+ else
195
+ raise "unknown infix operator #{op.text.inspect}"
196
+ end
197
+ end
198
+
199
+ # @param op [Pestle::Pair]
200
+ # @param left [Pestle::Pair]
201
+ def self.parse_postfix_expr(op, left)
202
+ raise "unknown postfix operator #{op.text.inspect}" unless POSTFIX_OPS.include?(op.rule)
203
+
204
+ PostfixExpr.new(left, :fact)
205
+ end
206
+
207
+ # @param pair [Pestle::Pair]
208
+ def self.parse_basic_expr(pair)
209
+ case pair
210
+ in :int, _
211
+ IntExpr.new(pair.text.to_i)
212
+ in :ident, _
213
+ VarExpr.new(pair.text)
214
+ in :expr, _
215
+ parse_expr(pair.inner.stream)
216
+ else
217
+ raise "unexpected #{pair.text.inspect}"
218
+ end
219
+ end
220
+ end
221
+
222
+ if __FILE__ == $PROGRAM_NAME
223
+ prog = PrecClimberExample.parse_program("1 + 2 + x")
224
+ puts prog.evaluate({ "x" => 39 })
225
+ end
@@ -0,0 +1,291 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Just for fun, a bytecode compiler and stack-based virtual machine for
4
+ # compiling and running expressions parsed with our Pratt parser example.
5
+
6
+ require_relative "calculator_pratt"
7
+
8
+ # Calculator expression parser, interpreter, compiler and virtual machine.
9
+ module PrattExample
10
+ Bytecode = Data.define(:instructions, :constants, :symbols)
11
+
12
+ # Bytecode definition and helpers for encoding and decoding bytecode.
13
+ module Code
14
+ # 8-bit integer op codes
15
+ OP_CONSTANT = 1
16
+ OP_ADD = 2
17
+ OP_SUB = 3
18
+ OP_MUL = 4
19
+ OP_DIV = 5
20
+ OP_POW = 6
21
+ OP_NEG = 7
22
+ OP_FAC = 8
23
+ OP_VAR = 9
24
+
25
+ # @param name [String] Human readable name for the op code.
26
+ # @param operand_widths [Array[Integer]] A byte count for each operand.
27
+ OpDef = Data.define(:name, :operand_widths)
28
+
29
+ # @type const DEFS: Hash[Integer, OpDef]
30
+ DEFS = {
31
+ OP_CONSTANT => OpDef.new("OpConstant", [2]),
32
+ OP_VAR => OpDef.new("OpVar", [2]),
33
+ OP_ADD => OpDef.new("OpAdd", []),
34
+ OP_SUB => OpDef.new("OpSub", []),
35
+ OP_MUL => OpDef.new("OpMul", []),
36
+ OP_DIV => OpDef.new("OpDiv", []),
37
+ OP_POW => OpDef.new("OpPow", []),
38
+ OP_NEG => OpDef.new("OpNeg", []),
39
+ OP_FAC => OpDef.new("OpFac", [])
40
+ }.freeze
41
+
42
+ # Make a bytecode instruction for operator `op` with operands from `operands`.
43
+ # @param op [Integer] Op code.
44
+ # @param operands [*Integer] Operands for `op`.
45
+ # @return [Array[Integer]] Bytes for one instruction.
46
+ def self.make(op, *operands)
47
+ op_def = DEFS[op]
48
+ raise "unknown op code #{op}" if op_def.nil?
49
+
50
+ instruction = [op]
51
+
52
+ # Encode operands with big-endian byte order.
53
+ operands.zip(op_def.operand_widths) do |operand, byte_count|
54
+ (byte_count - 1).downto(0) do |byte_index|
55
+ instruction << ((operand >> (byte_index * 8)) & 0xFF)
56
+ end
57
+ end
58
+
59
+ instruction
60
+ end
61
+
62
+ # Read operands for the operator defined by `op_def` from `instructions`
63
+ # starting from `offset`.
64
+ # @param op_def [OpDef]
65
+ # @param instructions [Array[Integer]] Array of bytes.
66
+ # @param offset [Integer] Index into `instructions`.
67
+ # @return [[Array[Integer], Integer]] Operands read and new offset.
68
+ def self.read(op_def, instructions, offset)
69
+ operands = []
70
+
71
+ op_def.operand_widths.each do |byte_count|
72
+ value = 0
73
+ (0...byte_count).each do |i|
74
+ value = (value << 8) | instructions[offset + i]
75
+ end
76
+ operands << value
77
+ offset += byte_count
78
+ end
79
+
80
+ [operands, offset]
81
+ end
82
+
83
+ # Read `n_bytes` bytes as a single operand from `instructions` starting at `offset`.
84
+ def self.read_bytes(n_bytes, instructions, offset)
85
+ value = 0
86
+ (0...n_bytes).each { |i| value = (value << 8) | instructions[offset + i] }
87
+ value
88
+ end
89
+
90
+ def self.to_s(instructions)
91
+ buf = []
92
+ i = 0
93
+
94
+ while i < instructions.length
95
+ op_def = DEFS[instructions[i]]
96
+ raise "unknown op code #{instructions[i]}" if op_def.nil?
97
+
98
+ operands, new_offset = read(op_def, instructions, i + 1)
99
+
100
+ unless op_def.operand_widths.length == operands.length
101
+ raise "expected #{op_def.operand_widths.length} operands, got #{operands.length}"
102
+ end
103
+
104
+ buf << format("%04d %s %s", i, op_def.name, operands.map(&:to_s).join(" "))
105
+ i = new_offset
106
+ end
107
+
108
+ buf.join("\n")
109
+ end
110
+ end
111
+
112
+ # A simple calculator compiler.
113
+ class Compiler
114
+ def initialize
115
+ @instructions = []
116
+ @constants = []
117
+ @symbols = []
118
+ end
119
+
120
+ def compile(node)
121
+ case node
122
+ when IntExpr
123
+ emit(Code::OP_CONSTANT, add_constant(node.value))
124
+ when VarExpr
125
+ emit(Code::OP_VAR, add_symbol(node.value))
126
+ when InfixExpr
127
+ compile(node.left)
128
+ compile(node.right)
129
+ case node.op
130
+ when :+
131
+ emit(Code::OP_ADD)
132
+ when :-
133
+ emit(Code::OP_SUB)
134
+ when :*
135
+ emit(Code::OP_MUL)
136
+ when :/
137
+ emit(Code::OP_DIV)
138
+ when :**
139
+ emit(Code::OP_POW)
140
+ else
141
+ raise "unknown infix operator #{node.op}"
142
+ end
143
+ when PrefixExpr
144
+ compile(node.expr)
145
+ case node.op
146
+ when :-@
147
+ emit(Code::OP_NEG)
148
+ else
149
+ raise "unknown prefix operator #{node.op}"
150
+ end
151
+ when PostfixExpr
152
+ compile(node.expr)
153
+ case node.op
154
+ when :fact
155
+ emit(Code::OP_FAC)
156
+ else
157
+ raise "unknown postfix operator #{node.op}"
158
+ end
159
+ else
160
+ raise "unexpected expression #{node.class}"
161
+ end
162
+ end
163
+
164
+ def bytecode
165
+ Bytecode.new(@instructions, @constants, @symbols)
166
+ end
167
+
168
+ private
169
+
170
+ def add_constant(const)
171
+ @constants << const
172
+ @constants.length - 1
173
+ end
174
+
175
+ def add_symbol(name)
176
+ @symbols << name
177
+ @symbols.length - 1
178
+ end
179
+
180
+ def emit(op, *operands)
181
+ add_instruction(Code.make(op, *operands))
182
+ end
183
+
184
+ def add_instruction(instruction)
185
+ @instructions.concat(instruction)
186
+ @instructions.length - 1
187
+ end
188
+ end
189
+
190
+ # A virtual machine running our simple calculator bytecode.
191
+ class VM
192
+ def initialize(bytecode, vars)
193
+ @instructions = bytecode.instructions
194
+ @constants = bytecode.constants
195
+ @symbols = bytecode.symbols
196
+ @stack = Array.new(2048)
197
+ @sp = 0
198
+
199
+ @vars = vars
200
+ end
201
+
202
+ def stack_top = @stack[@sp - 1]
203
+
204
+ def run
205
+ ip = 0
206
+ loop do
207
+ break if ip >= @instructions.length
208
+
209
+ op = @instructions[ip]
210
+ case op
211
+ when Code::OP_CONSTANT
212
+ const_index = Code.read_bytes(2, @instructions, ip + 1)
213
+ ip += 3
214
+ push(@constants[const_index])
215
+ when Code::OP_VAR
216
+ symbol_index = Code.read_bytes(2, @instructions, ip + 1)
217
+ ip += 3
218
+
219
+ name = @symbols[symbol_index]
220
+ val = @vars[name]
221
+ raise "undefined variable #{name}" if val.nil?
222
+
223
+ push(val)
224
+ when Code::OP_ADD
225
+ right = pop
226
+ left = pop
227
+
228
+ push(left + right)
229
+ ip += 1
230
+ when Code::OP_SUB
231
+ right = pop
232
+ left = pop
233
+
234
+ push(left - right)
235
+ ip += 1
236
+ when Code::OP_MUL
237
+ right = pop
238
+ left = pop
239
+
240
+ push(left * right)
241
+ ip += 1
242
+ when Code::OP_DIV
243
+ right = pop
244
+ left = pop
245
+
246
+ push(left / right)
247
+ ip += 1
248
+ when Code::OP_POW
249
+ right = pop
250
+ left = pop
251
+
252
+ push(left**right)
253
+ ip += 1
254
+ when Code::OP_NEG
255
+ push(-pop)
256
+ ip += 1
257
+ when Code::OP_FAC
258
+ push(pop.fact)
259
+ ip += 1
260
+ end
261
+ end
262
+ end
263
+
264
+ private
265
+
266
+ def push(obj)
267
+ @stack[@sp] = obj
268
+ @sp += 1
269
+ nil
270
+ end
271
+
272
+ def pop
273
+ obj = @stack[@sp - 1]
274
+ @sp -= 1
275
+ obj
276
+ end
277
+ end
278
+
279
+ def self.compile(prog)
280
+ compiler = Compiler.new
281
+ compiler.compile(prog)
282
+ compiler.bytecode
283
+ end
284
+
285
+ def self.compile_and_run(prog, vars)
286
+ bytecode = compile(prog)
287
+ vm = VM.new(bytecode, vars)
288
+ vm.run
289
+ vm.stack_top
290
+ end
291
+ end
data/examples/csv.rb ADDED
@@ -0,0 +1,73 @@
1
+ # frozen_string_literal: true
2
+
3
+ # A translation of the CSV example found in the pest book.
4
+ # https://pest.rs/book/examples/csv.html
5
+
6
+ # https://github.com/pest-parser/book/blob/master/LICENSE-MIT
7
+ #
8
+ # Permission is hereby granted, free of charge, to any
9
+ # person obtaining a copy of this software and associated
10
+ # documentation files (the "Software"), to deal in the
11
+ # Software without restriction, including without
12
+ # limitation the rights to use, copy, modify, merge,
13
+ # publish, distribute, sublicense, and/or sell copies of
14
+ # the Software, and to permit persons to whom the Software
15
+ # is furnished to do so, subject to the following
16
+ # conditions:
17
+ #
18
+ # The above copyright notice and this permission notice
19
+ # shall be included in all copies or substantial portions
20
+ # of the Software.
21
+ #
22
+ # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF
23
+ # ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED
24
+ # TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
25
+ # PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT
26
+ # SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
27
+ # CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
28
+ # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR
29
+ # IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
30
+ # DEALINGS IN THE SOFTWARE.
31
+
32
+ require_relative "../lib/pestle"
33
+
34
+ # NOTE: This simple example grammar requires input CSV to include a trailing
35
+ # `\r\n` or `\n`. Without it you will get a `PestParsingError`.
36
+
37
+ GRAMMAR = <<~'GRAMMAR'
38
+ field = { (ASCII_DIGIT | "." | "-")+ }
39
+ record = { field ~ ("," ~ field)* }
40
+ file = { SOI ~ (record ~ ("\r\n" | "\n"))* ~ EOI }
41
+ GRAMMAR
42
+
43
+ EXAMPLE_DATA = <<~CSV
44
+ 65279,1179403647,1463895090
45
+ 3.1415927,2.7182817,1.618034
46
+ -40,-273.15
47
+ 13,42
48
+ 65537
49
+ CSV
50
+
51
+ # The name of the rule to start parsing from. Can be a string or a symbol.
52
+ START_RULE = :file
53
+
54
+ parser = Pestle::Parser.from_grammar(GRAMMAR)
55
+ token_pairs = parser.parse(START_RULE, EXAMPLE_DATA).first
56
+
57
+ field_sum = 0.0
58
+ record_count = 0
59
+
60
+ token_pairs.each do |pair|
61
+ case pair
62
+ in :record, fields
63
+ record_count += 1
64
+ fields.each { |field| field_sum += field.text.to_f }
65
+ in :EOI, _
66
+ break
67
+ else
68
+ raise "unexpected rule #{pair.name}"
69
+ end
70
+ end
71
+
72
+ puts "Sum of fields: #{field_sum}"
73
+ puts "Number of records: #{record_count}"
data/examples/ini.rb ADDED
@@ -0,0 +1,90 @@
1
+ # frozen_string_literal: true
2
+
3
+ # A translation of the ini example found in the pest book.
4
+ # https://pest.rs/book/examples/ini.html
5
+
6
+ # https://github.com/pest-parser/book/blob/master/LICENSE-MIT
7
+ #
8
+ # Permission is hereby granted, free of charge, to any
9
+ # person obtaining a copy of this software and associated
10
+ # documentation files (the "Software"), to deal in the
11
+ # Software without restriction, including without
12
+ # limitation the rights to use, copy, modify, merge,
13
+ # publish, distribute, sublicense, and/or sell copies of
14
+ # the Software, and to permit persons to whom the Software
15
+ # is furnished to do so, subject to the following
16
+ # conditions:
17
+ #
18
+ # The above copyright notice and this permission notice
19
+ # shall be included in all copies or substantial portions
20
+ # of the Software.
21
+ #
22
+ # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF
23
+ # ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED
24
+ # TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
25
+ # PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT
26
+ # SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
27
+ # CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
28
+ # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR
29
+ # IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
30
+ # DEALINGS IN THE SOFTWARE.
31
+
32
+ require "json"
33
+ require_relative "../lib/pestle"
34
+
35
+ GRAMMAR = <<~GRAMMAR
36
+ file = {
37
+ SOI ~
38
+ ((section | property)? ~ NEWLINE)* ~
39
+ EOI
40
+ }
41
+
42
+ char = { ASCII_ALPHANUMERIC | "." | "_" | "/" }
43
+ name = @{ char+ }
44
+ value = @{ char* }
45
+ section = { "[" ~ name ~ "]" }
46
+ property = { name ~ "=" ~ value }
47
+
48
+ WHITESPACE = _{ " " }
49
+ GRAMMAR
50
+
51
+ EXAMPLE_INI = <<~INI
52
+ username = noha
53
+ password = plain_text
54
+ salt = NaCl
55
+
56
+ [server_1]
57
+ interface=eth0
58
+ ip=127.0.0.1
59
+ document_root=/var/www/example.org
60
+
61
+ [empty_section]
62
+
63
+ [second_server]
64
+ document_root=/var/www/example.com
65
+ ip=
66
+ interface=eth1
67
+ INI
68
+
69
+ START_RULE = :file
70
+
71
+ parser = Pestle::Parser.from_grammar(GRAMMAR)
72
+ pairs = parser.parse(START_RULE, EXAMPLE_INI).first
73
+
74
+ current_section_name = ""
75
+ properties = Hash.new { |hash, key| hash[key] = {} }
76
+
77
+ pairs.each do |pair|
78
+ case pair
79
+ in :section, [name]
80
+ current_section_name = name.text
81
+ in :property, [name, value]
82
+ properties[current_section_name][name.text] = value.text
83
+ in :EOI, _
84
+ break
85
+ else
86
+ raise "unexpected rule #{pair.name.inspect}"
87
+ end
88
+ end
89
+
90
+ puts JSON.pretty_generate(properties)