hayadentaku 3.5.7
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 +7 -0
- data/.github/workflows/rspec.yml +26 -0
- data/.github/workflows/rubocop.yml +14 -0
- data/.gitignore +14 -0
- data/.pryrc +2 -0
- data/.rubocop.yml +114 -0
- data/.travis.yml +10 -0
- data/CHANGELOG.md +328 -0
- data/Gemfile +4 -0
- data/LICENSE +21 -0
- data/README.md +352 -0
- data/Rakefile +31 -0
- data/hayadentaku.gemspec +35 -0
- data/lib/dentaku/ast/access.rb +44 -0
- data/lib/dentaku/ast/arithmetic.rb +292 -0
- data/lib/dentaku/ast/array.rb +38 -0
- data/lib/dentaku/ast/bitwise.rb +42 -0
- data/lib/dentaku/ast/case/case_conditional.rb +38 -0
- data/lib/dentaku/ast/case/case_else.rb +35 -0
- data/lib/dentaku/ast/case/case_switch_variable.rb +35 -0
- data/lib/dentaku/ast/case/case_then.rb +35 -0
- data/lib/dentaku/ast/case/case_when.rb +39 -0
- data/lib/dentaku/ast/case.rb +93 -0
- data/lib/dentaku/ast/combinators.rb +50 -0
- data/lib/dentaku/ast/comparators.rb +88 -0
- data/lib/dentaku/ast/datetime.rb +8 -0
- data/lib/dentaku/ast/function.rb +56 -0
- data/lib/dentaku/ast/function_registry.rb +107 -0
- data/lib/dentaku/ast/functions/abs.rb +5 -0
- data/lib/dentaku/ast/functions/all.rb +19 -0
- data/lib/dentaku/ast/functions/and.rb +25 -0
- data/lib/dentaku/ast/functions/any.rb +19 -0
- data/lib/dentaku/ast/functions/avg.rb +13 -0
- data/lib/dentaku/ast/functions/count.rb +26 -0
- data/lib/dentaku/ast/functions/duration.rb +51 -0
- data/lib/dentaku/ast/functions/enum.rb +54 -0
- data/lib/dentaku/ast/functions/filter.rb +21 -0
- data/lib/dentaku/ast/functions/if.rb +47 -0
- data/lib/dentaku/ast/functions/intercept.rb +33 -0
- data/lib/dentaku/ast/functions/map.rb +19 -0
- data/lib/dentaku/ast/functions/max.rb +5 -0
- data/lib/dentaku/ast/functions/min.rb +5 -0
- data/lib/dentaku/ast/functions/mul.rb +12 -0
- data/lib/dentaku/ast/functions/not.rb +5 -0
- data/lib/dentaku/ast/functions/or.rb +25 -0
- data/lib/dentaku/ast/functions/pluck.rb +34 -0
- data/lib/dentaku/ast/functions/reduce.rb +60 -0
- data/lib/dentaku/ast/functions/round.rb +5 -0
- data/lib/dentaku/ast/functions/rounddown.rb +8 -0
- data/lib/dentaku/ast/functions/roundup.rb +8 -0
- data/lib/dentaku/ast/functions/ruby_math.rb +57 -0
- data/lib/dentaku/ast/functions/string_functions.rb +212 -0
- data/lib/dentaku/ast/functions/sum.rb +12 -0
- data/lib/dentaku/ast/functions/switch.rb +8 -0
- data/lib/dentaku/ast/functions/xor.rb +44 -0
- data/lib/dentaku/ast/grouping.rb +23 -0
- data/lib/dentaku/ast/identifier.rb +52 -0
- data/lib/dentaku/ast/literal.rb +30 -0
- data/lib/dentaku/ast/logical.rb +8 -0
- data/lib/dentaku/ast/negation.rb +54 -0
- data/lib/dentaku/ast/nil.rb +13 -0
- data/lib/dentaku/ast/node.rb +29 -0
- data/lib/dentaku/ast/numeric.rb +8 -0
- data/lib/dentaku/ast/operation.rb +44 -0
- data/lib/dentaku/ast/string.rb +15 -0
- data/lib/dentaku/ast.rb +42 -0
- data/lib/dentaku/bulk_expression_solver.rb +158 -0
- data/lib/dentaku/calculator.rb +192 -0
- data/lib/dentaku/date_arithmetic.rb +60 -0
- data/lib/dentaku/dependency_resolver.rb +29 -0
- data/lib/dentaku/exceptions.rb +116 -0
- data/lib/dentaku/flat_hash.rb +161 -0
- data/lib/dentaku/parser.rb +318 -0
- data/lib/dentaku/print_visitor.rb +112 -0
- data/lib/dentaku/string_casing.rb +7 -0
- data/lib/dentaku/token.rb +48 -0
- data/lib/dentaku/token_matcher.rb +138 -0
- data/lib/dentaku/token_matchers.rb +29 -0
- data/lib/dentaku/token_scanner.rb +240 -0
- data/lib/dentaku/tokenizer.rb +127 -0
- data/lib/dentaku/version.rb +3 -0
- data/lib/dentaku/visitor/infix.rb +86 -0
- data/lib/dentaku.rb +69 -0
- data/spec/ast/abs_spec.rb +26 -0
- data/spec/ast/addition_spec.rb +67 -0
- data/spec/ast/all_spec.rb +38 -0
- data/spec/ast/and_function_spec.rb +35 -0
- data/spec/ast/and_spec.rb +32 -0
- data/spec/ast/any_spec.rb +36 -0
- data/spec/ast/arithmetic_spec.rb +147 -0
- data/spec/ast/avg_spec.rb +42 -0
- data/spec/ast/case_spec.rb +84 -0
- data/spec/ast/comparator_spec.rb +87 -0
- data/spec/ast/count_spec.rb +40 -0
- data/spec/ast/division_spec.rb +64 -0
- data/spec/ast/filter_spec.rb +25 -0
- data/spec/ast/function_spec.rb +69 -0
- data/spec/ast/intercept_spec.rb +30 -0
- data/spec/ast/map_spec.rb +40 -0
- data/spec/ast/max_spec.rb +33 -0
- data/spec/ast/min_spec.rb +33 -0
- data/spec/ast/mul_spec.rb +43 -0
- data/spec/ast/negation_spec.rb +48 -0
- data/spec/ast/node_spec.rb +43 -0
- data/spec/ast/numeric_spec.rb +16 -0
- data/spec/ast/or_spec.rb +35 -0
- data/spec/ast/pluck_spec.rb +49 -0
- data/spec/ast/reduce_spec.rb +22 -0
- data/spec/ast/round_spec.rb +35 -0
- data/spec/ast/rounddown_spec.rb +35 -0
- data/spec/ast/roundup_spec.rb +35 -0
- data/spec/ast/string_functions_spec.rb +217 -0
- data/spec/ast/sum_spec.rb +43 -0
- data/spec/ast/switch_spec.rb +30 -0
- data/spec/ast/xor_spec.rb +35 -0
- data/spec/benchmark.rb +70 -0
- data/spec/bulk_expression_solver_spec.rb +241 -0
- data/spec/calculator_spec.rb +1003 -0
- data/spec/dentaku_spec.rb +52 -0
- data/spec/dependency_resolver_spec.rb +18 -0
- data/spec/exceptions_spec.rb +9 -0
- data/spec/external_function_spec.rb +177 -0
- data/spec/parser_spec.rb +183 -0
- data/spec/print_visitor_spec.rb +77 -0
- data/spec/spec_helper.rb +69 -0
- data/spec/token_matcher_spec.rb +134 -0
- data/spec/token_scanner_spec.rb +49 -0
- data/spec/token_spec.rb +16 -0
- data/spec/tokenizer_spec.rb +375 -0
- data/spec/visitor/infix_spec.rb +52 -0
- data/spec/visitor_spec.rb +139 -0
- metadata +353 -0
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
module Dentaku
|
|
2
|
+
class FlatHash
|
|
3
|
+
# Flattens a nested hash so that each leaf becomes a top-level entry whose
|
|
4
|
+
# key is the dot-joined path of segments leading to it. Non-Hash values
|
|
5
|
+
# (including Arrays) are treated as leaves. Empty nested Hashes are dropped,
|
|
6
|
+
# matching the historical behavior of this method.
|
|
7
|
+
#
|
|
8
|
+
# The flattened key preserves the type (Symbol vs String) of the
|
|
9
|
+
# outermost key in the path: if the top-level key is a Symbol the joined
|
|
10
|
+
# key is a Symbol, otherwise it stays a String.
|
|
11
|
+
def self.from_hash(h)
|
|
12
|
+
return { "" => h } unless h.is_a?(Hash)
|
|
13
|
+
|
|
14
|
+
acc = {}
|
|
15
|
+
h.each do |k, v|
|
|
16
|
+
if v.is_a?(Hash)
|
|
17
|
+
unless v.empty?
|
|
18
|
+
if k.is_a?(Symbol)
|
|
19
|
+
flatten_leaves_sym(v, k.to_s, acc)
|
|
20
|
+
else
|
|
21
|
+
flatten_leaves_str(v, k.to_s, acc)
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
else
|
|
25
|
+
acc[k] = v
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
acc
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
# Like {.from_hash}, but additionally retains every intermediate nested Hash
|
|
32
|
+
# under its dot-joined path key.
|
|
33
|
+
def self.from_hash_with_intermediates(h)
|
|
34
|
+
return { "" => h } unless h.is_a?(Hash)
|
|
35
|
+
|
|
36
|
+
acc = {}
|
|
37
|
+
h.each do |k, v|
|
|
38
|
+
acc[k] = v
|
|
39
|
+
if v.is_a?(Hash) && !v.empty?
|
|
40
|
+
if k.is_a?(Symbol)
|
|
41
|
+
flatten_with_intermediates_sym(v, k.to_s, acc)
|
|
42
|
+
else
|
|
43
|
+
flatten_with_intermediates_str(v, k.to_s, acc)
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
acc
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def self.flatten_keys(hash)
|
|
51
|
+
result = {}
|
|
52
|
+
hash.each { |k, v| result[flatten_key(k)] = v }
|
|
53
|
+
result
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def self.flatten_key(segments)
|
|
57
|
+
return segments.first if segments.length == 1
|
|
58
|
+
key = segments.join('.')
|
|
59
|
+
segments.first.is_a?(Symbol) ? key.to_sym : key
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
# Inverse of {.from_hash}: re-nests a hash whose keys may contain dots.
|
|
63
|
+
def self.expand(hash)
|
|
64
|
+
result = {}
|
|
65
|
+
hash.each do |k, v|
|
|
66
|
+
key_str = k.to_s
|
|
67
|
+
if key_str.include?('.')
|
|
68
|
+
parts = key_str.split('.', -1)
|
|
69
|
+
last = parts.length - 1
|
|
70
|
+
current = result
|
|
71
|
+
if k.is_a?(Symbol)
|
|
72
|
+
i = 0
|
|
73
|
+
while i < last
|
|
74
|
+
current = (current[parts[i].to_sym] ||= {})
|
|
75
|
+
i += 1
|
|
76
|
+
end
|
|
77
|
+
current[parts[last].to_sym] = v
|
|
78
|
+
else
|
|
79
|
+
i = 0
|
|
80
|
+
while i < last
|
|
81
|
+
current = (current[parts[i]] ||= {})
|
|
82
|
+
i += 1
|
|
83
|
+
end
|
|
84
|
+
current[parts[last]] = v
|
|
85
|
+
end
|
|
86
|
+
else
|
|
87
|
+
result[k] = v
|
|
88
|
+
end
|
|
89
|
+
end
|
|
90
|
+
result
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
# Writes the flattened "intermediates" form of `{identifier => value}`
|
|
94
|
+
# directly into `target`, without allocating an intermediate Hash. Used by
|
|
95
|
+
# the iterator AST nodes (map/filter/reduce/all/any/enum) where the result
|
|
96
|
+
# of `from_hash_with_intermediates` is otherwise immediately discarded after
|
|
97
|
+
# being merged into the per-iteration context.
|
|
98
|
+
#
|
|
99
|
+
# Returns nothing meaningful; mutates `target`.
|
|
100
|
+
def self.write_pair_with_intermediates!(identifier, value, target)
|
|
101
|
+
target[identifier] = value
|
|
102
|
+
return unless value.is_a?(Hash) && !value.empty?
|
|
103
|
+
|
|
104
|
+
if identifier.is_a?(Symbol)
|
|
105
|
+
flatten_with_intermediates_sym(value, identifier.to_s, target)
|
|
106
|
+
else
|
|
107
|
+
flatten_with_intermediates_str(value, identifier.to_s, target)
|
|
108
|
+
end
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
# --- internals ---------------------------------------------------------
|
|
112
|
+
|
|
113
|
+
# The four helpers below are duplicated by output-key type (Symbol vs
|
|
114
|
+
# String) on purpose: hoisting the Symbol/String branch out of the inner
|
|
115
|
+
# loop is measurably faster than checking it on every leaf.
|
|
116
|
+
|
|
117
|
+
def self.flatten_leaves_sym(h, prefix, acc)
|
|
118
|
+
h.each do |k, v|
|
|
119
|
+
if v.is_a?(Hash)
|
|
120
|
+
flatten_leaves_sym(v, "#{prefix}.#{k}", acc) unless v.empty?
|
|
121
|
+
else
|
|
122
|
+
acc[:"#{prefix}.#{k}"] = v
|
|
123
|
+
end
|
|
124
|
+
end
|
|
125
|
+
end
|
|
126
|
+
private_class_method :flatten_leaves_sym
|
|
127
|
+
|
|
128
|
+
def self.flatten_leaves_str(h, prefix, acc)
|
|
129
|
+
h.each do |k, v|
|
|
130
|
+
if v.is_a?(Hash)
|
|
131
|
+
flatten_leaves_str(v, "#{prefix}.#{k}", acc) unless v.empty?
|
|
132
|
+
else
|
|
133
|
+
acc["#{prefix}.#{k}"] = v
|
|
134
|
+
end
|
|
135
|
+
end
|
|
136
|
+
end
|
|
137
|
+
private_class_method :flatten_leaves_str
|
|
138
|
+
|
|
139
|
+
def self.flatten_with_intermediates_sym(h, prefix, acc)
|
|
140
|
+
h.each do |k, v|
|
|
141
|
+
new_prefix = "#{prefix}.#{k}"
|
|
142
|
+
acc[new_prefix.to_sym] = v
|
|
143
|
+
if v.is_a?(Hash) && !v.empty?
|
|
144
|
+
flatten_with_intermediates_sym(v, new_prefix, acc)
|
|
145
|
+
end
|
|
146
|
+
end
|
|
147
|
+
end
|
|
148
|
+
private_class_method :flatten_with_intermediates_sym
|
|
149
|
+
|
|
150
|
+
def self.flatten_with_intermediates_str(h, prefix, acc)
|
|
151
|
+
h.each do |k, v|
|
|
152
|
+
new_prefix = "#{prefix}.#{k}"
|
|
153
|
+
acc[new_prefix] = v
|
|
154
|
+
if v.is_a?(Hash) && !v.empty?
|
|
155
|
+
flatten_with_intermediates_str(v, new_prefix, acc)
|
|
156
|
+
end
|
|
157
|
+
end
|
|
158
|
+
end
|
|
159
|
+
private_class_method :flatten_with_intermediates_str
|
|
160
|
+
end
|
|
161
|
+
end
|
|
@@ -0,0 +1,318 @@
|
|
|
1
|
+
require_relative './ast'
|
|
2
|
+
|
|
3
|
+
module Dentaku
|
|
4
|
+
class Parser
|
|
5
|
+
AST_OPERATIONS = {
|
|
6
|
+
add: AST::Addition,
|
|
7
|
+
subtract: AST::Subtraction,
|
|
8
|
+
multiply: AST::Multiplication,
|
|
9
|
+
divide: AST::Division,
|
|
10
|
+
pow: AST::Exponentiation,
|
|
11
|
+
negate: AST::Negation,
|
|
12
|
+
mod: AST::Modulo,
|
|
13
|
+
|
|
14
|
+
bitor: AST::BitwiseOr,
|
|
15
|
+
bitand: AST::BitwiseAnd,
|
|
16
|
+
bitshiftleft: AST::BitwiseShiftLeft,
|
|
17
|
+
bitshiftright: AST::BitwiseShiftRight,
|
|
18
|
+
|
|
19
|
+
lt: AST::LessThan,
|
|
20
|
+
gt: AST::GreaterThan,
|
|
21
|
+
le: AST::LessThanOrEqual,
|
|
22
|
+
ge: AST::GreaterThanOrEqual,
|
|
23
|
+
ne: AST::NotEqual,
|
|
24
|
+
eq: AST::Equal,
|
|
25
|
+
|
|
26
|
+
and: AST::And,
|
|
27
|
+
or: AST::Or,
|
|
28
|
+
xor: AST::Xor,
|
|
29
|
+
}.freeze
|
|
30
|
+
|
|
31
|
+
attr_reader :input, :output, :operations, :arities, :case_sensitive
|
|
32
|
+
|
|
33
|
+
def initialize(tokens, options = {})
|
|
34
|
+
@input = tokens.dup
|
|
35
|
+
@output = []
|
|
36
|
+
@operations = options.fetch(:operations, [])
|
|
37
|
+
@arities = options.fetch(:arities, [])
|
|
38
|
+
@function_registry = options.fetch(:function_registry, nil)
|
|
39
|
+
@case_sensitive = options.fetch(:case_sensitive, false)
|
|
40
|
+
@skip_indices = []
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def consume(count = 2)
|
|
44
|
+
operator = operations.pop
|
|
45
|
+
fail! :invalid_statement if operator.nil?
|
|
46
|
+
|
|
47
|
+
output_size = output.length
|
|
48
|
+
args_size = operator.arity || count
|
|
49
|
+
min_size = operator.arity || operator.min_param_count || count
|
|
50
|
+
max_size = operator.arity || operator.max_param_count || count
|
|
51
|
+
|
|
52
|
+
if output_size < min_size || args_size < min_size
|
|
53
|
+
expect = min_size == max_size ? min_size : min_size..max_size
|
|
54
|
+
fail! :too_few_operands, operator: operator, expect: expect, actual: output_size
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
if (output_size > max_size && operations.empty?) || args_size > max_size
|
|
58
|
+
expect = min_size == max_size ? min_size : min_size..max_size
|
|
59
|
+
fail! :too_many_operands, operator: operator, expect: expect, actual: output_size
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
args = []
|
|
63
|
+
if operator == AST::Array && output.empty?
|
|
64
|
+
# special case: empty array literal '{}'
|
|
65
|
+
output.push(operator.new)
|
|
66
|
+
else
|
|
67
|
+
fail! :invalid_statement if output_size < args_size
|
|
68
|
+
args = Array.new(args_size) { output.pop }.reverse
|
|
69
|
+
output.push operator.new(*args)
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
if operator.respond_to?(:callback) && !operator.callback.nil?
|
|
73
|
+
operator.callback.call(args)
|
|
74
|
+
end
|
|
75
|
+
rescue ::ArgumentError => e
|
|
76
|
+
raise Dentaku::ArgumentError, e.message
|
|
77
|
+
rescue NodeError => e
|
|
78
|
+
fail! :node_invalid, operator: operator, child: e.child, expect: e.expect, actual: e.actual
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
def parse
|
|
82
|
+
return AST::Nil.new if input.empty?
|
|
83
|
+
|
|
84
|
+
i = 0
|
|
85
|
+
while i < input.length
|
|
86
|
+
if @skip_indices.include?(i)
|
|
87
|
+
i += 1
|
|
88
|
+
next
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
token = input[i]
|
|
92
|
+
lookahead = input[i + 1]
|
|
93
|
+
process_token(token, lookahead, i)
|
|
94
|
+
i += 1
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
consume while operations.any?
|
|
98
|
+
|
|
99
|
+
fail! :invalid_statement unless output.count == 1
|
|
100
|
+
|
|
101
|
+
output.first
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
def operation(token)
|
|
105
|
+
AST_OPERATIONS.fetch(token.value)
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
def function(token)
|
|
109
|
+
function_registry.get(token.value)
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
def function_registry
|
|
113
|
+
@function_registry ||= Dentaku::AST::FunctionRegistry.new
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
private
|
|
117
|
+
|
|
118
|
+
def process_token(token, lookahead, index)
|
|
119
|
+
case token.category
|
|
120
|
+
when :datetime then output << AST::DateTime.new(token)
|
|
121
|
+
when :numeric then output << AST::Numeric.new(token)
|
|
122
|
+
when :logical then output << AST::Logical.new(token)
|
|
123
|
+
when :string then output << AST::String.new(token)
|
|
124
|
+
when :identifier then output << AST::Identifier.new(token, case_sensitive: case_sensitive)
|
|
125
|
+
when :operator, :comparator, :combinator
|
|
126
|
+
handle_operator(token, lookahead)
|
|
127
|
+
when :null
|
|
128
|
+
output << AST::Nil.new
|
|
129
|
+
when :function
|
|
130
|
+
handle_function(token)
|
|
131
|
+
when :case
|
|
132
|
+
handle_case(token)
|
|
133
|
+
when :access
|
|
134
|
+
handle_access(token)
|
|
135
|
+
when :array
|
|
136
|
+
handle_array(token)
|
|
137
|
+
when :grouping
|
|
138
|
+
handle_grouping(token, lookahead, index)
|
|
139
|
+
else
|
|
140
|
+
fail! :not_implemented_token_category, token_category: token.category
|
|
141
|
+
end
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
def handle_operator(token, lookahead)
|
|
145
|
+
op_class = operation(token).resolve_class(lookahead)
|
|
146
|
+
if op_class.right_associative?
|
|
147
|
+
consume while operations.last && operations.last < AST::Operation && op_class.precedence < operations.last.precedence
|
|
148
|
+
else
|
|
149
|
+
consume while operations.last && operations.last < AST::Operation && op_class.precedence <= operations.last.precedence
|
|
150
|
+
end
|
|
151
|
+
operations.push op_class
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
def handle_function(token)
|
|
155
|
+
func = function(token)
|
|
156
|
+
fail! :undefined_function, function_name: token.value if func.nil?
|
|
157
|
+
arities.push 0
|
|
158
|
+
operations.push func
|
|
159
|
+
end
|
|
160
|
+
|
|
161
|
+
def handle_case(token)
|
|
162
|
+
# We always operate on the innermost (most recent) CASE on the stack.
|
|
163
|
+
case_index = operations.rindex(AST::Case) || -1
|
|
164
|
+
token_index = case_index + 1
|
|
165
|
+
|
|
166
|
+
case token.value
|
|
167
|
+
when :open
|
|
168
|
+
# Start a new CASE context.
|
|
169
|
+
operations.push AST::Case
|
|
170
|
+
arities.push(0)
|
|
171
|
+
|
|
172
|
+
when :close
|
|
173
|
+
# Finalize any trailing THEN/ELSE expression still on the stack.
|
|
174
|
+
if operations[token_index] == AST::CaseThen
|
|
175
|
+
consume_until(AST::Case)
|
|
176
|
+
operations.push(AST::CaseConditional)
|
|
177
|
+
consume(2)
|
|
178
|
+
arities[-1] += 1
|
|
179
|
+
elsif operations[token_index] == AST::CaseElse
|
|
180
|
+
consume_until(AST::Case)
|
|
181
|
+
arities[-1] += 1
|
|
182
|
+
end
|
|
183
|
+
fail! :unprocessed_token, token_name: token.value unless operations.last == AST::Case
|
|
184
|
+
consume(arities.pop.succ)
|
|
185
|
+
|
|
186
|
+
when :when
|
|
187
|
+
if operations[token_index] == AST::CaseThen
|
|
188
|
+
# Close out previous WHEN/THEN pair.
|
|
189
|
+
consume_until([AST::CaseWhen, AST::Case])
|
|
190
|
+
operations.push(AST::CaseConditional)
|
|
191
|
+
consume(2)
|
|
192
|
+
arities[-1] += 1
|
|
193
|
+
elsif operations.last == AST::Case
|
|
194
|
+
# First WHEN: finalize switch variable expression.
|
|
195
|
+
operations.push(AST::CaseSwitchVariable)
|
|
196
|
+
consume
|
|
197
|
+
end
|
|
198
|
+
operations.push(AST::CaseWhen)
|
|
199
|
+
|
|
200
|
+
when :then
|
|
201
|
+
if operations[token_index] == AST::CaseWhen
|
|
202
|
+
consume_until([AST::CaseThen, AST::Case])
|
|
203
|
+
end
|
|
204
|
+
operations.push(AST::CaseThen)
|
|
205
|
+
|
|
206
|
+
when :else
|
|
207
|
+
if operations[token_index] == AST::CaseThen
|
|
208
|
+
consume_until(AST::Case)
|
|
209
|
+
operations.push(AST::CaseConditional)
|
|
210
|
+
consume(2)
|
|
211
|
+
arities[-1] += 1
|
|
212
|
+
end
|
|
213
|
+
operations.push(AST::CaseElse)
|
|
214
|
+
|
|
215
|
+
else
|
|
216
|
+
fail! :unknown_case_token, token_name: token.value
|
|
217
|
+
end
|
|
218
|
+
end
|
|
219
|
+
|
|
220
|
+
def consume_until(target)
|
|
221
|
+
matcher =
|
|
222
|
+
case target
|
|
223
|
+
when Array then ->(op) { target.include?(op) }
|
|
224
|
+
else ->(op) { op == target }
|
|
225
|
+
end
|
|
226
|
+
|
|
227
|
+
consume while operations.any? && !matcher.call(operations.last)
|
|
228
|
+
end
|
|
229
|
+
|
|
230
|
+
def handle_access(token)
|
|
231
|
+
case token.value
|
|
232
|
+
when :lbracket
|
|
233
|
+
operations.push AST::Access
|
|
234
|
+
|
|
235
|
+
when :rbracket
|
|
236
|
+
consume while operations.any? && operations.last != AST::Access
|
|
237
|
+
fail! :unbalanced_bracket, token: token unless operations.last == AST::Access
|
|
238
|
+
consume
|
|
239
|
+
end
|
|
240
|
+
end
|
|
241
|
+
|
|
242
|
+
def handle_array(token)
|
|
243
|
+
case token.value
|
|
244
|
+
when :array_start
|
|
245
|
+
operations.push AST::Array
|
|
246
|
+
arities.push 0
|
|
247
|
+
|
|
248
|
+
when :array_end
|
|
249
|
+
consume while operations.any? && operations.last != AST::Array
|
|
250
|
+
fail! :unbalanced_bracket, token: token unless operations.last == AST::Array
|
|
251
|
+
consume(arities.pop.succ)
|
|
252
|
+
end
|
|
253
|
+
end
|
|
254
|
+
|
|
255
|
+
def handle_grouping(token, lookahead, token_index)
|
|
256
|
+
case token.value
|
|
257
|
+
when :open
|
|
258
|
+
if lookahead && lookahead.value == :close
|
|
259
|
+
# empty grouping (e.g. function with zero arguments) — we trigger consume later
|
|
260
|
+
# skip to the end
|
|
261
|
+
@skip_indices << token_index + 1
|
|
262
|
+
arities.pop
|
|
263
|
+
consume(0)
|
|
264
|
+
else
|
|
265
|
+
operations.push AST::Grouping
|
|
266
|
+
end
|
|
267
|
+
|
|
268
|
+
when :close
|
|
269
|
+
consume while operations.any? && operations.last != AST::Grouping
|
|
270
|
+
lparen = operations.pop
|
|
271
|
+
fail! :unbalanced_parenthesis, token unless lparen == AST::Grouping
|
|
272
|
+
if operations.last && operations.last < AST::Function
|
|
273
|
+
consume(arities.pop.succ)
|
|
274
|
+
end
|
|
275
|
+
|
|
276
|
+
when :comma
|
|
277
|
+
fail! :invalid_statement if arities.empty?
|
|
278
|
+
arities[-1] += 1
|
|
279
|
+
consume while operations.any? && operations.last != AST::Grouping && operations.last != AST::Array
|
|
280
|
+
|
|
281
|
+
else
|
|
282
|
+
fail! :unknown_grouping_token, token_name: token.value
|
|
283
|
+
end
|
|
284
|
+
end
|
|
285
|
+
|
|
286
|
+
def fail!(reason, **meta)
|
|
287
|
+
message =
|
|
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
|
|
316
|
+
end
|
|
317
|
+
end
|
|
318
|
+
end
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
module Dentaku
|
|
2
|
+
class PrintVisitor
|
|
3
|
+
def initialize(node)
|
|
4
|
+
@output = ''
|
|
5
|
+
node.accept(self)
|
|
6
|
+
end
|
|
7
|
+
|
|
8
|
+
def visit_operation(node)
|
|
9
|
+
if node.left
|
|
10
|
+
visit_operand(node.left, node.class.precedence, suffix: node.operator_spacing, dir: :left)
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
@output << node.display_operator
|
|
14
|
+
|
|
15
|
+
if node.right
|
|
16
|
+
visit_operand(node.right, node.class.precedence, prefix: node.operator_spacing, dir: :right)
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def visit_operand(node, precedence, prefix: "", suffix: "", dir: :none)
|
|
21
|
+
@output << prefix
|
|
22
|
+
@output << "(" if should_output?(node, precedence, dir == :right)
|
|
23
|
+
node.accept(self)
|
|
24
|
+
@output << ")" if should_output?(node, precedence, dir == :right)
|
|
25
|
+
@output << suffix
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def should_output?(node, precedence, output_on_equal)
|
|
29
|
+
return false unless node.is_a?(Dentaku::AST::Operation)
|
|
30
|
+
|
|
31
|
+
target_precedence = node.class.precedence
|
|
32
|
+
target_precedence < precedence || (output_on_equal && target_precedence == precedence)
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def visit_function(node)
|
|
36
|
+
@output << node.name
|
|
37
|
+
@output << "("
|
|
38
|
+
arg_count = node.args.length
|
|
39
|
+
node.args.each_with_index do |a, index|
|
|
40
|
+
a.accept(self)
|
|
41
|
+
@output << ", " unless index >= arg_count - 1
|
|
42
|
+
end
|
|
43
|
+
@output << ")"
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def visit_case(node)
|
|
47
|
+
@output << "CASE "
|
|
48
|
+
node.switch.accept(self)
|
|
49
|
+
node.conditions.each { |c| c.accept(self) }
|
|
50
|
+
node.else && node.else.accept(self)
|
|
51
|
+
@output << " END"
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def visit_switch(node)
|
|
55
|
+
node.node.accept(self)
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def visit_case_conditional(node)
|
|
59
|
+
node.when.accept(self)
|
|
60
|
+
node.then.accept(self)
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def visit_when(node)
|
|
64
|
+
@output << " WHEN "
|
|
65
|
+
node.node.accept(self)
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def visit_then(node)
|
|
69
|
+
@output << " THEN "
|
|
70
|
+
node.node.accept(self)
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
def visit_else(node)
|
|
74
|
+
@output << " ELSE "
|
|
75
|
+
node.node.accept(self)
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
def visit_negation(node)
|
|
79
|
+
@output << "-"
|
|
80
|
+
@output << "(" unless node.node.is_a? Dentaku::AST::Literal
|
|
81
|
+
node.node.accept(self)
|
|
82
|
+
@output << ")" unless node.node.is_a? Dentaku::AST::Literal
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
def visit_access(node)
|
|
86
|
+
node.structure.accept(self)
|
|
87
|
+
@output << "["
|
|
88
|
+
node.index.accept(self)
|
|
89
|
+
@output << "]"
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
def visit_literal(node)
|
|
93
|
+
@output << node.quoted
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
def visit_identifier(node)
|
|
97
|
+
@output << node.identifier
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
def visit_nil(node)
|
|
101
|
+
@output << "NULL"
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
def visit_array(node)
|
|
105
|
+
@output << node.value.to_s
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
def to_s
|
|
109
|
+
@output
|
|
110
|
+
end
|
|
111
|
+
end
|
|
112
|
+
end
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
module Dentaku
|
|
2
|
+
class Token
|
|
3
|
+
attr_reader :category, :raw_value, :value
|
|
4
|
+
|
|
5
|
+
def initialize(category, value, raw_value = nil)
|
|
6
|
+
@category = category
|
|
7
|
+
@value = value
|
|
8
|
+
@raw_value = raw_value
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def to_s
|
|
12
|
+
raw_value || value
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def length
|
|
16
|
+
raw_value.to_s.length
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def empty?
|
|
20
|
+
length.zero?
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def operator?
|
|
24
|
+
is?(:operator)
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def grouping?
|
|
28
|
+
is?(:grouping)
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def open?
|
|
32
|
+
grouping? && value == :open
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def close?
|
|
36
|
+
grouping? && value == :close
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def is?(c)
|
|
40
|
+
category == c
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def ==(other)
|
|
44
|
+
(category.nil? || other.category.nil? || category == other.category) &&
|
|
45
|
+
(value.nil? || other.value.nil? || value == other.value)
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
end
|