Skip to content

Commit

Permalink
Merge pull request #75 from project-eutopia/freezing_of_variables
Browse files Browse the repository at this point in the history
Freezing of variables
  • Loading branch information
project-eutopia committed Jan 29, 2018
2 parents 809a545 + 124a356 commit 3a54232
Show file tree
Hide file tree
Showing 10 changed files with 248 additions and 137 deletions.
131 changes: 8 additions & 123 deletions lib/keisan/ast/assignment.rb
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
require_relative "variable_assignment"
require_relative "function_assignment"
require_relative "cell_assignment"

module Keisan
module AST
class Assignment < Operator
Expand Down Expand Up @@ -69,136 +73,17 @@ def is_function_definition?

private

def evaluate_cell_assignment(context, lhs, rhs)
lhs = lhs.evaluate(context)

unless lhs.is_a?(Cell)
raise Exceptions::InvalidExpression.new("Unhandled left hand side #{lhs} in assignment")
end

case compound_operator
when :"||"
evaluate_cell_or_assignment(context, lhs, rhs)
when :"&&"
evaluate_cell_and_assignment(context, lhs, rhs)
else
evaluate_cell_non_logical_assignment(context, lhs, rhs)
end
end

def evaluate_cell_or_assignment(context, lhs, rhs)
if lhs.false?
rhs = rhs.evaluate(context)
lhs.node = rhs.is_a?(Cell) ? rhs.node.deep_dup : rhs
rhs
else
lhs
end
end

def evaluate_cell_and_assignment(context, lhs, rhs)
if lhs.true?
rhs = rhs.evaluate(context)
lhs.node = rhs.is_a?(Cell) ? rhs.node.deep_dup : rhs
rhs
else
lhs
end
end

def evaluate_cell_non_logical_assignment(context, lhs, rhs)
rhs = rhs.evaluate(context)
if compound_operator
rhs = rhs.send(compound_operator, lhs.node).evaluate(context)
end

lhs.node = rhs.is_a?(Cell) ? rhs.node.deep_dup : rhs
rhs
end

def evaluate_variable_assignment(context, lhs, rhs)
case compound_operator
when :"||"
evaluate_variable_or_assignment(context, lhs, rhs)
when :"&&"
evaluate_variable_and_assignment(context, lhs, rhs)
else
evaluate_variable_non_logical_assignment(context, lhs, rhs)
end
end

def evaluate_variable_or_assignment(context, lhs, rhs)
if lhs.variable_truthy?(context)
lhs
else
rhs = rhs.evaluate(context)
context.register_variable!(lhs.name, rhs.value(context))
rhs
end
end

def evaluate_variable_and_assignment(context, lhs, rhs)
if lhs.variable_truthy?(context)
rhs = rhs.evaluate(context)
context.register_variable!(lhs.name, rhs.value(context))
rhs
else
context.register_variable!(lhs.name, nil) unless context.has_variable?(lhs.name)
lhs
end
end

def evaluate_variable_non_logical_assignment(context, lhs, rhs)
rhs = rhs.evaluate(context)
rhs_value = rhs.value(context)

if compound_operator
raise Exceptions::InvalidExpression.new("Compound assignment requires variable #{lhs.name} to already exist") unless context.has_variable?(lhs.name)
rhs_value = context.variable(lhs.name).value.send(compound_operator, rhs_value)
end

context.register_variable!(lhs.name, rhs_value, local: local)
# Return the variable assigned value
rhs
VariableAssignment.new(self, context, lhs, rhs).evaluate
end

def evaluate_function_assignment(context, lhs, rhs)
raise Exceptions::InvalidExpression.new("Cannot do compound assignment on functions") if compound_operator

unless lhs.children.all? {|arg| arg.is_a?(Variable)}
raise Exceptions::InvalidExpression.new("Left hand side function must have variables as arguments")
end

argument_names = lhs.children.map(&:name)

verify_rhs_of_function_assignment_is_valid!(context, rhs, argument_names)

context.register_function!(
lhs.name,
Functions::ExpressionFunction.new(
lhs.name,
argument_names,
rhs.evaluate_assignments(context.spawn_child(shadowed: argument_names, transient: true)),
context.transient_definitions
),
local: local
)

rhs
FunctionAssignment.new(context, lhs, rhs, local).evaluate
end

def verify_rhs_of_function_assignment_is_valid!(context, rhs, argument_names)
# Blocks might have local variable/function definitions, so skip check
return if rhs.is_a?(Block)

# Only variables that can appear are those that are arguments to the function
unless rhs.unbound_variables(context) <= Set.new(argument_names)
raise Exceptions::InvalidExpression.new("Unbound variables found in function definition")
end
# Cannot have undefined functions unless allowed by context
unless context.allow_recursive || rhs.unbound_functions(context).empty?
raise Exceptions::InvalidExpression.new("Unbound function definitions are not allowed by current context")
end
def evaluate_cell_assignment(context, lhs, rhs)
CellAssignment.new(self, context, lhs, rhs).evaluate
end
end
end
Expand Down
5 changes: 5 additions & 0 deletions lib/keisan/ast/cell.rb
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,11 @@ def deep_dup
dupped
end

def freeze
node.freeze
super
end

def value(context = nil)
node.value(context)
end
Expand Down
70 changes: 70 additions & 0 deletions lib/keisan/ast/cell_assignment.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
module Keisan
module AST
class CellAssignment
attr_reader :assignment, :context, :lhs, :rhs

def initialize(assignment, context, lhs, rhs)
@assignment = assignment
@context = context
@lhs = lhs
@rhs = rhs
end

def evaluate
lhs = lhs_evaluate_and_check_modifiable

