package fr.celticinfo.lox import fr.celticinfo.loxext.RpnPrinter import org.junit.jupiter.api.Assertions import org.junit.jupiter.api.assertDoesNotThrow import java.io.ByteArrayOutputStream import java.io.PrintStream import kotlin.test.* import kotlin.test.Test class InterpreterTest { @BeforeTest fun setUp() { Lox.resetError() } @Test fun `validate interpreter`() { val code = """ 1 + 2 * 3 - 4 / 5; """.trimIndent() val scanner = Scanner(code) val tokens = scanner.scanTokens() val parser = Parser(tokens) val statements = parser.parse() assertEquals(1, statements.size) val stmt = statements.first() assertTrue(stmt is Expression) val expr = stmt.expression assertEquals("1.0 2.0 3.0 * + 4.0 5.0 / -", RpnPrinter().print(expr)) } @Test fun `Division by zero should raise error`() { val code = """ 1 / 0; """.trimIndent() val scanner = Scanner(code) val tokens = scanner.scanTokens() val parser = Parser(tokens) val statements = parser.parse() assertEquals(1, statements.size) assertFailsWith( block = { Interpreter().interpret(statements) } ) } @Test fun `Invalid type raise error`() { val code = """ 1 + false; """.trimIndent() val scanner = Scanner(code) val tokens = scanner.scanTokens() val parser = Parser(tokens) val statements = parser.parse() assertEquals(1, statements.size) assertFailsWith( block = { Interpreter().interpret(statements) } ) } @Test fun `Shadowing a variable should not raise error`() { val code = """ var a = 1; { var a = 2; } print a; """.trimIndent() val scanner = Scanner(code) val tokens = scanner.scanTokens() val parser = Parser(tokens) val statements = parser.parse() assertEquals(3, statements.size) assertDoesNotThrow { Interpreter().interpret(statements) } } @Test fun `Variable reassignment should not raise error`() { val code = """ var a = 1; a = 2; print a; """.trimIndent() val scanner = Scanner(code) val tokens = scanner.scanTokens() val parser = Parser(tokens) val statements = parser.parse() assertEquals(3, statements.size) assertDoesNotThrow { Interpreter().interpret(statements) } } @Test fun `Variable reassignment with different type should not raise error`() { val code = """ var a = 1; a = false; print a; """.trimIndent() val scanner = Scanner(code) val tokens = scanner.scanTokens() val parser = Parser(tokens) val statements = parser.parse() assertEquals(3, statements.size) assertDoesNotThrow { Interpreter().interpret(statements) } } @Test fun `Variable shadowing should not raise error`() { val code = """ var a = 1; { var a = false; print a; } print a; """.trimIndent() val scanner = Scanner(code) val tokens = scanner.scanTokens() val parser = Parser(tokens) val statements = parser.parse() assertEquals(3, statements.size) assertDoesNotThrow { Interpreter().interpret(statements) } } @Test fun `Variable shadowing with different type should not raise error`() { val code = """ var a = 1; { var a = false; print a; } print a; """.trimIndent() val scanner = Scanner(code) val tokens = scanner.scanTokens() val parser = Parser(tokens) val statements = parser.parse() assertEquals(3, statements.size) assertDoesNotThrow { Interpreter().interpret(statements) } } @Test fun `Variable shadowing should work with block`() { val standardOut = System.out val outputStreamCaptor = ByteArrayOutputStream() System.setOut(PrintStream(outputStreamCaptor)) try { val code = """ 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; """.trimIndent() val scanner = Scanner(code) val tokens = scanner.scanTokens() val parser = Parser(tokens) val statements = parser.parse() assertEquals(7, statements.size) val interpreter = Interpreter() val resolver = Resolver(interpreter) resolver.resolve(statements) interpreter.interpret(statements) val output = outputStreamCaptor.toString().trim() assertEquals("inner a\nouter b\nglobal c\nouter a\nouter b\nglobal c\nglobal a\nglobal b\nglobal c", output) } finally { System.setOut(standardOut) } } @Test fun `If statement should work`() { val standardOut = System.out val outputStreamCaptor = ByteArrayOutputStream() System.setOut(PrintStream(outputStreamCaptor)) try { val code = """ if (1 < 2) print "true"; """.trimIndent() val scanner = Scanner(code) val tokens = scanner.scanTokens() val parser = Parser(tokens) val statements = parser.parse() assertEquals(1, statements.size) Interpreter().interpret(statements) val output = outputStreamCaptor.toString().trim() assertEquals("true", output) } finally { System.setOut(standardOut) } } @Test fun `If statement with else should work`() { val standardOut = System.out val outputStreamCaptor = ByteArrayOutputStream() System.setOut(PrintStream(outputStreamCaptor)) try { val code = """ if (1 > 2) print "true"; else print "false"; """.trimIndent() val scanner = Scanner(code) val tokens = scanner.scanTokens() val parser = Parser(tokens) val statements = parser.parse() assertEquals(1, statements.size) Interpreter().interpret(statements) val output = outputStreamCaptor.toString().trim() assertEquals("false", output) } finally { System.setOut(standardOut) } } @Test fun `If statement with else if should work`() { val standardOut = System.out val outputStreamCaptor = ByteArrayOutputStream() System.setOut(PrintStream(outputStreamCaptor)) try { val code = """ if (1 > 2) print "true"; else if (1 < 2) print "false"; """.trimIndent() val scanner = Scanner(code) val tokens = scanner.scanTokens() val parser = Parser(tokens) val statements = parser.parse() assertEquals(1, statements.size) Interpreter().interpret(statements) val output = outputStreamCaptor.toString().trim() assertEquals("false", output) } finally { System.setOut(standardOut) } } @Test fun `If statement with else if and else should work`() { val standardOut = System.out val outputStreamCaptor = ByteArrayOutputStream() System.setOut(PrintStream(outputStreamCaptor)) try { val code = """ if (1 > 2) print "true"; else if (1 < 2) print "false"; else print "else"; """.trimIndent() val scanner = Scanner(code) val tokens = scanner.scanTokens() val parser = Parser(tokens) val statements = parser.parse() assertEquals(1, statements.size) Interpreter().interpret(statements) val output = outputStreamCaptor.toString().trim() assertEquals("false", output) } finally { System.setOut(standardOut) } } @Test fun `Fibonacci test`() { val standardOut = System.out val outputStreamCaptor = ByteArrayOutputStream() System.setOut(PrintStream(outputStreamCaptor)) try { val code = """ var a = 0; var temp; for (var b = 1; a < 10000; b = temp + b) { print a; temp = a; a = b; } """.trimIndent() val scanner = Scanner(code) val tokens = scanner.scanTokens() val parser = Parser(tokens) val statements = parser.parse() assertEquals(3, statements.size) val interpreter = Interpreter() val resolver = Resolver(interpreter) resolver.resolve(statements) interpreter.interpret(statements) val output = outputStreamCaptor.toString().trim() assertEquals("0\n" + "1\n" + "1\n" + "2\n" + "3\n" + "5\n" + "8\n" + "13\n" + "21\n" + "34\n" + "55\n" + "89\n" + "144\n" + "233\n" + "377\n" + "610\n" + "987\n" + "1597\n" + "2584\n" + "4181\n" + "6765", output) } finally { System.setOut(standardOut) } } @Test fun `Clock native function should return a valid timestamp`() { val standardOut = System.out val outputStreamCaptor = ByteArrayOutputStream() System.setOut(PrintStream(outputStreamCaptor)) try { val code = """ print clock(); """.trimIndent() val scanner = Scanner(code) val tokens = scanner.scanTokens() val parser = Parser(tokens) val statements = parser.parse() Assertions.assertEquals(1, statements.size) Interpreter().interpret(statements) val output = outputStreamCaptor.toString().trim() val timestamp = output.toDoubleOrNull() Assertions.assertNotNull(timestamp) Assertions.assertTrue(timestamp!! >= 0) } finally { System.setOut(standardOut) } } @Test fun `Function should work`() { val standardOut = System.out val outputStreamCaptor = ByteArrayOutputStream() System.setOut(PrintStream(outputStreamCaptor)) try { val code = """ fun sayHi(first, last) { print "Hi, " + first + " " + last + "!"; } sayHi("Dear", "Reader"); """ val scanner = Scanner(code) val tokens = scanner.scanTokens() val parser = Parser(tokens) val statements = parser.parse() assertEquals(2, statements.size) val interpreter = Interpreter() val resolver = Resolver(interpreter) resolver.resolve(statements) interpreter.interpret(statements) val output = outputStreamCaptor.toString().trim() assertEquals("Hi, Dear Reader!", output) } finally { System.setOut(standardOut) } } @Test fun `Function should work with return statement`() { val standardOut = System.out val outputStreamCaptor = ByteArrayOutputStream() System.setOut(PrintStream(outputStreamCaptor)) try { val code = """ fun fib(n) { if (n <= 1) return n; return fib(n - 1) + fib(n - 2); } print fib(10); """ val scanner = Scanner(code) val tokens = scanner.scanTokens() val parser = Parser(tokens) val statements = parser.parse() assertEquals(2, statements.size) val interpreter = Interpreter() val resolver = Resolver(interpreter) resolver.resolve(statements) interpreter.interpret(statements) val output = outputStreamCaptor.toString().trim() assertEquals("55", output) } finally { System.setOut(standardOut) } } @Test fun `Function should work with return statement in block`() { val standardOut = System.out val outputStreamCaptor = ByteArrayOutputStream() System.setOut(PrintStream(outputStreamCaptor)) try { val code = """ fun fib(n) { if (n <= 1) { return n; } return fib(n - 1) + fib(n - 2); } print fib(10); """ val scanner = Scanner(code) val tokens = scanner.scanTokens() val parser = Parser(tokens) val statements = parser.parse() assertEquals(2, statements.size) val interpreter = Interpreter() val resolver = Resolver(interpreter) resolver.resolve(statements) interpreter.interpret(statements) val output = outputStreamCaptor.toString().trim() assertEquals("55", output) } finally { System.setOut(standardOut) } } @Test fun `Function should work with return statement in block with shadowing`() { val standardOut = System.out val outputStreamCaptor = ByteArrayOutputStream() System.setOut(PrintStream(outputStreamCaptor)) try { val code = """ fun fib(n) { var a = 1; if (n <= 1) { return n; } return fib(n - 1) + fib(n - 2); } print fib(10); """ val scanner = Scanner(code) val tokens = scanner.scanTokens() val parser = Parser(tokens) val statements = parser.parse() assertEquals(2, statements.size) val interpreter = Interpreter() val resolver = Resolver(interpreter) resolver.resolve(statements) interpreter.interpret(statements) val output = outputStreamCaptor.toString().trim() assertEquals("55", output) } finally { System.setOut(standardOut) } } @Test fun `Closures should work`() { val standardOut = System.out val outputStreamCaptor = ByteArrayOutputStream() System.setOut(PrintStream(outputStreamCaptor)) try { val code = """ fun makeCounter() { var i = 0; fun count() { i = i + 1; print i; } return count; } var counter = makeCounter(); counter(); counter(); counter(); """ val scanner = Scanner(code) val tokens = scanner.scanTokens() val parser = Parser(tokens) val statements = parser.parse() assertEquals(5, statements.size) val interpreter = Interpreter() val resolver = Resolver(interpreter) resolver.resolve(statements) interpreter.interpret(statements) val output = outputStreamCaptor.toString().trim() assertEquals("1\n2\n3", output) } finally { System.setOut(standardOut) } } @Test fun `Closures should work with shadowing`() { val standardOut = System.out val outputStreamCaptor = ByteArrayOutputStream() System.setOut(PrintStream(outputStreamCaptor)) try { val code = """ fun makeCounter() { var i = 0; fun count() { var i = 0; i = i + 1; print i; } return count; } var counter = makeCounter(); counter(); counter(); counter(); """ val scanner = Scanner(code) val tokens = scanner.scanTokens() val parser = Parser(tokens) val statements = parser.parse() assertEquals(5, statements.size) val interpreter = Interpreter() val resolver = Resolver(interpreter) resolver.resolve(statements) interpreter.interpret(statements) val output = outputStreamCaptor.toString().trim() assertEquals("1\n1\n1", output) } finally { System.setOut(standardOut) } } @Test fun `Initiate a variable with the same name as a function should not work`() { val code = """ fun a() { return 1; } var a = a(); print a; """ val scanner = Scanner(code) val tokens = scanner.scanTokens() val parser = Parser(tokens) val statements = parser.parse() assertEquals(3, statements.size) val interpreter = Interpreter() val resolver = Resolver(interpreter) resolver.resolve(statements) assert(Lox.hadError()) } @Test fun `Initiate a variable with the same name from an outer scope variable should not work`() { val code = """ var a = 1; { var a = a; print a; } """ assert(!Lox.hadError()) val scanner = Scanner(code) val tokens = scanner.scanTokens() val parser = Parser(tokens) val statements = parser.parse() assertEquals(2, statements.size) val interpreter = Interpreter() val resolver = Resolver(interpreter) resolver.resolve(statements) assert(Lox.hadError()) } @Test fun `Initiate a variable with the same name from an outer scope function should not work`() { val code = """ fun a() { return 1; } { var a = a; print a; } """ val scanner = Scanner(code) val tokens = scanner.scanTokens() val parser = Parser(tokens) val statements = parser.parse() assertEquals(2, statements.size) val interpreter = Interpreter() val resolver = Resolver(interpreter) resolver.resolve(statements) assert(Lox.hadError()) } @Test fun `Initiate a variable with the same name from an outer scope function should not work with shadowing`() { val code = """ fun a() { return 1; } { fun a() { return 2; } var a = a; print a; } """ val scanner = Scanner(code) val tokens = scanner.scanTokens() val parser = Parser(tokens) val statements = parser.parse() assertEquals(2, statements.size) val interpreter = Interpreter() val resolver = Resolver(interpreter) resolver.resolve(statements) assert(Lox.hadError()) } @Test fun `Closures should work with shadowing and outer scope variables with the same name`() { val standardOut = System.out val outputStreamCaptor = ByteArrayOutputStream() System.setOut(PrintStream(outputStreamCaptor)) try { val code = """ var a = "global"; { fun showA() { print a; } showA(); var a = "block"; showA(); } """ val scanner = Scanner(code) val tokens = scanner.scanTokens() val parser = Parser(tokens) val statements = parser.parse() assertEquals(2, statements.size) val interpreter = Interpreter() val resolver = Resolver(interpreter) resolver.resolve(statements) interpreter.interpret(statements) val output = outputStreamCaptor.toString().trim() assertEquals("global\nglobal", output) } finally { System.setOut(standardOut) } } @Test fun `valid code with class declaration`() { val standardOut = System.out val outputStreamCaptor = ByteArrayOutputStream() System.setOut(PrintStream(outputStreamCaptor)) try { val code = """ class DevonshireCream { serveOn() { return "Scones"; } } print DevonshireCream; """.trimIndent() val scanner = Scanner(code) val tokens = scanner.scanTokens() val parser = Parser(tokens) val statements = parser.parse() assertEquals(2, statements.size) val interpreter = Interpreter() val resolver = Resolver(interpreter) resolver.resolve(statements) interpreter.interpret(statements) val output = outputStreamCaptor.toString().trim() assertEquals("DevonshireCream", output) } finally { System.setOut(standardOut) } } @Test fun `valid code with class declaration and instantiation`() { val standardOut = System.out val outputStreamCaptor = ByteArrayOutputStream() System.setOut(PrintStream(outputStreamCaptor)) try { val code = """ class DevonshireCream {} var cream = DevonshireCream(); print cream; """.trimIndent() val scanner = Scanner(code) val tokens = scanner.scanTokens() val parser = Parser(tokens) val statements = parser.parse() assertEquals(3, statements.size) val interpreter = Interpreter() val resolver = Resolver(interpreter) resolver.resolve(statements) interpreter.interpret(statements) val output = outputStreamCaptor.toString().trim() assertEquals("DevonshireCream instance", output) } finally { System.setOut(standardOut) } } @Test fun `valid code with properties initialization and access`() { val standardOut = System.out val outputStreamCaptor = ByteArrayOutputStream() System.setOut(PrintStream(outputStreamCaptor)) try { val code = """ class DevonshireCream { } var cream = DevonshireCream(); cream.color = "White"; print cream.color; """.trimIndent() val scanner = Scanner(code) val tokens = scanner.scanTokens() val parser = Parser(tokens) val statements = parser.parse() assertEquals(4, statements.size) val interpreter = Interpreter() val resolver = Resolver(interpreter) resolver.resolve(statements) interpreter.interpret(statements) val output = outputStreamCaptor.toString().trim() assertEquals("White", output) } finally { System.setOut(standardOut) } } @Test fun `valid code with method`() { val standardOut = System.out val outputStreamCaptor = ByteArrayOutputStream() System.setOut(PrintStream(outputStreamCaptor)) try { val code = """ class Bacon { eat() { print "Crunch crunch crunch!"; } } Bacon().eat(); """.trimIndent() val scanner = Scanner(code) val tokens = scanner.scanTokens() val parser = Parser(tokens) val statements = parser.parse() assertEquals(2, statements.size) val interpreter = Interpreter() val resolver = Resolver(interpreter) resolver.resolve(statements) interpreter.interpret(statements) val output = outputStreamCaptor.toString().trim() assertEquals("Crunch crunch crunch!", output) } finally { System.setOut(standardOut) } }}