You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
583 lines
11 KiB
583 lines
11 KiB
require_relative "../test_helper"
|
|
|
|
require "lox/interpreter"
|
|
require "lox/parser"
|
|
require "lox/resolver"
|
|
require "lox/scanner"
|
|
|
|
class TestInterpreter < Lox::Test
|
|
def setup
|
|
@scanner = Lox::Scanner.new
|
|
end
|
|
|
|
# def test_literal
|
|
# assert_evaluated(42.0, "42")
|
|
# end
|
|
|
|
def test_grouping
|
|
assert_evaluated "42", "(42)"
|
|
end
|
|
|
|
def test_unary
|
|
assert_evaluated "-42", "-42"
|
|
assert_evaluated "false", "!42"
|
|
assert_evaluated "false", "!true"
|
|
assert_evaluated "true", "!false"
|
|
assert_evaluated "true", "!nil"
|
|
end
|
|
|
|
def test_binary
|
|
assert_evaluated "42", "100 - 58"
|
|
assert_evaluated "42", "84 / 2"
|
|
assert_evaluated "42", "21 * 2"
|
|
|
|
# precedence
|
|
assert_evaluated "42", "2 * 25 - 8"
|
|
|
|
assert_evaluated "42", "40 + 2"
|
|
assert_evaluated "42", "\"4\" + \"2\""
|
|
|
|
assert_evaluated "true", "1 > 0"
|
|
assert_evaluated "false", "0 > 0"
|
|
assert_evaluated "false", "0 > 1"
|
|
|
|
assert_evaluated "true", "1 >= 0"
|
|
assert_evaluated "true", "0 >= 0"
|
|
assert_evaluated "false", "0 >= 1"
|
|
|
|
assert_evaluated "false", "1 < 0"
|
|
assert_evaluated "false", "0 < 0"
|
|
assert_evaluated "true", "0 < 1"
|
|
|
|
assert_evaluated "false", "1 <= 0"
|
|
assert_evaluated "true", "0 <= 0"
|
|
assert_evaluated "true", "0 <= 1"
|
|
|
|
assert_evaluated "true", "0 != 1"
|
|
assert_evaluated "false", "0 != 0"
|
|
assert_evaluated "false", "nil != nil"
|
|
assert_evaluated "true", "nil != 1"
|
|
|
|
assert_evaluated "false", "0 == 1"
|
|
assert_evaluated "true", "0 == 0"
|
|
assert_evaluated "true", "nil == nil"
|
|
assert_evaluated "false", "nil == 1"
|
|
end
|
|
|
|
def test_errors
|
|
[
|
|
"-true",
|
|
"12 > true",
|
|
"true < 23",
|
|
"false * 23",
|
|
"false + 23",
|
|
].each do |src|
|
|
assert_raises Lox::RuntimeError do
|
|
assert_evaluated nil, src
|
|
end
|
|
end
|
|
end
|
|
|
|
def test_stringify
|
|
assert_evaluated "nil", "nil"
|
|
assert_evaluated "42", "42"
|
|
assert_evaluated "42.1", "42.1"
|
|
assert_evaluated "foo", "\"foo\""
|
|
end
|
|
|
|
def test_multiple_statements
|
|
assert_interpreted <<~EXPECTED.chomp, <<~SRC
|
|
one
|
|
true
|
|
3
|
|
EXPECTED
|
|
print "one";
|
|
print true;
|
|
print 2 + 1;
|
|
SRC
|
|
end
|
|
|
|
def test_environment
|
|
assert_interpreted <<~EXPECTED.chomp, <<~SRC
|
|
3
|
|
EXPECTED
|
|
var a = 1;
|
|
var b = 2;
|
|
print a + b;
|
|
SRC
|
|
end
|
|
|
|
def test_assignment
|
|
assert_interpreted <<~EXPECTED.chomp, <<~SRC
|
|
2
|
|
EXPECTED
|
|
var a = 1;
|
|
print a = 2;
|
|
SRC
|
|
end
|
|
|
|
def test_block
|
|
assert_interpreted <<~EXPECTED.chomp, <<~SRC
|
|
inner a
|
|
outer b
|
|
global c
|
|
outer a
|
|
outer b
|
|
global c
|
|
global a
|
|
global b
|
|
global c
|
|
EXPECTED
|
|
var a = "global a";
|
|
var b = "global b";
|
|
var c = "global c";
|
|
{
|
|
var a = "outer a";
|
|
var b = "outer b";
|
|
{
|
|
var a = "inner a";
|
|
print a;
|
|
print b;
|
|
print c;
|
|
}
|
|
print a;
|
|
print b;
|
|
print c;
|
|
}
|
|
print a;
|
|
print b;
|
|
print c;
|
|
SRC
|
|
end
|
|
|
|
def test_uninitialized_vars_are_nil
|
|
assert_interpreted <<~EXPECTED.chomp, <<~SRC
|
|
nil
|
|
EXPECTED
|
|
var a;
|
|
print a;
|
|
SRC
|
|
end
|
|
|
|
def test_if
|
|
assert_interpreted <<~EXPECTED.chomp, <<~SRC
|
|
true
|
|
false
|
|
EXPECTED
|
|
if (true)
|
|
print "true";
|
|
else
|
|
print "false";
|
|
|
|
if (false)
|
|
print "true";
|
|
else
|
|
print "false";
|
|
SRC
|
|
end
|
|
|
|
def test_logical
|
|
assert_interpreted <<~EXPECTED.chomp, <<~SRC
|
|
hi
|
|
yes
|
|
EXPECTED
|
|
print "hi" or 2; // "hi".
|
|
print nil or "yes"; // "yes".
|
|
SRC
|
|
end
|
|
|
|
def test_while
|
|
assert_interpreted <<~EXPECTED.chomp, <<~SRC
|
|
0
|
|
1
|
|
2
|
|
EXPECTED
|
|
var a = 0;
|
|
while (a < 3) {
|
|
print a;
|
|
a = a + 1;
|
|
}
|
|
SRC
|
|
end
|
|
|
|
def test_for
|
|
assert_interpreted <<~EXPECTED.chomp, <<~SRC
|
|
0
|
|
1
|
|
1
|
|
2
|
|
3
|
|
5
|
|
8
|
|
13
|
|
21
|
|
34
|
|
EXPECTED
|
|
var a = 0;
|
|
var temp;
|
|
|
|
for (var b = 1; a < 50; b = temp + b) {
|
|
print a;
|
|
temp = a;
|
|
a = b;
|
|
}
|
|
SRC
|
|
end
|
|
|
|
def test_function
|
|
assert_interpreted <<~EXPECTED.chomp, <<~SRC
|
|
Hi, Dear Reader!
|
|
EXPECTED
|
|
fun sayHi(first, last) {
|
|
print "Hi, " + first + " " + last + "!";
|
|
}
|
|
|
|
sayHi("Dear", "Reader");
|
|
SRC
|
|
|
|
assert_interpreted <<~EXPECTED.chomp, <<~SRC
|
|
0
|
|
1
|
|
2
|
|
EXPECTED
|
|
{
|
|
var i = 0; while (i < 3) { { print i; } i = i + 1; }
|
|
}
|
|
SRC
|
|
|
|
assert_interpreted <<~EXPECTED.chomp, <<~SRC
|
|
0
|
|
1
|
|
2
|
|
EXPECTED
|
|
for (var i = 0; i < 3; i = i + 1) print i;
|
|
SRC
|
|
|
|
assert_interpreted <<~EXPECTED.chomp, <<~SRC
|
|
0
|
|
1
|
|
1
|
|
2
|
|
3
|
|
5
|
|
8
|
|
EXPECTED
|
|
fun fib(n) {
|
|
if (n <= 1) return n;
|
|
return fib(n - 2) + fib(n - 1);
|
|
}
|
|
|
|
for (var i = 0; i < 7; i = i + 1) {
|
|
print fib(i);
|
|
}
|
|
SRC
|
|
end
|
|
|
|
def test_local_functions_and_closures
|
|
assert_interpreted <<~EXPECTED.chomp, <<~SRC
|
|
1
|
|
2
|
|
EXPECTED
|
|
fun makeCounter() {
|
|
var i = 0;
|
|
fun count() {
|
|
i = i + 1;
|
|
print i;
|
|
}
|
|
|
|
return count;
|
|
}
|
|
|
|
var counter = makeCounter();
|
|
counter(); // "1".
|
|
counter(); // "2".
|
|
SRC
|
|
end
|
|
|
|
def test_resolver
|
|
assert_interpreted <<~EXPECTED.chomp, <<~SRC
|
|
1
|
|
EXPECTED
|
|
var foo = 1;
|
|
{
|
|
print foo;
|
|
var foo = 2;
|
|
}
|
|
SRC
|
|
end
|
|
|
|
def test_class
|
|
assert_interpreted "", <<~SRC
|
|
class Breakfast {
|
|
cook() {
|
|
print "Eggs a-fryin'!";
|
|
}
|
|
|
|
serve(who) {
|
|
print "Enjoy your breakfast, " + who + ".";
|
|
}
|
|
}
|
|
SRC
|
|
|
|
assert_interpreted "DevonshireCream", <<~SRC
|
|
class DevonshireCream {
|
|
serveOn() {
|
|
return "Scones";
|
|
}
|
|
}
|
|
|
|
print DevonshireCream; // Prints "DevonshireCream".
|
|
SRC
|
|
|
|
assert_interpreted "Bagel instance", <<~SRC
|
|
class Bagel {}
|
|
var bagel = Bagel();
|
|
print bagel; // Prints "Bagel instance".
|
|
SRC
|
|
end
|
|
|
|
def test_set
|
|
assert_interpreted "bar", <<~SRC
|
|
class Bagel {}
|
|
var bagel = Bagel();
|
|
bagel.foo = "bar";
|
|
print bagel.foo;
|
|
SRC
|
|
end
|
|
|
|
def test_class_methods
|
|
assert_interpreted "Crunch crunch crunch!", <<~SRC
|
|
class Bacon {
|
|
eat() {
|
|
print "Crunch crunch crunch!";
|
|
}
|
|
}
|
|
|
|
Bacon().eat(); // Prints "Crunch crunch crunch!".
|
|
SRC
|
|
|
|
assert_interpreted "The German chocolate cake is delicious!", <<~SRC
|
|
class Cake {
|
|
taste() {
|
|
var adjective = "delicious";
|
|
print "The " + this.flavor + " cake is " + adjective + "!";
|
|
}
|
|
}
|
|
|
|
var cake = Cake();
|
|
cake.flavor = "German chocolate";
|
|
cake.taste(); // Prints "The German chocolate cake is delicious!".
|
|
SRC
|
|
|
|
assert_interpreted "Thing instance", <<~SRC
|
|
class Thing {
|
|
getCallback() {
|
|
fun localFunction() {
|
|
print this;
|
|
}
|
|
|
|
return localFunction;
|
|
}
|
|
}
|
|
|
|
var callback = Thing().getCallback();
|
|
callback();
|
|
SRC
|
|
|
|
assert_interpreted "Jane", <<~SRC
|
|
class Person {
|
|
sayName() {
|
|
print this.name;
|
|
}
|
|
}
|
|
|
|
var jane = Person();
|
|
jane.name = "Jane";
|
|
|
|
var bill = Person();
|
|
bill.name = "Bill";
|
|
|
|
bill.sayName = jane.sayName;
|
|
bill.sayName(); // "Jane"
|
|
SRC
|
|
end
|
|
|
|
def test_invalid_this
|
|
assert_raises Lox::ResolverError, "Can't use 'this' outside of a class." do
|
|
interpret("print this;")
|
|
end
|
|
|
|
assert_raises Lox::ResolverError, "Can't use 'this' outside of a class." do
|
|
interpret(<<~SRC)
|
|
fun notAMethod() {
|
|
print this;
|
|
}
|
|
SRC
|
|
end
|
|
end
|
|
|
|
def test_init
|
|
assert_interpreted <<~EXPECTED.chomp, <<~SRC
|
|
Foo instance
|
|
Foo instance
|
|
Foo instance
|
|
EXPECTED
|
|
class Foo {
|
|
init() {
|
|
print this;
|
|
}
|
|
}
|
|
|
|
var foo = Foo();
|
|
print foo.init();
|
|
SRC
|
|
|
|
assert_raises Lox::ResolverError do
|
|
interpret(<<~SRC)
|
|
class Foo {
|
|
init() {
|
|
return "something else";
|
|
}
|
|
}
|
|
SRC
|
|
end
|
|
|
|
assert_interpreted "", <<~SRC
|
|
class Foo {
|
|
init() {
|
|
return;
|
|
}
|
|
}
|
|
SRC
|
|
end
|
|
|
|
def test_inheritance
|
|
assert_interpreted "", <<~SRC
|
|
class Doughnut {}
|
|
class BostonCream < Doughnut {}
|
|
SRC
|
|
|
|
assert_raises Lox::ResolverError do
|
|
interpret("class Oops < Oops {}")
|
|
end
|
|
|
|
assert_raises Lox::RuntimeError do
|
|
interpret(<<~SRC)
|
|
var NotAClass = "I am totally not a class";
|
|
|
|
class Subclass < NotAClass {} // ?!
|
|
SRC
|
|
end
|
|
end
|
|
|
|
def test_inheriting_methods
|
|
assert_interpreted "Fry until golden brown.", <<~SRC
|
|
class Doughnut {
|
|
cook() {
|
|
print "Fry until golden brown.";
|
|
}
|
|
}
|
|
|
|
class BostonCream < Doughnut {}
|
|
|
|
BostonCream().cook();
|
|
SRC
|
|
end
|
|
|
|
def test_calling_superclass_methods
|
|
assert_interpreted <<~OUT.chomp, <<~SRC
|
|
Fry until golden brown.
|
|
Pipe full of custard and coat with chocolate.
|
|
OUT
|
|
class Doughnut {
|
|
cook() {
|
|
print "Fry until golden brown.";
|
|
}
|
|
}
|
|
|
|
class BostonCream < Doughnut {
|
|
cook() {
|
|
super.cook();
|
|
print "Pipe full of custard and coat with chocolate.";
|
|
}
|
|
}
|
|
|
|
BostonCream().cook();
|
|
SRC
|
|
|
|
assert_interpreted "A method", <<~SRC
|
|
class A {
|
|
method() {
|
|
print "A method";
|
|
}
|
|
}
|
|
|
|
class B < A {
|
|
method() {
|
|
print "B method";
|
|
}
|
|
|
|
test() {
|
|
super.method();
|
|
}
|
|
}
|
|
|
|
class C < B {}
|
|
|
|
C().test();
|
|
SRC
|
|
end
|
|
|
|
def test_invalid_super
|
|
assert_raises Lox::ResolverError do
|
|
interpret(<<~SRC)
|
|
class Eclair {
|
|
cook() {
|
|
super.cook();
|
|
print "Pipe full of crème pâtissière.";
|
|
}
|
|
}
|
|
SRC
|
|
end
|
|
|
|
assert_raises Lox::ResolverError do
|
|
interpret("super.notEvenInAClass();")
|
|
end
|
|
end
|
|
|
|
private
|
|
|
|
def assert_interpreted(expected, src)
|
|
output = interpret(src)
|
|
assert_equal expected, output
|
|
end
|
|
|
|
def assert_evaluated(expected, src)
|
|
assert_interpreted(expected, "print #{src};")
|
|
end
|
|
|
|
def with_stdout
|
|
original = $stdout
|
|
$stdout = StringIO.new
|
|
|
|
yield
|
|
|
|
output = $stdout.string.chomp
|
|
$stdout = original
|
|
output
|
|
end
|
|
|
|
def interpret(src)
|
|
with_stdout {
|
|
stmts = Lox::Parser.new(@scanner.scan(src)).parse!
|
|
interpreter = Lox::Interpreter.new
|
|
|
|
resolver = Lox::Resolver.new(interpreter);
|
|
resolver.resolve(*stmts);
|
|
|
|
interpreter.interpret(stmts)
|
|
}
|
|
end
|
|
|
|
end
|