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.
- checksums.yaml +7 -0
- checksums.yaml.gz.sig +0 -0
- data/.rubocop.yml +59 -0
- data/.ruby-version +1 -0
- data/CHANGELOG.md +3 -0
- data/LICENSE.txt +21 -0
- data/LICENSE_PEST.txt +23 -0
- data/README.md +124 -0
- data/Rakefile +23 -0
- data/Steepfile +19 -0
- data/benchmarks/jsonpath_ips.rb +33 -0
- data/examples/calculator_pratt.rb +157 -0
- data/examples/calculator_prec_climber.rb +225 -0
- data/examples/calculator_stack_vm.rb +291 -0
- data/examples/csv.rb +73 -0
- data/examples/ini.rb +90 -0
- data/examples/json_example.rb +141 -0
- data/examples/jsonpath/README.md +3 -0
- data/examples/jsonpath/jsonpath.pest +182 -0
- data/examples/jsonpath/lib/jsonpath/ast.rb +362 -0
- data/examples/jsonpath/lib/jsonpath/function_extensions.rb +201 -0
- data/examples/jsonpath/lib/jsonpath/node.rb +20 -0
- data/examples/jsonpath/lib/jsonpath/query.rb +25 -0
- data/examples/jsonpath/lib/jsonpath.rb +453 -0
- data/lib/pestle/errors.rb +98 -0
- data/lib/pestle/grammar/builtin_rules/ascii.rb +38 -0
- data/lib/pestle/grammar/builtin_rules/special.rb +63 -0
- data/lib/pestle/grammar/builtin_rules/unicode.rb +291 -0
- data/lib/pestle/grammar/errors.rb +62 -0
- data/lib/pestle/grammar/expression.rb +90 -0
- data/lib/pestle/grammar/expressions/choice.rb +36 -0
- data/lib/pestle/grammar/expressions/group.rb +27 -0
- data/lib/pestle/grammar/expressions/identifier.rb +26 -0
- data/lib/pestle/grammar/expressions/postfix.rb +272 -0
- data/lib/pestle/grammar/expressions/prefix.rb +51 -0
- data/lib/pestle/grammar/expressions/range.rb +26 -0
- data/lib/pestle/grammar/expressions/sequence.rb +38 -0
- data/lib/pestle/grammar/expressions/stack.rb +192 -0
- data/lib/pestle/grammar/expressions/string.rb +46 -0
- data/lib/pestle/grammar/lexer.rb +464 -0
- data/lib/pestle/grammar/parser.rb +340 -0
- data/lib/pestle/grammar/rule.rb +98 -0
- data/lib/pestle/pair.rb +325 -0
- data/lib/pestle/parser.rb +48 -0
- data/lib/pestle/pratt.rb +74 -0
- data/lib/pestle/state.rb +220 -0
- data/lib/pestle/version.rb +5 -0
- data/lib/pestle.rb +24 -0
- data/sig/errors.rbs +22 -0
- data/sig/grammar/ascii.rbs +9 -0
- data/sig/grammar/choice.rbs +14 -0
- data/sig/grammar/errors.rbs +22 -0
- data/sig/grammar/expression.rbs +39 -0
- data/sig/grammar/group.rbs +14 -0
- data/sig/grammar/identifier.rbs +11 -0
- data/sig/grammar/lexer.rbs +85 -0
- data/sig/grammar/parser.rbs +57 -0
- data/sig/grammar/postfix.rbs +112 -0
- data/sig/grammar/prefix.rbs +27 -0
- data/sig/grammar/range.rbs +20 -0
- data/sig/grammar/rule.rbs +40 -0
- data/sig/grammar/sequence.rbs +14 -0
- data/sig/grammar/special.rbs +39 -0
- data/sig/grammar/stack.rbs +57 -0
- data/sig/grammar/string.rbs +27 -0
- data/sig/grammar/unicode.rbs +15 -0
- data/sig/pair.rbs +168 -0
- data/sig/parser.rbs +16 -0
- data/sig/pestle.rbs +5 -0
- data/sig/pratt.rbs +27 -0
- data/sig/state.rbs +95 -0
- data/sig/stdlib/strscan.rbs +3 -0
- data.tar.gz.sig +0 -0
- metadata +141 -0
- 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)
|