dentaku 3.5.8 → 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 -4
- data/.github/workflows/rubocop.yml +1 -1
- data/.rubocop.yml +1 -1
- data/CHANGELOG.md +74 -1
- data/Gemfile +1 -1
- data/README.md +87 -0
- data/dentaku.gemspec +15 -8
- data/lib/dentaku/ast/access.rb +18 -1
- data/lib/dentaku/ast/arithmetic.rb +3 -4
- 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 +2 -3
- data/lib/dentaku/ast/function.rb +15 -2
- data/lib/dentaku/ast/function_registry.rb +10 -1
- 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 +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 +9 -10
- data/lib/dentaku/calculator.rb +45 -17
- 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/case_spec.rb +7 -0
- data/spec/bulk_expression_solver_spec.rb +13 -3
- data/spec/calculator_spec.rb +67 -0
- data/spec/exceptions_spec.rb +87 -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/volatile_functions_spec.rb +121 -0
- metadata +21 -63
- data/.travis.yml +0 -10
|
@@ -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
|
|
@@ -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,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
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
|
data/lib/dentaku/calculator.rb
CHANGED
|
@@ -9,38 +9,46 @@ require 'dentaku/token'
|
|
|
9
9
|
module Dentaku
|
|
10
10
|
class Calculator
|
|
11
11
|
include StringCasing
|
|
12
|
-
attr_reader :result, :memory, :tokenizer, :case_sensitive,
|
|
12
|
+
attr_reader :result, :memory, :tokenizer, :case_sensitive,
|
|
13
13
|
:nested_data_support, :ast_cache, :raw_date_literals
|
|
14
14
|
|
|
15
|
-
def initialize(
|
|
15
|
+
def initialize(case_sensitive: false, aliases: nil, nested_data_support: true,
|
|
16
|
+
raw_date_literals: true, ast_cache: {},
|
|
17
|
+
cache_ast: nil, cache_dependency_order: nil)
|
|
16
18
|
clear
|
|
17
19
|
@tokenizer = Tokenizer.new
|
|
18
|
-
@case_sensitive =
|
|
19
|
-
@aliases =
|
|
20
|
-
@nested_data_support =
|
|
21
|
-
|
|
22
|
-
@
|
|
23
|
-
|
|
24
|
-
@
|
|
20
|
+
@case_sensitive = case_sensitive
|
|
21
|
+
@aliases = aliases
|
|
22
|
+
@nested_data_support = nested_data_support
|
|
23
|
+
@raw_date_literals = raw_date_literals
|
|
24
|
+
@ast_cache = ast_cache
|
|
25
|
+
@cache_ast = cache_ast
|
|
26
|
+
@cache_dependency_order = cache_dependency_order
|
|
25
27
|
@disable_ast_cache = false
|
|
26
28
|
@function_registry = Dentaku::AST::FunctionRegistry.new
|
|
27
29
|
end
|
|
28
30
|
|
|
29
|
-
|
|
30
|
-
|
|
31
|
+
# explicitly configured aliases win; otherwise the module-level default is
|
|
32
|
+
# resolved lazily so it can be set after this calculator was created
|
|
33
|
+
def aliases
|
|
34
|
+
@aliases || Dentaku.aliases
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def self.add_function(name, type, body, callback = nil, volatile: false)
|
|
38
|
+
Dentaku::AST::FunctionRegistry.default.register(name, type, body, callback, volatile: volatile)
|
|
31
39
|
end
|
|
32
40
|
|
|
33
41
|
def self.add_functions(functions)
|
|
34
|
-
functions.each { |(name, type, body, callback)| add_function(name, type, body, callback) }
|
|
42
|
+
functions.each { |(name, type, body, callback, volatile)| add_function(name, type, body, callback, volatile: !!volatile) }
|
|
35
43
|
end
|
|
36
44
|
|
|
37
|
-
def add_function(name, type, body, callback = nil)
|
|
38
|
-
@function_registry.register(name, type, body, callback)
|
|
45
|
+
def add_function(name, type, body, callback = nil, volatile: false)
|
|
46
|
+
@function_registry.register(name, type, body, callback, volatile: volatile)
|
|
39
47
|
self
|
|
40
48
|
end
|
|
41
49
|
|
|
42
50
|
def add_functions(functions)
|
|
43
|
-
functions.each { |(name, type, body, callback)| add_function(name, type, body, callback) }
|
|
51
|
+
functions.each { |(name, type, body, callback, volatile)| add_function(name, type, body, callback, volatile: !!volatile) }
|
|
44
52
|
self
|
|
45
53
|
end
|
|
46
54
|
|
|
@@ -56,7 +64,7 @@ module Dentaku
|
|
|
56
64
|
return evaluate_array(expression, context, &block) if expression.is_a?(Array)
|
|
57
65
|
|
|
58
66
|
evaluate!(expression, context)
|
|
59
|
-
rescue Dentaku::Error
|
|
67
|
+
rescue Dentaku::Error => ex
|
|
60
68
|
block.call(expression, ex) if block_given?
|
|
61
69
|
end
|
|
62
70
|
|
|
@@ -106,6 +114,20 @@ module Dentaku
|
|
|
106
114
|
end
|
|
107
115
|
end
|
|
108
116
|
|
|
117
|
+
# every identifier the expression could reference, regardless of
|
|
118
|
+
# branching: purely syntactic, ignores stored memory, and never
|
|
119
|
+
# evaluates guards or functions
|
|
120
|
+
def identifiers(expression)
|
|
121
|
+
case expression
|
|
122
|
+
when Dentaku::AST::Node
|
|
123
|
+
expression.dependencies(AST::Node::STATIC_CONTEXT).uniq
|
|
124
|
+
when Array
|
|
125
|
+
expression.flat_map { |e| identifiers(e) }.uniq
|
|
126
|
+
else
|
|
127
|
+
ast(expression).dependencies(AST::Node::STATIC_CONTEXT).uniq
|
|
128
|
+
end
|
|
129
|
+
end
|
|
130
|
+
|
|
109
131
|
def ast(expression)
|
|
110
132
|
return expression if expression.is_a?(AST::Node)
|
|
111
133
|
return expression.map { |e| ast(e) } if expression.is_a? Array
|
|
@@ -186,7 +208,13 @@ module Dentaku
|
|
|
186
208
|
end
|
|
187
209
|
|
|
188
210
|
def cache_ast?
|
|
189
|
-
|
|
211
|
+
return false if @disable_ast_cache
|
|
212
|
+
|
|
213
|
+
@cache_ast.nil? ? Dentaku.cache_ast? : @cache_ast
|
|
214
|
+
end
|
|
215
|
+
|
|
216
|
+
def cache_dependency_order?
|
|
217
|
+
@cache_dependency_order.nil? ? Dentaku.cache_dependency_order? : @cache_dependency_order
|
|
190
218
|
end
|
|
191
219
|
end
|
|
192
220
|
end
|
|
@@ -20,7 +20,7 @@ module Dentaku
|
|
|
20
20
|
change_datetime(@base, duration.unit, duration.value)
|
|
21
21
|
end
|
|
22
22
|
else
|
|
23
|
-
raise Dentaku::ArgumentError.for(:incompatible_type,
|
|
23
|
+
raise Dentaku::ArgumentError.for(:incompatible_type, actual: duration, expected: Numeric),
|
|
24
24
|
"'#{duration || duration.class}' is not coercible for date arithmetic"
|
|
25
25
|
end
|
|
26
26
|
end
|
|
@@ -39,7 +39,7 @@ module Dentaku
|
|
|
39
39
|
when Dentaku::TokenScanner::DATE_TIME_REGEXP
|
|
40
40
|
@base - Time.parse(duration).to_datetime
|
|
41
41
|
else
|
|
42
|
-
raise Dentaku::ArgumentError.for(:incompatible_type,
|
|
42
|
+
raise Dentaku::ArgumentError.for(:incompatible_type, actual: duration, expected: Numeric),
|
|
43
43
|
"'#{duration || duration.class}' is not coercible for date arithmetic"
|
|
44
44
|
end
|
|
45
45
|
end
|
data/lib/dentaku/exceptions.rb
CHANGED
|
@@ -1,9 +1,16 @@
|
|
|
1
1
|
module Dentaku
|
|
2
|
-
|
|
3
|
-
|
|
2
|
+
# Shared by all Dentaku exceptions so that `rescue Dentaku::Error` catches
|
|
3
|
+
# everything the gem raises, including exceptions that subclass Ruby
|
|
4
|
+
# built-ins (Dentaku::ArgumentError, Dentaku::ZeroDivisionError).
|
|
5
|
+
module Error
|
|
6
|
+
attr_accessor :assigned_to
|
|
4
7
|
end
|
|
5
8
|
|
|
6
|
-
class
|
|
9
|
+
class BaseError < StandardError
|
|
10
|
+
include Error
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
class UnboundVariableError < BaseError
|
|
7
14
|
attr_reader :unbound_variables
|
|
8
15
|
|
|
9
16
|
def initialize(unbound_variables)
|
|
@@ -11,7 +18,7 @@ module Dentaku
|
|
|
11
18
|
end
|
|
12
19
|
end
|
|
13
20
|
|
|
14
|
-
class MathDomainError <
|
|
21
|
+
class MathDomainError < BaseError
|
|
15
22
|
attr_reader :function_name, :args
|
|
16
23
|
|
|
17
24
|
def initialize(function_name, args)
|
|
@@ -20,22 +27,23 @@ module Dentaku
|
|
|
20
27
|
end
|
|
21
28
|
end
|
|
22
29
|
|
|
23
|
-
class NodeError <
|
|
24
|
-
attr_reader :
|
|
30
|
+
class NodeError < BaseError
|
|
31
|
+
attr_reader :operand, :expected, :actual
|
|
25
32
|
|
|
26
|
-
def initialize(
|
|
27
|
-
@
|
|
33
|
+
def initialize(expected, actual, operand)
|
|
34
|
+
@expected = Array(expected)
|
|
28
35
|
@actual = actual
|
|
29
|
-
@
|
|
36
|
+
@operand = operand
|
|
30
37
|
end
|
|
31
38
|
end
|
|
32
39
|
|
|
33
|
-
class ParseError <
|
|
40
|
+
class ParseError < BaseError
|
|
34
41
|
attr_reader :reason, :meta
|
|
35
42
|
|
|
36
43
|
def initialize(reason, **meta)
|
|
37
44
|
@reason = reason
|
|
38
45
|
@meta = meta
|
|
46
|
+
super(default_message)
|
|
39
47
|
end
|
|
40
48
|
|
|
41
49
|
private_class_method :new
|
|
@@ -54,14 +62,49 @@ module Dentaku
|
|
|
54
62
|
|
|
55
63
|
new(reason, **meta)
|
|
56
64
|
end
|
|
65
|
+
|
|
66
|
+
private
|
|
67
|
+
|
|
68
|
+
def default_message
|
|
69
|
+
case reason
|
|
70
|
+
when :node_invalid
|
|
71
|
+
if meta.key?(:operation)
|
|
72
|
+
expected = Array(meta[:expected]).map { |e| e == :incompatible ? :compatible : e }
|
|
73
|
+
"#{meta[:operation]} requires #{expected.join(', ')} operands, but got #{meta[:actual]}"
|
|
74
|
+
else
|
|
75
|
+
"Node is invalid"
|
|
76
|
+
end
|
|
77
|
+
when :too_few_operands
|
|
78
|
+
"#{meta[:operation]} has too few operands (given #{meta[:actual]}, expected #{meta[:expected]})"
|
|
79
|
+
when :too_many_operands
|
|
80
|
+
"#{meta[:operation]} has too many operands (given #{meta[:actual]}, expected #{meta[:expected]})"
|
|
81
|
+
when :undefined_function
|
|
82
|
+
"Undefined function #{meta[:function_name]}"
|
|
83
|
+
when :unprocessed_token
|
|
84
|
+
"Unprocessed token #{meta[:token_name]}"
|
|
85
|
+
when :unknown_case_token
|
|
86
|
+
"Unknown case token #{meta[:token_name]}"
|
|
87
|
+
when :unbalanced_bracket
|
|
88
|
+
"Unbalanced bracket"
|
|
89
|
+
when :unbalanced_parenthesis
|
|
90
|
+
"Unbalanced parenthesis"
|
|
91
|
+
when :unknown_grouping_token
|
|
92
|
+
"Unknown grouping token #{meta[:token_name]}"
|
|
93
|
+
when :not_implemented_token_category
|
|
94
|
+
"Not implemented for tokens of category #{meta[:token_category]}"
|
|
95
|
+
when :invalid_statement
|
|
96
|
+
"Invalid statement"
|
|
97
|
+
end
|
|
98
|
+
end
|
|
57
99
|
end
|
|
58
100
|
|
|
59
|
-
class TokenizerError <
|
|
101
|
+
class TokenizerError < BaseError
|
|
60
102
|
attr_reader :reason, :meta
|
|
61
103
|
|
|
62
104
|
def initialize(reason, **meta)
|
|
63
105
|
@reason = reason
|
|
64
106
|
@meta = meta
|
|
107
|
+
super(default_message)
|
|
65
108
|
end
|
|
66
109
|
|
|
67
110
|
private_class_method :new
|
|
@@ -80,15 +123,32 @@ module Dentaku
|
|
|
80
123
|
|
|
81
124
|
new(reason, **meta)
|
|
82
125
|
end
|
|
126
|
+
|
|
127
|
+
private
|
|
128
|
+
|
|
129
|
+
def default_message
|
|
130
|
+
case reason
|
|
131
|
+
when :parse_error
|
|
132
|
+
"parse error at: '#{meta[:at]}'"
|
|
133
|
+
when :too_many_opening_parentheses
|
|
134
|
+
"too many opening parentheses"
|
|
135
|
+
when :too_many_closing_parentheses
|
|
136
|
+
"too many closing parentheses"
|
|
137
|
+
when :unexpected_zero_width_match
|
|
138
|
+
"unexpected zero-width match (:#{meta[:token_category]}) at '#{meta[:at]}'"
|
|
139
|
+
end
|
|
140
|
+
end
|
|
83
141
|
end
|
|
84
142
|
|
|
85
143
|
class ArgumentError < ::ArgumentError
|
|
144
|
+
include Error
|
|
145
|
+
|
|
86
146
|
attr_reader :reason, :meta
|
|
87
|
-
attr_accessor :recipient_variable
|
|
88
147
|
|
|
89
148
|
def initialize(reason, **meta)
|
|
90
149
|
@reason = reason
|
|
91
150
|
@meta = meta
|
|
151
|
+
super(default_message)
|
|
92
152
|
end
|
|
93
153
|
|
|
94
154
|
private_class_method :new
|
|
@@ -108,9 +168,41 @@ module Dentaku
|
|
|
108
168
|
|
|
109
169
|
new(reason, **meta)
|
|
110
170
|
end
|
|
171
|
+
|
|
172
|
+
private
|
|
173
|
+
|
|
174
|
+
def default_message
|
|
175
|
+
case reason
|
|
176
|
+
when :incompatible_type
|
|
177
|
+
if meta.key?(:function_name)
|
|
178
|
+
"#{meta[:function_name]}() requires #{meta[:expected]} arguments, but got #{meta[:actual].class}"
|
|
179
|
+
else
|
|
180
|
+
"#{meta[:actual].class} is not compatible with #{meta[:expected]}"
|
|
181
|
+
end
|
|
182
|
+
when :invalid_operator
|
|
183
|
+
"#{meta[:operation]} requires operands that respond to #{meta[:operator]}"
|
|
184
|
+
when :invalid_value
|
|
185
|
+
if meta.key?(:expected)
|
|
186
|
+
"'#{meta[:actual]}' is not a valid #{meta[:expected]}"
|
|
187
|
+
elsif meta.key?(:actual)
|
|
188
|
+
"'#{meta[:actual]}' is not a valid value"
|
|
189
|
+
else
|
|
190
|
+
"Invalid value"
|
|
191
|
+
end
|
|
192
|
+
when :too_few_arguments
|
|
193
|
+
"#{meta[:function_name]}() has too few arguments (given #{meta[:actual]}, expected #{expected_count})"
|
|
194
|
+
when :wrong_number_of_arguments
|
|
195
|
+
"#{meta[:function_name]}() has the wrong number of arguments (given #{meta[:actual]}, expected #{expected_count})"
|
|
196
|
+
end
|
|
197
|
+
end
|
|
198
|
+
|
|
199
|
+
def expected_count
|
|
200
|
+
expected = meta[:expected]
|
|
201
|
+
expected.is_a?(Range) && expected.end.nil? ? "at least #{expected.begin}" : expected
|
|
202
|
+
end
|
|
111
203
|
end
|
|
112
204
|
|
|
113
205
|
class ZeroDivisionError < ::ZeroDivisionError
|
|
114
|
-
|
|
206
|
+
include Error
|
|
115
207
|
end
|
|
116
208
|
end
|
|
@@ -54,7 +54,7 @@ module Dentaku
|
|
|
54
54
|
return number if number
|
|
55
55
|
end
|
|
56
56
|
|
|
57
|
-
raise Dentaku::ArgumentError.for(:incompatible_type,
|
|
57
|
+
raise Dentaku::ArgumentError.for(:incompatible_type, actual: value, expected: Numeric),
|
|
58
58
|
"'#{value || value.class}' is not coercible to numeric"
|
|
59
59
|
end
|
|
60
60
|
|