Skip to content

Commit

Permalink
Merge 0a5ace2 into bb55ed8
Browse files Browse the repository at this point in the history
  • Loading branch information
project-eutopia committed Nov 23, 2018
2 parents bb55ed8 + 0a5ace2 commit 170a12f
Show file tree
Hide file tree
Showing 11 changed files with 97 additions and 9 deletions.
5 changes: 4 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,9 @@ calculator.evaluate("f(0-a)", a: 2)
#=> -20
calculator.evaluate("n") # n only exists in the definition of f(x)
#=> Keisan::Exceptions::UndefinedVariableError: n
calculator.evaluate("includes(a, element) = a.reduce(false, found, x, found || (x == element))")
calculator.evaluate("[3, 9].map(x, [1, 3, 5].includes(x))").value
#=> [true, false]
```

This form even supports recursion, but you must explicitly allow it.
Expand Down Expand Up @@ -398,7 +401,7 @@ calculator.evaluate("log10(1000)")
#=> 3.0
```

Furthermore, the constants `PI`, `E`, and `I` are included.
Furthermore, the constants `PI`, `E`, `I`, and `INF` are included.

```ruby
calculator = Keisan::Calculator.new
Expand Down
9 changes: 9 additions & 0 deletions lib/keisan/ast/function.rb
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,15 @@ def value(context = nil)
function_from_context(context).value(self, context)
end

def unbound_variables(context = nil)
context ||= Context.new
if context.has_function?(name)
function_from_context(context).unbound_variables(children, context)
else
super
end
end

def unbound_functions(context = nil)
context ||= Context.new

Expand Down
10 changes: 6 additions & 4 deletions lib/keisan/ast/logical_less_than.rb
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,14 @@ def self.symbol
:"<"
end

def evaluate(context = nil)
children[0].evaluate(context) < children[1].evaluate(context)
protected

def value_operator
:<
end

def value(context = nil)
children.first.value(context) < children.last.value(context)
def operator
:<
end
end
end
Expand Down
6 changes: 6 additions & 0 deletions lib/keisan/function.rb
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,12 @@ def differentiate(ast_function, variable, context = nil)
raise Exceptions::NotImplementedError.new
end

def unbound_variables(children, context)
children.inject(Set.new) do |vars, child|
vars | child.unbound_variables(context)
end
end

protected

def validate_arguments!(count)
Expand Down
12 changes: 10 additions & 2 deletions lib/keisan/functions/enumerable_function.rb
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,14 @@ def value(ast_function, context = nil)
evaluate(ast_function, context)
end

def unbound_variables(children, context)
super - Set.new(shadowing_variable_names(children).map(&:name))
end

def shadowing_variable_names(children)
raise Exceptions::NotImplementedError.new
end

def evaluate(ast_function, context = nil)
validate_arguments!(ast_function.children.count)
context ||= Context.new
Expand All @@ -20,9 +28,9 @@ def evaluate(ast_function, context = nil)

case operand
when AST::List
evaluate_list(operand, arguments, expression, context)
evaluate_list(operand, arguments, expression, context).evaluate(context)
when AST::Hash
evaluate_hash(operand, arguments, expression, context)
evaluate_hash(operand, arguments, expression, context).evaluate(context)
else
raise Exceptions::InvalidFunctionError.new("Unhandled first argument to #{name}: #{operand}")
end
Expand Down
8 changes: 8 additions & 0 deletions lib/keisan/functions/filter.rb
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,14 @@ def initialize
super("filter")
end

def shadowing_variable_names(children)
if children.size == 3
children[1..1]
else
children[1..2]
end
end

private

def evaluate_list(list, arguments, expression, context)
Expand Down
8 changes: 8 additions & 0 deletions lib/keisan/functions/map.rb
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,14 @@ def initialize
super("map")
end

def shadowing_variable_names(children)
if children.size == 3
children[1..1]
else
children[1..2]
end
end

private

def evaluate_list(list, arguments, expression, context)
Expand Down
9 changes: 9 additions & 0 deletions lib/keisan/functions/reduce.rb
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,19 @@ class Reduce < EnumerableFunction
# Reduces (list, initial, accumulator, variable, expression)
# e.g. reduce([1,2,3,4], 0, total, x, total+x)
# should give 10
# When hash: (hash, initial, accumulator, key, value, expression)
def initialize
super("reduce")
end

def shadowing_variable_names(children)
if children.size == 5
children[2..3]
else
children[2..4]
end
end

protected

def verify_arguments!(arguments)
Expand Down
3 changes: 2 additions & 1 deletion lib/keisan/variables/default_registry.rb
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,8 @@ class DefaultRegistry < Registry
VARIABLES = {
"PI" => Math::PI,
"E" => Math::E,
"I" => Complex(0,1)
"I" => Complex(0,1),
"INF" => Float::INFINITY
}

def self.registry
Expand Down
34 changes: 34 additions & 0 deletions spec/keisan/ast/function_assignment_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
require "spec_helper"

RSpec.describe Keisan::AST::FunctionAssignment do
describe "unbound_variables" do
it "shadows the appropriate enumerable method variables" do
ast = Keisan::AST.parse("double(a) = a.map(x, 2*x)")
expect(ast.unbound_variables).to eq Set["a"]

ast = Keisan::AST.parse("triple(h) = h.map(k, v, [k, 3*v])")
expect(ast.unbound_variables).to eq Set["h"]

ast = Keisan::AST.parse("even(a) = a.filter(x, x % 2 == 0)")
expect(ast.unbound_variables).to eq Set["a"]

ast = Keisan::AST.parse("odd(h) = h.filter(k, v, v % 2 == 1)")
expect(ast.unbound_variables).to eq Set["h"]

ast = Keisan::AST.parse("include(a,x) = a.reduce(false, found, y, found || (x == y))")
expect(ast.unbound_variables).to eq Set["a", "x"]

ast = Keisan::AST.parse("include(h,x) = h.reduce(false, found, k, v, found || (v == x))")
expect(ast.unbound_variables).to eq Set["h", "x"]
end
end

it "works with complex reduce expression" do
calculator = Keisan::Calculator.new
calculator.evaluate("minimum(a) = a.reduce(INF, current_min, element, if (element < current_min, element, current_min))")
expect(calculator.evaluate("minimum([5,1,3])")).to eq 1

calculator.evaluate("includes(a, element) = a.reduce(false, found, x, found || (x == element))")
expect(calculator.evaluate("[3, 9].map(x, [1, 3, 5].includes(x))")).to eq([true, false])
end
end
2 changes: 1 addition & 1 deletion spec/readme_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
digest = Digest::SHA256.hexdigest(content)

# cat README.md | sha256sum
expected_digest = "2b80635dad47bcbaf98d081e0a041ed5357377d89fb272e2081052e1b1b4e605"
expected_digest = "c430ca4bd926e553e7414c96f89b8f81ef8bf3c491c5484e9f9599f38e06e2f7"
if digest != expected_digest
raise "Invalid README file detected with SHA256 digest of #{digest}. Use command `cat README.md | sha256sum` to get correct digest if your changes to the README are safe. Aborting README test."
end
Expand Down

0 comments on commit 170a12f

Please sign in to comment.