keisan 0.8.9 → 0.9.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/README.md +42 -0
- data/lib/keisan.rb +1 -0
- data/lib/keisan/ast/assignment.rb +11 -0
- data/lib/keisan/ast/cache.rb +31 -0
- data/lib/keisan/ast/cell.rb +1 -1
- data/lib/keisan/ast/cell_assignment.rb +5 -1
- data/lib/keisan/ast/constant_literal.rb +164 -0
- data/lib/keisan/ast/function.rb +7 -0
- data/lib/keisan/ast/hash.rb +5 -0
- data/lib/keisan/ast/list.rb +1 -0
- data/lib/keisan/ast/list_assignment.rb +38 -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 +14 -4
- data/lib/keisan/evaluator.rb +8 -7
- data/lib/keisan/functions/enumerable_function.rb +1 -1
- data/lib/keisan/functions/filter.rb +2 -2
- data/lib/keisan/functions/if.rb +6 -3
- data/lib/keisan/functions/while.rb +37 -14
- data/lib/keisan/variables/registry.rb +3 -1
- data/lib/keisan/version.rb +1 -1
- metadata +4 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: c77b933e609cf2ed878939973a183082fa01b87ac49a8bece5278c0df5e66aed
|
4
|
+
data.tar.gz: e737e883ba1fadfa8c9c9ab19f6b995e4cc33ab4f5f2f7f7750bcd0091dd9cfc
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 1cbf2ba0b83b1c74b6d70530dbdebe5a6519cff81c90a2e79216fff1ced7d7ded853d88691afbcdcb75b3995349d82c7b02b0cda915cb871c545080b37c25b7f
|
7
|
+
data.tar.gz: 7877b72629fe6f90c7d508451dc580f88c8f10b9d14529917fb9235c805d84c55d972188dadd85ffb6acef6a81e728911e60a0012968a66408bb8a600cfaca45
|
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
|
@@ -83,6 +113,18 @@ calculator.evaluate("3*x + 1")
|
|
83
113
|
#=> 61
|
84
114
|
```
|
85
115
|
|
116
|
+
To perform multiple assignments, lists can be used
|
117
|
+
|
118
|
+
```ruby
|
119
|
+
calculator = Keisan::Calculator.new
|
120
|
+
calculator.evaluate("x = [1, 2]")
|
121
|
+
calculator.evaluate("[x[1], y] = [11, 22]")
|
122
|
+
calculator.evaluate("x")
|
123
|
+
#=> [1, 11]
|
124
|
+
calculator.evaluate("y")
|
125
|
+
#=> 22
|
126
|
+
```
|
127
|
+
|
86
128
|
|
87
129
|
##### Specifying functions
|
88
130
|
|
data/lib/keisan.rb
CHANGED
@@ -1,5 +1,6 @@
|
|
1
1
|
require_relative "variable_assignment"
|
2
2
|
require_relative "function_assignment"
|
3
|
+
require_relative "list_assignment"
|
3
4
|
require_relative "cell_assignment"
|
4
5
|
|
5
6
|
module Keisan
|
@@ -31,6 +32,8 @@ module Keisan
|
|
31
32
|
evaluate_variable_assignment(context, lhs, rhs)
|
32
33
|
elsif is_function_definition?
|
33
34
|
evaluate_function_assignment(context, lhs, rhs)
|
35
|
+
elsif is_list_assignment?
|
36
|
+
evaluate_list_assignment(context, lhs, rhs)
|
34
37
|
else
|
35
38
|
# Try cell assignment
|
36
39
|
evaluate_cell_assignment(context, lhs, rhs)
|
@@ -71,6 +74,10 @@ module Keisan
|
|
71
74
|
children.first.is_a?(Function)
|
72
75
|
end
|
73
76
|
|
77
|
+
def is_list_assignment?
|
78
|
+
children.first.is_a?(List)
|
79
|
+
end
|
80
|
+
|
74
81
|
private
|
75
82
|
|
76
83
|
def evaluate_variable_assignment(context, lhs, rhs)
|
@@ -82,6 +89,10 @@ module Keisan
|
|
82
89
|
FunctionAssignment.new(context, lhs, rhs, local).evaluate
|
83
90
|
end
|
84
91
|
|
92
|
+
def evaluate_list_assignment(context, lhs, rhs)
|
93
|
+
ListAssignment.new(self, context, lhs, rhs).evaluate
|
94
|
+
end
|
95
|
+
|
85
96
|
def evaluate_cell_assignment(context, lhs, rhs)
|
86
97
|
CellAssignment.new(self, context, lhs, rhs).evaluate
|
87
98
|
end
|
@@ -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
@@ -30,7 +30,11 @@ module Keisan
|
|
30
30
|
private
|
31
31
|
|
32
32
|
def lhs_evaluate_and_check_modifiable
|
33
|
-
lhs.evaluate(context)
|
33
|
+
res = lhs.evaluate(context)
|
34
|
+
if res.frozen?
|
35
|
+
raise Exceptions::UnmodifiableError.new("Cannot modify frozen variables")
|
36
|
+
end
|
37
|
+
res
|
34
38
|
rescue RuntimeError => e
|
35
39
|
raise Exceptions::UnmodifiableError.new("Cannot modify frozen variables") if e.message =~ /can't modify frozen/
|
36
40
|
raise
|
@@ -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
@@ -34,6 +34,7 @@ module Keisan
|
|
34
34
|
end
|
35
35
|
|
36
36
|
def evaluate(context = nil)
|
37
|
+
return self if frozen?
|
37
38
|
context ||= Context.new
|
38
39
|
|
39
40
|
@hash = ::Hash[
|
@@ -89,6 +90,10 @@ module Keisan
|
|
89
90
|
])
|
90
91
|
AST::Cell.new(h)
|
91
92
|
end
|
93
|
+
|
94
|
+
def is_constant?
|
95
|
+
@hash.all? {|k,v| v.is_constant?}
|
96
|
+
end
|
92
97
|
end
|
93
98
|
end
|
94
99
|
end
|
data/lib/keisan/ast/list.rb
CHANGED
@@ -0,0 +1,38 @@
|
|
1
|
+
module Keisan
|
2
|
+
module AST
|
3
|
+
class ListAssignment
|
4
|
+
attr_reader :assignment, :context, :lhs, :rhs
|
5
|
+
|
6
|
+
def initialize(assignment, context, lhs, rhs)
|
7
|
+
@assignment = assignment
|
8
|
+
@context = context
|
9
|
+
@lhs = lhs
|
10
|
+
@rhs = rhs
|
11
|
+
end
|
12
|
+
|
13
|
+
def evaluate
|
14
|
+
rhs = @rhs.evaluate(context)
|
15
|
+
|
16
|
+
if !rhs.is_a?(List)
|
17
|
+
raise Exceptions::InvalidExpression.new("To do multiple assignment, RHS must be a list")
|
18
|
+
end
|
19
|
+
if lhs.children.size != rhs.children.size
|
20
|
+
raise Exceptions::InvalidExpression.new("To do multiple assignment, RHS list must have same length as LHS list")
|
21
|
+
end
|
22
|
+
|
23
|
+
i = 0
|
24
|
+
while i < lhs.children.size
|
25
|
+
lhs_variable = lhs.children[i]
|
26
|
+
rhs_assignment = rhs.children[i]
|
27
|
+
individual_assignment = Assignment.new(
|
28
|
+
children = [lhs_variable, rhs_assignment],
|
29
|
+
local: assignment.local,
|
30
|
+
compound_operator: assignment.compound_operator
|
31
|
+
)
|
32
|
+
individual_assignment.evaluate(context)
|
33
|
+
i += 1
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
38
|
+
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,15 @@ 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)
|
38
48
|
end
|
39
49
|
|
40
50
|
def simplify(expression, definitions = {})
|
41
|
-
Evaluator.new(self).simplify(expression, definitions)
|
51
|
+
Evaluator.new(self, cache: @cache).simplify(expression, definitions)
|
42
52
|
end
|
43
53
|
|
44
54
|
def ast(expression)
|
45
|
-
Evaluator.new(self).parse_ast(expression)
|
55
|
+
Evaluator.new(self, cache: @cache).parse_ast(expression)
|
46
56
|
end
|
47
57
|
|
48
58
|
def define_variable!(name, value)
|
data/lib/keisan/evaluator.rb
CHANGED
@@ -2,8 +2,9 @@ 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 = {})
|
@@ -25,16 +26,16 @@ module Keisan
|
|
25
26
|
def simplify(expression, definitions = {})
|
26
27
|
context = calculator.context.spawn_child(definitions: definitions, transient: true)
|
27
28
|
ast = parse_ast(expression)
|
28
|
-
ast.
|
29
|
+
ast.simplified(context)
|
29
30
|
end
|
30
31
|
|
31
32
|
def parse_ast(expression)
|
32
|
-
AST.parse(expression)
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
end
|
33
|
+
ast = @cache.nil? ? AST.parse(expression) : @cache.fetch_or_build(expression)
|
34
|
+
disallowed = disallowed_nodes
|
35
|
+
if !disallowed.empty? && ast.contains_a?(disallowed)
|
36
|
+
raise Keisan::Exceptions::InvalidExpression.new("Context does not permit expressions with #{disallowed}")
|
37
37
|
end
|
38
|
+
ast
|
38
39
|
end
|
39
40
|
|
40
41
|
private
|
@@ -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
|
@@ -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
|
@@ -1,3 +1,5 @@
|
|
1
|
+
require "set"
|
2
|
+
|
1
3
|
module Keisan
|
2
4
|
module Variables
|
3
5
|
class Registry
|
@@ -5,7 +7,7 @@ module Keisan
|
|
5
7
|
|
6
8
|
def initialize(variables: {}, shadowed: [], parent: nil, use_defaults: true, force: false)
|
7
9
|
@hash = {}
|
8
|
-
@shadowed = Set.new(shadowed.map(&:to_s))
|
10
|
+
@shadowed = ::Set.new(shadowed.map(&:to_s))
|
9
11
|
@parent = parent
|
10
12
|
@use_defaults = use_defaults
|
11
13
|
|
data/lib/keisan/version.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.0
|
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-05-
|
11
|
+
date: 2021-05-26 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
|
@@ -151,6 +152,7 @@ files:
|
|
151
152
|
- lib/keisan/ast/indexing.rb
|
152
153
|
- lib/keisan/ast/line_builder.rb
|
153
154
|
- lib/keisan/ast/list.rb
|
155
|
+
- lib/keisan/ast/list_assignment.rb
|
154
156
|
- lib/keisan/ast/literal.rb
|
155
157
|
- lib/keisan/ast/logical_and.rb
|
156
158
|
- lib/keisan/ast/logical_equal.rb
|