unless lhs.is_a?(Cell)
raise Exceptions::InvalidExpression.new("Unhandled left hand side #{lhs} in assignment")
end

case assignment.compound_operator
when :"||"
evaluate_cell_or_assignment(context, lhs, rhs)
when :"&&"
evaluate_cell_and_assignment(context, lhs, rhs)
else
evaluate_cell_non_logical_assignment(context, lhs, rhs)
end
end

private

def lhs_evaluate_and_check_modifiable
lhs.evaluate(context)
rescue RuntimeError => e
raise Exceptions::UnmodifiableError.new("Cannot modify frozen variables") if e.message =~ /can't modify frozen/
raise
end

def evaluate_cell_or_assignment(context, lhs, rhs)
if lhs.false?
rhs = rhs.evaluate(context)
lhs.node = rhs.is_a?(Cell) ? rhs.node.deep_dup : rhs
rhs
else
lhs
end
end

def evaluate_cell_and_assignment(context, lhs, rhs)
if lhs.true?
rhs = rhs.evaluate(context)
lhs.node = rhs.is_a?(Cell) ? rhs.node.deep_dup : rhs
rhs
else
lhs
end
end

def evaluate_cell_non_logical_assignment(context, lhs, rhs)
rhs = rhs.evaluate(context)
if assignment.compound_operator
rhs = rhs.send(assignment.compound_operator, lhs.node).evaluate(context)
end

lhs.node = rhs.is_a?(Cell) ? rhs.node.deep_dup : rhs
rhs
end
end
end
end
12 changes: 0 additions & 12 deletions lib/keisan/ast/compound_assignment.rb

This file was deleted.

52 changes: 52 additions & 0 deletions lib/keisan/ast/function_assignment.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
module Keisan
module AST
class FunctionAssignment
attr_reader :context, :lhs, :rhs, :local

def initialize(context, lhs, rhs, local)
@context = context
@lhs = lhs
@rhs = rhs
@local = local

unless lhs.children.all? {|arg| arg.is_a?(Variable)}
raise Exceptions::InvalidExpression.new("Left hand side function must have variables as arguments")
end
end

def argument_names
@argument_names ||= lhs.children.map(&:name)
end

def expression_function
Functions::ExpressionFunction.new(
lhs.name,
argument_names,
rhs.evaluate_assignments(context.spawn_child(shadowed: argument_names, transient: true)),
context.transient_definitions
)
end

def evaluate
# Blocks might have local variable/function definitions, so skip check
verify_rhs_of_function_assignment_is_valid! unless rhs.is_a?(Block)

context.register_function!(lhs.name, expression_function, local: local)
rhs
end

private

def verify_rhs_of_function_assignment_is_valid!
# Only variables that can appear are those that are arguments to the function
unless rhs.unbound_variables(context) <= Set.new(argument_names)
raise Exceptions::InvalidExpression.new("Unbound variables found in function definition")
end
# Cannot have undefined functions unless allowed by context
unless context.allow_recursive || rhs.unbound_functions(context).empty?
raise Exceptions::InvalidExpression.new("Unbound function definitions are not allowed by current context")
end
end
end
end
end
5 changes: 5 additions & 0 deletions lib/keisan/ast/hash.rb
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,11 @@ def initialize(key_value_pairs)
@hash = ::Hash[key_value_pairs.map(&:to_a).map {|k,v| [k.value, v.to_node]}]
end

def freeze
values.each(&:freeze)
super
end

def [](key)
key = key.to_node
return nil unless key.is_a?(AST::ConstantLiteral)
Expand Down
5 changes: 5 additions & 0 deletions lib/keisan/ast/parent.rb
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,11 @@ def unbound_functions(context = nil)
end
end

def freeze
children.each(&:freeze)
super
end

def ==(other)
return false unless self.class == other.class

Expand Down
62 changes: 62 additions & 0 deletions lib/keisan/ast/variable_assignment.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
module Keisan
module AST
class VariableAssignment
attr_reader :assignment, :context, :lhs, :rhs

def initialize(assignment, context, lhs, rhs)
@assignment = assignment
@context = context
@lhs = lhs
@rhs = rhs
end

def evaluate
case assignment.compound_operator
when :"||"
evaluate_variable_or_assignment(context, lhs, rhs)
when :"&&"
evaluate_variable_and_assignment(context, lhs, rhs)
else
evaluate_variable_non_logical_assignment(context, lhs, rhs)
end
end

private

def evaluate_variable_or_assignment(context, lhs, rhs)
if lhs.variable_truthy?(context)
lhs
else
rhs = rhs.evaluate(context)
context.register_variable!(lhs.name, rhs.value(context))
rhs
end
end

def evaluate_variable_and_assignment(context, lhs, rhs)
if lhs.variable_truthy?(context)
rhs = rhs.evaluate(context)
context.register_variable!(lhs.name, rhs.value(context))
rhs
else
context.register_variable!(lhs.name, nil) unless context.has_variable?(lhs.name)
lhs
end
end

def evaluate_variable_non_logical_assignment(context, lhs, rhs)
rhs = rhs.evaluate(context)
rhs_value = rhs.value(context)

if assignment.compound_operator
raise Exceptions::InvalidExpression.new("Compound assignment requires variable #{lhs.name} to already exist") unless context.has_variable?(lhs.name)
rhs_value = context.variable(lhs.name).value.send(assignment.compound_operator, rhs_value)
end

context.register_variable!(lhs.name, rhs_value, local: assignment.local)
# Return the variable assigned value
rhs
end
end
end
end
Loading

0 comments on commit 3a54232

Please sign in to comment.