-
Notifications
You must be signed in to change notification settings - Fork 17
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
[interpreter] (#23) Support operational assignments.
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
1 parent
4c9dc40
commit fca0fbb
Showing
6 changed files
with
408 additions
and
42 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
Oops, something went wrong.