Skip to content

Commit

Permalink
Merge pull request #117 from project-eutopia/fix_handling_of_constant…
Browse files Browse the repository at this point in the history
…s_in_expressions

Fix handling of constants in expressions
  • Loading branch information
project-eutopia committed May 20, 2021
2 parents a9b2d02 + 70c3009 commit 033a9bb
Show file tree
Hide file tree
Showing 18 changed files with 363 additions and 15 deletions.
164 changes: 164 additions & 0 deletions lib/keisan/ast/constant_literal.rb
Expand Up @@ -22,6 +22,170 @@ def to_s
value.to_s
end
end

def is_constant?
true
end

def +(other)
if other.is_constant?
raise Keisan::Exceptions::InvalidExpression.new("Cannot add #{self.class} to #{other.class}")
else
super
end
end

def -(other)
if other.is_constant?
raise Keisan::Exceptions::InvalidExpression.new("Cannot subtract #{self.class} from #{other.class}")
else
super
end
end

def *(other)
if other.is_constant?
raise Keisan::Exceptions::InvalidExpression.new("Cannot multiply #{self.class} and #{other.class}")
else
super
end
end

def /(other)
if other.is_constant?
raise Keisan::Exceptions::InvalidExpression.new("Cannot divide #{self.class} and #{other.class}")
else
super
end
end

def %(other)
if other.is_constant?
raise Keisan::Exceptions::InvalidExpression.new("Cannot modulo #{self.class} and #{other.class}")
else
super
end
end

def !
raise Keisan::Exceptions::InvalidExpression.new("Cannot take logical not of #{self.class}")
end

def ~
raise Keisan::Exceptions::InvalidExpression.new("Cannot take bitwise not of #{self.class}")
end

def +@
raise Keisan::Exceptions::InvalidExpression.new("Cannot take unary plus of #{self.class}")
end

def -@
raise Keisan::Exceptions::InvalidExpression.new("Cannot take unary minus of #{self.class}")
end

def **(other)
if other.is_constant?
raise Keisan::Exceptions::InvalidExpression.new("Cannot exponentiate #{self.class} and #{other.class}")
else
super
end
end

def &(other)
if other.is_constant?
raise Keisan::Exceptions::InvalidExpression.new("Cannot bitwise and #{self.class} and #{other.class}")
else
super
end
end

def ^(other)
if other.is_constant?
raise Keisan::Exceptions::InvalidExpression.new("Cannot bitwise xor #{self.class} and #{other.class}")
else
super
end
end

def |(other)
if other.is_constant?
raise Keisan::Exceptions::InvalidExpression.new("Cannot bitwise or #{self.class} and #{other.class}")
else
super
end
end

def <<(other)
if other.is_constant?
raise Keisan::Exceptions::InvalidExpression.new("Cannot bitwise left shift #{self.class} and #{other.class}")
else
super
end
end

def >>(other)
if other.is_constant?
raise Keisan::Exceptions::InvalidExpression.new("Cannot bitwise right shift #{self.class} and #{other.class}")
else
super
end
end

def >(other)
if other.is_constant?
raise Keisan::Exceptions::InvalidExpression.new("Cannot compute #{self.class} > #{other.class}")
else
super
end
end

def >=(other)
if other.is_constant?
raise Keisan::Exceptions::InvalidExpression.new("Cannot compute #{self.class} >= #{other.class}")
else
super
end
end

def <(other)
if other.is_constant?
raise Keisan::Exceptions::InvalidExpression.new("Cannot compute #{self.class} < #{other.class}")
else
super
end
end

def <=(other)
if other.is_constant?
raise Keisan::Exceptions::InvalidExpression.new("Cannot compute #{self.class} <= #{other.class}")
else
super
end
end

def equal(other)
other.is_constant? ? Boolean.new(false) : super
end

def not_equal(other)
other.is_constant? ? Boolean.new(true) : super
end

def and(other)
if other.is_constant?
raise Keisan::Exceptions::InvalidExpression.new("Cannot logical and #{self.class} and #{other.class}")
else
super
end
end

