diff --git a/src/main/ExprGenerator.ws.kts b/src/main/ExprGenerator.ws.kts index 0edcf7e..d296f67 100644 --- a/src/main/ExprGenerator.ws.kts +++ b/src/main/ExprGenerator.ws.kts @@ -43,9 +43,11 @@ val exprTypes = listOf( "Assign : Token name, Expr value", "Binary : Expr left, Token operator, Expr right", "Call : Expr callee, Token paren, List arguments", + "Get : Expr obj, Token name", "Grouping : Expr expression", "Literal : Any? value", "Logical : Expr left, Token operator, Expr right", + "Set : Expr obj, Token name, Expr value", "Unary : Token operator, Expr right", "Variable : Token name" ) diff --git a/src/main/fr/celticinfo/lox/AstPrinter.kt b/src/main/fr/celticinfo/lox/AstPrinter.kt index 99464f4..e510bdc 100644 --- a/src/main/fr/celticinfo/lox/AstPrinter.kt +++ b/src/main/fr/celticinfo/lox/AstPrinter.kt @@ -9,6 +9,10 @@ class AstPrinter : ExprVisitor { return parenthesize("call", expr.callee, *expr.arguments.toTypedArray()) } + override fun visitGet(expr: Get): String { + return parenthesize("get", expr.obj, Literal(expr.name)) + } + override fun visitAssign(expr: Assign): String { return parenthesize("=", expr) } @@ -29,6 +33,10 @@ class AstPrinter : ExprVisitor { return parenthesize(expr.operator.lexeme, expr.left, expr.right) } + override fun visitSet(expr: Set): String { + return parenthesize("set", expr.obj, Literal(expr.name), expr.value) + } + override fun visitUnary(expr: Unary): String { return parenthesize(expr.operator.lexeme, expr.right) } diff --git a/src/main/fr/celticinfo/lox/Expr.kt b/src/main/fr/celticinfo/lox/Expr.kt index 5212681..f70389e 100644 --- a/src/main/fr/celticinfo/lox/Expr.kt +++ b/src/main/fr/celticinfo/lox/Expr.kt @@ -7,9 +7,11 @@ interface ExprVisitor { fun visitAssign(expr: Assign): R fun visitBinary(expr: Binary): R fun visitCall(expr: Call): R + fun visitGet(expr: Get): R fun visitGrouping(expr: Grouping): R fun visitLiteral(expr: Literal): R fun visitLogical(expr: Logical): R + fun visitSet(expr: Set): R fun visitUnary(expr: Unary): R fun visitVariable(expr: Variable): R } @@ -48,6 +50,15 @@ data class Call( } } +data class Get( + val obj: Expr, + val name: Token +) : Expr() { + override fun accept(visitor: ExprVisitor): R { + return visitor.visitGet(this) + } +} + data class Grouping( val expression: Expr ) : Expr() { @@ -74,6 +85,16 @@ data class Logical( } } +data class Set( + val obj: Expr, + val name: Token, + val value: Expr +) : Expr() { + override fun accept(visitor: ExprVisitor): R { + return visitor.visitSet(this) + } +} + data class Unary( val operator: Token, val right: Expr ) : Expr() { diff --git a/src/main/fr/celticinfo/lox/Interpreter.kt b/src/main/fr/celticinfo/lox/Interpreter.kt index cb782c3..ed33de9 100644 --- a/src/main/fr/celticinfo/lox/Interpreter.kt +++ b/src/main/fr/celticinfo/lox/Interpreter.kt @@ -168,6 +168,14 @@ class Interpreter: ExprVisitor, StmtVisitor{ return callee.call(this, arguments) } + override fun visitGet(expr: Get): Any? { + val obj = evaluate(expr.obj) + if (obj is LoxInstance) { + return obj.get(expr.name) + } + throw RuntimeError(expr.name, "Only instances have properties") + } + override fun visitGrouping(expr: Grouping): Any? { return evaluate(expr.expression) } @@ -188,6 +196,14 @@ class Interpreter: ExprVisitor, StmtVisitor{ return evaluate(expr.right) } + override fun visitSet(expr: Set): Any? { + val obj = evaluate(expr.obj) as? LoxInstance + ?: throw RuntimeError(expr.name, "Only instances have fields") + val value = evaluate(expr.value) + obj.set(expr.name, value) + return value + } + override fun visitUnary(expr: Unary): Any? { val right = evaluate(expr.right) diff --git a/src/main/fr/celticinfo/lox/LoxInstance.kt b/src/main/fr/celticinfo/lox/LoxInstance.kt index bf94d47..f104c4a 100644 --- a/src/main/fr/celticinfo/lox/LoxInstance.kt +++ b/src/main/fr/celticinfo/lox/LoxInstance.kt @@ -1,5 +1,19 @@ package fr.celticinfo.lox class LoxInstance(private val klass: LoxClass) { + private val fields = mutableMapOf() + + fun get(name: Token): Any? { + if (fields.containsKey(name.lexeme)) { + return fields[name.lexeme] + } + + throw RuntimeError(name, "Undefined property '${name.lexeme}'.") + } + + fun set(name: Token, value: Any?) { + fields[name.lexeme] = value + } + override fun toString() = "$klass instance" } \ No newline at end of file diff --git a/src/main/fr/celticinfo/lox/Parser.kt b/src/main/fr/celticinfo/lox/Parser.kt index 6570af8..73f7c02 100644 --- a/src/main/fr/celticinfo/lox/Parser.kt +++ b/src/main/fr/celticinfo/lox/Parser.kt @@ -194,6 +194,8 @@ class Parser(private val tokens: List) { if (expr is Variable) { val name = expr.name return Assign(name, value) + } else if (expr is Get) { + return Set(expr.obj, expr.name, value) } error(equals, "Invalid assignment target.") @@ -300,7 +302,11 @@ class Parser(private val tokens: List) { while (true) { if (match(LEFT_PAREN)) { expr = finishCall(expr) - } else { + } else if (match(DOT)) { + val name = consume(IDENTIFIER, "Expect property name after '.'.") + expr = Get(expr, name) + } + else { break } } diff --git a/src/main/fr/celticinfo/lox/Resolver.kt b/src/main/fr/celticinfo/lox/Resolver.kt index 570b85b..9fb92ea 100644 --- a/src/main/fr/celticinfo/lox/Resolver.kt +++ b/src/main/fr/celticinfo/lox/Resolver.kt @@ -83,6 +83,10 @@ class Resolver(private val interpreter: Interpreter) : ExprVisitor, StmtVi expr.arguments.forEach { resolve(it) } } + override fun visitGet(expr: Get) { + resolve(expr.obj) + } + override fun visitGrouping(expr: Grouping) { resolve(expr.expression) } @@ -96,6 +100,11 @@ class Resolver(private val interpreter: Interpreter) : ExprVisitor, StmtVi resolve(expr.right) } + override fun visitSet(expr: Set) { + resolve(expr.value) + resolve(expr.obj) + } + override fun visitUnary(expr: Unary) { resolve(expr.right) } diff --git a/src/main/fr/celticinfo/loxext/RpnPrinter.kt b/src/main/fr/celticinfo/loxext/RpnPrinter.kt index 533b523..5a341ee 100644 --- a/src/main/fr/celticinfo/loxext/RpnPrinter.kt +++ b/src/main/fr/celticinfo/loxext/RpnPrinter.kt @@ -11,6 +11,10 @@ class RpnPrinter : ExprVisitor { return stack("call", expr.callee, *expr.arguments.toTypedArray()) } + override fun visitGet(expr: Get): String { + return stack("get", expr.obj, Literal(expr.name)) + } + override fun visitAssign(expr: Assign): String { return stack("=", expr) } @@ -31,6 +35,10 @@ class RpnPrinter : ExprVisitor { return stack(expr.operator.lexeme, expr.left, expr.right) } + override fun visitSet(expr: fr.celticinfo.lox.Set): String { + return stack("set", expr.obj, Literal(expr.name), expr.value) + } + override fun visitUnary(expr: Unary): String { return stack(expr.operator.lexeme, expr.right) } diff --git a/src/test/fr/celticinfo/lox/InterpreterTest.kt b/src/test/fr/celticinfo/lox/InterpreterTest.kt index cddb1ad..b629cd8 100644 --- a/src/test/fr/celticinfo/lox/InterpreterTest.kt +++ b/src/test/fr/celticinfo/lox/InterpreterTest.kt @@ -837,4 +837,40 @@ var a = "global"; 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) + } + } } diff --git a/src/test/fr/celticinfo/lox/ParserTest.kt b/src/test/fr/celticinfo/lox/ParserTest.kt index 3b0dcd1..fdc91ff 100644 --- a/src/test/fr/celticinfo/lox/ParserTest.kt +++ b/src/test/fr/celticinfo/lox/ParserTest.kt @@ -270,4 +270,25 @@ class ParserTest { val stmt = statements[1] assertTrue(stmt is Var) } + + @Test + fun `valid code with properties`() { + 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) + assertTrue(statements[0] is ClassStmt) + assertTrue(statements[1] is Var) + assertTrue(statements[2] is Expression) + assertTrue(statements[3] is Print) + } } \ No newline at end of file