dentaku 1.2.6 → 2.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/README.md +52 -57
- data/Rakefile +1 -1
- data/lib/dentaku.rb +8 -0
- data/lib/dentaku/ast.rb +22 -0
- data/lib/dentaku/ast/addition.rb +15 -0
- data/lib/dentaku/ast/combinators.rb +15 -0
- data/lib/dentaku/ast/comparators.rb +47 -0
- data/lib/dentaku/ast/division.rb +15 -0
- data/lib/dentaku/ast/exponentiation.rb +15 -0
- data/lib/dentaku/ast/function.rb +54 -0
- data/lib/dentaku/ast/functions/if.rb +26 -0
- data/lib/dentaku/ast/functions/max.rb +5 -0
- data/lib/dentaku/ast/functions/min.rb +5 -0
- data/lib/dentaku/ast/functions/not.rb +5 -0
- data/lib/dentaku/ast/functions/round.rb +5 -0
- data/lib/dentaku/ast/functions/rounddown.rb +5 -0
- data/lib/dentaku/ast/functions/roundup.rb +5 -0
- data/lib/dentaku/ast/functions/ruby_math.rb +8 -0
- data/lib/dentaku/ast/grouping.rb +13 -0
- data/lib/dentaku/ast/identifier.rb +29 -0
- data/lib/dentaku/ast/multiplication.rb +15 -0
- data/lib/dentaku/ast/negation.rb +25 -0
- data/lib/dentaku/ast/nil.rb +9 -0
- data/lib/dentaku/ast/node.rb +13 -0
- data/lib/dentaku/ast/numeric.rb +17 -0
- data/lib/dentaku/ast/operation.rb +20 -0
- data/lib/dentaku/ast/string.rb +17 -0
- data/lib/dentaku/ast/subtraction.rb +15 -0
- data/lib/dentaku/bulk_expression_solver.rb +6 -11
- data/lib/dentaku/calculator.rb +26 -20
- data/lib/dentaku/parser.rb +131 -0
- data/lib/dentaku/token.rb +4 -0
- data/lib/dentaku/token_matchers.rb +29 -0
- data/lib/dentaku/token_scanner.rb +18 -3
- data/lib/dentaku/tokenizer.rb +10 -2
- data/lib/dentaku/version.rb +1 -1
- data/spec/ast/function_spec.rb +19 -0
- data/spec/ast/node_spec.rb +37 -0
- data/spec/bulk_expression_solver_spec.rb +12 -5
- data/spec/calculator_spec.rb +14 -1
- data/spec/external_function_spec.rb +12 -28
- data/spec/parser_spec.rb +88 -0
- data/spec/spec_helper.rb +2 -1
- data/spec/token_scanner_spec.rb +4 -3
- data/spec/tokenizer_spec.rb +32 -6
- metadata +36 -16
- data/lib/dentaku/binary_operation.rb +0 -35
- data/lib/dentaku/evaluator.rb +0 -166
- data/lib/dentaku/expression.rb +0 -56
- data/lib/dentaku/external_function.rb +0 -10
- data/lib/dentaku/rule_set.rb +0 -153
- data/spec/binary_operation_spec.rb +0 -45
- data/spec/evaluator_spec.rb +0 -145
- data/spec/expression_spec.rb +0 -25
- data/spec/rule_set_spec.rb +0 -43
@@ -0,0 +1,29 @@
|
|
1
|
+
require_relative '../exceptions'
|
2
|
+
|
3
|
+
module Dentaku
|
4
|
+
module AST
|
5
|
+
class Identifier < Node
|
6
|
+
attr_reader :identifier
|
7
|
+
|
8
|
+
def initialize(token)
|
9
|
+
@identifier = token.value.downcase
|
10
|
+
end
|
11
|
+
|
12
|
+
def value(context={})
|
13
|
+
v = context[identifier]
|
14
|
+
case v
|
15
|
+
when Node
|
16
|
+
v.value
|
17
|
+
when NilClass
|
18
|
+
raise UnboundVariableError.new([identifier])
|
19
|
+
else
|
20
|
+
v
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
def dependencies(context={})
|
25
|
+
context.has_key?(identifier) ? [] : [identifier]
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
@@ -0,0 +1,25 @@
|
|
1
|
+
module Dentaku
|
2
|
+
module AST
|
3
|
+
class Negation < Operation
|
4
|
+
def initialize(node)
|
5
|
+
@node = node
|
6
|
+
end
|
7
|
+
|
8
|
+
def value(context={})
|
9
|
+
@node.value(context) * -1
|
10
|
+
end
|
11
|
+
|
12
|
+
def self.arity
|
13
|
+
1
|
14
|
+
end
|
15
|
+
|
16
|
+
def self.right_associative?
|
17
|
+
true
|
18
|
+
end
|
19
|
+
|
20
|
+
def self.precedence
|
21
|
+
40
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
@@ -0,0 +1,20 @@
|
|
1
|
+
module Dentaku
|
2
|
+
module AST
|
3
|
+
class Operation < Node
|
4
|
+
attr_reader :left, :right
|
5
|
+
|
6
|
+
def initialize(left, right)
|
7
|
+
@left = left
|
8
|
+
@right = right
|
9
|
+
end
|
10
|
+
|
11
|
+
def dependencies(context={})
|
12
|
+
(left.dependencies(context) + right.dependencies(context)).uniq
|
13
|
+
end
|
14
|
+
|
15
|
+
def self.right_associative?
|
16
|
+
false
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
@@ -1,13 +1,14 @@
|
|
1
1
|
require 'dentaku/calculator'
|
2
2
|
require 'dentaku/dependency_resolver'
|
3
3
|
require 'dentaku/exceptions'
|
4
|
-
require 'dentaku/
|
4
|
+
require 'dentaku/parser'
|
5
|
+
require 'dentaku/tokenizer'
|
5
6
|
|
6
7
|
module Dentaku
|
7
8
|
class BulkExpressionSolver
|
8
9
|
def initialize(expression_hash, memory)
|
9
10
|
self.expression_hash = expression_hash
|
10
|
-
self.
|
11
|
+
self.calculator = Calculator.new.store(memory)
|
11
12
|
end
|
12
13
|
|
13
14
|
def solve!
|
@@ -25,7 +26,7 @@ module Dentaku
|
|
25
26
|
|
26
27
|
private
|
27
28
|
|
28
|
-
attr_accessor :expression_hash, :
|
29
|
+
attr_accessor :expression_hash, :calculator
|
29
30
|
|
30
31
|
def return_undefined_handler
|
31
32
|
->(*) { :undefined }
|
@@ -38,7 +39,7 @@ module Dentaku
|
|
38
39
|
def load_results(&block)
|
39
40
|
variables_in_resolve_order.each_with_object({}) do |var_name, r|
|
40
41
|
begin
|
41
|
-
r[var_name] = evaluate!(expressions[var_name], r)
|
42
|
+
r[var_name] = calculator.memory[var_name] || evaluate!(expressions[var_name], r)
|
42
43
|
rescue Dentaku::UnboundVariableError, ZeroDivisionError => ex
|
43
44
|
r[var_name] = block.call(ex)
|
44
45
|
end
|
@@ -46,7 +47,7 @@ module Dentaku
|
|
46
47
|
end
|
47
48
|
|
48
49
|
def dependencies(expression)
|
49
|
-
|
50
|
+
Parser.new(Tokenizer.new.tokenize(expression)).parse.dependencies
|
50
51
|
end
|
51
52
|
|
52
53
|
def expressions
|
@@ -63,13 +64,7 @@ module Dentaku
|
|
63
64
|
end
|
64
65
|
|
65
66
|
def evaluate!(expression, results)
|
66
|
-
expr = Expression.new(expression, memory.merge(expressions))
|
67
|
-
raise UnboundVariableError.new(expr.identifiers) if expr.unbound?
|
68
67
|
calculator.evaluate!(expression, results)
|
69
68
|
end
|
70
|
-
|
71
|
-
def calculator
|
72
|
-
@calculator ||= Calculator.new.store(memory)
|
73
|
-
end
|
74
69
|
end
|
75
70
|
end
|
data/lib/dentaku/calculator.rb
CHANGED
@@ -1,27 +1,26 @@
|
|
1
1
|
require 'dentaku/bulk_expression_solver'
|
2
|
-
require 'dentaku/evaluator'
|
3
2
|
require 'dentaku/exceptions'
|
4
|
-
require 'dentaku/expression'
|
5
|
-
require 'dentaku/rule_set'
|
6
3
|
require 'dentaku/token'
|
7
4
|
require 'dentaku/dependency_resolver'
|
5
|
+
require 'dentaku/parser'
|
8
6
|
|
9
7
|
module Dentaku
|
10
8
|
class Calculator
|
11
|
-
attr_reader :result, :
|
9
|
+
attr_reader :result, :memory, :tokenizer
|
12
10
|
|
13
11
|
def initialize
|
14
12
|
clear
|
15
|
-
@
|
13
|
+
@tokenizer = Tokenizer.new
|
14
|
+
@ast_cache = {}
|
16
15
|
end
|
17
16
|
|
18
|
-
def add_function(
|
19
|
-
|
17
|
+
def add_function(name, body)
|
18
|
+
Dentaku::AST::Function.register(name, body)
|
20
19
|
self
|
21
20
|
end
|
22
21
|
|
23
22
|
def add_functions(fns)
|
24
|
-
fns.each { |
|
23
|
+
fns.each { |(name, body)| add_function(name, body) }
|
25
24
|
self
|
26
25
|
end
|
27
26
|
|
@@ -32,35 +31,42 @@ module Dentaku
|
|
32
31
|
end
|
33
32
|
|
34
33
|
def evaluate!(expression, data={})
|
35
|
-
store(data) do
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
@result = @evaluator.evaluate(expr.tokens)
|
34
|
+
memory[expression] || store(data) do
|
35
|
+
node = expression
|
36
|
+
node = ast(node) unless node.is_a?(AST::Node)
|
37
|
+
node.value(memory)
|
40
38
|
end
|
41
39
|
end
|
42
40
|
|
43
41
|
def solve!(expression_hash)
|
44
|
-
BulkExpressionSolver.new(expression_hash,
|
42
|
+
BulkExpressionSolver.new(expression_hash, memory).solve!
|
45
43
|
end
|
46
44
|
|
47
45
|
def solve(expression_hash, &block)
|
48
|
-
BulkExpressionSolver.new(expression_hash,
|
46
|
+
BulkExpressionSolver.new(expression_hash, memory).solve(&block)
|
49
47
|
end
|
50
48
|
|
51
49
|
def dependencies(expression)
|
52
|
-
|
50
|
+
ast(expression).dependencies(memory)
|
51
|
+
end
|
52
|
+
|
53
|
+
def ast(expression)
|
54
|
+
@ast_cache.fetch(expression) {
|
55
|
+
Parser.new(tokenizer.tokenize(expression)).parse.tap do |node|
|
56
|
+
@ast_cache[expression] = node if Dentaku.cache_ast?
|
57
|
+
end
|
58
|
+
}
|
53
59
|
end
|
54
60
|
|
55
61
|
def store(key_or_hash, value=nil)
|
56
|
-
restore =
|
62
|
+
restore = memory.dup
|
57
63
|
|
58
64
|
if value.nil?
|
59
65
|
key_or_hash.each do |key, val|
|
60
|
-
|
66
|
+
memory[key.downcase.to_s] = val
|
61
67
|
end
|
62
68
|
else
|
63
|
-
|
69
|
+
memory[key_or_hash.to_s] = value
|
64
70
|
end
|
65
71
|
|
66
72
|
if block_given?
|
@@ -78,7 +84,7 @@ module Dentaku
|
|
78
84
|
end
|
79
85
|
|
80
86
|
def empty?
|
81
|
-
|
87
|
+
memory.empty?
|
82
88
|
end
|
83
89
|
end
|
84
90
|
end
|
@@ -0,0 +1,131 @@
|
|
1
|
+
require_relative './ast'
|
2
|
+
|
3
|
+
module Dentaku
|
4
|
+
class Parser
|
5
|
+
attr_reader :input, :output, :operations, :arities
|
6
|
+
|
7
|
+
def initialize(tokens)
|
8
|
+
@input = tokens.dup
|
9
|
+
@output = []
|
10
|
+
@operations = []
|
11
|
+
@arities = []
|
12
|
+
end
|
13
|
+
|
14
|
+
def get_args(count)
|
15
|
+
Array.new(count) { output.pop }.reverse
|
16
|
+
end
|
17
|
+
|
18
|
+
def consume(count=2)
|
19
|
+
operator = operations.pop
|
20
|
+
output.push operator.new(*get_args(operator.arity || count))
|
21
|
+
end
|
22
|
+
|
23
|
+
def parse
|
24
|
+
return AST::Nil.new if input.empty?
|
25
|
+
|
26
|
+
while token = input.shift
|
27
|
+
case token.category
|
28
|
+
when :numeric
|
29
|
+
output.push AST::Numeric.new(token)
|
30
|
+
|
31
|
+
when :string
|
32
|
+
output.push AST::String.new(token)
|
33
|
+
|
34
|
+
when :identifier
|
35
|
+
output.push AST::Identifier.new(token)
|
36
|
+
|
37
|
+
when :operator, :comparator, :combinator
|
38
|
+
op_class = operation(token)
|
39
|
+
|
40
|
+
if op_class.right_associative?
|
41
|
+
while operations.last && operations.last < AST::Operation && op_class.precedence < operations.last.precedence
|
42
|
+
consume
|
43
|
+
end
|
44
|
+
|
45
|
+
operations.push op_class
|
46
|
+
else
|
47
|
+
while operations.last && operations.last < AST::Operation && op_class.precedence <= operations.last.precedence
|
48
|
+
consume
|
49
|
+
end
|
50
|
+
|
51
|
+
operations.push op_class
|
52
|
+
end
|
53
|
+
|
54
|
+
when :function
|
55
|
+
arities.push 0
|
56
|
+
operations.push function(token)
|
57
|
+
|
58
|
+
when :grouping
|
59
|
+
case token.value
|
60
|
+
when :open, :fopen
|
61
|
+
if input.first && input.first.value == :close
|
62
|
+
input.shift
|
63
|
+
consume(0)
|
64
|
+
else
|
65
|
+
operations.push AST::Grouping
|
66
|
+
end
|
67
|
+
|
68
|
+
when :close
|
69
|
+
while operations.any? && operations.last != AST::Grouping
|
70
|
+
consume
|
71
|
+
end
|
72
|
+
|
73
|
+
lparen = operations.pop
|
74
|
+
fail "Unbalanced parenthesis" unless lparen == AST::Grouping
|
75
|
+
|
76
|
+
if operations.last && operations.last < AST::Function
|
77
|
+
consume(arities.pop.succ)
|
78
|
+
end
|
79
|
+
|
80
|
+
when :comma
|
81
|
+
arities[-1] += 1
|
82
|
+
while operations.any? && operations.last != AST::Grouping
|
83
|
+
consume
|
84
|
+
end
|
85
|
+
|
86
|
+
else
|
87
|
+
fail "Unknown grouping token #{ token.value }"
|
88
|
+
end
|
89
|
+
|
90
|
+
else
|
91
|
+
fail "Not implemented for tokens of category #{ token.category }"
|
92
|
+
end
|
93
|
+
end
|
94
|
+
|
95
|
+
while operations.any?
|
96
|
+
consume
|
97
|
+
end
|
98
|
+
|
99
|
+
unless output.count == 1
|
100
|
+
fail "Parse error"
|
101
|
+
end
|
102
|
+
|
103
|
+
output.first
|
104
|
+
end
|
105
|
+
|
106
|
+
def operation(token)
|
107
|
+
{
|
108
|
+
add: AST::Addition,
|
109
|
+
subtract: AST::Subtraction,
|
110
|
+
multiply: AST::Multiplication,
|
111
|
+
divide: AST::Division,
|
112
|
+
pow: AST::Exponentiation,
|
113
|
+
negate: AST::Negation,
|
114
|
+
|
115
|
+
lt: AST::LessThan,
|
116
|
+
gt: AST::GreaterThan,
|
117
|
+
le: AST::LessThanOrEqual,
|
118
|
+
ge: AST::GreaterThanOrEqual,
|
119
|
+
ne: AST::NotEqual,
|
120
|
+
eq: AST::Equal,
|
121
|
+
|
122
|
+
and: AST::And,
|
123
|
+
or: AST::Or,
|
124
|
+
}.fetch(token.value)
|
125
|
+
end
|
126
|
+
|
127
|
+
def function(token)
|
128
|
+
Dentaku::AST::Function.get(token.value)
|
129
|
+
end
|
130
|
+
end
|
131
|
+
end
|