dentaku 3.5.8 → 4.0.1
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/.github/workflows/rspec.yml +2 -4
- data/.github/workflows/rubocop.yml +1 -1
- data/.rubocop.yml +1 -1
- data/CHANGELOG.md +108 -1
- data/Gemfile +1 -1
- data/README.md +123 -0
- data/dentaku.gemspec +15 -8
- data/lib/dentaku/ast/access.rb +18 -1
- data/lib/dentaku/ast/arithmetic.rb +22 -9
- data/lib/dentaku/ast/array.rb +6 -0
- data/lib/dentaku/ast/bitwise.rb +6 -4
- data/lib/dentaku/ast/case/case_conditional.rb +6 -0
- data/lib/dentaku/ast/case/case_else.rb +6 -0
- data/lib/dentaku/ast/case/case_switch_variable.rb +6 -0
- data/lib/dentaku/ast/case/case_then.rb +6 -0
- data/lib/dentaku/ast/case/case_when.rb +7 -0
- data/lib/dentaku/ast/case.rb +30 -3
- data/lib/dentaku/ast/combinators.rb +52 -5
- data/lib/dentaku/ast/comparators.rb +2 -3
- data/lib/dentaku/ast/function.rb +15 -2
- data/lib/dentaku/ast/function_registry.rb +28 -2
- data/lib/dentaku/ast/functions/and.rb +4 -4
- data/lib/dentaku/ast/functions/avg.rb +2 -2
- data/lib/dentaku/ast/functions/duration.rb +1 -1
- data/lib/dentaku/ast/functions/if.rb +9 -1
- data/lib/dentaku/ast/functions/intercept.rb +2 -2
- data/lib/dentaku/ast/functions/mul.rb +2 -2
- data/lib/dentaku/ast/functions/or.rb +4 -4
- data/lib/dentaku/ast/functions/pluck.rb +1 -1
- data/lib/dentaku/ast/functions/string_functions.rb +1 -1
- data/lib/dentaku/ast/functions/sum.rb +2 -2
- data/lib/dentaku/ast/functions/xor.rb +4 -4
- data/lib/dentaku/ast/grouping.rb +6 -0
- data/lib/dentaku/ast/negation.rb +8 -1
- data/lib/dentaku/ast/node.rb +25 -0
- data/lib/dentaku/ast/operation.rb +7 -0
- data/lib/dentaku/bulk_expression_solver.rb +9 -10
- data/lib/dentaku/calculator.rb +83 -34
- data/lib/dentaku/date_arithmetic.rb +2 -2
- data/lib/dentaku/exceptions.rb +105 -13
- data/lib/dentaku/numeric_parser.rb +1 -1
- data/lib/dentaku/parser.rb +24 -48
- data/lib/dentaku/token_matcher.rb +31 -37
- data/lib/dentaku/token_matchers.rb +3 -3
- data/lib/dentaku/tokenizer.rb +1 -15
- data/lib/dentaku/version.rb +1 -1
- data/spec/ast/arithmetic_spec.rb +93 -0
- data/spec/ast/case_spec.rb +7 -0
- data/spec/bulk_expression_solver_spec.rb +13 -3
- data/spec/calculator_spec.rb +146 -0
- data/spec/exceptions_spec.rb +87 -0
- data/spec/external_function_spec.rb +33 -0
- data/spec/gemspec_spec.rb +10 -0
- data/spec/identifiers_spec.rb +66 -0
- data/spec/parser_spec.rb +35 -0
- data/spec/print_visitor_spec.rb +5 -0
- data/spec/spec_helper.rb +9 -0
- data/spec/volatile_functions_spec.rb +121 -0
- metadata +21 -63
- data/.travis.yml +0 -10
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
require_relative './operation'
|
|
2
|
+
require 'dentaku/exceptions'
|
|
2
3
|
|
|
3
4
|
module Dentaku
|
|
4
5
|
module AST
|
|
@@ -20,10 +21,52 @@ module Dentaku
|
|
|
20
21
|
:logical
|
|
21
22
|
end
|
|
22
23
|
|
|
24
|
+
def dependencies(context = {})
|
|
25
|
+
return super if static_mode?(context)
|
|
26
|
+
|
|
27
|
+
left_deps = left.dependencies(context)
|
|
28
|
+
right_deps = right.dependencies(context)
|
|
29
|
+
return [] if left_deps.empty? && left.pure? && decisive?(left.value(context))
|
|
30
|
+
return [] if right_deps.empty? && right.pure? && decisive?(right.value(context))
|
|
31
|
+
|
|
32
|
+
(left_deps + right_deps).uniq
|
|
33
|
+
rescue Dentaku::Error
|
|
34
|
+
super
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def value(context = {})
|
|
38
|
+
left_value = begin
|
|
39
|
+
left.value(context)
|
|
40
|
+
rescue UnboundVariableError => unbound
|
|
41
|
+
unbound
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
unless left_value.is_a?(UnboundVariableError)
|
|
45
|
+
return left_value if decisive?(left_value)
|
|
46
|
+
|
|
47
|
+
return right.value(context)
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
# The left operand is unbound; the right operand can still decide the
|
|
51
|
+
# result on its own. If it does not, the left value was needed.
|
|
52
|
+
right_value = right.value(context)
|
|
53
|
+
raise left_value unless decisive?(right_value)
|
|
54
|
+
|
|
55
|
+
right_value
|
|
56
|
+
end
|
|
57
|
+
|
|
23
58
|
private
|
|
24
59
|
|
|
60
|
+
# static dependency check: this runs at parse time, and evaluating
|
|
61
|
+
# short-circuit guards here would execute user functions (#197)
|
|
25
62
|
def valid_node?(node)
|
|
26
|
-
node && (node.dependencies.any? || node.type == :logical)
|
|
63
|
+
node && (node.dependencies(Node::STATIC_CONTEXT).any? || node.type == :logical)
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
# whether a single operand with this value already determines the
|
|
67
|
+
# result, regardless of the other operand
|
|
68
|
+
def decisive?(operand_value)
|
|
69
|
+
raise NotImplementedError
|
|
27
70
|
end
|
|
28
71
|
end
|
|
29
72
|
|
|
@@ -32,8 +75,10 @@ module Dentaku
|
|
|
32
75
|
:and
|
|
33
76
|
end
|
|
34
77
|
|
|
35
|
-
|
|
36
|
-
|
|
78
|
+
private
|
|
79
|
+
|
|
80
|
+
def decisive?(operand_value)
|
|
81
|
+
!operand_value
|
|
37
82
|
end
|
|
38
83
|
end
|
|
39
84
|
|
|
@@ -42,8 +87,10 @@ module Dentaku
|
|
|
42
87
|
:or
|
|
43
88
|
end
|
|
44
89
|
|
|
45
|
-
|
|
46
|
-
|
|
90
|
+
private
|
|
91
|
+
|
|
92
|
+
def decisive?(operand_value)
|
|
93
|
+
!!operand_value
|
|
47
94
|
end
|
|
48
95
|
end
|
|
49
96
|
end
|
|
@@ -21,7 +21,7 @@ module Dentaku
|
|
|
21
21
|
|
|
22
22
|
l.public_send(operator, r)
|
|
23
23
|
rescue ::ArgumentError, ::TypeError => e
|
|
24
|
-
raise Dentaku::ArgumentError.for(:incompatible_type,
|
|
24
|
+
raise Dentaku::ArgumentError.for(:incompatible_type, actual: r, expected: l.class), e.message
|
|
25
25
|
end
|
|
26
26
|
|
|
27
27
|
private
|
|
@@ -34,8 +34,7 @@ module Dentaku
|
|
|
34
34
|
|
|
35
35
|
def validate_value(value)
|
|
36
36
|
unless value.respond_to?(operator)
|
|
37
|
-
raise Dentaku::ArgumentError.for(:invalid_operator, operation: self.class, operator: operator)
|
|
38
|
-
"#{ self.class } requires operands that respond to #{operator}"
|
|
37
|
+
raise Dentaku::ArgumentError.for(:invalid_operator, operation: self.class, operator: operator)
|
|
39
38
|
end
|
|
40
39
|
|
|
41
40
|
value
|
data/lib/dentaku/ast/function.rb
CHANGED
|
@@ -22,12 +22,19 @@ module Dentaku
|
|
|
22
22
|
.flat_map { |a, _| a.dependencies(context) }
|
|
23
23
|
end
|
|
24
24
|
|
|
25
|
+
# volatile functions may not be evaluated during dependency
|
|
26
|
+
# resolution; registry-generated classes override this per the
|
|
27
|
+
# volatile: option passed at registration
|
|
28
|
+
def self.volatile?
|
|
29
|
+
false
|
|
30
|
+
end
|
|
31
|
+
|
|
25
32
|
def self.get(name)
|
|
26
33
|
registry.get(name)
|
|
27
34
|
end
|
|
28
35
|
|
|
29
|
-
def self.register(name, type, implementation)
|
|
30
|
-
registry.register(name, type, implementation)
|
|
36
|
+
def self.register(name, type, implementation, volatile: false)
|
|
37
|
+
registry.register(name, type, implementation, volatile: volatile)
|
|
31
38
|
end
|
|
32
39
|
|
|
33
40
|
def self.register_class(name, function_class)
|
|
@@ -37,6 +44,12 @@ module Dentaku
|
|
|
37
44
|
def self.registry
|
|
38
45
|
@registry ||= FunctionRegistry.new
|
|
39
46
|
end
|
|
47
|
+
|
|
48
|
+
private
|
|
49
|
+
|
|
50
|
+
def compute_pure?
|
|
51
|
+
!self.class.volatile? && args.all?(&:pure?)
|
|
52
|
+
end
|
|
40
53
|
end
|
|
41
54
|
end
|
|
42
55
|
end
|
|
@@ -8,7 +8,7 @@ module Dentaku
|
|
|
8
8
|
nil
|
|
9
9
|
end
|
|
10
10
|
|
|
11
|
-
def register(name, type, implementation, callback = nil)
|
|
11
|
+
def register(name, type, implementation, callback = nil, volatile: false)
|
|
12
12
|
function = Class.new(Function) do
|
|
13
13
|
def self.name=(name)
|
|
14
14
|
@name = name.to_s
|
|
@@ -18,6 +18,21 @@ module Dentaku
|
|
|
18
18
|
@name
|
|
19
19
|
end
|
|
20
20
|
|
|
21
|
+
# only the first registration of a given name claims the constant
|
|
22
|
+
# under Dentaku::AST::Function, so later ones stay anonymous and
|
|
23
|
+
# would otherwise render as "#<Class:0x...>" in error messages
|
|
24
|
+
def self.display_name=(display_name)
|
|
25
|
+
@display_name = display_name
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def self.to_s
|
|
29
|
+
@display_name || super
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def self.inspect
|
|
33
|
+
to_s
|
|
34
|
+
end
|
|
35
|
+
|
|
21
36
|
def self.implementation=(impl)
|
|
22
37
|
@implementation = impl
|
|
23
38
|
end
|
|
@@ -42,6 +57,14 @@ module Dentaku
|
|
|
42
57
|
@callback
|
|
43
58
|
end
|
|
44
59
|
|
|
60
|
+
def self.volatile=(volatile)
|
|
61
|
+
@volatile = volatile
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def self.volatile?
|
|
65
|
+
@volatile
|
|
66
|
+
end
|
|
67
|
+
|
|
45
68
|
def self.arity
|
|
46
69
|
@implementation.arity < 0 ? nil : @implementation.arity
|
|
47
70
|
end
|
|
@@ -64,12 +87,15 @@ module Dentaku
|
|
|
64
87
|
end
|
|
65
88
|
end
|
|
66
89
|
|
|
90
|
+
function.name = name
|
|
91
|
+
function.display_name = "#{Function}::#{normalize_name(name)}"
|
|
92
|
+
|
|
67
93
|
define_class(name, function)
|
|
68
94
|
|
|
69
|
-
function.name = name
|
|
70
95
|
function.type = type
|
|
71
96
|
function.implementation = implementation
|
|
72
97
|
function.callback = callback
|
|
98
|
+
function.volatile = volatile
|
|
73
99
|
|
|
74
100
|
self[function_name(name)] = function
|
|
75
101
|
end
|
|
@@ -5,8 +5,8 @@ Dentaku::AST::Function.register(:and, :logical, lambda { |*args|
|
|
|
5
5
|
if args.empty?
|
|
6
6
|
raise Dentaku::ArgumentError.for(
|
|
7
7
|
:too_few_arguments,
|
|
8
|
-
function_name: 'AND
|
|
9
|
-
)
|
|
8
|
+
function_name: 'AND', expected: 1.., actual: 0
|
|
9
|
+
)
|
|
10
10
|
end
|
|
11
11
|
|
|
12
12
|
args.all? do |arg|
|
|
@@ -18,8 +18,8 @@ Dentaku::AST::Function.register(:and, :logical, lambda { |*args|
|
|
|
18
18
|
else
|
|
19
19
|
raise Dentaku::ArgumentError.for(
|
|
20
20
|
:incompatible_type,
|
|
21
|
-
function_name: 'AND
|
|
22
|
-
)
|
|
21
|
+
function_name: 'AND', expected: :logical, actual: arg
|
|
22
|
+
)
|
|
23
23
|
end
|
|
24
24
|
end
|
|
25
25
|
})
|
|
@@ -5,8 +5,8 @@ Dentaku::AST::Function.register(:avg, :numeric, ->(*args) {
|
|
|
5
5
|
if flatten_args.empty?
|
|
6
6
|
raise Dentaku::ArgumentError.for(
|
|
7
7
|
:too_few_arguments,
|
|
8
|
-
function_name: 'AVG
|
|
9
|
-
)
|
|
8
|
+
function_name: 'AVG', expected: 1.., actual: 0
|
|
9
|
+
)
|
|
10
10
|
end
|
|
11
11
|
|
|
12
12
|
flatten_args.map { |arg| Dentaku::NumericParser.ensure_numeric!(arg) }.reduce(0, :+) / BigDecimal(flatten_args.length)
|
|
@@ -25,7 +25,7 @@ module Dentaku
|
|
|
25
25
|
when /months?/ then :month
|
|
26
26
|
when /days?/ then :day
|
|
27
27
|
else
|
|
28
|
-
raise Dentaku::ArgumentError.for(:incompatible_type,
|
|
28
|
+
raise Dentaku::ArgumentError.for(:incompatible_type, actual: unit, expected: Duration),
|
|
29
29
|
"'#{unit || unit.class}' is not a valid duration unit"
|
|
30
30
|
end
|
|
31
31
|
end
|
|
@@ -36,8 +36,16 @@ module Dentaku
|
|
|
36
36
|
end
|
|
37
37
|
|
|
38
38
|
def dependencies(context = {})
|
|
39
|
+
return all_arg_dependencies(context) if static_mode?(context) || !predicate.pure?
|
|
40
|
+
|
|
39
41
|
predicate.value(context) ? left.dependencies(context) : right.dependencies(context)
|
|
40
|
-
rescue Dentaku::Error
|
|
42
|
+
rescue Dentaku::Error
|
|
43
|
+
all_arg_dependencies(context)
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
private
|
|
47
|
+
|
|
48
|
+
def all_arg_dependencies(context)
|
|
41
49
|
args.flat_map { |arg| arg.dependencies(context) }.uniq
|
|
42
50
|
end
|
|
43
51
|
end
|
|
@@ -4,7 +4,7 @@ Dentaku::AST::Function.register(:intercept, :list, ->(*args) {
|
|
|
4
4
|
if args.length != 2
|
|
5
5
|
raise Dentaku::ArgumentError.for(
|
|
6
6
|
:wrong_number_of_arguments,
|
|
7
|
-
function_name: 'INTERCEPT
|
|
7
|
+
function_name: 'INTERCEPT', expected: 2, actual: args.length
|
|
8
8
|
), 'INTERCEPT() requires exactly two arrays of numbers'
|
|
9
9
|
end
|
|
10
10
|
|
|
@@ -12,7 +12,7 @@ Dentaku::AST::Function.register(:intercept, :list, ->(*args) {
|
|
|
12
12
|
if !x_values.is_a?(Array) || !y_values.is_a?(Array) || x_values.length != y_values.length
|
|
13
13
|
raise Dentaku::ArgumentError.for(
|
|
14
14
|
:invalid_value,
|
|
15
|
-
function_name: 'INTERCEPT
|
|
15
|
+
function_name: 'INTERCEPT'
|
|
16
16
|
), 'INTERCEPT() requires arrays of equal length'
|
|
17
17
|
end
|
|
18
18
|
|
|
@@ -4,8 +4,8 @@ Dentaku::AST::Function.register(:mul, :numeric, ->(*args) {
|
|
|
4
4
|
if args.empty?
|
|
5
5
|
raise Dentaku::ArgumentError.for(
|
|
6
6
|
:too_few_arguments,
|
|
7
|
-
function_name: 'MUL
|
|
8
|
-
)
|
|
7
|
+
function_name: 'MUL', expected: 1.., actual: 0
|
|
8
|
+
)
|
|
9
9
|
end
|
|
10
10
|
|
|
11
11
|
args.flatten.map { |arg| Dentaku::NumericParser.ensure_numeric!(arg) }.reduce(1, :*)
|
|
@@ -5,8 +5,8 @@ Dentaku::AST::Function.register(:or, :logical, lambda { |*args|
|
|
|
5
5
|
if args.empty?
|
|
6
6
|
raise Dentaku::ArgumentError.for(
|
|
7
7
|
:too_few_arguments,
|
|
8
|
-
function_name: 'OR
|
|
9
|
-
)
|
|
8
|
+
function_name: 'OR', expected: 1.., actual: 0
|
|
9
|
+
)
|
|
10
10
|
end
|
|
11
11
|
|
|
12
12
|
args.any? do |arg|
|
|
@@ -18,8 +18,8 @@ Dentaku::AST::Function.register(:or, :logical, lambda { |*args|
|
|
|
18
18
|
else
|
|
19
19
|
raise Dentaku::ArgumentError.for(
|
|
20
20
|
:incompatible_type,
|
|
21
|
-
function_name: 'OR
|
|
22
|
-
)
|
|
21
|
+
function_name: 'OR', expected: :logical, actual: arg
|
|
22
|
+
)
|
|
23
23
|
end
|
|
24
24
|
end
|
|
25
25
|
})
|
|
@@ -16,7 +16,7 @@ module Dentaku
|
|
|
16
16
|
collection = Array(@args[0].value(context))
|
|
17
17
|
|
|
18
18
|
unless collection.all? { |elem| elem.is_a?(Hash) }
|
|
19
|
-
raise ArgumentError.for(:incompatible_type,
|
|
19
|
+
raise ArgumentError.for(:incompatible_type, actual: collection),
|
|
20
20
|
'PLUCK() requires first argument to be an array of hashes'
|
|
21
21
|
end
|
|
22
22
|
|
|
@@ -4,8 +4,8 @@ Dentaku::AST::Function.register(:sum, :numeric, ->(*args) {
|
|
|
4
4
|
if args.empty?
|
|
5
5
|
raise Dentaku::ArgumentError.for(
|
|
6
6
|
:too_few_arguments,
|
|
7
|
-
function_name: 'SUM
|
|
8
|
-
)
|
|
7
|
+
function_name: 'SUM', expected: 1.., actual: 0
|
|
8
|
+
)
|
|
9
9
|
end
|
|
10
10
|
|
|
11
11
|
args.flatten.map { |arg| Dentaku::NumericParser.ensure_numeric!(arg) }.reduce(0, :+)
|
|
@@ -16,8 +16,8 @@ module Dentaku
|
|
|
16
16
|
if @args.empty?
|
|
17
17
|
raise Dentaku::ArgumentError.for(
|
|
18
18
|
:too_few_arguments,
|
|
19
|
-
function_name: 'XOR
|
|
20
|
-
)
|
|
19
|
+
function_name: 'XOR', expected: 1.., actual: 0
|
|
20
|
+
)
|
|
21
21
|
end
|
|
22
22
|
|
|
23
23
|
true_arg_count = 0
|
|
@@ -31,8 +31,8 @@ module Dentaku
|
|
|
31
31
|
else
|
|
32
32
|
raise Dentaku::ArgumentError.for(
|
|
33
33
|
:incompatible_type,
|
|
34
|
-
function_name: 'XOR
|
|
35
|
-
)
|
|
34
|
+
function_name: 'XOR', expected: :logical, actual: arg
|
|
35
|
+
)
|
|
36
36
|
end
|
|
37
37
|
end
|
|
38
38
|
true_arg_count == 1
|
data/lib/dentaku/ast/grouping.rb
CHANGED
data/lib/dentaku/ast/negation.rb
CHANGED
|
@@ -46,8 +46,15 @@ module Dentaku
|
|
|
46
46
|
|
|
47
47
|
private
|
|
48
48
|
|
|
49
|
+
# bypasses Operation's binary initialize, so left/right are unset
|
|
50
|
+
def compute_pure?
|
|
51
|
+
@node.pure?
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
# static dependency check: this runs at parse time, and evaluating
|
|
55
|
+
# short-circuit guards here would execute user functions (#197)
|
|
49
56
|
def valid_node?(node)
|
|
50
|
-
node && (node.dependencies.any? || node.type == :numeric)
|
|
57
|
+
node && (node.dependencies(Node::STATIC_CONTEXT).any? || node.type == :numeric)
|
|
51
58
|
end
|
|
52
59
|
end
|
|
53
60
|
end
|
data/lib/dentaku/ast/node.rb
CHANGED
|
@@ -1,6 +1,12 @@
|
|
|
1
1
|
module Dentaku
|
|
2
2
|
module AST
|
|
3
3
|
class Node
|
|
4
|
+
# reserved context key (double-underscore prefix, like
|
|
5
|
+
# __evaluation_mode) that switches dependencies() into static mode:
|
|
6
|
+
# guards are never evaluated and every branch is reported
|
|
7
|
+
STATIC_MODE_KEY = "__static_dependencies".freeze
|
|
8
|
+
STATIC_CONTEXT = { STATIC_MODE_KEY => true }.freeze
|
|
9
|
+
|
|
4
10
|
def self.precedence
|
|
5
11
|
0
|
|
6
12
|
end
|
|
@@ -17,6 +23,15 @@ module Dentaku
|
|
|
17
23
|
[]
|
|
18
24
|
end
|
|
19
25
|
|
|
26
|
+
# whether this subtree may be evaluated during dependency resolution
|
|
27
|
+
# without running volatile user code; a property of the parsed AST,
|
|
28
|
+
# computed once and memoized
|
|
29
|
+
def pure?
|
|
30
|
+
return @pure if defined?(@pure)
|
|
31
|
+
|
|
32
|
+
@pure = compute_pure?
|
|
33
|
+
end
|
|
34
|
+
|
|
20
35
|
def type
|
|
21
36
|
nil
|
|
22
37
|
end
|
|
@@ -24,6 +39,16 @@ module Dentaku
|
|
|
24
39
|
def name
|
|
25
40
|
self.class.name.to_s.split("::").last.upcase
|
|
26
41
|
end
|
|
42
|
+
|
|
43
|
+
private
|
|
44
|
+
|
|
45
|
+
def static_mode?(context)
|
|
46
|
+
context[STATIC_MODE_KEY] == true
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def compute_pure?
|
|
50
|
+
true
|
|
51
|
+
end
|
|
27
52
|
end
|
|
28
53
|
end
|
|
29
54
|
end
|
|
@@ -11,8 +11,8 @@ module Dentaku
|
|
|
11
11
|
@calculator = calculator
|
|
12
12
|
end
|
|
13
13
|
|
|
14
|
-
def evaluate(*args)
|
|
15
|
-
@calculator.evaluate!(*args)
|
|
14
|
+
def evaluate(*args, &block)
|
|
15
|
+
@calculator.evaluate!(*args, &block)
|
|
16
16
|
end
|
|
17
17
|
end
|
|
18
18
|
|
|
@@ -22,10 +22,9 @@ module Dentaku
|
|
|
22
22
|
@block = block || ->(*) { :undefined }
|
|
23
23
|
end
|
|
24
24
|
|
|
25
|
-
def evaluate(*args)
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
}
|
|
25
|
+
def evaluate(*args, &block)
|
|
26
|
+
handler = block || ->(_expr, ex) { @block.call(ex) }
|
|
27
|
+
@calculator.evaluate(*args, &handler)
|
|
29
28
|
end
|
|
30
29
|
end
|
|
31
30
|
|
|
@@ -79,7 +78,7 @@ module Dentaku
|
|
|
79
78
|
|
|
80
79
|
def expression_with_exception_handler(var_name, &block)
|
|
81
80
|
->(_expr, ex) {
|
|
82
|
-
ex.
|
|
81
|
+
ex.assigned_to = var_name
|
|
83
82
|
block.call(ex)
|
|
84
83
|
}
|
|
85
84
|
end
|
|
@@ -106,7 +105,7 @@ module Dentaku
|
|
|
106
105
|
context.merge(results),
|
|
107
106
|
&expression_with_exception_handler(var_name, &block)
|
|
108
107
|
).tap { |res|
|
|
109
|
-
res.
|
|
108
|
+
res.assigned_to = var_name if res.respond_to?(:assigned_to=)
|
|
110
109
|
res
|
|
111
110
|
}
|
|
112
111
|
end
|
|
@@ -120,7 +119,7 @@ module Dentaku
|
|
|
120
119
|
def with_rescues(var_name, results, block)
|
|
121
120
|
yield
|
|
122
121
|
rescue Dentaku::UnboundVariableError, Dentaku::ZeroDivisionError, Dentaku::ArgumentError => ex
|
|
123
|
-
ex.
|
|
122
|
+
ex.assigned_to = var_name
|
|
124
123
|
results[var_name] = block.call(ex)
|
|
125
124
|
ensure
|
|
126
125
|
stored_name = normalized_name(var_name)
|
|
@@ -157,7 +156,7 @@ module Dentaku
|
|
|
157
156
|
cache_key = "#{!!calculator.case_sensitive}|#{expressions.keys.map(&:to_s).sort.join("|")}"
|
|
158
157
|
@ordered_deps ||= self.class.dependency_cache.fetch(cache_key) {
|
|
159
158
|
DependencyResolver.find_resolve_order(dependencies, calculator.case_sensitive).tap do |d|
|
|
160
|
-
self.class.dependency_cache[cache_key] = d if
|
|
159
|
+
self.class.dependency_cache[cache_key] = d if calculator.cache_dependency_order?
|
|
161
160
|
end
|
|
162
161
|
}
|
|
163
162
|
end
|