Skip to content

Commit 033a9bb

Browse files
Merge pull request project-eutopia#117 from project-eutopia/fix_handling_of_constants_in_expressions
Fix handling of constants in expressions
2 parents a9b2d02 + 70c3009 commit 033a9bb

18 files changed

+363
-15
lines changed

lib/keisan/ast/constant_literal.rb

Lines changed: 164 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,170 @@ def to_s
2222
value.to_s
2323
end
2424
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
25189
end
26190
end
27191
end

lib/keisan/ast/function.rb

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,13 @@ def differentiate(variable, context = nil)
9191

9292
self.class.new([self, variable], "diff")
9393
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
94101
end
95102
end
96103
end

lib/keisan/ast/hash.rb

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,10 @@ def to_cell
9090
])
9191
AST::Cell.new(h)
9292
end
93+
94+
def is_constant?
95+
@hash.all? {|k,v| v.is_constant?}
96+
end
9397
end
9498
end
9599
end

lib/keisan/ast/node.rb

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -202,6 +202,10 @@ def and(other)
202202
def or(other)
203203
LogicalOr.new([self, other.to_node])
204204
end
205+
206+
def is_constant?
207+
false
208+
end
205209
end
206210
end
207211
end

lib/keisan/ast/parent.rb

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,10 @@ def replace(variable, replacement)
7575
@children = children.map {|child| child.replace(variable, replacement)}
7676
self
7777
end
78+
79+
def is_constant?
80+
@children.all?(&:is_constant?)
81+
end
7882
end
7983
end
8084
end

lib/keisan/functions/enumerable_function.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ def evaluate(ast_function, context = nil)
2121
context ||= Context.new
2222

2323
operand, arguments, expression = operand_arguments_expression_for(ast_function, context)
24-
24+
2525
# Extract underlying operand for cells
2626
real_operand = operand.is_a?(AST::Cell) ? operand.node : operand
2727

lib/keisan/version.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
11
module Keisan
2-
VERSION = "0.8.11"
2+
VERSION = "0.8.12"
33
end

spec/keisan/ast/boolean_spec.rb

Lines changed: 20 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,12 @@
99
end
1010
end
1111

12+
describe "is_constant?" do
13+
it "is true" do
14+
expect(described_class.new(true).is_constant?).to eq true
15+
end
16+
end
17+
1218
describe "operations" do
1319
it "should reduce to the answer right away" do
1420
res = !Keisan::AST::Boolean.new(true)
@@ -41,11 +47,14 @@
4147
expect(negative_or).to be_a(Keisan::AST::Boolean)
4248
expect(negative_or.value).to eq false
4349

44-
and_other = described_class.new(true).and Keisan::AST::Number.new(1)
45-
or_other = described_class.new(true).or Keisan::AST::Number.new(1)
50+
and_other = described_class.new(true).and Keisan::AST::Variable.new("x")
51+
or_other = described_class.new(true).or Keisan::AST::Variable.new("x")
4652

4753
expect(and_other).to be_a(Keisan::AST::LogicalAnd)
4854
expect(or_other).to be_a(Keisan::AST::LogicalOr)
55+
56+
expect{described_class.new(true).and Keisan::AST::Number.new(1)}.to raise_error(Keisan::Exceptions::InvalidExpression)
57+
expect{described_class.new(true).or Keisan::AST::Number.new(1)}.to raise_error(Keisan::Exceptions::InvalidExpression)
4958
end
5059

5160
it "can do == and != checks" do
@@ -63,11 +72,18 @@
6372
expect(negative_not_equal).to be_a(Keisan::AST::Boolean)
6473
expect(negative_not_equal.value).to eq false
6574

66-
equal_other = described_class.new(true).equal Keisan::AST::Number.new(1)
67-
not_equal_other = described_class.new(true).not_equal Keisan::AST::Number.new(1)
75+
equal_other = described_class.new(true).equal Keisan::AST::Variable.new("x")
76+
not_equal_other = described_class.new(true).not_equal Keisan::AST::Variable.new("x")
6877

6978
expect(equal_other).to be_a(Keisan::AST::LogicalEqual)
7079
expect(not_equal_other).to be_a(Keisan::AST::LogicalNotEqual)
80+
81+
equal_number = described_class.new(true).equal Keisan::AST::Number.new(1)
82+
expect(equal_number).to be_a(Keisan::AST::Boolean)
83+
expect(equal_number.value).to eq false
84+
not_equal_number = described_class.new(true).not_equal Keisan::AST::Number.new(1)
85+
expect(not_equal_number).to be_a(Keisan::AST::Boolean)
86+
expect(not_equal_number.value).to eq true
7187
end
7288
end
7389
end

spec/keisan/ast/date_spec.rb

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,13 @@
11
require "spec_helper"
22

33
RSpec.describe Keisan::AST::Date do
4+
describe "is_constant?" do
5+
it "is true" do
6+
date = Keisan::AST.parse("date(2018, 11, 20)").evaluate
7+
expect(date.is_constant?).to eq true
8+
end
9+
end
10+
411
describe "evaluate" do
512
it "reduces to a date when adding numbers" do
613
ast = Keisan::AST.parse("date(2018, 11, 20) + 1")
@@ -40,8 +47,7 @@
4047
expect(ast.evaluate.value).to eq false
4148

4249
ast = Keisan::AST.parse("date(2000) + date(2000)")
43-
expect(ast.evaluate).to be_a(Keisan::AST::Plus)
44-
expect{ast.evaluate.value}.to raise_error(TypeError)
50+
expect{ast.evaluate}.to raise_error(Keisan::Exceptions::InvalidExpression)
4551
end
4652

4753
it "works in arrays" do

spec/keisan/ast/function_spec.rb

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
require "spec_helper"
2+
3+
RSpec.describe Keisan::AST::Function do
4+
describe "is_constant?" do
5+
it "is false" do
6+
ast = Keisan::Calculator.new.ast("f(1)")
7+
expect(ast.is_constant?).to eq false
8+
end
9+
end
10+
end

0 commit comments

Comments
 (0)