keisan 0.8.7 → 0.8.12

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: f7bc1aae204b2b2e112ca0c8f2567023684ddbec83cd1c5c4c1a354cf27eb75b
4
- data.tar.gz: 6353f1b48be09dbbef2cf30167b9fe2d0ac9400fb728def8c75a2241294196e6
3
+ metadata.gz: 39ac1f6356b788327b43e6819d18b136da5bbada5f410b64c3f6995b2877973b
4
+ data.tar.gz: 60522376948b4806c0e45cb4f8e09e5187d37448fae0831630fea1f07608485c
5
5
  SHA512:
6
- metadata.gz: 8fa93fed5b5fd0e55c0330e017e1628b3a3c4dc2b9fcdb2dbbf521b6e7149c9f2ee44544cb1f3cdbc84d09d171fb794f4d6a3a22997b5d8728607ea07542d3ee
7
- data.tar.gz: 471b9f1378dab5e0dae5556ed693e00bd73c2e841f463486b5f7e013867dd1f05b52e675c381607b5f564d84ee0cfd1a4c69a49805f338d7823798bed7f94bff
6
+ metadata.gz: 964905af3a5734456ea5e3f81ddb5183a7512e9cf81567c72586dbab813123160d4aaa085467f2626bd0e29df861f87e44425e216b8ccf174cb9f1251d578122
7
+ data.tar.gz: be653ccfd93f2fa6ce3a33e26be4e14db33e9ccea8d2273d491e8df0aaa16b400291b08e90d66cd48020efc691742bf49667b3547fcae57e8adeaab52bf18d5d
data/README.md CHANGED
@@ -83,6 +83,18 @@ calculator.evaluate("3*x + 1")
83
83
  #=> 61
