Add typechecking

This commit is contained in:
Aaro Koivukangas
2026-03-04 16:04:49 +02:00
parent 29bdb3d507
commit da0fc52f2d
6 changed files with 250 additions and 0 deletions
+35
View File
@@ -0,0 +1,35 @@
package compiler.Interpreter
import compiler.Parser._
import compiler.Tokenizer._
type Value = Option[Int | Boolean]
// TODO: Let's do this later...
class Interpreter() {
def interpret(expr: Expression): Value =
return expr match {
case that: BinaryOperator => {
val left = interpret(that.left).get
val right = interpret(that.right).get
that.operator match {
// case "+" => left + right
case _ => throw new Exception("NOT IMPLEMENTED")
}
}
case that: Literal => {
return that.value match {
case value: Int => Some(value)
case value: Unit => throw new Exception("NOT IMPLEMENTED")
}
}
case that: IfThenElse => throw new Exception("NOT IMPLEMENTED")
case _ => throw new Exception(s"Couldn't match expression type, got: $expr")
}
}
object Interpreter:
def interpret(ast: Expression): Value =
val interpreter = Interpreter()
return interpreter.interpret(ast)
+1
View File
@@ -218,6 +218,7 @@ class Parser(tokens: List[Token]):
if peek().value == ";" then consume(Some(";")).get
return Block(expressions)
def parseExpression(initialDepth: Int = 0, topLevelOrBlock: Boolean = false): Expression =
var depth = initialDepth
var left = parseFactor()
+82
View File
@@ -0,0 +1,82 @@
package compiler.TypeChecker
import compiler.Parser._
abstract class CType()
case class CInt() extends CType
case class CBool() extends CType
case class CUnit() extends CType
case class CNone() extends CType
class TypeChecker() {
def typecheck(ast: Expression): CType =
return ast match {
case that: Literal => {
that.value match {
case _: Int => CInt()
case _: Unit => CUnit()
}
}
case that: BinaryOperator => {
val leftType = typecheck(that.left)
val rightType = typecheck(that.right)
return that.operator match {
case "+" | "-" | "*" | "%" | "/" => {
(leftType, rightType) match {
case (_: CInt, _: CInt) => CInt()
case _ => throw new Exception(s"Incorrect left or right value in BinaryOperator l: $leftType r: $rightType, expected CInt")
}
}
}
}
case that: IfThenElse => {
typecheck(that.condition) match {
case _: CBool => {}
case conditionType => throw new Exception(s"IfThenElse condition not bool: $conditionType")
}
val ifBranchType = typecheck(that.body)
val elseBranchType = that.elseBody match {
case None => CNone()
case Some(body) => typecheck(body)
}
// TODO: var x = if x>1 then 1 could have type CNone()
if elseBranchType == CNone() then
return ifBranchType
if ifBranchType != elseBranchType then
throw new Exception(s"IfThenElse branches don't share a type: $ifBranchType vs $elseBranchType")
return ifBranchType
}
case that: Identifier => {
return that.name match {
case "true" | "false" => CBool()
case _ => throw new Exception("NOT IMPLEMENTED")
}
}
case that: WhileDo => {
// NOTE: Should While do return only CUNit?
typecheck(that.condition) match {
case _: CBool => CUnit()
case conditionType => throw new Exception(s"WhileDo condition not bool: $conditionType")
}
}
// case _ => throw new Exception("NOT IMPLEMENTED")
}
}
object TypeChecker:
def typecheck(ast: Expression): CType =
val ts = TypeChecker()
ts.typecheck(ast)
@@ -0,0 +1,32 @@
import compiler.Tokenizer._
import compiler.Tokenizer.TokenType as tt
import compiler.Parser._
import compiler.Interpreter._
import scala.util.{Try, Failure, Success}
abstract class BaseInterpreterTests extends munit.FunSuite {
def getTokens(code: String): Option[List[Token]] =
val tokens = Tokenizer.tokenize(code)
return tokens match {
case Success(tokens) => Some(tokens)
case Failure(tokens) => fail("Tokenizing failed: " + tokens)
}
def testParser(code: String): Parser =
val tokens = getTokens(code)
return Parser(tokens.getOrElse(List(Token("error", tt.Error, TokenLocationDebug()))))
def testInterpreter(code: String) =
val parser = testParser(code)
val ast = parser.parseExpression()
Interpreter.interpret(ast)
}
class InterpreterLiteralTests extends BaseInterpreterTests {
test("should interpret 1 correctly") {
val result = testInterpreter("1").get
val expected = 1
assertEquals(result, expected)
}
}
+11
View File
@@ -535,6 +535,17 @@ class ParserUnaryOperatorTests extends BaseParserTests {
None
)
assertEquals(result, expected)
}
test("should parse unary `-` operator in BinaryOperator") {
val p = testParser("1--2")
val result = p.parseExpression()
val expected = BinaryOperator(
Literal(1),
"-",
UnaryOperator("-", Literal(2))
)
assertEquals(result, expected)
}
}
@@ -0,0 +1,89 @@
import scala.util.{Try, Failure, Success}
import compiler.Tokenizer._
import compiler.Tokenizer.TokenType as tt
import compiler.Parser._
import compiler.TypeChecker._
abstract class BaseTypeCheckerTests extends munit.FunSuite {
def getTokens(code: String): Option[List[Token]] =
val tokens = Tokenizer.tokenize(code)
return tokens match {
case Success(tokens) => Some(tokens)
case Failure(tokens) => fail("Tokenizing failed: " + tokens)
}
def testParser(code: String): Parser =
val tokens = getTokens(code)
return Parser(tokens.getOrElse(List(Token("error", tt.Error, TokenLocationDebug()))))
def testTypeChecker(code: String): CType =
val tokens = getTokens(code)
val ast = Parser(tokens.getOrElse(List(Token("error", tt.Error, TokenLocationDebug())))).parseExpression()
val typechecker = TypeChecker()
return typechecker.typecheck(ast)
}
class TypeCheckerLiteralTests extends BaseTypeCheckerTests {
test("should typecheck IntLiteral correctly") {
List("1", "45910", "0").map(value => {
val result = testTypeChecker(value)
val expected = CInt()
assertEquals(result, expected)
})
}
test("should typecheck booleans") {
List("true", "false").map(value => {
val result = testTypeChecker(value)
val expected = CBool()
assertEquals(result, expected)
})
}
// TODO
test("should typecheck negative IntLiteral".fail) {
val result = testTypeChecker("-14")
val expected = CInt()
assertEquals(result, expected)
}
}
class TypeCheckerBinaryOperatorTests extends BaseTypeCheckerTests {
test("should typecheck BinaryOperator with simple operators") {
List("1 + 1", "1580 + 0", "594 - 594", "5 * 5", "47 / 3", "2 % 5").map(value => {
val result = testTypeChecker(value)
val expected = CInt()
assertEquals(result, expected)
})
}
}
class TypeCheckerIfThenElseTests extends BaseTypeCheckerTests {
test("should typecheck simple IfThenElse with bodies as IntLiterals") {
val result = testTypeChecker("if true then 1 else 2")
val expected = CInt()
assertEquals(result, expected)
}
test("should typecheck simple IfThenElse with if-branch as IntLiteral and else-branch None") {
val result = testTypeChecker("if true then 1")
val expected = CInt()
assertEquals(result, expected)
}
test("should fail typecheck fi IfThenElse branches have different types") {
interceptMessage[java.lang.Exception]("IfThenElse branches don't share a type: CInt() vs CBool()") {
testTypeChecker("if true then 1 else false")
}
}
}
class TypeCheckerWhileDoTests extends BaseTypeCheckerTests {
test("should typecheck WhileDo with correct condition") {
val result = testTypeChecker("while true do 1")
val expected = CUnit()
assertEquals(result, expected)
}
test("should not typecheck WhileDo with incorrect condition") {
interceptMessage[java.lang.Exception]("WhileDo condition not bool: CInt()") {
testTypeChecker("while 4 do true")
}
}
}