def or(other)
if other.is_constant?
raise Keisan::Exceptions::InvalidExpression.new("Cannot logical or #{self.class} and #{other.class}")
else
super
end
end
end
end
end
7 changes: 7 additions & 0 deletions lib/keisan/ast/function.rb
Expand Up @@ -91,6 +91,13 @@ def differentiate(variable, context = nil)

self.class.new([self, variable], "diff")
end

# Functions cannot be guaranteed to be constant even if the arguments
# are constants, because there might be randomness involved in the
# outputs.
def is_constant?
false
end
end
end
end
4 changes: 4 additions & 0 deletions lib/keisan/ast/hash.rb
Expand Up @@ -90,6 +90,10 @@ def to_cell
])
AST::Cell.new(h)
end

def is_constant?
@hash.all? {|k,v| v.is_constant?}
end
end
end
end
4 changes: 4 additions & 0 deletions lib/keisan/ast/node.rb
Expand Up @@ -202,6 +202,10 @@ def and(other)
def or(other)
LogicalOr.new([self, other.to_node])
end

def is_constant?
false
end
end
end
end
4 changes: 4 additions & 0 deletions lib/keisan/ast/parent.rb
Expand Up @@ -75,6 +75,10 @@ def replace(variable, replacement)
@children = children.map {|child| child.replace(variable, replacement)}
self
end

def is_constant?
@children.all?(&:is_constant?)
end
end
end
end
2 changes: 1 addition & 1 deletion lib/keisan/functions/enumerable_function.rb
Expand Up @@ -21,7 +21,7 @@ def evaluate(ast_function, context = nil)
context ||= Context.new

operand, arguments, expression = operand_arguments_expression_for(ast_function, context)

# Extract underlying operand for cells
real_operand = operand.is_a?(AST::Cell) ? operand.node : operand

Expand Down
2 changes: 1 addition & 1 deletion lib/keisan/version.rb
@@ -1,3 +1,3 @@
module Keisan
VERSION = "0.8.11"
VERSION = "0.8.12"
end
24 changes: 20 additions & 4 deletions spec/keisan/ast/boolean_spec.rb
Expand Up @@ -9,6 +9,12 @@
end
end

describe "is_constant?" do
it "is true" do
expect(described_class.new(true).is_constant?).to eq true
end
end

describe "operations" do
it "should reduce to the answer right away" do
res = !Keisan::AST::Boolean.new(true)
Expand Down Expand Up @@ -41,11 +47,14 @@
expect(negative_or).to be_a(Keisan::AST::Boolean)
expect(negative_or.value).to eq false

and_other = described_class.new(true).and Keisan::AST::Number.new(1)
or_other = described_class.new(true).or Keisan::AST::Number.new(1)
and_other = described_class.new(true).and Keisan::AST::Variable.new("x")
or_other = described_class.new(true).or Keisan::AST::Variable.new("x")

expect(and_other).to be_a(Keisan::AST::LogicalAnd)
expect(or_other).to be_a(Keisan::AST::LogicalOr)

expect{described_class.new(true).and Keisan::AST::Number.new(1)}.to raise_error(Keisan::Exceptions::InvalidExpression)
expect{described_class.new(true).or Keisan::AST::Number.new(1)}.to raise_error(Keisan::Exceptions::InvalidExpression)
end

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

equal_other = described_class.new(true).equal Keisan::AST::Number.new(1)
not_equal_other = described_class.new(true).not_equal Keisan::AST::Number.new(1)
equal_other = described_class.new(true).equal Keisan::AST::Variable.new("x")
not_equal_other = described_class.new(true).not_equal Keisan::AST::Variable.new("x")

expect(equal_other).to be_a(Keisan::AST::LogicalEqual)
expect(not_equal_other).to be_a(Keisan::AST::LogicalNotEqual)

equal_number = described_class.new(true).equal Keisan::AST::Number.new(1)
expect(equal_number).to be_a(Keisan::AST::Boolean)
expect(equal_number.value).to eq false
not_equal_number = described_class.new(true).not_equal Keisan::AST::Number.new(1)
expect(not_equal_number).to be_a(Keisan::AST::Boolean)
expect(not_equal_number.value).to eq true
end
end
end
10 changes: 8 additions & 2 deletions spec/keisan/ast/date_spec.rb
@@ -1,6 +1,13 @@
require "spec_helper"

