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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 982db21218a4096daa15314fb7de88f1710e1a82be5b122c1b1c4a76323fb44a
4
- data.tar.gz: 4df1eec9b4afd5da69db4a6d40d6900dc4bd9bd249dd1b67646766ba8468fd36
3
+ metadata.gz: 11f1a2644fbb02817db69dd289564d6fdb9bfd09d294cda62b07156723aebc0e
4
+ data.tar.gz: 4b9787c0d558af15d466759e40fe4cfac35131d254d227416144588256e5ea9f
5
5
  SHA512:
6
- metadata.gz: bcc26f352e4f13139a9f0e2c0e1e97be8b3801ca6c2590349aad746fb1283776bf14d4f8ff4eb98049dfd624e7d1b71ce5be5770080f3187a4349c01a5fba4a6
7
- data.tar.gz: 70e240222845d98e231ceb577939a37020b03ba1e3a5b9b88165a3c62ce4ffb6d5aa16cff0d4d2eed3078746b4595d3e104a2545b09f55f6d5b784dba1307bf1
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
@@ -66,7 +66,7 @@ module Keisan
66
66
  end
67
67
 
68
68
  def to_cell
69
- self.class.new(node.to_cell)
69
+ self.class.new(node.to_node)
70
70
  end
71
71
 
72
72
  def to_s
@@ -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
@@ -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
@@ -90,6 +90,10 @@ module Keisan
90
90
  ])
91
91
  AST::Cell.new(h)
92
92
  end
93
+
94
+ def is_constant?
95
+ @hash.all? {|k,v| v.is_constant?}
96
+ end
93
97
  end
94
98
  end
95
99
  end
@@ -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)
@@ -202,6 +202,10 @@ module Keisan
202
202
  def or(other)
203
203
  LogicalOr.new([self, other.to_node])
204
204
  end
205
+
206
+ def is_constant?
207
+ false
208
+ end
205
209
  end
206
210
  end
207
211
  end
@@ -45,8 +45,6 @@ module Keisan
45
45
 
46
46
  children = Array(children)
47
47
  super(children)
48
-
49
- @parsing_operators = parsing_operators
50
48
  end
51
49
 
52
50
  def evaluate_assignments(context = nil)
@@ -75,6 +75,10 @@ module Keisan
75
75
  @children = children.map {|child| child.replace(variable, replacement)}
76
76
  self
77
77
  end
78
+
79
+ def is_constant?
80
+ @children.all?(&:is_constant?)
81
+ end
78
82
  end
79
83
  end
80
84
  end
@@ -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
- @parsing_operators.each.with_index do |parsing_operator, index|
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
@@ -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
- @parsing_operators.each.with_index do |parsing_operator, index|
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
@@ -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)
@@ -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
- ast = parse_ast(expression)
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
- ast = parse_ast(expression)
28
- ast.simplify(context)
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).tap do |ast|
33
- disallowed = disallowed_nodes
34
- if !disallowed.empty? && ast.contains_a?(disallowed)
35
- raise Keisan::Exceptions::InvalidExpression.new("Context does not permit expressions with #{disallowed}")
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].simplify(context)
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
- @argument_variables ||= arguments.map {|argument| AST::Variable.new(argument)}
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
@@ -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
- simplify(ast_function, context)
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.children[0], ast_function.children[1], context)
25
+ while_loop(ast_function, context, simplify: true)
23
26
  end
24
27
 
25
28
  private
26
29
 
27
- def while_loop(logical_node, body_node, context)
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
- while logical_node_evaluates_to_true(logical_node, context)
31
- begin
32
- current = body_node.evaluated(context)
33
- rescue Exceptions::BreakError
34
- break
35
- rescue Exceptions::ContinueError
36
- next
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
- unless bool.is_a?(AST::Boolean)
46
- raise Keisan::Exceptions::InvalidFunctionError.new("while condition must evaluate to a boolean")
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
- if token.sub_tokens.any? {|token| token.is_a?(Tokens::Colon)}
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)
@@ -4,15 +4,25 @@ module Keisan
4
4
  attr_reader :key_value_pairs
5
5
 
6
6
  def initialize(key_value_pairs)
7
- @key_value_pairs = Array(key_value_pairs).map {|key_value_pair|
8
- validate_and_extract_key_value_pair(key_value_pair)
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
- key, value = Util.array_split(key_value_pair) {|token| token.is_a?(Tokens::Colon)}
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
@@ -1,3 +1,3 @@
1
1
  module Keisan
2
- VERSION = "0.8.11"
2
+ VERSION = "0.9.1"
3
3
  end
data/lib/keisan.rb CHANGED
@@ -55,6 +55,7 @@ require "keisan/ast/indexing"
55
55
 
56
56
  require "keisan/ast/line_builder"
57
57
  require "keisan/ast/builder"
58
+ require "keisan/ast/cache"
58
59
  require "keisan/ast"
59
60
 
60
61
  require "keisan/function"
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.8.11
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-05-20 00:00:00.000000000 Z
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