dentaku 2.0.11 → 3.0.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|