RSpec.describe Keisan::AST::Date do
describe "is_constant?" do
it "is true" do
date = Keisan::AST.parse("date(2018, 11, 20)").evaluate
expect(date.is_constant?).to eq true
end
end

describe "evaluate" do
it "reduces to a date when adding numbers" do
ast = Keisan::AST.parse("date(2018, 11, 20) + 1")
Expand Down Expand Up @@ -40,8 +47,7 @@
expect(ast.evaluate.value).to eq false

ast = Keisan::AST.parse("date(2000) + date(2000)")
expect(ast.evaluate).to be_a(Keisan::AST::Plus)
expect{ast.evaluate.value}.to raise_error(TypeError)
expect{ast.evaluate}.to raise_error(Keisan::Exceptions::InvalidExpression)
end

it "works in arrays" do
Expand Down
10 changes: 10 additions & 0 deletions spec/keisan/ast/function_spec.rb
@@ -0,0 +1,10 @@
require "spec_helper"

RSpec.describe Keisan::AST::Function do
describe "is_constant?" do
it "is false" do
ast = Keisan::Calculator.new.ast("f(1)")
expect(ast.is_constant?).to eq false
end
end
end
16 changes: 16 additions & 0 deletions spec/keisan/ast/hash_spec.rb
@@ -1,6 +1,22 @@
require "spec_helper"

RSpec.describe Keisan::AST::Hash do
describe "is_constant?" do
it "is true when all elements are constant" do
hash = {"foo" => {"a" => 1, "b" => 2}, "bar" => {"c" => 3, "d" => 4}}.to_node
expect(hash.is_constant?).to eq true
end

it "is false if one element is not constant" do
hash = {"foo" => {"a" => 1, "b" => 2}, "bar" => {"c" => 3, "d" => 4}}.to_node
hash = described_class.new([
["a".to_node, 1.to_node],
["b".to_node, Keisan::AST::Variable.new("x")]
])
expect(hash.is_constant?).to eq false
end
end

describe "to_node" do
it "can created nested hashes" do
hash = {"foo" => {"a" => 1, "b" => 2}, "bar" => {"c" => 3, "d" => 4}}.to_node
Expand Down
14 changes: 14 additions & 0 deletions spec/keisan/ast/list_spec.rb
@@ -1,6 +1,20 @@
require "spec_helper"

RSpec.describe Keisan::AST::List do
describe "is_constant?" do
it "is true when all elements are constant" do
list = [[1,"x"],[2,"y",true]].to_node
expect(list.is_constant?).to eq true
end

it "is false if one element is not constant" do
list = described_class.new([
"a".to_node, Keisan::AST::Variable.new("x")
])
expect(list.is_constant?).to eq false
end
end

describe "to_node" do
it "can created nested lists" do
node = [[1,"x"],[2,"y",true]].to_node
Expand Down
12 changes: 10 additions & 2 deletions spec/keisan/ast/null_spec.rb
@@ -1,6 +1,12 @@
require "spec_helper"

RSpec.describe Keisan::AST::Null do
describe "is_constant?" do
it "is true" do
expect(Keisan::AST::Null.new.is_constant?).to eq true
end
end

describe "logical operations" do
it "can do == and != checks" do
positive_equal = described_class.new.equal described_class.new
Expand All @@ -14,8 +20,10 @@
expect(negative_not_equal).to be_a(Keisan::AST::Boolean)
expect(negative_not_equal.value).to eq false

expect(other_equal).to be_a(Keisan::AST::LogicalEqual)
expect(other_not_equal).to be_a(Keisan::AST::LogicalNotEqual)
expect(other_equal).to be_a(Keisan::AST::Boolean)
expect(other_equal.value).to eq false
expect(other_not_equal).to be_a(Keisan::AST::Boolean)
expect(other_not_equal.value).to eq true
end
end
end

0 comments on commit 033a9bb

Please sign in to comment.