Skip to content

Commit

Permalink
[interpreter] (#23) Support operational assignments.
Browse files Browse the repository at this point in the history
The interpreter now nows how to visit `OpAssign` nodes, including the conditional assignment variants, `||=` and `&&=`. For normal cases, the assignments are rewritten from `a op= b` to `a = a op b`. For conditional assignments, the expansion is closest to `a op a = b`, but with some safeguarding to avoid raising errors when `a` does not exist (it will be initialized to `nil`).
  • Loading branch information
faultyserver committed Nov 6, 2017
1 parent 4c9dc40 commit fca0fbb
Show file tree
Hide file tree
Showing 6 changed files with 408 additions and 42 deletions.
264 changes: 264 additions & 0 deletions spec/interpreter/nodes/op_assign_spec.cr
Original file line number Diff line number Diff line change
@@ -0,0 +1,264 @@
require "../../spec_helper.cr"
require "../../support/nodes.cr"
require "../../support/interpret.cr"

describe "Interpreter - OpAssign" do
# Normal OpAssigns are all handled identically. The exceptions are
# conditional assignments, `||=` and `&&=`. These will be tested later on.
["+=", "-=", "*=", "/=", "%="].each do |op|
describe op do
it "cannot assign to a literal value" do
# This is already asserted by the parser. It is simply repeated here for
# completeness.
expect_raises{ parse_and_interpret %Q(false #{op} 1) }
end


it "does not allow assignment to non-existant values" do
itr = interpret_with_mocked_output %Q(a #{op} 2)
itr.errput.to_s.downcase.should match(/no variable or method `a`/)
end

it "does not allow re-assignment to constants" do
error = expect_raises do
parse_and_interpret %Q(
THING = 1
THING #{op} 2
)
end

(error.message || "").downcase.should match(/re-assignment/)
end
end
end


# Conditional assignments flip the rewritten from `a = a op b` to
# `a op a = b`. The logical semantics then act as normal.
describe "||=" do
it "assigns the target if it is currently `nil`" do
itr = parse_and_interpret %q(
a = nil
a ||= 1
a
)

itr.stack.pop.should eq(val(1))
end

it "does not assign the target if it is not `nil`" do
itr = parse_and_interpret %q(
a = 1
a ||= 2
a
)

itr.stack.pop.should eq(val(1))
end

it "does not visit the value if the target is not `nil`" do
itr = parse_and_interpret %q(
@was_called = false
def foo
@was_called = true
end
a = 1
a ||= foo
@was_called
)

itr.stack.pop.should eq(val(false))
end

it "assigns new ivars" do
itr = parse_and_interpret %q(
@a ||= 1
@a
)

itr.stack.pop.should eq(val(1))
end

it "assigns new vars" do
itr = parse_and_interpret %q(
a ||= 1
a
)

itr.stack.pop.should eq(val(1))
end

it "assigns new underscores" do
itr = parse_and_interpret %q(
_a ||= 1
_a
)

itr.stack.pop.should eq(val(1))
end

it "assigns new constants" do
itr = parse_and_interpret %q(
THING ||= 1
THING
)

itr.stack.pop.should eq(val(1))
end

describe "with a Call target" do
it "calls the assignment method when assigning" do
itr = parse_and_interpret %q(
deftype Foo
def a; @a; end
def a=(other); @a = other; end
end
f = %Foo{}
f.a = nil
f.a ||= 2
f.a
)

itr.stack.pop.should eq(val(2))
end


it "does not visit the value if the call result is truthy" do
itr = parse_and_interpret %q(
deftype Foo
def a; @a; end
def a=(other); @a = other; end
end
@called = false
def not_called
@called = true
end
f = %Foo{}
f.a = 2
f.a ||= not_called
@called
)

itr.stack.pop.should eq(val(false))
end
end
end

describe "&&=" do
it "assigns the target if it is currently truthy" do
itr = parse_and_interpret %q(
a = 1
a &&= 2
a
)

itr.stack.pop.should eq(val(2))
end

it "does not assign the target if it is not truthy" do
itr = parse_and_interpret %q(
a = nil
a &&= 2
a
)

itr.stack.pop.should eq(val(nil))
end

it "does not visit the value if the target is not truthy" do
itr = parse_and_interpret %q(
@was_called = false
def foo
@was_called = true
end
a = nil
a &&= foo
@was_called
)

itr.stack.pop.should eq(val(false))
end

it "assigns new ivars as nil" do
itr = parse_and_interpret %q(
@a &&= 1
@a
)

itr.stack.pop.should eq(val(nil))
end

it "assigns new vars as nil" do
itr = parse_and_interpret %q(
a &&= 1
a
)

itr.stack.pop.should eq(val(nil))
end

it "assigns new underscores as nil" do
itr = parse_and_interpret %q(
_a &&= 1
_a
)

itr.stack.pop.should eq(val(nil))
end

it "assigns new constants as nil" do
itr = parse_and_interpret %q(
THING &&= 1
THING
)

itr.stack.pop.should eq(val(nil))
end

describe "with a Call target" do
it "calls the assignment method when assigning" do
itr = parse_and_interpret %q(
deftype Foo
def a; @a; end
def a=(other); @a = other; end
end
f = %Foo{}
f.a = 1
f.a &&= 2
f.a
)

itr.stack.pop.should eq(val(2))
end


it "does not visit the value if the call result is truthy" do
itr = parse_and_interpret %q(
deftype Foo
def a; @a; end
def a=(other); @a = other; end
end
@called = false
def not_called
@called = true
end
f = %Foo{}
f.a = nil
f.a &&= not_called
@called
)

itr.stack.pop.should eq(val(false))
end
end
end

end
2 changes: 1 addition & 1 deletion src/myst/interpreter.cr
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ module Myst


def visit(node : Node)
raise "Compiler bug: #{node} nodes are not yet supported."
raise "Compiler bug: #{node.class.name} nodes are not yet supported."
end


Expand Down
104 changes: 104 additions & 0 deletions src/myst/interpreter/nodes/op_assign.cr
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
module Myst
class Interpreter
def visit(node : OpAssign)
# Conditional assignments have a different rewritten form than normal
# OpAssigns. Instead of `a = a op b`, they are rewritten to a rough
# equivalent of `a op a = b`.
op_without_assign = node.op[0..-2]

case node.op
when "||="
visit_or_assign(node)
when "&&="
visit_and_assign(node)
else
visit(
SimpleAssign.new(
node.target,
Call.new(node.target, op_without_assign, [node.value]).at(node)
).at(node)
)
end
end


def visit_or_assign(node : OpAssign)
# Vars, Underscores, and Consts will all raise an error if the name
# does not yet exist in the scope, so they should be created in
# advance with a `nil` value to ensure that an assignment happens.
target = node.target
should_assign =
case target
when StaticAssignable
if existing_value = current_scope[target.name]?
!existing_value.truthy?
else
true
end
else
visit(target)
value = stack.pop
!value.truthy?
end

return unless should_assign


rewrite =
# Calls can not get re-written as SimpleAssigns. Although the syntactic
# expansion is the same, the parser handles Calls specially to modify
# the name of the method, rather than create a SimpleAssign. That is
# simply replicated here.
if target.is_a?(Call)
# Equivalent to `receiver.method=(value)
Call.new(target.receiver?, "#{target.name}=", [node.value], nil, infix: false)
else
SimpleAssign.new(node.target, node.value).at(node)
end

visit(rewrite)
end


def visit_and_assign(node : OpAssign)
# Vars, Underscores, and Consts will all raise an error if the name
# does not yet exist in the scope, so they should be created in
# advance with a `nil` value to ensure that an assignment happens.
target = node.target
should_assign =
case target
when StaticAssignable
if existing_value = current_scope[target.name]?
existing_value.truthy?
else
# If the current scope does not contain the requested name, it
# should be created with a default value of `nil`. This will still
# avoid doing the assignment, but ensures that the variable exists.
current_scope.assign(target.name, TNil.new)
false
end
else
visit(target)
value = stack.pop
value.truthy?
end

return unless should_assign


rewrite =
# Calls can not get re-written as SimpleAssigns. Although the syntactic
# expansion is the same, the parser handles Calls specially to modify
# the name of the method, rather than create a SimpleAssign. That is
# simply replicated here.
if target.is_a?(Call)
# Equivalent to `receiver.method=(value)
Call.new(target.receiver?, "#{target.name}=", [node.value], nil, infix: false).at(node)
else
SimpleAssign.new(node.target, node.value).at(node)
end

visit(rewrite)
end
end
end
Loading

0 comments on commit fca0fbb

Please sign in to comment.