dentaku 2.0.11 → 3.0.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 +4 -4
- data/.gitignore +3 -0
- data/.travis.yml +0 -1
- data/CHANGELOG.md +19 -0
- data/README.md +3 -2
- data/dentaku.gemspec +1 -0
- data/lib/dentaku/ast.rb +4 -0
- data/lib/dentaku/ast/access.rb +27 -0
- data/lib/dentaku/ast/arithmetic.rb +49 -7
- data/lib/dentaku/ast/case.rb +17 -3
- data/lib/dentaku/ast/combinators.rb +8 -2
- data/lib/dentaku/ast/function.rb +16 -0
- data/lib/dentaku/ast/function_registry.rb +15 -2
- data/lib/dentaku/ast/functions/and.rb +25 -0
- data/lib/dentaku/ast/functions/max.rb +1 -1
- data/lib/dentaku/ast/functions/min.rb +1 -1
- data/lib/dentaku/ast/functions/or.rb +25 -0
- data/lib/dentaku/ast/functions/round.rb +2 -2
- data/lib/dentaku/ast/functions/rounddown.rb +3 -2
- data/lib/dentaku/ast/functions/roundup.rb +3 -2
- data/lib/dentaku/ast/functions/ruby_math.rb +3 -3
- data/lib/dentaku/ast/functions/switch.rb +8 -0
- data/lib/dentaku/ast/identifier.rb +3 -2
- data/lib/dentaku/ast/negation.rb +5 -1
- data/lib/dentaku/ast/node.rb +3 -0
- data/lib/dentaku/bulk_expression_solver.rb +1 -2
- data/lib/dentaku/calculator.rb +7 -6
- data/lib/dentaku/exceptions.rb +75 -1
- data/lib/dentaku/parser.rb +73 -12
- data/lib/dentaku/token.rb +4 -0
- data/lib/dentaku/token_scanner.rb +20 -3
- data/lib/dentaku/tokenizer.rb +31 -4
- data/lib/dentaku/version.rb +1 -1
- data/spec/ast/addition_spec.rb +6 -6
- data/spec/ast/and_function_spec.rb +35 -0
- data/spec/ast/and_spec.rb +1 -1
- data/spec/ast/arithmetic_spec.rb +56 -0
- data/spec/ast/division_spec.rb +1 -1
- data/spec/ast/function_spec.rb +43 -6
- data/spec/ast/max_spec.rb +15 -0
- data/spec/ast/min_spec.rb +15 -0
- data/spec/ast/or_spec.rb +35 -0
- data/spec/ast/round_spec.rb +25 -0
- data/spec/ast/rounddown_spec.rb +25 -0
- data/spec/ast/roundup_spec.rb +25 -0
- data/spec/ast/switch_spec.rb +30 -0
- data/spec/calculator_spec.rb +26 -4
- data/spec/exceptions_spec.rb +1 -1
- data/spec/parser_spec.rb +22 -3
- data/spec/spec_helper.rb +12 -2
- data/spec/token_scanner_spec.rb +0 -4
- data/spec/tokenizer_spec.rb +40 -2
- metadata +39 -3
@@ -1,7 +1,8 @@
|
|
1
1
|
require_relative '../function'
|
2
2
|
|
3
|
-
Dentaku::AST::Function.register(:roundup, :numeric,
|
3
|
+
Dentaku::AST::Function.register(:roundup, :numeric, lambda { |numeric, precision = 0|
|
4
|
+
precision = precision.to_i
|
4
5
|
tens = 10.0**precision
|
5
|
-
result = (numeric * tens).ceil / tens
|
6
|
+
result = (Dentaku::AST::Function.numeric(numeric) * tens).ceil / tens
|
6
7
|
precision <= 0 ? result.to_i : result
|
7
8
|
})
|
@@ -1,8 +1,8 @@
|
|
1
1
|
# import all functions from Ruby's Math module
|
2
|
-
require_relative
|
2
|
+
require_relative '../function'
|
3
3
|
|
4
4
|
Math.methods(false).each do |method|
|
5
|
-
Dentaku::AST::Function.register(method, :numeric,
|
6
|
-
Math.send(method, *args)
|
5
|
+
Dentaku::AST::Function.register(method, :numeric, lambda { |*args|
|
6
|
+
Math.send(method, *args.map { |arg| Dentaku::AST::Function.numeric(arg) })
|
7
7
|
})
|
8
8
|
end
|
@@ -0,0 +1,8 @@
|
|
1
|
+
require_relative '../function'
|
2
|
+
|
3
|
+
Dentaku::AST::Function.register(:switch, :logical, lambda { |*args|
|
4
|
+
value = args.shift
|
5
|
+
default = args.pop if args.size.odd?
|
6
|
+
match = args.find_index.each_with_index { |arg, index| index.even? && arg == value }
|
7
|
+
match ? args[match + 1] : default
|
8
|
+
})
|
@@ -11,7 +11,8 @@ module Dentaku
|
|
11
11
|
|
12
12
|
def value(context={})
|
13
13
|
v = context.fetch(identifier) do
|
14
|
-
raise UnboundVariableError.new([identifier])
|
14
|
+
raise UnboundVariableError.new([identifier]),
|
15
|
+
"no value provided for variables: #{identifier}"
|
15
16
|
end
|
16
17
|
|
17
18
|
case v
|
@@ -23,7 +24,7 @@ module Dentaku
|
|
23
24
|
end
|
24
25
|
|
25
26
|
def dependencies(context={})
|
26
|
-
context.
|
27
|
+
context.key?(identifier) ? dependencies_of(context[identifier]) : [identifier]
|
27
28
|
end
|
28
29
|
|
29
30
|
private
|
data/lib/dentaku/ast/negation.rb
CHANGED
@@ -3,7 +3,11 @@ module Dentaku
|
|
3
3
|
class Negation < Arithmetic
|
4
4
|
def initialize(node)
|
5
5
|
@node = node
|
6
|
-
|
6
|
+
|
7
|
+
unless valid_node?(node)
|
8
|
+
raise NodeError.new(:numeric, left.type, :left),
|
9
|
+
"#{self.class} requires numeric operands"
|
10
|
+
end
|
7
11
|
end
|
8
12
|
|
9
13
|
def operator
|
data/lib/dentaku/ast/node.rb
CHANGED
@@ -1,4 +1,3 @@
|
|
1
|
-
require 'dentaku/calculator'
|
2
1
|
require 'dentaku/dependency_resolver'
|
3
2
|
require 'dentaku/exceptions'
|
4
3
|
require 'dentaku/parser'
|
@@ -55,7 +54,7 @@ module Dentaku
|
|
55
54
|
evaluate!(expressions[var_name], expressions.merge(r))
|
56
55
|
|
57
56
|
r[var_name] = value
|
58
|
-
rescue
|
57
|
+
rescue UnboundVariableError, Dentaku::ZeroDivisionError => ex
|
59
58
|
ex.recipient_variable = var_name
|
60
59
|
r[var_name] = block.call(ex)
|
61
60
|
end
|
data/lib/dentaku/calculator.rb
CHANGED
@@ -21,10 +21,6 @@ module Dentaku
|
|
21
21
|
Dentaku::AST::FunctionRegistry.default.register(name, type, body)
|
22
22
|
end
|
23
23
|
|
24
|
-
def add_functions(fns)
|
25
|
-
fns.each { |(name, type, body)| add_function(name, type, body) }
|
26
|
-
end
|
27
|
-
|
28
24
|
def add_function(name, type, body)
|
29
25
|
@function_registry.register(name, type, body)
|
30
26
|
self
|
@@ -44,7 +40,7 @@ module Dentaku
|
|
44
40
|
|
45
41
|
def evaluate(expression, data={})
|
46
42
|
evaluate!(expression, data)
|
47
|
-
rescue UnboundVariableError, ArgumentError
|
43
|
+
rescue UnboundVariableError, Dentaku::ArgumentError
|
48
44
|
yield expression if block_given?
|
49
45
|
end
|
50
46
|
|
@@ -52,6 +48,11 @@ module Dentaku
|
|
52
48
|
store(data) do
|
53
49
|
node = expression
|
54
50
|
node = ast(node) unless node.is_a?(AST::Node)
|
51
|
+
unbound = node.dependencies - memory.keys
|
52
|
+
unless unbound.empty?
|
53
|
+
raise UnboundVariableError.new(unbound),
|
54
|
+
"no value provided for variables: #{unbound.join(', ')}"
|
55
|
+
end
|
55
56
|
node.value(memory)
|
56
57
|
end
|
57
58
|
end
|
@@ -85,7 +86,7 @@ module Dentaku
|
|
85
86
|
when Regexp
|
86
87
|
@ast_cache.delete_if { |k,_| k =~ pattern }
|
87
88
|
else
|
88
|
-
|
89
|
+
raise ::ArgumentError
|
89
90
|
end
|
90
91
|
end
|
91
92
|
|
data/lib/dentaku/exceptions.rb
CHANGED
@@ -6,17 +6,91 @@ module Dentaku
|
|
6
6
|
|
7
7
|
def initialize(unbound_variables)
|
8
8
|
@unbound_variables = unbound_variables
|
9
|
-
|
9
|
+
end
|
10
|
+
end
|
11
|
+
|
12
|
+
class NodeError < StandardError
|
13
|
+
attr_reader :child, :expect, :actual
|
14
|
+
|
15
|
+
def initialize(expect, actual, child)
|
16
|
+
@expect = Array(expect)
|
17
|
+
@actual = actual
|
18
|
+
@child = child
|
10
19
|
end
|
11
20
|
end
|
12
21
|
|
13
22
|
class ParseError < StandardError
|
23
|
+
attr_reader :reason, :meta
|
24
|
+
|
25
|
+
def initialize(reason, **meta)
|
26
|
+
@reason = reason
|
27
|
+
@meta = meta
|
28
|
+
end
|
29
|
+
|
30
|
+
private_class_method :new
|
31
|
+
|
32
|
+
VALID_REASONS = %i[
|
33
|
+
node_invalid too_few_operands undefined_function
|
34
|
+
unprocessed_token unknown_case_token unbalanced_bracket
|
35
|
+
unbalanced_parenthesis unknown_grouping_token not_implemented_token_category
|
36
|
+
invalid_statement
|
37
|
+
].freeze
|
38
|
+
|
39
|
+
def self.for(reason, **meta)
|
40
|
+
unless VALID_REASONS.include?(reason)
|
41
|
+
raise ::ArgumentError, "Unhandled #{reason}"
|
42
|
+
end
|
43
|
+
|
44
|
+
new reason, meta
|
45
|
+
end
|
14
46
|
end
|
15
47
|
|
16
48
|
class TokenizerError < StandardError
|
49
|
+
attr_reader :reason, :meta
|
50
|
+
|
51
|
+
def initialize(reason, **meta)
|
52
|
+
@reason = reason
|
53
|
+
@meta = meta
|
54
|
+
end
|
55
|
+
|
56
|
+
private_class_method :new
|
57
|
+
|
58
|
+
VALID_REASONS = %i[
|
59
|
+
parse_error too_many_opening_parentheses too_many_closing_parentheses
|
60
|
+
unexpected_zero_width_match
|
61
|
+
].freeze
|
62
|
+
|
63
|
+
def self.for(reason, **meta)
|
64
|
+
unless VALID_REASONS.include?(reason)
|
65
|
+
raise ::ArgumentError, "Unhandled #{reason}"
|
66
|
+
end
|
67
|
+
|
68
|
+
new reason, meta
|
69
|
+
end
|
17
70
|
end
|
18
71
|
|
19
72
|
class ArgumentError < ::ArgumentError
|
73
|
+
attr_reader :reason, :meta
|
74
|
+
|
75
|
+
def initialize(reason, **meta)
|
76
|
+
@reason = reason
|
77
|
+
@meta = meta
|
78
|
+
end
|
79
|
+
|
80
|
+
private_class_method :new
|
81
|
+
|
82
|
+
VALID_REASONS = %i[
|
83
|
+
invalid_operator invalid_value too_few_arguments
|
84
|
+
too_much_arguments incompatible_type
|
85
|
+
].freeze
|
86
|
+
|
87
|
+
def self.for(reason, **meta)
|
88
|
+
unless VALID_REASONS.include?(reason)
|
89
|
+
raise ::ArgumentError, "Unhandled #{reason}"
|
90
|
+
end
|
91
|
+
|
92
|
+
new reason, meta
|
93
|
+
end
|
20
94
|
end
|
21
95
|
|
22
96
|
class ZeroDivisionError < ::ZeroDivisionError
|
data/lib/dentaku/parser.rb
CHANGED
@@ -12,13 +12,19 @@ module Dentaku
|
|
12
12
|
@function_registry = options.fetch(:function_registry, nil)
|
13
13
|
end
|
14
14
|
|
15
|
-
def get_args(count)
|
16
|
-
Array.new(count) { output.pop }.reverse
|
17
|
-
end
|
18
|
-
|
19
15
|
def consume(count=2)
|
20
16
|
operator = operations.pop
|
21
|
-
|
17
|
+
operator.peek(output)
|
18
|
+
|
19
|
+
args_size = operator.arity || count
|
20
|
+
if args_size > output.length
|
21
|
+
fail! :too_few_operands, operator: operator, expect: args_size, actual: output.length
|
22
|
+
end
|
23
|
+
args = Array.new(args_size) { output.pop }.reverse
|
24
|
+
|
25
|
+
output.push operator.new(*args)
|
26
|
+
rescue NodeError => e
|
27
|
+
fail! :node_invalid, operator: operator, child: e.child, expect: e.expect, actual: e.actual
|
22
28
|
end
|
23
29
|
|
24
30
|
def parse
|
@@ -62,8 +68,13 @@ module Dentaku
|
|
62
68
|
output.push AST::Nil.new
|
63
69
|
|
64
70
|
when :function
|
71
|
+
func = function(token)
|
72
|
+
if func.nil?
|
73
|
+
fail! :undefined_function, function_name: token.value
|
74
|
+
end
|
75
|
+
|
65
76
|
arities.push 0
|
66
|
-
operations.push
|
77
|
+
operations.push func
|
67
78
|
|
68
79
|
when :case
|
69
80
|
case token.value
|
@@ -118,7 +129,7 @@ module Dentaku
|
|
118
129
|
end
|
119
130
|
|
120
131
|
unless operations.count == 1 && operations.last == AST::Case
|
121
|
-
fail
|
132
|
+
fail! :unprocessed_token, token_name: token.value
|
122
133
|
end
|
123
134
|
consume(arities.pop.succ)
|
124
135
|
when :when
|
@@ -155,7 +166,22 @@ module Dentaku
|
|
155
166
|
|
156
167
|
operations.push(AST::CaseElse)
|
157
168
|
else
|
158
|
-
fail
|
169
|
+
fail! :unknown_case_token, token_name: token.value
|
170
|
+
end
|
171
|
+
|
172
|
+
when :access
|
173
|
+
case token.value
|
174
|
+
when :lbracket
|
175
|
+
operations.push AST::Access
|
176
|
+
when :rbracket
|
177
|
+
while operations.any? && operations.last != AST::Access
|
178
|
+
consume
|
179
|
+
end
|
180
|
+
|
181
|
+
unless operations.last == AST::Access
|
182
|
+
fail! :unbalanced_bracket, token
|
183
|
+
end
|
184
|
+
consume
|
159
185
|
end
|
160
186
|
|
161
187
|
when :grouping
|
@@ -163,6 +189,7 @@ module Dentaku
|
|
163
189
|
when :open
|
164
190
|
if input.first && input.first.value == :close
|
165
191
|
input.shift
|
192
|
+
arities.pop
|
166
193
|
consume(0)
|
167
194
|
else
|
168
195
|
operations.push AST::Grouping
|
@@ -174,7 +201,9 @@ module Dentaku
|
|
174
201
|
end
|
175
202
|
|
176
203
|
lparen = operations.pop
|
177
|
-
|
204
|
+
unless lparen == AST::Grouping
|
205
|
+
fail! :unbalanced_parenthesis, token
|
206
|
+
end
|
178
207
|
|
179
208
|
if operations.last && operations.last < AST::Function
|
180
209
|
consume(arities.pop.succ)
|
@@ -187,11 +216,11 @@ module Dentaku
|
|
187
216
|
end
|
188
217
|
|
189
218
|
else
|
190
|
-
fail
|
219
|
+
fail! :unknown_grouping_token, token_name: token.value
|
191
220
|
end
|
192
221
|
|
193
222
|
else
|
194
|
-
fail
|
223
|
+
fail! :not_implemented_token_category, token_category: token.category
|
195
224
|
end
|
196
225
|
end
|
197
226
|
|
@@ -200,7 +229,7 @@ module Dentaku
|
|
200
229
|
end
|
201
230
|
|
202
231
|
unless output.count == 1
|
203
|
-
fail
|
232
|
+
fail! :invalid_statement
|
204
233
|
end
|
205
234
|
|
206
235
|
output.first
|
@@ -237,5 +266,37 @@ module Dentaku
|
|
237
266
|
def function_registry
|
238
267
|
@function_registry ||= Dentaku::AST::FunctionRegistry.new
|
239
268
|
end
|
269
|
+
|
270
|
+
private
|
271
|
+
|
272
|
+
def fail!(reason, **meta)
|
273
|
+
message =
|
274
|
+
case reason
|
275
|
+
when :node_invalid
|
276
|
+
"#{meta.fetch(:operator)} requires #{meta.fetch(:expect).join(', ')} operands, but got #{meta.fetch(:actual)}"
|
277
|
+
when :too_few_operands
|
278
|
+
"#{meta.fetch(:operator)} has too few operands"
|
279
|
+
when :undefined_function
|
280
|
+
"Undefined function #{meta.fetch(:function_name)}"
|
281
|
+
when :unprocessed_token
|
282
|
+
"Unprocessed token #{meta.fetch(:token_name)}"
|
283
|
+
when :unknown_case_token
|
284
|
+
"Unknown case token #{meta.fetch(:token_name)}"
|
285
|
+
when :unbalanced_bracket
|
286
|
+
"Unbalanced bracket"
|
287
|
+
when :unbalanced_parenthesis
|
288
|
+
"Unbalanced parenthesis"
|
289
|
+
when :unknown_grouping_token
|
290
|
+
"Unknown grouping token #{meta.fetch(:token_name)}"
|
291
|
+
when :not_implemented_token_category
|
292
|
+
"Not implemented for tokens of category #{meta.fetch(:token_category)}"
|
293
|
+
when :invalid_statement
|
294
|
+
"Invalid statement"
|
295
|
+
else
|
296
|
+
raise ::ArgumentError, "Unhandled #{reason}"
|
297
|
+
end
|
298
|
+
|
299
|
+
raise ParseError.for(reason, meta), message
|
300
|
+
end
|
240
301
|
end
|
241
302
|
end
|
data/lib/dentaku/token.rb
CHANGED
@@ -31,14 +31,16 @@ module Dentaku
|
|
31
31
|
:whitespace,
|
32
32
|
:datetime, # before numeric so it can pick up timestamps
|
33
33
|
:numeric,
|
34
|
+
:hexadecimal,
|
34
35
|
:double_quoted_string,
|
35
36
|
:single_quoted_string,
|
36
37
|
:negate,
|
38
|
+
:combinator,
|
37
39
|
:operator,
|
38
40
|
:grouping,
|
41
|
+
:access,
|
39
42
|
:case_statement,
|
40
43
|
:comparator,
|
41
|
-
:combinator,
|
42
44
|
:boolean,
|
43
45
|
:function,
|
44
46
|
:identifier
|
@@ -81,7 +83,13 @@ module Dentaku
|
|
81
83
|
end
|
82
84
|
|
83
85
|
def numeric
|
84
|
-
new(:numeric, '(
|
86
|
+
new(:numeric, '((?:\d+(\.\d+)?|\.\d+)(?:(e|E)(\+|-)?\d+)?)\b', lambda { |raw|
|
87
|
+
raw =~ /\./ ? BigDecimal.new(raw) : raw.to_i
|
88
|
+
})
|
89
|
+
end
|
90
|
+
|
91
|
+
def hexadecimal
|
92
|
+
new(:numeric, '(0x[0-9a-f]+)\b', lambda { |raw| raw[2..-1].to_i(16) })
|
85
93
|
end
|
86
94
|
|
87
95
|
def double_quoted_string
|
@@ -113,6 +121,11 @@ module Dentaku
|
|
113
121
|
new(:grouping, '\(|\)|,', lambda { |raw| names[raw] })
|
114
122
|
end
|
115
123
|
|
124
|
+
def access
|
125
|
+
names = { lbracket: '[', rbracket: ']' }.invert
|
126
|
+
new(:access, '\[|\]', lambda { |raw| names[raw] })
|
127
|
+
end
|
128
|
+
|
116
129
|
def case_statement
|
117
130
|
names = { open: 'case', close: 'end', then: 'then', when: 'when', else: 'else' }.invert
|
118
131
|
new(:case, '(case|end|then|when|else)\b', lambda { |raw| names[raw.downcase] })
|
@@ -125,7 +138,11 @@ module Dentaku
|
|
125
138
|
end
|
126
139
|
|
127
140
|
def combinator
|
128
|
-
|
141
|
+
names = { and: '&&', or: '||' }.invert
|
142
|
+
new(:combinator, '(and|or|&&|\|\|)\s', lambda { |raw|
|
143
|
+
norm = raw.strip.downcase
|
144
|
+
names.fetch(norm) { norm.to_sym }
|
145
|
+
})
|
129
146
|
end
|
130
147
|
|
131
148
|
def boolean
|
data/lib/dentaku/tokenizer.rb
CHANGED
@@ -13,13 +13,17 @@ module Dentaku
|
|
13
13
|
input = strip_comments(string.to_s.dup)
|
14
14
|
|
15
15
|
until input.empty?
|
16
|
-
|
16
|
+
scanned = TokenScanner.scanners.any? do |scanner|
|
17
17
|
scanned, input = scan(input, scanner)
|
18
18
|
scanned
|
19
19
|
end
|
20
|
+
|
21
|
+
unless scanned
|
22
|
+
fail! :parse_error, at: input
|
23
|
+
end
|
20
24
|
end
|
21
25
|
|
22
|
-
fail
|
26
|
+
fail! :too_many_opening_parentheses if @nesting > 0
|
23
27
|
|
24
28
|
@tokens
|
25
29
|
end
|
@@ -31,11 +35,14 @@ module Dentaku
|
|
31
35
|
def scan(string, scanner)
|
32
36
|
if tokens = scanner.scan(string, last_token)
|
33
37
|
tokens.each do |token|
|
34
|
-
|
38
|
+
if token.empty?
|
39
|
+
fail! :unexpected_zero_width_match,
|
40
|
+
token_category: token.category, at: string
|
41
|
+
end
|
35
42
|
|
36
43
|
@nesting += 1 if LPAREN == token
|
37
44
|
@nesting -= 1 if RPAREN == token
|
38
|
-
fail
|
45
|
+
fail! :too_many_closing_parentheses if @nesting < 0
|
39
46
|
|
40
47
|
@tokens << token unless token.is?(:whitespace)
|
41
48
|
end
|
@@ -50,5 +57,25 @@ module Dentaku
|
|
50
57
|
def strip_comments(input)
|
51
58
|
input.gsub(/\/\*[^*]*\*+(?:[^*\/][^*]*\*+)*\//, '')
|
52
59
|
end
|
60
|
+
|
61
|
+
private
|
62
|
+
|
63
|
+
def fail!(reason, **meta)
|
64
|
+
message =
|
65
|
+
case reason
|
66
|
+
when :parse_error
|
67
|
+
"parse error at: '#{meta.fetch(:at)}'"
|
68
|
+
when :too_many_opening_parentheses
|
69
|
+
"too many opening parentheses"
|
70
|
+
when :too_many_closing_parentheses
|
71
|
+
"too many closing parentheses"
|
72
|
+
when :unexpected_zero_width_match
|
73
|
+
"unexpected zero-width match (:#{meta.fetch(:category)}) at '#{meta.fetch(:at)}'"
|
74
|
+
else
|
75
|
+
raise ::ArgumentError, "Unhandled #{reason}"
|
76
|
+
end
|
77
|
+
|
78
|
+
raise TokenizerError.for(reason, meta), message
|
79
|
+
end
|
53
80
|
end
|
54
81
|
end
|