84
84
  ```
85
85
 
86
+ To perform multiple assignments, lists can be used
87
+
88
+ ```ruby
89
+ calculator = Keisan::Calculator.new
90
+ calculator.evaluate("x = [1, 2]")
91
+ calculator.evaluate("[x[1], y] = [11, 22]")
92
+ calculator.evaluate("x")
93
+ #=> [1, 11]
94
+ calculator.evaluate("y")
95
+ #=> 22
96
+ ```
97
+
86
98
 
87
99
  ##### Specifying functions
88
100
 
@@ -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
@@ -17,8 +17,8 @@ module Keisan
17
17
  child.unbound_functions(local)
18
18
  end
19
19
 
20
- def contains_a?(klass)
21
- super || child.contains_a?(klass)
20
+ def traverse(&block)
21
+ super(&block) || child.traverse(&block)
22
22
  end
23
23
 
24
24
  def deep_dup
@@ -15,8 +15,8 @@ module Keisan
15
15
  node.unbound_functions(context)
16
16
  end
17
17
 
18
- def contains_a?(klass)
19
- super || node.contains_a?(klass)
18
+ def traverse(&block)
19
+ super(&block) || node.traverse(&block)
20
20
  end
21
21
 
22
22
  def deep_dup
@@ -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
@@ -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
@@ -21,11 +21,20 @@ module Keisan
21
21
  end
22
22
  end
23
23
 
24
- def contains_a?(klass)
25
- super || @hash.any? {|k, v| k.to_node.contains_a?(klass) || v.contains_a?(klass) }
24
+ def traverse(&block)
25
+ value = super(&block)
26
+ return value if value
27
+ @hash.each do |k, v|
28
+ value = k.to_node.traverse(&block)
29
+ return value if value
30
+ value = v.traverse(&block)
31
+ return value if value
32
+ end
33
+ false
26
34
  end
27
35
 
28
36
  def evaluate(context = nil)
37
+ return self if frozen?
29
38
  context ||= Context.new
30
39
 
31
40
  @hash = ::Hash[
@@ -81,6 +90,10 @@ module Keisan
81
90
  ])
82
91
  AST::Cell.new(h)
83
92
  end
93
+
94
+ def is_constant?
95
+ @hash.all? {|k,v| v.is_constant?}
96
+ end
84
97
  end
85
98
  end
86
99
  end
@@ -6,6 +6,7 @@ module Keisan
6
6
  end
7
7
 
8
8
  def evaluate(context = nil)
9
+ return self if frozen?
9
10
  context ||= Context.new
10
11
  @children = children.map {|child| child.is_a?(Cell) ? child : child.evaluate(context)}
11
12
  self
@@ -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
@@ -37,12 +37,26 @@ module Keisan
37
37
  value(context)
38
38
  end
39
39
 
40
+ # Takes a block, and does a DFS down the AST, evaluating the received block
41
+ # at each node, passing in the node as the single argument. If the block
42
+ # returns a truthy value at any point, the DFS ends and the return value is
43
+ # percolated up the tree.
44
+ def traverse(&block)
45
+ block.call(self)
46
+ end
47
+
40
48
  def contains_a?(klass)
41
49
  case klass
42
50
  when Array
43
- klass.any? {|k| is_a?(k) }
51
+ klass.any? do |k|
52
+ traverse do |node|
53
+ node.is_a?(k)
54
+ end
55
+ end
44
56
  else
45
- is_a?(klass)
57
+ traverse do |node|
58
+ node.is_a?(klass)
59
+ end
46
60
  end
47
61
  end
48
62
 
@@ -188,6 +202,10 @@ module Keisan
188
202
  def or(other)
189
203
  LogicalOr.new([self, other.to_node])
190
204
  end
205
+
206
+ def is_constant?
207
+ false
208
+ end
191
209
  end
192
210
  end
193
211
  end
@@ -5,6 +5,11 @@ module Keisan
5
5
 
6
6
  def initialize(number)
7
7
  @number = number
8
+ # Reduce the number if possible
9
+ case @number
10
+ when Rational
11
+ @number = @number.numerator if @number.denominator == 1
12
+ end
8
13
  end
9
14
 
10
15
  def value(context = nil)
@@ -25,8 +25,14 @@ module Keisan
25
25
  end
26
26
  end
27
27
 
28
- def contains_a?(klass)
29
- super || children.any? {|child| child.contains_a?(klass) }
28
+ def traverse(&block)
29
+ value = super(&block)
30
+ return value if value
31
+ children.each do |child|
32
+ value = child.traverse(&block)
33
+ return value if value
34
+ end
35
+ false
30
36
  end
31
37
 
32
38
  def freeze
@@ -69,6 +75,10 @@ module Keisan
69
75
  @children = children.map {|child| child.replace(variable, replacement)}
70
76
  self
71
77
  end
78
+
79
+ def is_constant?
80
+ @children.all?(&:is_constant?)
81
+ end
72
82
  end
73
83
  end
74
84
  end
@@ -4,11 +4,12 @@ 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)
7
+ def initialize(context: nil, allow_recursive: false, allow_blocks: true, allow_multiline: true, allow_random: true)
8
8
  @context = context || Context.new(
9
9
  allow_recursive: allow_recursive,
10
10
  allow_blocks: allow_blocks,
11
- allow_multiline: allow_multiline
11
+ allow_multiline: allow_multiline,
12
+ allow_random: allow_random
12
13
  )
13
14
  end
14
15
 
@@ -28,6 +29,10 @@ module Keisan
28
29
  context.allow_multiline
29
30
  end
30
31
 
32
+ def allow_random
33
+ context.allow_random
34
+ end
35
+
31
36
  def evaluate(expression, definitions = {})
32
37
  Evaluator.new(self).evaluate(expression, definitions)
33
38
  end
@@ -4,13 +4,15 @@ module Keisan
4
4
  :variable_registry,
5
5
  :allow_recursive,
6
6
  :allow_multiline,
7
- :allow_blocks
7
+ :allow_blocks,
8
+ :allow_random
8
9
 
9
10
  def initialize(parent: nil,
10
11
  random: nil,
11
12
  allow_recursive: false,
12
13
  allow_multiline: true,
13
14
  allow_blocks: true,
15
+ allow_random: true,
14
16
  shadowed: [])
15
17
  @parent = parent
16
18
  @function_registry = Functions::Registry.new(parent: @parent&.function_registry)
@@ -19,6 +21,7 @@ module Keisan
19
21
  @allow_recursive = allow_recursive
20
22
  @allow_multiline = allow_multiline
21
23
  @allow_blocks = allow_blocks
24
+ @allow_random = allow_random
22
25
  end
23
26
 
24
27
  def allow_recursive!
@@ -111,10 +114,12 @@ module Keisan
111
114
  end
112
115
 
113
116
  def random
117
+ raise Keisan::Exceptions::InvalidExpression.new("Context does not permit expressions with randomness") unless allow_random
114
118
  @random ||= @parent&.random || Random.new
115
119
  end
116
120
 
117
121
  def set_random(random)
122
+ raise Keisan::Exceptions::InvalidExpression.new("Context does not permit expressions with randomness") unless allow_random
118
123
  @random = random
119
124
  end
120
125
 
@@ -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
 
@@ -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
 
@@ -1,3 +1,3 @@
1
1
  module Keisan
2
- VERSION = "0.8.7"
2
+ VERSION = "0.8.12"
3
3
  end
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.7
4
+ version: 0.8.12
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-04-29 00:00:00.000000000 Z
11
+ date: 2021-05-20 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: cmath
@@ -151,6 +151,7 @@ files:
151
151
  - lib/keisan/ast/indexing.rb
152
152
  - lib/keisan/ast/line_builder.rb
153
153
  - lib/keisan/ast/list.rb
154
+ - lib/keisan/ast/list_assignment.rb
154
155
  - lib/keisan/ast/literal.rb
155
156
  - lib/keisan/ast/logical_and.rb
156
157
  - lib/keisan/ast/logical_equal.rb