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
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
|
|
|
@@ -94,7 +102,9 @@ module Dentaku
|
|
|
94
102
|
end
|
|
95
103
|
|
|
96
104
|
def dependencies(expression, context = {})
|
|
97
|
-
|
|
105
|
+
# dup inside the block: `store` now restores in place, so the caller
|
|
106
|
+
# needs its own copy of the merged context rather than the live memory
|
|
107
|
+
test_context = context.nil? ? {} : store(context) { memory.dup }
|
|
98
108
|
|
|
99
109
|
case expression
|
|
100
110
|
when Dentaku::AST::Node
|
|
@@ -106,6 +116,20 @@ module Dentaku
|
|
|
106
116
|
end
|
|
107
117
|
end
|
|
108
118
|
|
|
119
|
+
# every identifier the expression could reference, regardless of
|
|
120
|
+
# branching: purely syntactic, ignores stored memory, and never
|
|
121
|
+
# evaluates guards or functions
|
|
122
|
+
def identifiers(expression)
|
|
123
|
+
case expression
|
|
124
|
+
when Dentaku::AST::Node
|
|
125
|
+
expression.dependencies(AST::Node::STATIC_CONTEXT).uniq
|
|
126
|
+
when Array
|
|
127
|
+
expression.flat_map { |e| identifiers(e) }.uniq
|
|
128
|
+
else
|
|
129
|
+
ast(expression).dependencies(AST::Node::STATIC_CONTEXT).uniq
|
|
130
|
+
end
|
|
131
|
+
end
|
|
132
|
+
|
|
109
133
|
def ast(expression)
|
|
110
134
|
return expression if expression.is_a?(AST::Node)
|
|
111
135
|
return expression.map { |e| ast(e) } if expression.is_a? Array
|
|
@@ -147,32 +171,51 @@ module Dentaku
|
|
|
147
171
|
end
|
|
148
172
|
|
|
149
173
|
def store(key_or_hash, value = nil)
|
|
150
|
-
|
|
174
|
+
pairs = pairs_to_store(key_or_hash, value)
|
|
151
175
|
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
memory[standardize_case(key.to_s)] = val
|
|
156
|
-
end
|
|
157
|
-
else
|
|
158
|
-
memory[standardize_case(key_or_hash.to_s)] = value
|
|
176
|
+
unless block_given?
|
|
177
|
+
pairs.each { |key, val| memory[key] = val }
|
|
178
|
+
return self
|
|
159
179
|
end
|
|
160
180
|
|
|
161
|
-
|
|
181
|
+
# `evaluate!` routes every call through here, so snapshotting the whole
|
|
182
|
+
# memory hash makes each evaluation scale with the number of stored
|
|
183
|
+
# variables (#336). Undoing just the keys this call touched is O(pairs)
|
|
184
|
+
# instead of O(memory) -- but it is only equivalent while nothing else
|
|
185
|
+
# writes into memory during the block. The identifier cache does exactly
|
|
186
|
+
# that, so when it is enabled we still need the wholesale snapshot to
|
|
187
|
+
# keep cached values scoped to a single evaluation.
|
|
188
|
+
if Dentaku.cache_identifier?
|
|
189
|
+
restore = Hash[memory]
|
|
190
|
+
pairs.each { |key, val| memory[key] = val }
|
|
191
|
+
|
|
162
192
|
begin
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
return result
|
|
166
|
-
rescue => e
|
|
193
|
+
yield
|
|
194
|
+
ensure
|
|
167
195
|
@memory = restore
|
|
168
|
-
raise e
|
|
169
196
|
end
|
|
170
|
-
|
|
197
|
+
else
|
|
198
|
+
undo = pairs.map { |key, _| [key, memory.key?(key), memory[key]] }
|
|
199
|
+
pairs.each { |key, val| memory[key] = val }
|
|
171
200
|
|
|
172
|
-
|
|
201
|
+
begin
|
|
202
|
+
yield
|
|
203
|
+
ensure
|
|
204
|
+
undo.each { |key, present, val| present ? memory[key] = val : memory.delete(key) }
|
|
205
|
+
end
|
|
206
|
+
end
|
|
173
207
|
end
|
|
174
208
|
alias_method :bind, :store
|
|
175
209
|
|
|
210
|
+
private def pairs_to_store(key_or_hash, value)
|
|
211
|
+
if value.nil?
|
|
212
|
+
key_or_hash = FlatHash.from_hash_with_intermediates(key_or_hash) if nested_data_support
|
|
213
|
+
key_or_hash.map { |key, val| [standardize_case(key.to_s), val] }
|
|
214
|
+
else
|
|
215
|
+
[[standardize_case(key_or_hash.to_s), value]]
|
|
216
|
+
end
|
|
217
|
+
end
|
|
218
|
+
|
|
176
219
|
def store_formula(key, formula)
|
|
177
220
|
store(key, ast(formula))
|
|
178
221
|
end
|
|
@@ -186,7 +229,13 @@ module Dentaku
|
|
|
186
229
|
end
|
|
187
230
|
|
|
188
231
|
def cache_ast?
|
|
189
|
-
|
|
232
|
+
return false if @disable_ast_cache
|
|
233
|
+
|
|
234
|
+
@cache_ast.nil? ? Dentaku.cache_ast? : @cache_ast
|
|
235
|
+
end
|
|
236
|
+
|
|
237
|
+
def cache_dependency_order?
|
|
238
|
+
@cache_dependency_order.nil? ? Dentaku.cache_dependency_order? : @cache_dependency_order
|
|
190
239
|
end
|
|
191
240
|
end
|
|
192
241
|
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
|
|
data/lib/dentaku/parser.rb
CHANGED
|
@@ -41,41 +41,42 @@ module Dentaku
|
|
|
41
41
|
end
|
|
42
42
|
|
|
43
43
|
def consume(count = 2)
|
|
44
|
-
|
|
45
|
-
fail! :invalid_statement if
|
|
44
|
+
operation = operations.pop
|
|
45
|
+
fail! :invalid_statement if operation.nil?
|
|
46
46
|
|
|
47
47
|
output_size = output.length
|
|
48
|
-
args_size =
|
|
49
|
-
min_size =
|
|
50
|
-
max_size =
|
|
48
|
+
args_size = operation.arity || count
|
|
49
|
+
min_size = operation.arity || operation.min_param_count || count
|
|
50
|
+
max_size = operation.arity || operation.max_param_count || count
|
|
51
51
|
|
|
52
52
|
if output_size < min_size || args_size < min_size
|
|
53
|
-
|
|
54
|
-
fail! :too_few_operands,
|
|
53
|
+
expected = min_size == max_size ? min_size : min_size..max_size
|
|
54
|
+
fail! :too_few_operands, operation: operation, expected: expected, actual: output_size
|
|
55
55
|
end
|
|
56
56
|
|
|
57
57
|
if (output_size > max_size && operations.empty?) || args_size > max_size
|
|
58
|
-
|
|
59
|
-
fail! :too_many_operands,
|
|
58
|
+
expected = min_size == max_size ? min_size : min_size..max_size
|
|
59
|
+
fail! :too_many_operands, operation: operation, expected: expected, actual: output_size
|
|
60
60
|
end
|
|
61
61
|
|
|
62
62
|
args = []
|
|
63
|
-
if
|
|
63
|
+
if operation == AST::Array && output.empty?
|
|
64
64
|
# special case: empty array literal '{}'
|
|
65
|
-
output.push(
|
|
65
|
+
output.push(operation.new)
|
|
66
66
|
else
|
|
67
67
|
fail! :invalid_statement if output_size < args_size
|
|
68
68
|
args = Array.new(args_size) { output.pop }.reverse
|
|
69
|
-
output.push
|
|
69
|
+
output.push operation.new(*args)
|
|
70
70
|
end
|
|
71
71
|
|
|
72
|
-
if
|
|
73
|
-
|
|
72
|
+
if operation.respond_to?(:callback) && !operation.callback.nil?
|
|
73
|
+
operation.callback.call(args)
|
|
74
74
|
end
|
|
75
75
|
rescue ::ArgumentError => e
|
|
76
|
-
raise Dentaku::ArgumentError
|
|
76
|
+
raise if e.is_a?(Dentaku::ArgumentError)
|
|
77
|
+
raise Dentaku::ArgumentError.for(:invalid_value), e.message
|
|
77
78
|
rescue NodeError => e
|
|
78
|
-
|
|
79
|
+
raise ParseError.for(:node_invalid, operation: operation, operand: e.operand, expected: e.expected, actual: e.actual), e.message
|
|
79
80
|
end
|
|
80
81
|
|
|
81
82
|
def parse
|
|
@@ -190,8 +191,11 @@ module Dentaku
|
|
|
190
191
|
operations.push(AST::CaseConditional)
|
|
191
192
|
consume(2)
|
|
192
193
|
arities[-1] += 1
|
|
193
|
-
elsif operations.
|
|
194
|
-
|
|
194
|
+
elsif operations[token_index].nil? ||
|
|
195
|
+
(operations[token_index] != AST::CaseWhen && operations[token_index] < AST::Operation)
|
|
196
|
+
# First WHEN: fold any operators pending from an unparenthesized
|
|
197
|
+
# switch expression, then finalize the switch variable.
|
|
198
|
+
consume_until(AST::Case)
|
|
195
199
|
operations.push(AST::CaseSwitchVariable)
|
|
196
200
|
consume
|
|
197
201
|
end
|
|
@@ -268,7 +272,7 @@ module Dentaku
|
|
|
268
272
|
when :close
|
|
269
273
|
consume while operations.any? && operations.last != AST::Grouping
|
|
270
274
|
lparen = operations.pop
|
|
271
|
-
fail! :unbalanced_parenthesis, token unless lparen == AST::Grouping
|
|
275
|
+
fail! :unbalanced_parenthesis, token: token unless lparen == AST::Grouping
|
|
272
276
|
if operations.last && operations.last < AST::Function
|
|
273
277
|
consume(arities.pop.succ)
|
|
274
278
|
end
|
|
@@ -284,35 +288,7 @@ module Dentaku
|
|
|
284
288
|
end
|
|
285
289
|
|
|
286
290
|
def fail!(reason, **meta)
|
|
287
|
-
|
|
288
|
-
case reason
|
|
289
|
-
when :node_invalid
|
|
290
|
-
"#{meta.fetch(:operator)} requires #{meta.fetch(:expect).join(', ')} operands, but got #{meta.fetch(:actual)}"
|
|
291
|
-
when :too_few_operands
|
|
292
|
-
"#{meta.fetch(:operator)} has too few operands (given #{meta.fetch(:actual)}, expected #{meta.fetch(:expect)})"
|
|
293
|
-
when :too_many_operands
|
|
294
|
-
"#{meta.fetch(:operator)} has too many operands (given #{meta.fetch(:actual)}, expected #{meta.fetch(:expect)})"
|
|
295
|
-
when :undefined_function
|
|
296
|
-
"Undefined function #{meta.fetch(:function_name)}"
|
|
297
|
-
when :unprocessed_token
|
|
298
|
-
"Unprocessed token #{meta.fetch(:token_name)}"
|
|
299
|
-
when :unknown_case_token
|
|
300
|
-
"Unknown case token #{meta.fetch(:token_name)}"
|
|
301
|
-
when :unbalanced_bracket
|
|
302
|
-
"Unbalanced bracket"
|
|
303
|
-
when :unbalanced_parenthesis
|
|
304
|
-
"Unbalanced parenthesis"
|
|
305
|
-
when :unknown_grouping_token
|
|
306
|
-
"Unknown grouping token #{meta.fetch(:token_name)}"
|
|
307
|
-
when :not_implemented_token_category
|
|
308
|
-
"Not implemented for tokens of category #{meta.fetch(:token_category)}"
|
|
309
|
-
when :invalid_statement
|
|
310
|
-
"Invalid statement"
|
|
311
|
-
else
|
|
312
|
-
raise ::ArgumentError, "Unhandled #{reason}"
|
|
313
|
-
end
|
|
314
|
-
|
|
315
|
-
raise ParseError.for(reason, **meta), message
|
|
291
|
+
raise ParseError.for(reason, **meta)
|
|
316
292
|
end
|
|
317
293
|
end
|
|
318
294
|
end
|
|
@@ -93,46 +93,40 @@ module Dentaku
|
|
|
93
93
|
@values.empty? || @values.key?(value)
|
|
94
94
|
end
|
|
95
95
|
|
|
96
|
-
def self.datetime
|
|
97
|
-
def self.numeric
|
|
98
|
-
def self.string
|
|
99
|
-
def self.logical
|
|
96
|
+
def self.datetime = new(:datetime)
|
|
97
|
+
def self.numeric = new(:numeric)
|
|
98
|
+
def self.string = new(:string)
|
|
99
|
+
def self.logical = new(:logical)
|
|
100
100
|
def self.value
|
|
101
101
|
new(:datetime) | new(:numeric) | new(:string) | new(:logical)
|
|
102
102
|
end
|
|
103
103
|
|
|
104
|
-
def self.addsub
|
|
105
|
-
def self.subtract
|
|
106
|
-
def self.anchored_minus
|
|
107
|
-
def self.muldiv
|
|
108
|
-
def self.pow
|
|
109
|
-
def self.mod
|
|
110
|
-
def self.combinator
|
|
111
|
-
|
|
112
|
-
def self.comparator
|
|
113
|
-
def self.comp_gt
|
|
114
|
-
def self.comp_lt
|
|
115
|
-
|
|
116
|
-
def self.open
|
|
117
|
-
def self.close
|
|
118
|
-
def self.comma
|
|
119
|
-
def self.non_group
|
|
120
|
-
def self.non_group_star
|
|
121
|
-
def self.non_close_plus
|
|
122
|
-
def self.arguments
|
|
123
|
-
|
|
124
|
-
def self.
|
|
125
|
-
|
|
126
|
-
def self.
|
|
127
|
-
def self.
|
|
128
|
-
def self.
|
|
129
|
-
|
|
130
|
-
def self.
|
|
131
|
-
new(:function, name)
|
|
132
|
-
end
|
|
133
|
-
|
|
134
|
-
def self.respond_to_missing?(name, include_priv)
|
|
135
|
-
true
|
|
136
|
-
end
|
|
104
|
+
def self.addsub = new(:operator, [:add, :subtract])
|
|
105
|
+
def self.subtract = new(:operator, :subtract)
|
|
106
|
+
def self.anchored_minus = new(:operator, :subtract).caret
|
|
107
|
+
def self.muldiv = new(:operator, [:multiply, :divide])
|
|
108
|
+
def self.pow = new(:operator, :pow)
|
|
109
|
+
def self.mod = new(:operator, :mod)
|
|
110
|
+
def self.combinator = new(:combinator)
|
|
111
|
+
|
|
112
|
+
def self.comparator = new(:comparator)
|
|
113
|
+
def self.comp_gt = new(:comparator, [:gt, :ge])
|
|
114
|
+
def self.comp_lt = new(:comparator, [:lt, :le])
|
|
115
|
+
|
|
116
|
+
def self.open = new(:grouping, :open)
|
|
117
|
+
def self.close = new(:grouping, :close)
|
|
118
|
+
def self.comma = new(:grouping, :comma)
|
|
119
|
+
def self.non_group = new(:grouping).invert
|
|
120
|
+
def self.non_group_star = new(:grouping).invert.star
|
|
121
|
+
def self.non_close_plus = new(:grouping, :close).invert.plus
|
|
122
|
+
def self.arguments = (value | comma).plus
|
|
123
|
+
|
|
124
|
+
def self.function(name) = new(:function, name)
|
|
125
|
+
|
|
126
|
+
def self.if = function(:if)
|
|
127
|
+
def self.round = function(:round)
|
|
128
|
+
def self.roundup = function(:roundup)
|
|
129
|
+
def self.rounddown = function(:rounddown)
|
|
130
|
+
def self.not = function(:not)
|
|
137
131
|
end
|
|
138
132
|
end
|
|
@@ -6,7 +6,7 @@ module Dentaku
|
|
|
6
6
|
|
|
7
7
|
def self.function_token_matchers(function_name, *symbols)
|
|
8
8
|
token_matchers(:open, *symbols, :close).unshift(
|
|
9
|
-
TokenMatcher.
|
|
9
|
+
TokenMatcher.function(function_name)
|
|
10
10
|
)
|
|
11
11
|
end
|
|
12
12
|
|
|
@@ -16,9 +16,9 @@ module Dentaku
|
|
|
16
16
|
:comparator, :comp_gt, :comp_lt, :open, :close, :comma,
|
|
17
17
|
:non_close_plus, :non_group, :non_group_star, :arguments,
|
|
18
18
|
:logical, :combinator, :if, :round, :roundup, :rounddown, :not,
|
|
19
|
-
:anchored_minus
|
|
19
|
+
:anchored_minus
|
|
20
20
|
].each_with_object({}) do |name, matchers|
|
|
21
|
-
matchers[name] = TokenMatcher.
|
|
21
|
+
matchers[name] = TokenMatcher.public_send(name)
|
|
22
22
|
end
|
|
23
23
|
|
|
24
24
|
@matchers.fetch(symbol) do
|
data/lib/dentaku/tokenizer.rb
CHANGED
|
@@ -94,21 +94,7 @@ module Dentaku
|
|
|
94
94
|
end
|
|
95
95
|
|
|
96
96
|
def fail!(reason, **meta)
|
|
97
|
-
|
|
98
|
-
case reason
|
|
99
|
-
when :parse_error
|
|
100
|
-
"parse error at: '#{meta.fetch(:at)}'"
|
|
101
|
-
when :too_many_opening_parentheses
|
|
102
|
-
"too many opening parentheses"
|
|
103
|
-
when :too_many_closing_parentheses
|
|
104
|
-
"too many closing parentheses"
|
|
105
|
-
when :unexpected_zero_width_match
|
|
106
|
-
"unexpected zero-width match (:#{meta.fetch(:category)}) at '#{meta.fetch(:at)}'"
|
|
107
|
-
else
|
|
108
|
-
raise ::ArgumentError, "Unhandled #{reason}"
|
|
109
|
-
end
|
|
110
|
-
|
|
111
|
-
raise TokenizerError.for(reason, **meta), message
|
|
97
|
+
raise TokenizerError.for(reason, **meta)
|
|
112
98
|
end
|
|
113
99
|
end
|
|
114
100
|
end
|
data/lib/dentaku/version.rb
CHANGED