keisan 0.8.11 → 0.9.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/README.md +38 -0
- data/lib/keisan/ast/cache.rb +31 -0
- data/lib/keisan/ast/cell.rb +1 -1
- data/lib/keisan/ast/constant_literal.rb +164 -0
- data/lib/keisan/ast/function.rb +7 -0
- data/lib/keisan/ast/hash.rb +4 -0
- data/lib/keisan/ast/indexing.rb +13 -0
- data/lib/keisan/ast/logical_and.rb +1 -1
- data/lib/keisan/ast/logical_or.rb +1 -1
- data/lib/keisan/ast/node.rb +4 -0
- data/lib/keisan/ast/operator.rb +0 -2
- data/lib/keisan/ast/parent.rb +4 -0
- data/lib/keisan/ast/plus.rb +3 -3
- data/lib/keisan/ast/times.rb +3 -3
- data/lib/keisan/ast/unary_logical_not.rb +26 -0
- data/lib/keisan/calculator.rb +22 -4
- data/lib/keisan/evaluator.rb +18 -9
- data/lib/keisan/functions/default_registry.rb +4 -1
- data/lib/keisan/functions/enumerable_function.rb +2 -2
- data/lib/keisan/functions/expression_function.rb +7 -1
- data/lib/keisan/functions/filter.rb +2 -2
- data/lib/keisan/functions/if.rb +6 -3
- data/lib/keisan/functions/registry.rb +3 -0
- data/lib/keisan/functions/while.rb +37 -14
- data/lib/keisan/parser.rb +2 -1
- data/lib/keisan/parsing/hash.rb +14 -4
- data/lib/keisan/version.rb +1 -1
- data/lib/keisan.rb +1 -0
- metadata +3 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 11f1a2644fbb02817db69dd289564d6fdb9bfd09d294cda62b07156723aebc0e
|
4
|
+
data.tar.gz: 4b9787c0d558af15d466759e40fe4cfac35131d254d227416144588256e5ea9f
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 8149fee69d598164334b96f8be59b9763168af97e3803c9abea7285da5ed1d90bb9f87710a3de94498461baec65db335168f8e721fe73c4a837c684a30bc4114
|
7
|
+
data.tar.gz: 8a4e301fb14461b164bf0331684fe7ebe3cd660d4abd38f3c44aba5f50fbed58b6b945082accdf13f9da55bcb76579890fd63db75e8b17e5c098c52d0e2d80ae
|
data/README.md
CHANGED
@@ -64,6 +64,36 @@ ast.children.map(&:to_s)
|
|
64
64
|
#=> ["x**2", "1"]
|
65
65
|
```
|
66
66
|
|
67
|
+
#### Caching AST results
|
68
|
+
|
69
|
+
Computing the AST from a string takes some non-zero amount of time.
|
70
|
+
For applications of this gem that evaluate some set of fixed expressions (possibly with different variable values, but with fixed ASTs), it might be worthwhile to cache the ASTs for faster computation.
|
71
|
+
To accomplish this, you can use the `Keisan::AST::Cache` class.
|
72
|
+
Passing an instance of this class into the `Calculator` will mean everytime a new expression is encountered it will compute the AST and store it in this cache for retrieval next time the expression is encountered.
|
73
|
+
|
74
|
+
```ruby
|
75
|
+
cache = Keisan::AST::Cache.new
|
76
|
+
# Note: if you don't want to create the Cache instance, you can just pass `cache: true` here as well
|
77
|
+
calculator = Keisan::Calculator.new(cache: cache)
|
78
|
+
calculator.evaluate("exp(-x/T)", x: 1.0, T: 10)
|
79
|
+
#=> 0.9048374180359595
|
80
|
+
# This call will use the cached AST for "exp(-x/T)"
|
81
|
+
calculator.evaluate("exp(-x/T)", x: 2.0, T: 10)
|
82
|
+
#=> 0.8187307530779818
|
83
|
+
```
|
84
|
+
|
85
|
+
If you just want to pre-populate the cache with some predetermined values, you can call `#fetch_or_build` on the `Cache` for each instance, `freeze` the cache, then use this frozen cache in your calculator.
|
86
|
+
A cache that has been frozen will only fetch from the cache, never write new values to it.
|
87
|
+
|
88
|
+
```ruby
|
89
|
+
cache = Keisan::AST::Cache.new
|
90
|
+
cache.fetch_or_build("f(x) + diff(g(x), x)")
|
91
|
+
cache.freeze
|
92
|
+
# This calculator will never write new values to the cache, but when
|
93
|
+
# evaluating `"f(x) + diff(g(x), x)"` will fetch this cached AST.
|
94
|
+
calculator = Keisan::Calculator.new(cache: cache)
|
95
|
+
```
|
96
|
+
|
67
97
|
##### Specifying variables
|
68
98
|
|
69
99
|
Passing in a hash of variable (`name`, `value`) pairs to the `evaluate` method is one way of defining variables
|
@@ -250,6 +280,14 @@ calculator.evaluate("range(5,10)")
|
|
250
280
|
#=> [5,6,7,8,9]
|
251
281
|
calculator.evaluate("range(0,10,2)")
|
252
282
|
#=> [0,2,4,6,8]
|
283
|
+
calculator.evaluate("[1, 2, 2, 3].uniq")
|
284
|
+
#=> [1,2,3]
|
285
|
+
calculator.evaluate("[1, 2, 3].difference([2, 3, 4])")
|
286
|
+
#=> [1]
|
287
|
+
calculator.evaluate("[1, 2, 3].intersection([2, 3, 4])")
|
288
|
+
#=> [2, 3]
|
289
|
+
calculator.evaluate("[1, 2, 3].union([2, 3, 4])")
|
290
|
+
#=> [1, 2, 3, 4]
|
253
291
|
```
|
254
292
|
|
255
293
|
##### Hashes
|
@@ -0,0 +1,31 @@
|
|
1
|
+
module Keisan
|
2
|
+
module AST
|
3
|
+
class Cache
|
4
|
+
def initialize
|
5
|
+
@cache = {}
|
6
|
+
end
|
7
|
+
|
8
|
+
def fetch_or_build(string)
|
9
|
+
return @cache[string] if @cache.has_key?(string)
|
10
|
+
|
11
|
+
build_from_scratch(string).tap do |ast|
|
12
|
+
unless frozen?
|
13
|
+
# Freeze the AST to keep it from changing in the cache
|
14
|
+
ast.freeze
|
15
|
+
@cache[string] = ast
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
def has_key?(string)
|
21
|
+
@cache.has_key?(string)
|
22
|
+
end
|
23
|
+
|
24
|
+
private
|
25
|
+
|
26
|
+
def build_from_scratch(string)
|
27
|
+
Builder.new(string: string).ast
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
data/lib/keisan/ast/cell.rb
CHANGED
@@ -22,6 +22,170 @@ module Keisan
|
|
22
22
|
value.to_s
|
23
23
|
end
|
24
24
|
end
|
25
|
+
|
26
|
+
def is_constant?
|
27
|
+
true
|
28
|
+
end
|
29
|
+
|
30
|
+
def +(other)
|
31
|
+
if other.is_constant?
|
32
|
+
raise Keisan::Exceptions::InvalidExpression.new("Cannot add #{self.class} to #{other.class}")
|
33
|
+
else
|
34
|
+
super
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
def -(other)
|
39
|
+
if other.is_constant?
|
40
|
+
raise Keisan::Exceptions::InvalidExpression.new("Cannot subtract #{self.class} from #{other.class}")
|
41
|
+
else
|
42
|
+
super
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
def *(other)
|
47
|
+
if other.is_constant?
|
48
|
+
raise Keisan::Exceptions::InvalidExpression.new("Cannot multiply #{self.class} and #{other.class}")
|
49
|
+
else
|
50
|
+
super
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
def /(other)
|
55
|
+
if other.is_constant?
|
56
|
+
raise Keisan::Exceptions::InvalidExpression.new("Cannot divide #{self.class} and #{other.class}")
|
57
|
+
else
|
58
|
+
super
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
62
|
+
def %(other)
|
63
|
+
if other.is_constant?
|
64
|
+
raise Keisan::Exceptions::InvalidExpression.new("Cannot modulo #{self.class} and #{other.class}")
|
65
|
+
else
|
66
|
+
super
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
70
|
+
def !
|
71
|
+
raise Keisan::Exceptions::InvalidExpression.new("Cannot take logical not of #{self.class}")
|
72
|
+
end
|
73
|
+
|
74
|
+
def ~
|
75
|
+
raise Keisan::Exceptions::InvalidExpression.new("Cannot take bitwise not of #{self.class}")
|
76
|
+
end
|
77
|
+
|
78
|
+
def +@
|
79
|
+
raise Keisan::Exceptions::InvalidExpression.new("Cannot take unary plus of #{self.class}")
|
80
|
+
end
|
81
|
+
|
82
|
+
def -@
|
83
|
+
raise Keisan::Exceptions::InvalidExpression.new("Cannot take unary minus of #{self.class}")
|
84
|
+
end
|
85
|
+
|
86
|
+
def **(other)
|
87
|
+
if other.is_constant?
|
88
|
+
raise Keisan::Exceptions::InvalidExpression.new("Cannot exponentiate #{self.class} and #{other.class}")
|
89
|
+
else
|
90
|
+
super
|
91
|
+
end
|
92
|
+
end
|
93
|
+
|
94
|
+
def &(other)
|
95
|
+
if other.is_constant?
|
96
|
+
raise Keisan::Exceptions::InvalidExpression.new("Cannot bitwise and #{self.class} and #{other.class}")
|
97
|
+
else
|
98
|
+
super
|
99
|
+
end
|
100
|
+
end
|
101
|
+
|
102
|
+
def ^(other)
|
103
|
+
if other.is_constant?
|
104
|
+
raise Keisan::Exceptions::InvalidExpression.new("Cannot bitwise xor #{self.class} and #{other.class}")
|
105
|
+
else
|
106
|
+
super
|
107
|
+
end
|
108
|
+
end
|
109
|
+
|
110
|
+
def |(other)
|
111
|
+
if other.is_constant?
|
112
|
+
raise Keisan::Exceptions::InvalidExpression.new("Cannot bitwise or #{self.class} and #{other.class}")
|
113
|
+
else
|
114
|
+
super
|
115
|
+
end
|
116
|
+
end
|
117
|
+
|
118
|
+
def <<(other)
|
119
|
+
if other.is_constant?
|
120
|
+
raise Keisan::Exceptions::InvalidExpression.new("Cannot bitwise left shift #{self.class} and #{other.class}")
|
121
|
+
else
|
122
|
+
super
|
123
|
+
end
|
124
|
+
end
|
125
|
+
|
126
|
+
def >>(other)
|
127
|
+
if other.is_constant?
|
128
|
+
raise Keisan::Exceptions::InvalidExpression.new("Cannot bitwise right shift #{self.class} and #{other.class}")
|
129
|
+
else
|
130
|
+
super
|
131
|
+
end
|
132
|
+
end
|
133
|
+
|
134
|
+
def >(other)
|
135
|
+
if other.is_constant?
|
136
|
+
raise Keisan::Exceptions::InvalidExpression.new("Cannot compute #{self.class} > #{other.class}")
|
137
|
+
else
|
138
|
+
super
|
139
|
+
end
|
140
|
+
end
|
141
|
+
|
142
|
+
def >=(other)
|
143
|
+
if other.is_constant?
|
144
|
+
raise Keisan::Exceptions::InvalidExpression.new("Cannot compute #{self.class} >= #{other.class}")
|
145
|
+
else
|
146
|
+
super
|
147
|
+
end
|
148
|
+
end
|
149
|
+
|
150
|
+
def <(other)
|
151
|
+
if other.is_constant?
|
152
|
+
raise Keisan::Exceptions::InvalidExpression.new("Cannot compute #{self.class} < #{other.class}")
|
153
|
+
else
|
154
|
+
super
|
155
|
+
end
|
156
|
+
end
|
157
|
+
|
158
|
+
def <=(other)
|
159
|
+
if other.is_constant?
|
160
|
+
raise Keisan::Exceptions::InvalidExpression.new("Cannot compute #{self.class} <= #{other.class}")
|
161
|
+
else
|
162
|
+
super
|
163
|
+
end
|
164
|
+
end
|
165
|
+
|
166
|
+
def equal(other)
|
167
|
+
other.is_constant? ? Boolean.new(false) : super
|
168
|
+
end
|
169
|
+
|
170
|
+
def not_equal(other)
|
171
|
+
other.is_constant? ? Boolean.new(true) : super
|
172
|
+
end
|
173
|
+
|
174
|
+
def and(other)
|
175
|
+
if other.is_constant?
|
176
|
+
raise Keisan::Exceptions::InvalidExpression.new("Cannot logical and #{self.class} and #{other.class}")
|
177
|
+
else
|
178
|
+
super
|
179
|
+
end
|
180
|
+
end
|
181
|
+
|
182
|
+
def or(other)
|
183
|
+
if other.is_constant?
|
184
|
+
raise Keisan::Exceptions::InvalidExpression.new("Cannot logical or #{self.class} and #{other.class}")
|
185
|
+
else
|
186
|
+
super
|
187
|
+
end
|
188
|
+
end
|
25
189
|
end
|
26
190
|
end
|
27
191
|
end
|
data/lib/keisan/ast/function.rb
CHANGED
@@ -91,6 +91,13 @@ module Keisan
|
|
91
91
|
|
92
92
|
self.class.new([self, variable], "diff")
|
93
93
|
end
|
94
|
+
|
95
|
+
# Functions cannot be guaranteed to be constant even if the arguments
|
96
|
+
# are constants, because there might be randomness involved in the
|
97
|
+
# outputs.
|
98
|
+
def is_constant?
|
99
|
+
false
|
100
|
+
end
|
94
101
|
end
|
95
102
|
end
|
96
103
|
end
|
data/lib/keisan/ast/hash.rb
CHANGED
data/lib/keisan/ast/indexing.rb
CHANGED
@@ -8,6 +8,19 @@ module Keisan
|
|
8
8
|
@indexes = indexes
|
9
9
|
end
|
10
10
|
|
11
|
+
def deep_dup
|
12
|
+
dupped = super
|
13
|
+
dupped.instance_variable_set(
|
14
|
+
:@indexes, indexes.map(&:deep_dup)
|
15
|
+
)
|
16
|
+
dupped
|
17
|
+
end
|
18
|
+
|
19
|
+
def freeze
|
20
|
+
indexes.each(&:freeze)
|
21
|
+
super
|
22
|
+
end
|
23
|
+
|
11
24
|
def value(context = nil)
|
12
25
|
return child.value(context).send(:[], *indexes.map {|index| index.value(context)})
|
13
26
|
end
|
@@ -26,7 +26,7 @@ module Keisan
|
|
26
26
|
|
27
27
|
def short_circuit_do(method, context)
|
28
28
|
context ||= Context.new
|
29
|
-
lhs = children[0].send(method, context)
|
29
|
+
lhs = children[0].send(method, context).to_node
|
30
30
|
case lhs
|
31
31
|
when AST::Boolean
|
32
32
|
lhs.false? ? AST::Boolean.new(false) : children[1].send(method, context)
|
@@ -26,7 +26,7 @@ module Keisan
|
|
26
26
|
|
27
27
|
def short_circuit_do(method, context)
|
28
28
|
context ||= Context.new
|
29
|
-
lhs = children[0].send(method, context)
|
29
|
+
lhs = children[0].send(method, context).to_node
|
30
30
|
case lhs
|
31
31
|
when AST::Boolean
|
32
32
|
lhs.true? ? AST::Boolean.new(true) : children[1].send(method, context)
|
data/lib/keisan/ast/node.rb
CHANGED
data/lib/keisan/ast/operator.rb
CHANGED
data/lib/keisan/ast/parent.rb
CHANGED
data/lib/keisan/ast/plus.rb
CHANGED
@@ -3,7 +3,7 @@ module Keisan
|
|
3
3
|
class Plus < ArithmeticOperator
|
4
4
|
def initialize(children = [], parsing_operators = [])
|
5
5
|
super
|
6
|
-
convert_minus_to_plus!
|
6
|
+
convert_minus_to_plus!(parsing_operators)
|
7
7
|
end
|
8
8
|
|
9
9
|
def self.symbol
|
@@ -87,8 +87,8 @@ module Keisan
|
|
87
87
|
date_time + others.inject(0, &:+)
|
88
88
|
end
|
89
89
|
|
90
|
-
def convert_minus_to_plus!
|
91
|
-
|
90
|
+
def convert_minus_to_plus!(parsing_operators)
|
91
|
+
parsing_operators.each.with_index do |parsing_operator, index|
|
92
92
|
if parsing_operator.is_a?(Parsing::Minus)
|
93
93
|
@children[index+1] = UnaryMinus.new(@children[index+1])
|
94
94
|
end
|
data/lib/keisan/ast/times.rb
CHANGED
@@ -3,7 +3,7 @@ module Keisan
|
|
3
3
|
class Times < ArithmeticOperator
|
4
4
|
def initialize(children = [], parsing_operators = [])
|
5
5
|
super
|
6
|
-
convert_divide_to_inverse!
|
6
|
+
convert_divide_to_inverse!(parsing_operators)
|
7
7
|
end
|
8
8
|
|
9
9
|
def self.symbol
|
@@ -63,8 +63,8 @@ module Keisan
|
|
63
63
|
|
64
64
|
private
|
65
65
|
|
66
|
-
def convert_divide_to_inverse!
|
67
|
-
|
66
|
+
def convert_divide_to_inverse!(parsing_operators)
|
67
|
+
parsing_operators.each.with_index do |parsing_operator, index|
|
68
68
|
if parsing_operator.is_a?(Parsing::Divide)
|
69
69
|
@children[index+1] = UnaryInverse.new(@children[index+1])
|
70
70
|
end
|
@@ -8,6 +8,32 @@ module Keisan
|
|
8
8
|
def self.symbol
|
9
9
|
:"!"
|
10
10
|
end
|
11
|
+
|
12
|
+
def evaluate(context = nil)
|
13
|
+
context ||= Context.new
|
14
|
+
node = child.evaluate(context).to_node
|
15
|
+
case node
|
16
|
+
when AST::Boolean
|
17
|
+
AST::Boolean.new(!node.value)
|
18
|
+
else
|
19
|
+
if node.is_constant?
|
20
|
+
raise Keisan::Exceptions::InvalidFunctionError.new("Cannot take unary logical not of non-boolean constant")
|
21
|
+
else
|
22
|
+
super
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
def simplify(context = nil)
|
28
|
+
context ||= Context.new
|
29
|
+
node = child.simplify(context).to_node
|
30
|
+
case node
|
31
|
+
when AST::Boolean
|
32
|
+
AST::Boolean.new(!node.value)
|
33
|
+
else
|
34
|
+
super
|
35
|
+
end
|
36
|
+
end
|
11
37
|
end
|
12
38
|
end
|
13
39
|
end
|
data/lib/keisan/calculator.rb
CHANGED
@@ -4,13 +4,23 @@ module Keisan
|
|
4
4
|
|
5
5
|
# Note, allow_recursive would be more appropriately named:
|
6
6
|
# allow_unbound_functions_in_function_definitions, but it is too late for that.
|
7
|
-
def initialize(context: nil, allow_recursive: false, allow_blocks: true, allow_multiline: true, allow_random: true)
|
7
|
+
def initialize(context: nil, allow_recursive: false, allow_blocks: true, allow_multiline: true, allow_random: true, cache: nil)
|
8
8
|
@context = context || Context.new(
|
9
9
|
allow_recursive: allow_recursive,
|
10
10
|
allow_blocks: allow_blocks,
|
11
11
|
allow_multiline: allow_multiline,
|
12
12
|
allow_random: allow_random
|
13
13
|
)
|
14
|
+
@cache = case cache
|
15
|
+
when nil, false
|
16
|
+
nil
|
17
|
+
when true
|
18
|
+
AST::Cache.new
|
19
|
+
when AST::Cache
|
20
|
+
cache
|
21
|
+
else
|
22
|
+
raise Exceptions::StandardError.new("cache must be either nil, false, true, or an instance of Keisan::AST::Cache")
|
23
|
+
end
|
14
24
|
end
|
15
25
|
|
16
26
|
def allow_recursive
|
@@ -34,15 +44,23 @@ module Keisan
|
|
34
44
|
end
|
35
45
|
|
36
46
|
def evaluate(expression, definitions = {})
|
37
|
-
Evaluator.new(self).evaluate(expression, definitions)
|
47
|
+
Evaluator.new(self, cache: @cache).evaluate(expression, definitions)
|
48
|
+
end
|
49
|
+
|
50
|
+
def evaluate_ast(ast, definitions = {})
|
51
|
+
Evaluator.new(self, cache: @cache).evaluate_ast(ast, definitions: definitions)
|
38
52
|
end
|
39
53
|
|
40
54
|
def simplify(expression, definitions = {})
|
41
|
-
Evaluator.new(self).simplify(expression, definitions)
|
55
|
+
Evaluator.new(self, cache: @cache).simplify(expression, definitions)
|
56
|
+
end
|
57
|
+
|
58
|
+
def simplify_ast(ast, definitions = {})
|
59
|
+
Evaluator.new(self, cache: @cache).simplify_ast(ast, definitions: definitions)
|
42
60
|
end
|
43
61
|
|
44
62
|
def ast(expression)
|
45
|
-
Evaluator.new(self).parse_ast(expression)
|
63
|
+
Evaluator.new(self, cache: @cache).parse_ast(expression)
|
46
64
|
end
|
47
65
|
|
48
66
|
def define_variable!(name, value)
|
data/lib/keisan/evaluator.rb
CHANGED
@@ -2,13 +2,18 @@ module Keisan
|
|
2
2
|
class Evaluator
|
3
3
|
attr_reader :calculator
|
4
4
|
|
5
|
-
def initialize(calculator)
|
5
|
+
def initialize(calculator, cache: nil)
|
6
6
|
@calculator = calculator
|
7
|
+
@cache = cache
|
7
8
|
end
|
8
9
|
|
9
10
|
def evaluate(expression, definitions = {})
|
10
11
|
context = calculator.context.spawn_child(definitions: definitions, transient: true)
|
11
|
-
|
12
|
+
evaluate_ast(parse_ast(expression), context: context)
|
13
|
+
end
|
14
|
+
|
15
|
+
def evaluate_ast(ast, definitions: {}, context: nil)
|
16
|
+
context ||= calculator.context.spawn_child(definitions: definitions, transient: true)
|
12
17
|
last_line = last_line(ast)
|
13
18
|
|
14
19
|
evaluation = ast.evaluated(context)
|
@@ -24,17 +29,21 @@ module Keisan
|
|
24
29
|
|
25
30
|
def simplify(expression, definitions = {})
|
26
31
|
context = calculator.context.spawn_child(definitions: definitions, transient: true)
|
27
|
-
|
28
|
-
|
32
|
+
simplify_ast(parse_ast(expression), context: context)
|
33
|
+
end
|
34
|
+
|
35
|
+
def simplify_ast(ast, definitions: {}, context: nil)
|
36
|
+
context ||= calculator.context.spawn_child(definitions: definitions, transient: true)
|
37
|
+
ast.simplified(context)
|
29
38
|
end
|
30
39
|
|
31
40
|
def parse_ast(expression)
|
32
|
-
AST.parse(expression)
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
end
|
41
|
+
ast = @cache.nil? ? AST.parse(expression) : @cache.fetch_or_build(expression)
|
42
|
+
disallowed = disallowed_nodes
|
43
|
+
if !disallowed.empty? && ast.contains_a?(disallowed)
|
44
|
+
raise Keisan::Exceptions::InvalidExpression.new("Context does not permit expressions with #{disallowed}")
|
37
45
|
end
|
46
|
+
ast
|
38
47
|
end
|
39
48
|
|
40
49
|
private
|
@@ -118,10 +118,13 @@ module Keisan
|
|
118
118
|
end
|
119
119
|
|
120
120
|
def self.register_array_methods!(registry)
|
121
|
-
%i(min max size flatten reverse).each do |method|
|
121
|
+
%i(min max size flatten reverse uniq).each do |method|
|
122
122
|
registry.register!(method, Proc.new {|a| a.send(method)}, force: true)
|
123
123
|
end
|
124
124
|
|
125
|
+
registry.register!(:difference, Proc.new {|a, b| a - b}, force: true)
|
126
|
+
registry.register!(:intersection, Proc.new {|a, b| a & b}, force: true)
|
127
|
+
registry.register!(:union, Proc.new {|a, b| a | b}, force: true)
|
125
128
|
registry.register!("range", Functions::Range.new, force: true)
|
126
129
|
end
|
127
130
|
|
@@ -21,7 +21,7 @@ module Keisan
|
|
21
21
|
context ||= Context.new
|
22
22
|
|
23
23
|
operand, arguments, expression = operand_arguments_expression_for(ast_function, context)
|
24
|
-
|
24
|
+
|
25
25
|
# Extract underlying operand for cells
|
26
26
|
real_operand = operand.is_a?(AST::Cell) ? operand.node : operand
|
27
27
|
|
@@ -54,7 +54,7 @@ module Keisan
|
|
54
54
|
private
|
55
55
|
|
56
56
|
def operand_arguments_expression_for(ast_function, context)
|
57
|
-
operand = ast_function.children[0].
|
57
|
+
operand = ast_function.children[0].evaluate(context)
|
58
58
|
arguments = ast_function.children[1...-1]
|
59
59
|
expression = ast_function.children[-1]
|
60
60
|
|
@@ -14,6 +14,12 @@ module Keisan
|
|
14
14
|
@transient_definitions = transient_definitions
|
15
15
|
end
|
16
16
|
|
17
|
+
def freeze
|
18
|
+
@arguments.freeze
|
19
|
+
@expression.freeze
|
20
|
+
super
|
21
|
+
end
|
22
|
+
|
17
23
|
def call(context, *args)
|
18
24
|
validate_arguments!(args.count)
|
19
25
|
|
@@ -96,7 +102,7 @@ module Keisan
|
|
96
102
|
private
|
97
103
|
|
98
104
|
def argument_variables
|
99
|
-
|
105
|
+
arguments.map {|argument| AST::Variable.new(argument)}
|
100
106
|
end
|
101
107
|
|
102
108
|
def calculate_partial_derivatives(context)
|
@@ -29,7 +29,7 @@ module Keisan
|
|
29
29
|
AST::List.new(
|
30
30
|
list.children.select do |element|
|
31
31
|
local.register_variable!(variable, element)
|
32
|
-
result = expression.evaluated(local)
|
32
|
+
result = expression.evaluated(local).to_node
|
33
33
|
|
34
34
|
case result
|
35
35
|
when AST::Boolean
|
@@ -54,7 +54,7 @@ module Keisan
|
|
54
54
|
hash.select do |cur_key, cur_value|
|
55
55
|
local.register_variable!(key, cur_key)
|
56
56
|
local.register_variable!(value, cur_value)
|
57
|
-
result = expression.evaluated(local)
|
57
|
+
result = expression.evaluated(local).to_node
|
58
58
|
|
59
59
|
case result
|
60
60
|
when AST::Boolean
|
data/lib/keisan/functions/if.rb
CHANGED
@@ -13,12 +13,13 @@ module Keisan
|
|
13
13
|
def evaluate(ast_function, context = nil)
|
14
14
|
validate_arguments!(ast_function.children.count)
|
15
15
|
context ||= Context.new
|
16
|
-
|
17
|
-
bool = ast_function.children[0].evaluate(context)
|
16
|
+
bool = ast_function.children[0].evaluate(context).to_node
|
18
17
|
|
19
18
|
if bool.is_a?(AST::Boolean)
|
20
19
|
node = bool.value ? ast_function.children[1] : ast_function.children[2]
|
21
20
|
node.to_node.evaluate(context)
|
21
|
+
elsif bool.is_constant?
|
22
|
+
raise Keisan::Exceptions::InvalidFunctionError.new("if statement must work on booleans, other constants are not supported")
|
22
23
|
else
|
23
24
|
ast_function
|
24
25
|
end
|
@@ -27,7 +28,7 @@ module Keisan
|
|
27
28
|
def simplify(ast_function, context = nil)
|
28
29
|
validate_arguments!(ast_function.children.count)
|
29
30
|
context ||= Context.new
|
30
|
-
bool = ast_function.children[0].simplify(context)
|
31
|
+
bool = ast_function.children[0].simplify(context).to_node
|
31
32
|
|
32
33
|
if bool.is_a?(AST::Boolean)
|
33
34
|
if bool.value
|
@@ -37,6 +38,8 @@ module Keisan
|
|
37
38
|
else
|
38
39
|
Keisan::AST::Null.new
|
39
40
|
end
|
41
|
+
elsif bool.is_constant?
|
42
|
+
raise Keisan::Exceptions::InvalidFunctionError.new("if statement must work on booleans, other constants are not supported")
|
40
43
|
else
|
41
44
|
ast_function
|
42
45
|
end
|
@@ -44,6 +44,9 @@ module Keisan
|
|
44
44
|
when Proc
|
45
45
|
self[name] = ProcFunction.new(name, function)
|
46
46
|
when Function
|
47
|
+
# The expression AST which represents the function should be constant,
|
48
|
+
# so we freeze it so it will always have the same behavior.
|
49
|
+
function.freeze if function.is_a?(ExpressionFunction)
|
47
50
|
self[name] = function
|
48
51
|
else
|
49
52
|
raise Exceptions::InvalidFunctionError.new
|
@@ -1,6 +1,9 @@
|
|
1
1
|
module Keisan
|
2
2
|
module Functions
|
3
3
|
class While < Keisan::Function
|
4
|
+
class WhileLogicalNodeIsNotConstant < Keisan::Exceptions::StandardError; end
|
5
|
+
class WhileLogicalNodeIsNonBoolConstant < Keisan::Exceptions::StandardError; end
|
6
|
+
|
4
7
|
def initialize
|
5
8
|
super("while", 2)
|
6
9
|
end
|
@@ -13,27 +16,43 @@ module Keisan
|
|
13
16
|
def evaluate(ast_function, context = nil)
|
14
17
|
validate_arguments!(ast_function.children.count)
|
15
18
|
context ||= Keisan::Context.new
|
16
|
-
|
19
|
+
while_loop(ast_function, context, simplify: false)
|
17
20
|
end
|
18
21
|
|
19
22
|
def simplify(ast_function, context = nil)
|
20
23
|
validate_arguments!(ast_function.children.count)
|
21
24
|
context ||= Context.new
|
22
|
-
while_loop(ast_function
|
25
|
+
while_loop(ast_function, context, simplify: true)
|
23
26
|
end
|
24
27
|
|
25
28
|
private
|
26
29
|
|
27
|
-
def while_loop(
|
30
|
+
def while_loop(ast_function, context, simplify: true)
|
31
|
+
logical_node, body_node = ast_function.children[0], ast_function.children[1]
|
28
32
|
current = Keisan::AST::Null.new
|
29
33
|
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
34
|
+
begin
|
35
|
+
while logical_node_evaluates_to_true(logical_node, context)
|
36
|
+
begin
|
37
|
+
current = body_node.evaluated(context)
|
38
|
+
rescue Exceptions::BreakError
|
39
|
+
break
|
40
|
+
rescue Exceptions::ContinueError
|
41
|
+
next
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
# While loops should work on booleans, not other types of constants
|
46
|
+
rescue WhileLogicalNodeIsNonBoolConstant
|
47
|
+
raise Keisan::Exceptions::InvalidFunctionError.new("while condition must evaluate to a boolean")
|
48
|
+
|
49
|
+
# If the logical expression is not constant (e.g. boolean), then we
|
50
|
+
# cannot simplify the while loop, and an evaluate should raise an error.
|
51
|
+
rescue WhileLogicalNodeIsNotConstant
|
52
|
+
if simplify
|
53
|
+
return ast_function
|
54
|
+
else
|
55
|
+
raise Keisan::Exceptions::InvalidFunctionError.new("while condition must evaluate to a boolean")
|
37
56
|
end
|
38
57
|
end
|
39
58
|
|
@@ -41,11 +60,15 @@ module Keisan
|
|
41
60
|
end
|
42
61
|
|
43
62
|
def logical_node_evaluates_to_true(logical_node, context)
|
44
|
-
bool = logical_node.evaluated(context)
|
45
|
-
|
46
|
-
|
63
|
+
bool = logical_node.evaluated(context).to_node
|
64
|
+
|
65
|
+
if bool.is_a?(AST::Boolean)
|
66
|
+
bool.value(context)
|
67
|
+
elsif bool.is_constant?
|
68
|
+
raise WhileLogicalNodeIsNonBoolConstant.new
|
69
|
+
else
|
70
|
+
raise WhileLogicalNodeIsNotConstant.new
|
47
71
|
end
|
48
|
-
bool.value(context)
|
49
72
|
end
|
50
73
|
end
|
51
74
|
end
|
data/lib/keisan/parser.rb
CHANGED
@@ -211,7 +211,8 @@ module Keisan
|
|
211
211
|
when :square
|
212
212
|
@components << Parsing::List.new(arguments_from_group(token))
|
213
213
|
when :curly
|
214
|
-
|
214
|
+
# A hash either has a colon, or is empty
|
215
|
+
if token.sub_tokens.any? {|token| token.is_a?(Tokens::Colon)} || token.sub_tokens.empty?
|
215
216
|
@components << Parsing::Hash.new(Util.array_split(token.sub_tokens) {|token| token.is_a?(Tokens::Comma)})
|
216
217
|
else
|
217
218
|
@components << Parsing::CurlyGroup.new(token.sub_tokens)
|
data/lib/keisan/parsing/hash.rb
CHANGED
@@ -4,15 +4,25 @@ module Keisan
|
|
4
4
|
attr_reader :key_value_pairs
|
5
5
|
|
6
6
|
def initialize(key_value_pairs)
|
7
|
-
|
8
|
-
|
9
|
-
|
7
|
+
key_value_pairs = Array(key_value_pairs)
|
8
|
+
if key_value_pairs.size == 1 && key_value_pairs.first.empty?
|
9
|
+
@key_value_pairs = []
|
10
|
+
else
|
11
|
+
@key_value_pairs = key_value_pairs.map {|key_value_pair|
|
12
|
+
validate_and_extract_key_value_pair(key_value_pair)
|
13
|
+
}
|
14
|
+
end
|
10
15
|
end
|
11
16
|
|
12
17
|
private
|
13
18
|
|
14
19
|
def validate_and_extract_key_value_pair(key_value_pair)
|
15
|
-
|
20
|
+
filtered_key_value_pair = key_value_pair.select {|token|
|
21
|
+
!token.is_a?(Tokens::LineSeparator)
|
22
|
+
}
|
23
|
+
key, value = Util.array_split(filtered_key_value_pair) {|token|
|
24
|
+
token.is_a?(Tokens::Colon)
|
25
|
+
}
|
16
26
|
raise Exceptions::ParseError.new("Invalid hash") unless key.size == 1 && value.size >= 1
|
17
27
|
|
18
28
|
key = key.first
|
data/lib/keisan/version.rb
CHANGED
data/lib/keisan.rb
CHANGED
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: keisan
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.9.1
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Christopher Locke
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2021-
|
11
|
+
date: 2021-11-01 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: cmath
|
@@ -139,6 +139,7 @@ files:
|
|
139
139
|
- lib/keisan/ast/block.rb
|
140
140
|
- lib/keisan/ast/boolean.rb
|
141
141
|
- lib/keisan/ast/builder.rb
|
142
|
+
- lib/keisan/ast/cache.rb
|
142
143
|
- lib/keisan/ast/cell.rb
|
143
144
|
- lib/keisan/ast/cell_assignment.rb
|
144
145
|
- lib/keisan/ast/constant_literal.rb
|