dentaku 3.5.7 → 4.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/.github/workflows/rspec.yml +2 -6
- data/.github/workflows/rubocop.yml +1 -1
- data/.rubocop.yml +1 -1
- data/CHANGELOG.md +91 -0
- data/Gemfile +14 -1
- data/README.md +87 -0
- data/dentaku.gemspec +14 -14
- data/lib/dentaku/ast/access.rb +18 -1
- data/lib/dentaku/ast/arithmetic.rb +5 -17
- 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 +49 -4
- data/lib/dentaku/ast/comparators.rb +3 -7
- data/lib/dentaku/ast/function.rb +12 -13
- data/lib/dentaku/ast/function_registry.rb +11 -2
- data/lib/dentaku/ast/functions/abs.rb +1 -1
- data/lib/dentaku/ast/functions/and.rb +4 -4
- data/lib/dentaku/ast/functions/avg.rb +3 -3
- 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 +4 -4
- data/lib/dentaku/ast/functions/max.rb +1 -1
- data/lib/dentaku/ast/functions/min.rb +1 -1
- data/lib/dentaku/ast/functions/mul.rb +3 -3
- data/lib/dentaku/ast/functions/or.rb +4 -4
- data/lib/dentaku/ast/functions/pluck.rb +1 -1
- data/lib/dentaku/ast/functions/round.rb +1 -1
- data/lib/dentaku/ast/functions/rounddown.rb +2 -2
- data/lib/dentaku/ast/functions/roundup.rb +2 -2
- data/lib/dentaku/ast/functions/ruby_math.rb +5 -2
- data/lib/dentaku/ast/functions/string_functions.rb +5 -5
- data/lib/dentaku/ast/functions/sum.rb +3 -3
- data/lib/dentaku/ast/functions/xor.rb +4 -4
- data/lib/dentaku/ast/grouping.rb +6 -0
- data/lib/dentaku/ast/negation.rb +5 -0
- data/lib/dentaku/ast/node.rb +25 -0
- data/lib/dentaku/ast/operation.rb +7 -0
- data/lib/dentaku/bulk_expression_solver.rb +22 -16
- data/lib/dentaku/calculator.rb +45 -17
- data/lib/dentaku/date_arithmetic.rb +2 -2
- data/lib/dentaku/dependency_resolver.rb +12 -5
- data/lib/dentaku/exceptions.rb +105 -13
- data/lib/dentaku/numeric_parser.rb +68 -0
- data/lib/dentaku/parser.rb +24 -48
- data/lib/dentaku/print_visitor.rb +1 -0
- data/lib/dentaku/token_matcher.rb +31 -37
- data/lib/dentaku/token_matchers.rb +3 -3
- data/lib/dentaku/token_scanner.rb +3 -4
- data/lib/dentaku/tokenizer.rb +2 -16
- data/lib/dentaku/version.rb +1 -1
- data/spec/ast/case_spec.rb +7 -0
- data/spec/ast/function_spec.rb +11 -25
- data/spec/bulk_expression_solver_spec.rb +48 -3
- data/spec/calculator_spec.rb +77 -0
- data/spec/dependency_resolver_spec.rb +14 -0
- data/spec/exceptions_spec.rb +87 -0
- data/spec/gemspec_spec.rb +10 -0
- data/spec/identifiers_spec.rb +66 -0
- data/spec/numeric_parser_spec.rb +66 -0
- data/spec/parser_spec.rb +35 -0
- data/spec/print_visitor_spec.rb +5 -0
- data/spec/tokenizer_spec.rb +6 -0
- data/spec/volatile_functions_spec.rb +121 -0
- metadata +27 -150
- data/.travis.yml +0 -10
|
@@ -21,24 +21,20 @@ 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
|
|
28
28
|
|
|
29
29
|
def cast(val)
|
|
30
30
|
return val unless val.is_a?(::String)
|
|
31
|
-
return val unless val.match?(Arithmetic::DECIMAL) || val.match?(Arithmetic::INTEGER)
|
|
32
31
|
|
|
33
|
-
|
|
34
|
-
v = v.to_i if v.frac.zero?
|
|
35
|
-
v
|
|
32
|
+
Dentaku::NumericParser.parse_string(val) || val
|
|
36
33
|
end
|
|
37
34
|
|
|
38
35
|
def validate_value(value)
|
|
39
36
|
unless value.respond_to?(operator)
|
|
40
|
-
raise Dentaku::ArgumentError.for(:invalid_operator, operation: self.class, operator: operator)
|
|
41
|
-
"#{ self.class } requires operands that respond to #{operator}"
|
|
37
|
+
raise Dentaku::ArgumentError.for(:invalid_operator, operation: self.class, operator: operator)
|
|
42
38
|
end
|
|
43
39
|
|
|
44
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)
|
|
@@ -38,18 +45,10 @@ module Dentaku
|
|
|
38
45
|
@registry ||= FunctionRegistry.new
|
|
39
46
|
end
|
|
40
47
|
|
|
41
|
-
|
|
42
|
-
# An Exception will be raised if a value is passed that cannot be cast to a Number.
|
|
43
|
-
def self.numeric(value)
|
|
44
|
-
return value if value.is_a?(::Numeric)
|
|
45
|
-
|
|
46
|
-
if value.is_a?(::String)
|
|
47
|
-
number = value[/\A-?\d*\.?\d+\z/]
|
|
48
|
-
return number.include?('.') ? BigDecimal(number, DIG) : number.to_i if number
|
|
49
|
-
end
|
|
48
|
+
private
|
|
50
49
|
|
|
51
|
-
|
|
52
|
-
|
|
50
|
+
def compute_pure?
|
|
51
|
+
!self.class.volatile? && args.all?(&:pure?)
|
|
53
52
|
end
|
|
54
53
|
end
|
|
55
54
|
end
|
|
@@ -8,10 +8,10 @@ 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
|
-
@name = name
|
|
14
|
+
@name = name.to_s
|
|
15
15
|
end
|
|
16
16
|
|
|
17
17
|
def self.name
|
|
@@ -42,6 +42,14 @@ module Dentaku
|
|
|
42
42
|
@callback
|
|
43
43
|
end
|
|
44
44
|
|
|
45
|
+
def self.volatile=(volatile)
|
|
46
|
+
@volatile = volatile
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def self.volatile?
|
|
50
|
+
@volatile
|
|
51
|
+
end
|
|
52
|
+
|
|
45
53
|
def self.arity
|
|
46
54
|
@implementation.arity < 0 ? nil : @implementation.arity
|
|
47
55
|
end
|
|
@@ -70,6 +78,7 @@ module Dentaku
|
|
|
70
78
|
function.type = type
|
|
71
79
|
function.implementation = implementation
|
|
72
80
|
function.callback = callback
|
|
81
|
+
function.volatile = volatile
|
|
73
82
|
|
|
74
83
|
self[function_name(name)] = function
|
|
75
84
|
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,9 +5,9 @@ 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
|
-
flatten_args.map { |arg| Dentaku::
|
|
12
|
+
flatten_args.map { |arg| Dentaku::NumericParser.ensure_numeric!(arg) }.reduce(0, :+) / BigDecimal(flatten_args.length)
|
|
13
13
|
})
|
|
@@ -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,13 +12,13 @@ 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
|
|
|
19
19
|
n = x_values.length.to_f
|
|
20
|
-
x_values = x_values.map { |arg| Dentaku::
|
|
21
|
-
y_values = y_values.map { |arg| Dentaku::
|
|
20
|
+
x_values = x_values.map { |arg| Dentaku::NumericParser.ensure_numeric!(arg) }
|
|
21
|
+
y_values = y_values.map { |arg| Dentaku::NumericParser.ensure_numeric!(arg) }
|
|
22
22
|
|
|
23
23
|
x_avg = x_values.sum / n
|
|
24
24
|
y_avg = y_values.sum / n
|
|
@@ -4,9 +4,9 @@ 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
|
-
args.flatten.map { |arg| Dentaku::
|
|
11
|
+
args.flatten.map { |arg| Dentaku::NumericParser.ensure_numeric!(arg) }.reduce(1, :*)
|
|
12
12
|
})
|
|
@@ -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
|
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
require_relative '../function'
|
|
2
2
|
|
|
3
3
|
Dentaku::AST::Function.register(:round, :numeric, lambda { |numeric, places = 0|
|
|
4
|
-
Dentaku::
|
|
4
|
+
Dentaku::NumericParser.ensure_numeric!(numeric).round(Dentaku::NumericParser.ensure_numeric!(places || 0).to_i)
|
|
5
5
|
})
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
require_relative '../function'
|
|
2
2
|
|
|
3
3
|
Dentaku::AST::Function.register(:rounddown, :numeric, lambda { |numeric, precision = 0|
|
|
4
|
-
precision = Dentaku::
|
|
4
|
+
precision = Dentaku::NumericParser.ensure_numeric!(precision || 0).to_i
|
|
5
5
|
tens = 10.0**precision
|
|
6
|
-
result = (Dentaku::
|
|
6
|
+
result = (Dentaku::NumericParser.ensure_numeric!(numeric) * tens).floor / tens
|
|
7
7
|
precision <= 0 ? result.to_i : result
|
|
8
8
|
})
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
require_relative '../function'
|
|
2
2
|
|
|
3
3
|
Dentaku::AST::Function.register(:roundup, :numeric, lambda { |numeric, precision = 0|
|
|
4
|
-
precision = Dentaku::
|
|
4
|
+
precision = Dentaku::NumericParser.ensure_numeric!(precision || 0).to_i
|
|
5
5
|
tens = 10.0**precision
|
|
6
|
-
result = (Dentaku::
|
|
6
|
+
result = (Dentaku::NumericParser.ensure_numeric!(numeric) * tens).ceil / tens
|
|
7
7
|
precision <= 0 ? result.to_i : result
|
|
8
8
|
})
|
|
@@ -16,8 +16,11 @@ module Dentaku
|
|
|
16
16
|
@implementation = Math.method(method)
|
|
17
17
|
end
|
|
18
18
|
|
|
19
|
+
# Ruby documents Module#name as a String (or nil for anonymous
|
|
20
|
+
# classes), so expose one even though the function is registered
|
|
21
|
+
# under a Symbol
|
|
19
22
|
def self.name
|
|
20
|
-
@name
|
|
23
|
+
@name.to_s
|
|
21
24
|
end
|
|
22
25
|
|
|
23
26
|
def self.arity
|
|
@@ -39,7 +42,7 @@ module Dentaku
|
|
|
39
42
|
end
|
|
40
43
|
|
|
41
44
|
def value(context = {})
|
|
42
|
-
args = @args.flatten.map { |a|
|
|
45
|
+
args = @args.flatten.map { |a|Dentaku::NumericParser.ensure_numeric!(a.value(context)) }
|
|
43
46
|
self.class.call(*args)
|
|
44
47
|
end
|
|
45
48
|
|
|
@@ -11,7 +11,7 @@ module Dentaku
|
|
|
11
11
|
def negative_argument_failure(fun, arg = 'length')
|
|
12
12
|
raise Dentaku::ArgumentError.for(
|
|
13
13
|
:invalid_value,
|
|
14
|
-
function_name:
|
|
14
|
+
function_name: fun
|
|
15
15
|
), "#{fun}() requires #{arg} to be positive"
|
|
16
16
|
end
|
|
17
17
|
end
|
|
@@ -32,7 +32,7 @@ module Dentaku
|
|
|
32
32
|
|
|
33
33
|
def value(context = {})
|
|
34
34
|
string = @string.value(context).to_s
|
|
35
|
-
length = Dentaku::
|
|
35
|
+
length = Dentaku::NumericParser.ensure_numeric!(@length.value(context)).to_i
|
|
36
36
|
negative_argument_failure('LEFT') if length < 0
|
|
37
37
|
string[0, length]
|
|
38
38
|
end
|
|
@@ -54,7 +54,7 @@ module Dentaku
|
|
|
54
54
|
|
|
55
55
|
def value(context = {})
|
|
56
56
|
string = @string.value(context).to_s
|
|
57
|
-
length = Dentaku::
|
|
57
|
+
length = Dentaku::NumericParser.ensure_numeric!(@length.value(context)).to_i
|
|
58
58
|
negative_argument_failure('RIGHT') if length < 0
|
|
59
59
|
string[length * -1, length] || string
|
|
60
60
|
end
|
|
@@ -76,9 +76,9 @@ module Dentaku
|
|
|
76
76
|
|
|
77
77
|
def value(context = {})
|
|
78
78
|
string = @string.value(context).to_s
|
|
79
|
-
offset = Dentaku::
|
|
79
|
+
offset = Dentaku::NumericParser.ensure_numeric!(@offset.value(context)).to_i
|
|
80
80
|
negative_argument_failure('MID', 'offset') if offset < 0
|
|
81
|
-
length = Dentaku::
|
|
81
|
+
length = Dentaku::NumericParser.ensure_numeric!(@length.value(context)).to_i
|
|
82
82
|
negative_argument_failure('MID') if length < 0
|
|
83
83
|
string[offset - 1, length].to_s
|
|
84
84
|
end
|
|
@@ -4,9 +4,9 @@ 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
|
-
args.flatten.map { |arg| Dentaku::
|
|
11
|
+
args.flatten.map { |arg| Dentaku::NumericParser.ensure_numeric!(arg) }.reduce(0, :+)
|
|
12
12
|
})
|
|
@@ -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
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,13 +78,13 @@ 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
|
|
86
85
|
|
|
87
86
|
def load_results(&block)
|
|
88
|
-
facts, _formulas = expressions.transform_keys(
|
|
87
|
+
facts, _formulas = expressions.transform_keys { |k| normalized_name(k) }
|
|
89
88
|
.transform_values { |v| calculator.ast(v) }
|
|
90
89
|
.partition { |_, v| calculator.dependencies(v, nil).empty? }
|
|
91
90
|
|
|
@@ -101,12 +100,12 @@ module Dentaku
|
|
|
101
100
|
next if expressions[var_name].nil?
|
|
102
101
|
|
|
103
102
|
with_rescues(var_name, results, block) do
|
|
104
|
-
results[var_name] = evaluated_facts[var_name] || evaluator.evaluate(
|
|
103
|
+
results[var_name] = evaluated_facts[normalized_name(var_name)] || evaluator.evaluate(
|
|
105
104
|
expressions[var_name],
|
|
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,14 +119,21 @@ 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
|
-
|
|
127
|
-
|
|
125
|
+
stored_name = normalized_name(var_name)
|
|
126
|
+
if results[var_name] == :undefined && calculator.memory.has_key?(stored_name)
|
|
127
|
+
results[var_name] = calculator.memory[stored_name]
|
|
128
128
|
end
|
|
129
129
|
end
|
|
130
130
|
|
|
131
|
+
# names are normalized the same way the calculator stores them, so that
|
|
132
|
+
# facts and memory lookups line up with the identifiers the parser emits
|
|
133
|
+
def normalized_name(variable_name)
|
|
134
|
+
calculator.standardize_case(variable_name.to_s)
|
|
135
|
+
end
|
|
136
|
+
|
|
131
137
|
def expressions
|
|
132
138
|
@expressions ||= Hash[expression_hash.map { |k, v| [k.to_s, v] }]
|
|
133
139
|
end
|
|
@@ -147,10 +153,10 @@ module Dentaku
|
|
|
147
153
|
end
|
|
148
154
|
|
|
149
155
|
def variables_in_resolve_order
|
|
150
|
-
cache_key = expressions.keys.map(&:to_s).sort.join("|")
|
|
156
|
+
cache_key = "#{!!calculator.case_sensitive}|#{expressions.keys.map(&:to_s).sort.join("|")}"
|
|
151
157
|
@ordered_deps ||= self.class.dependency_cache.fetch(cache_key) {
|
|
152
|
-
DependencyResolver.find_resolve_order(dependencies).tap do |d|
|
|
153
|
-
self.class.dependency_cache[cache_key] = d if
|
|
158
|
+
DependencyResolver.find_resolve_order(dependencies, calculator.case_sensitive).tap do |d|
|
|
159
|
+
self.class.dependency_cache[cache_key] = d if calculator.cache_dependency_order?
|
|
154
160
|
end
|
|
155
161
|
}
|
|
156
162
|
end
|