diff --git a/build.gradle.kts b/build.gradle.kts index dbd3a74..ddacb9d 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -1,7 +1,7 @@ plugins { kotlin("multiplatform") version "2.2.0" id("org.jlleitschuh.gradle.ktlint") version "11.3.2" - id("org.jetbrains.kotlinx.kover") version "0.7.6" + id("org.jetbrains.kotlinx.kover") version "0.9.1" kotlin("plugin.serialization") version "2.2.0" } @@ -54,21 +54,6 @@ kotlin { } } -koverReport { - filters { - includes { - classes("*") - } - } - verify { - rule { - bound { - minValue = 0 - } - } - } -} - // Task to automatically sync JVM sources from JS sources tasks.register("syncJvmSources") { group = "build" @@ -103,6 +88,11 @@ tasks.register("syncJvmSources") { } } +// Ensure syncJvmSources runs before any compilation tasks +tasks.withType { + dependsOn("syncJvmSources") +} + listOf( "runKtlintCheckOverJsMainSourceSet", "runKtlintCheckOverJsTestSourceSet", @@ -118,6 +108,16 @@ tasks.named("jvmTest") { dependsOn("syncJvmSources") } +// Also make jsTest depend on syncJvmSources to ensure consistency +tasks.named("jsTest") { + dependsOn("syncJvmSources") +} + +// Also make jsTest depend on syncJvmSources to ensure consistency +tasks.named("jsTest") { + dependsOn("syncJvmSources") +} + tasks.named("clean") { delete( file("src/jvmMain"), @@ -125,8 +125,18 @@ tasks.named("clean") { ) } -// Ensure Kover HTML report is generated when running the standard build -// Using finalizedBy so the report runs after a successful build without affecting task up-to-date checks +// Ensure tests run and Kover HTML report is generated when running the standard build tasks.named("build") { + dependsOn("jsTest", "jvmTest") finalizedBy("koverHtmlReport") } + +// Ensure koverHtmlReport depends on test execution to have coverage data +tasks.named("koverHtmlReport") { + dependsOn("jsTest", "jvmTest") +} + +// Ensure koverHtmlReport depends on test execution to have coverage data +tasks.named("koverHtmlReport") { + dependsOn("jsTest", "jvmTest") +} diff --git a/src/jsMain/kotlin/exceptions/CompilationExceptions.kt b/src/jsMain/kotlin/exceptions/CompilationExceptions.kt index e7b7ad1..ea4fe1c 100644 --- a/src/jsMain/kotlin/exceptions/CompilationExceptions.kt +++ b/src/jsMain/kotlin/exceptions/CompilationExceptions.kt @@ -50,6 +50,18 @@ class UndeclaredVariableException( column: Int? = null ) : CompilationExceptions(CompilerStage.PARSER, "Variable is used before being declared", line, column) +class UndeclaredLabelException( + label: String, + line: Int? = null, + column: Int? = null +) : CompilationExceptions(CompilerStage.PARSER, "Goto target '$label' is not defined.", line, column) + +class DuplicateLabelException( + label: String, + line: Int? = null, + column: Int? = null +) : CompilationExceptions(CompilerStage.PARSER, "Label '$label' is already defined.", line, column) + class InvalidLValueException( line: Int? = null, column: Int? = null diff --git a/src/jsMain/kotlin/export/ASTExport.kt b/src/jsMain/kotlin/export/ASTExport.kt index 3f9892c..018da94 100644 --- a/src/jsMain/kotlin/export/ASTExport.kt +++ b/src/jsMain/kotlin/export/ASTExport.kt @@ -7,11 +7,15 @@ import kotlinx.serialization.json.JsonObject import kotlinx.serialization.json.JsonPrimitive import parser.AssignmentExpression import parser.BinaryExpression +import parser.ConditionalExpression import parser.D import parser.Declaration import parser.ExpressionStatement import parser.Function +import parser.GotoStatement +import parser.IfStatement import parser.IntExpression +import parser.LabeledStatement import parser.NullStatement import parser.ReturnStatement import parser.S @@ -181,6 +185,88 @@ class ASTExport : Visitor { return Json.encodeToString(jsonNode) } + override fun visit(node: IfStatement): String { + val childrenMap = + mutableMapOf( + "condition" to JsonPrimitive(node.condition.accept(this)), + "then" to JsonPrimitive(node.then.accept(this)) + ) + // Handle the optional 'else' branch + node._else?.let { + childrenMap["else"] = JsonPrimitive(it.accept(this)) + } + + val jsonNode = + JsonObject( + mapOf( + "type" to JsonPrimitive("IfStatement"), + "label" to JsonPrimitive("if-then-else"), + "children" to JsonObject(childrenMap) + ) + ) + return Json.encodeToString(jsonNode) + } + + override fun visit(node: ConditionalExpression): String { + val children = + JsonObject( + mapOf( + "condition" to JsonPrimitive(node.codition.accept(this)), + "thenExpression" to JsonPrimitive(node.thenExpression.accept(this)), + "elseExpression" to JsonPrimitive(node.elseExpression.accept(this)) + ) + ) + + val jsonNode = + JsonObject( + mapOf( + "type" to JsonPrimitive("ConditionalExpression"), + "label" to JsonPrimitive("cond ? then : else"), + "children" to children + ) + ) + return Json.encodeToString(jsonNode) + } + + override fun visit(node: GotoStatement): String { + val children = + JsonObject( + mapOf( + "targetLabel" to JsonPrimitive(node.label) + ) + ) + + val jsonNode = + JsonObject( + mapOf( + "type" to JsonPrimitive("GotoStatement"), + "label" to JsonPrimitive("goto"), + "children" to children + ) + ) + return Json.encodeToString(jsonNode) + } + + override fun visit(node: LabeledStatement): String { + val children = + JsonObject( + mapOf( + "label" to JsonPrimitive(node.label), + "statement" to JsonPrimitive(node.statement.accept(this)) + ) + ) + + val jsonNode = + JsonObject( + mapOf( + "type" to JsonPrimitive("LabeledStatement"), + "label" to JsonPrimitive("label: statement"), + "children" to children + ) + ) + return Json.encodeToString(jsonNode) + } + override fun visit(node: AssignmentExpression): String { val children = JsonObject( diff --git a/src/jsMain/kotlin/export/CompilerExport.kt b/src/jsMain/kotlin/export/CompilerExport.kt index f2e9c8c..75e1763 100644 --- a/src/jsMain/kotlin/export/CompilerExport.kt +++ b/src/jsMain/kotlin/export/CompilerExport.kt @@ -4,6 +4,7 @@ import assembly.AsmProgram import assembly.CodeEmitter import assembly.InstructionFixer import assembly.PseudoEliminator +import compiler.parser.LabelAnalysis import compiler.parser.VariableResolution import exceptions.CodeGenerationException import exceptions.CompilationExceptions @@ -29,6 +30,7 @@ class CompilerExport { val parser = Parser() val variableResolution = VariableResolution() + val labelAnalysis = LabelAnalysis() val tackyGenVisitor = TackyGenVisitor() val tackyToAsmConverter = TackyToAsm() val pseudoEliminator = PseudoEliminator() @@ -68,6 +70,7 @@ class CompilerExport { if (lexerOutput.errors.isEmpty() && tokens != null) { try { ast = parser.parseTokens(tokens) + labelAnalysis.analyze(ast) ast = ast.accept(variableResolution) ParserOutput( errors = emptyArray(), diff --git a/src/jsMain/kotlin/lexer/Lexer.kt b/src/jsMain/kotlin/lexer/Lexer.kt index 9812646..d1a444d 100644 --- a/src/jsMain/kotlin/lexer/Lexer.kt +++ b/src/jsMain/kotlin/lexer/Lexer.kt @@ -12,6 +12,12 @@ sealed class TokenType { object IDENTIFIER : TokenType() + object IF : TokenType() + + object ELSE : TokenType() + + object GOTO : TokenType() + // literals object INT_LITERAL : TokenType() @@ -64,6 +70,10 @@ sealed class TokenType { object RIGHT_BRACK : TokenType() + object QUESTION_MARK : TokenType() + + object COLON : TokenType() + // Special token for End of File object EOF : TokenType() @@ -93,7 +103,10 @@ class Lexer( mapOf( "int" to TokenType.KEYWORD_INT, "void" to TokenType.KEYWORD_VOID, - "return" to TokenType.KEYWORD_RETURN + "return" to TokenType.KEYWORD_RETURN, + "if" to TokenType.IF, + "else" to TokenType.ELSE, + "goto" to TokenType.GOTO ) fun tokenize(): List { @@ -130,6 +143,8 @@ class Lexer( '%' -> addToken(TokenType.REMAINDER) '*' -> addToken(TokenType.MULTIPLY) '/' -> addToken(TokenType.DIVIDE) + '?' -> addToken(TokenType.QUESTION_MARK) + ':' -> addToken(TokenType.COLON) '~' -> addToken(TokenType.TILDE) '-' -> { diff --git a/src/jsMain/kotlin/parser/BlockItems.kt b/src/jsMain/kotlin/parser/BlockItems.kt index 1ff2a13..fc8c259 100644 --- a/src/jsMain/kotlin/parser/BlockItems.kt +++ b/src/jsMain/kotlin/parser/BlockItems.kt @@ -20,6 +20,27 @@ class NullStatement : Statement() { override fun equals(other: Any?): Boolean = other is NullStatement } +class IfStatement( + val condition: Expression, + val then: Statement, + val _else: Statement? +) : Statement() { + override fun accept(visitor: Visitor): T = visitor.visit(this) +} + +class GotoStatement( + val label: String +) : Statement() { + override fun accept(visitor: Visitor): T = visitor.visit(this) +} + +class LabeledStatement( + val label: String, + val statement: Statement +) : Statement() { + override fun accept(visitor: Visitor): T = visitor.visit(this) +} + data class Declaration( val name: String, val init: Expression? diff --git a/src/jsMain/kotlin/parser/Expressions.kt b/src/jsMain/kotlin/parser/Expressions.kt index 59a44a0..d5000bd 100644 --- a/src/jsMain/kotlin/parser/Expressions.kt +++ b/src/jsMain/kotlin/parser/Expressions.kt @@ -37,3 +37,11 @@ data class AssignmentExpression( ) : Expression() { override fun accept(visitor: Visitor): T = visitor.visit(this) } + +data class ConditionalExpression( + val codition: Expression, + val thenExpression: Expression, + val elseExpression: Expression +) : Expression() { + override fun accept(visitor: Visitor): T = visitor.visit(this) +} diff --git a/src/jsMain/kotlin/parser/LabelAnalysis.kt b/src/jsMain/kotlin/parser/LabelAnalysis.kt new file mode 100644 index 0000000..a43b71a --- /dev/null +++ b/src/jsMain/kotlin/parser/LabelAnalysis.kt @@ -0,0 +1,153 @@ +package compiler.parser + +import exceptions.DuplicateLabelException +import exceptions.UndeclaredLabelException +import parser.ASTNode +import parser.AssignmentExpression +import parser.BinaryExpression +import parser.ConditionalExpression +import parser.D +import parser.Declaration +import parser.ExpressionStatement +import parser.Function +import parser.GotoStatement +import parser.IfStatement +import parser.IntExpression +import parser.LabeledStatement +import parser.NullStatement +import parser.ReturnStatement +import parser.S +import parser.SimpleProgram +import parser.UnaryExpression +import parser.VariableExpression +import parser.Visitor + +class LabelCollector : Visitor { + val definedLabels: MutableSet = mutableSetOf() + + override fun visit(node: LabeledStatement) { + if (!definedLabels.add(node.label)) { + throw DuplicateLabelException(node.label) + } + node.statement.accept(this) + } + + override fun visit(node: AssignmentExpression) { + } + + override fun visit(node: Declaration) { + } + + override fun visit(node: SimpleProgram) { + node.functionDefinition.accept(this) + } + + override fun visit(node: Function) { + node.body.forEach { it.accept(this) } + } + + override fun visit(node: VariableExpression) { + } + + override fun visit(node: UnaryExpression) { + } + + override fun visit(node: BinaryExpression) { + } + + override fun visit(node: IntExpression) { + } + + override fun visit(node: IfStatement) { + node.then.accept(this) + node._else?.accept(this) + } + + override fun visit(node: ConditionalExpression) { + } + + override fun visit(node: S) { + node.statement.accept(this) + } + + override fun visit(node: D) {} + + override fun visit(node: ReturnStatement) {} + + override fun visit(node: ExpressionStatement) {} + + override fun visit(node: NullStatement) {} + + override fun visit(node: GotoStatement) {} +} + +private class GotoValidator( + private val definedLabels: Set +) : Visitor { + override fun visit(node: GotoStatement) { + if (node.label !in definedLabels) { + throw UndeclaredLabelException(node.label) + } + } + + // --- Pass-through methods --- + override fun visit(node: SimpleProgram) { + node.functionDefinition.accept(this) + } + + override fun visit(node: Function) { + node.body.forEach { it.accept(this) } + } + + override fun visit(node: VariableExpression) { + } + + override fun visit(node: UnaryExpression) { + } + + override fun visit(node: BinaryExpression) { + } + + override fun visit(node: IntExpression) { + } + + override fun visit(node: IfStatement) { + node.then.accept(this) + node._else?.accept(this) + } + + override fun visit(node: ConditionalExpression) { + } + + override fun visit(node: LabeledStatement) { + node.statement.accept(this) + } + + override fun visit(node: AssignmentExpression) { + } + + override fun visit(node: Declaration) { + } + + override fun visit(node: S) { + node.statement.accept(this) + } + + override fun visit(node: D) {} + + override fun visit(node: ReturnStatement) {} + + override fun visit(node: ExpressionStatement) {} + + override fun visit(node: NullStatement) {} +} + +class LabelAnalysis { + fun analyze(ast: ASTNode) { + val collector = LabelCollector() + ast.accept(collector) + + val validator = GotoValidator(collector.definedLabels) + ast.accept(validator) + } +} diff --git a/src/jsMain/kotlin/parser/Parser.kt b/src/jsMain/kotlin/parser/Parser.kt index 7d7af22..fb3d99b 100644 --- a/src/jsMain/kotlin/parser/Parser.kt +++ b/src/jsMain/kotlin/parser/Parser.kt @@ -10,6 +10,7 @@ class Parser { private val precedenceMap = mapOf( TokenType.ASSIGN to 1, + TokenType.QUESTION_MARK to 3, TokenType.OR to 5, TokenType.AND to 10, TokenType.EQUAL_TO to 30, @@ -126,22 +127,65 @@ class Parser { } private fun parseStatement(tokens: MutableList): Statement { - var first: Token? = null - if (!tokens.isEmpty() && tokens.first().type == TokenType.KEYWORD_RETURN) { - first = tokens.removeFirst() - } else if (!tokens.isEmpty() && tokens.first().type == TokenType.SEMICOLON) { - tokens.removeFirst() - return NullStatement() - } - val expression = parseExpression(tokens = tokens) - expect(TokenType.SEMICOLON, tokens) + val first = tokens.firstOrNull() + val second = if (tokens.size > 1) tokens[1] else null - return if (first != null) { - ReturnStatement( - expression = expression - ) - } else { - ExpressionStatement(expression) + return when { + // if-statement + first?.type == TokenType.IF -> { + tokens.removeFirst() // consume 'if' + expect(TokenType.LEFT_PAREN, tokens) + val condition = parseExpression(0, tokens) + expect(TokenType.RIGHT_PAREN, tokens) + val thenBranch = parseStatement(tokens) + + val elseBranch = + if (tokens.firstOrNull()?.type == TokenType.ELSE) { + tokens.removeFirst() + parseStatement(tokens) + } else { + null + } + + IfStatement(condition, thenBranch, elseBranch) + } + + // return-statement + first?.type == TokenType.KEYWORD_RETURN -> { + tokens.removeFirst() // consume 'return' + val expr = parseExpression(0, tokens) + expect(TokenType.SEMICOLON, tokens) + ReturnStatement(expr) + } + + // empty statement ; + first?.type == TokenType.SEMICOLON -> { + tokens.removeFirst() + NullStatement() + } + + // goto-statement + first?.type == TokenType.GOTO -> { + tokens.removeFirst() // consume 'goto' + val label = parseIdentifier(tokens) + expect(TokenType.SEMICOLON, tokens) + GotoStatement(label) + } + + // label: statement + first?.type == TokenType.IDENTIFIER && second?.type == TokenType.COLON -> { + val labelName = parseIdentifier(tokens) + expect(TokenType.COLON, tokens) + val stmt = parseStatement(tokens) + LabeledStatement(labelName, stmt) + } + + // expression statement (default case) + else -> { + val expr = parseExpression(0, tokens) + expect(TokenType.SEMICOLON, tokens) + ExpressionStatement(expr) + } } } @@ -165,6 +209,11 @@ class Parser { throw InvalidLValueException() } AssignmentExpression(left, right) + } else if (nextType == TokenType.QUESTION_MARK) { + val thenExpression = parseExpression(prec, tokens) + expect(TokenType.COLON, tokens) + val elseExpression = parseExpression(prec, tokens) + return ConditionalExpression(left, thenExpression, elseExpression) } else { val right = parseExpression(prec + 1, tokens) BinaryExpression( diff --git a/src/jsMain/kotlin/parser/VariableResolution.kt b/src/jsMain/kotlin/parser/VariableResolution.kt index 4a90845..a48b896 100644 --- a/src/jsMain/kotlin/parser/VariableResolution.kt +++ b/src/jsMain/kotlin/parser/VariableResolution.kt @@ -6,12 +6,16 @@ import parser.ASTNode import parser.AssignmentExpression import parser.BinaryExpression import parser.BlockItem +import parser.ConditionalExpression import parser.D import parser.Declaration import parser.Expression import parser.ExpressionStatement import parser.Function +import parser.GotoStatement +import parser.IfStatement import parser.IntExpression +import parser.LabeledStatement import parser.NullStatement import parser.ReturnStatement import parser.S @@ -66,6 +70,27 @@ class VariableResolution : Visitor { override fun visit(node: IntExpression): ASTNode = node + override fun visit(node: IfStatement): ASTNode { + val condition = node.condition.accept(this) as Expression + val thenStatement = node.then.accept(this) as Statement + var elseStatement = node._else?.accept(this) as Statement? + return IfStatement(condition, thenStatement, elseStatement) + } + + override fun visit(node: ConditionalExpression): ASTNode { + val condition = node.codition.accept(this) as Expression + val thenExpression = node.thenExpression.accept(this) as Expression + val elseExpression = node.elseExpression.accept(this) as Expression + return ConditionalExpression(condition, thenExpression, elseExpression) + } + + override fun visit(node: GotoStatement): ASTNode = node + + override fun visit(node: LabeledStatement): ASTNode { + val statement = node.statement.accept(this) as Statement + return LabeledStatement(node.label, statement) + } + override fun visit(node: AssignmentExpression): ASTNode { val lvalue = node.lvalue.accept(this) as VariableExpression val rvalue = node.rvalue.accept(this) as Expression diff --git a/src/jsMain/kotlin/parser/Visitor.kt b/src/jsMain/kotlin/parser/Visitor.kt index 82b4561..11cbd23 100644 --- a/src/jsMain/kotlin/parser/Visitor.kt +++ b/src/jsMain/kotlin/parser/Visitor.kt @@ -19,6 +19,14 @@ interface Visitor { fun visit(node: IntExpression): T + fun visit(node: IfStatement): T + + fun visit(node: ConditionalExpression): T + + fun visit(node: GotoStatement): T + + fun visit(node: LabeledStatement): T + fun visit(node: AssignmentExpression): T fun visit(node: Declaration): T diff --git a/src/jsMain/kotlin/tacky/TackyGenVisitor.kt b/src/jsMain/kotlin/tacky/TackyGenVisitor.kt index e574a18..df343ea 100644 --- a/src/jsMain/kotlin/tacky/TackyGenVisitor.kt +++ b/src/jsMain/kotlin/tacky/TackyGenVisitor.kt @@ -4,11 +4,15 @@ import exceptions.TackyException import lexer.TokenType import parser.AssignmentExpression import parser.BinaryExpression +import parser.ConditionalExpression import parser.D import parser.Declaration import parser.ExpressionStatement import parser.Function +import parser.GotoStatement +import parser.IfStatement import parser.IntExpression +import parser.LabeledStatement import parser.NullStatement import parser.ReturnStatement import parser.S @@ -70,6 +74,7 @@ class TackyGenVisitor : Visitor { override fun visit(node: SimpleProgram): TackyConstruct { // Reset counter for test assertions tempCounter = 0 + labelCounter = 0 val tackyFunction = node.functionDefinition.accept(this) as TackyFunction return TackyProgram(tackyFunction) } @@ -160,6 +165,58 @@ class TackyGenVisitor : Visitor { override fun visit(node: IntExpression): TackyConstruct = TackyConstant(node.value) + override fun visit(node: IfStatement): TackyConstruct? { + val endLabel = newLabel("end") + + val condition = node.condition.accept(this) as TackyVal + if (node._else == null) { + currentInstructions += JumpIfZero(condition, endLabel) + node.then.accept(this) + currentInstructions += endLabel + } else { + val elseLabel = newLabel("else_label") + currentInstructions += JumpIfZero(condition, elseLabel) + node.then.accept(this) + currentInstructions += TackyJump(endLabel) + currentInstructions += elseLabel + node._else.accept(this) + currentInstructions += endLabel + } + return null + } + + override fun visit(node: ConditionalExpression): TackyConstruct? { + val resultVar = newTemporary() + + val elseLabel = newLabel("cond_else") + val endLabel = newLabel("cond_end") + + val conditionResult = node.codition.accept(this) as TackyVal + currentInstructions += JumpIfZero(conditionResult, elseLabel) + + val thenResult = node.thenExpression.accept(this) as TackyVal + currentInstructions += TackyCopy(thenResult, resultVar) + currentInstructions += TackyJump(endLabel) + currentInstructions += elseLabel + val elseResult = node.elseExpression.accept(this) as TackyVal + currentInstructions += TackyCopy(elseResult, resultVar) + currentInstructions += endLabel + + return resultVar + } + + override fun visit(node: GotoStatement): TackyConstruct? { + currentInstructions += TackyJump(TackyLabel(node.label)) + return null + } + + override fun visit(node: LabeledStatement): TackyConstruct? { + // val label = newLabel(node.label) + currentInstructions += TackyLabel(node.label) + node.statement.accept(this) + return null + } + override fun visit(node: AssignmentExpression): TackyConstruct { val rvalue = node.rvalue.accept(this) as TackyVal val dest = TackyVar(node.lvalue.name) diff --git a/src/jsTest/kotlin/integration/CompilerTestSuite.kt b/src/jsTest/kotlin/integration/CompilerTestSuite.kt index b1270d8..390fb1e 100644 --- a/src/jsTest/kotlin/integration/CompilerTestSuite.kt +++ b/src/jsTest/kotlin/integration/CompilerTestSuite.kt @@ -3,6 +3,7 @@ package integration import assembly.InstructionFixer import assembly.PseudoEliminator import compiler.CompilerStage +import compiler.parser.LabelAnalysis import compiler.parser.VariableResolution import lexer.Lexer import parser.Parser @@ -18,7 +19,8 @@ import kotlin.test.assertIs class CompilerTestSuite { private val parser = Parser() private val tackyGenVisitor = TackyGenVisitor() - private val variableResolution = VariableResolution() + + // private val variableResolution = VariableResolution() private val tackyToAsmConverter = TackyToAsm() private val instructionFixer = InstructionFixer() private val pseudoEliminator = PseudoEliminator() @@ -44,6 +46,7 @@ class CompilerTestSuite { // Parser stage val ast = parser.parseTokens(tokens) assertIs(ast) + val variableResolution = VariableResolution() val transformedAst = variableResolution.visit(ast) if (testCase.expectedAst != null) { assertEquals( @@ -107,12 +110,16 @@ class CompilerTestSuite { // Parser stage if (testCase.failingStage == CompilerStage.PARSER) { assertFailsWith(testCase.expectedException) { + val labelAnalysis = LabelAnalysis() + val variableResolution = VariableResolution() val ast = parser.parseTokens(tokens) as SimpleProgram + labelAnalysis.analyze(ast) variableResolution.visit(ast) } continue } val ast = parser.parseTokens(tokens) as SimpleProgram + val variableResolution = VariableResolution() val transformedAst = variableResolution.visit(ast) // Tacky generation stage diff --git a/src/jsTest/kotlin/integration/InvalidTestCases.kt b/src/jsTest/kotlin/integration/InvalidTestCases.kt index b5740b5..0cee321 100644 --- a/src/jsTest/kotlin/integration/InvalidTestCases.kt +++ b/src/jsTest/kotlin/integration/InvalidTestCases.kt @@ -110,6 +110,54 @@ object InvalidTestCases { code = "int main(void) { 2 = 3; }", failingStage = CompilerStage.PARSER, expectedException = InvalidLValueException::class + ), + InvalidTestCase( + code = "int main(void) { if (1) return; else }", // 'else' without a statement + failingStage = CompilerStage.PARSER, + expectedException = UnexpectedTokenException::class + ), + InvalidTestCase( + code = "int main(void) { if 1 return 1; }", // Missing parentheses around condition + failingStage = CompilerStage.PARSER, + expectedException = UnexpectedTokenException::class + ), + InvalidTestCase( + code = "int main(void) { else return 1; }", // 'else' without a preceding 'if' + failingStage = CompilerStage.PARSER, + expectedException = UnexpectedTokenException::class + ), + // Syntax Errors for Conditional Operator (? :) + InvalidTestCase( + code = "int main(void) { return 1 ? 2; }", // Missing the ':' part + failingStage = CompilerStage.PARSER, + expectedException = UnexpectedTokenException::class + ), + InvalidTestCase( + code = "int main(void) { return 1 : 2; }", // Missing the '?' part + failingStage = CompilerStage.PARSER, + expectedException = UnexpectedTokenException::class + ), + // Syntax Errors for GOTO and LABELS + InvalidTestCase( + code = "int main(void) { goto ; }", // 'goto' without a label identifier + failingStage = CompilerStage.PARSER, + expectedException = UnexpectedTokenException::class + ), + // Semantic Errors (caught after parsing) + InvalidTestCase( + code = "int main(void) { int a; int a; return a; }", // Duplicate variable + failingStage = CompilerStage.PARSER, + expectedException = DuplicateVariableDeclaration::class + ), + InvalidTestCase( + code = "int main(void) { return a; }", // Undeclared variable + failingStage = CompilerStage.PARSER, + expectedException = UndeclaredVariableException::class + ), + InvalidTestCase( + code = "int main(void) { 1 = 2; return 0; }", // Invalid L-value + failingStage = CompilerStage.PARSER, + expectedException = InvalidLValueException::class ) ) } diff --git a/src/jsTest/kotlin/integration/ValidTestCases.kt b/src/jsTest/kotlin/integration/ValidTestCases.kt index 216c6d1..8a0bafe 100644 --- a/src/jsTest/kotlin/integration/ValidTestCases.kt +++ b/src/jsTest/kotlin/integration/ValidTestCases.kt @@ -191,7 +191,6 @@ object ValidTestCases { // Note: We use R10D here because the destination of the MOV is a register Mov(Stack(-4), Register(HardwareRegister.R10D)), Mov(Register(HardwareRegister.R10D), Stack(-8)), - // --- THIS IS THE KEY CHANGE --- // The fixer sees `imul Imm(4), Stack(-8)` and replaces it Mov(Stack(-8), Register(HardwareRegister.R11D)), // Load dest AsmBinary(AsmBinaryOp.MUL, Imm(4), Register(HardwareRegister.R11D)), // Multiply @@ -207,14 +206,14 @@ object ValidTestCases { // Block for tmp.4 = tmp.3 / 6 Mov(Stack(-16), Register(HardwareRegister.EAX)), Cdq, - // FIXER: The Idiv(Imm(6)) will be fixed here + // The Idiv(Imm(6)) will be fixed here Mov(Imm(6), Register(HardwareRegister.R10D)), Idiv(Register(HardwareRegister.R10D)), Mov(Register(HardwareRegister.EAX), Stack(-20)), // Block for tmp.5 = tmp.4 % 3 Mov(Stack(-20), Register(HardwareRegister.EAX)), Cdq, - // FIXER: The Idiv(Imm(3)) will be fixed here + // The Idiv(Imm(3)) will be fixed here Mov(Imm(3), Register(HardwareRegister.R10D)), Idiv(Register(HardwareRegister.R10D)), Mov(Register(HardwareRegister.EDX), Stack(-24)), @@ -445,6 +444,153 @@ object ValidTestCases { ) ) // No need to test the assembly here since this feature doesn't effect the assembly generation stage + ), + // --- Test Case for an IF-ELSE statement --- + ValidTestCase( + title = "Testing an if-else statement.", + code = + """ + int main(void) { + int a = 0; + if (a == 0) + return 10; + else + return 20; + } + """.trimIndent(), + expectedTacky = + TackyProgram( + TackyFunction( + name = "main", + body = + listOf( + // int a = 0; + TackyCopy(TackyConstant(0), TackyVar("a.0")), + // tmp.0 = a == 0 + TackyBinary(TackyBinaryOP.EQUAL, TackyVar("a.0"), TackyConstant(0), TackyVar("tmp.0")), + // if (tmp.0 == 0) goto .L_else_label_1 + JumpIfZero(TackyVar("tmp.0"), TackyLabel(".L_else_label_1")), + // then block: return 10; + TackyRet(TackyConstant(10)), + // goto .L_end_0; + TackyJump(TackyLabel(".L_end_0")), + // else block + TackyLabel(".L_else_label_1"), + TackyRet(TackyConstant(20)), + // end of if + TackyLabel(".L_end_0"), + TackyRet(TackyConstant(0)) + ) + ) + ), + expectedAssembly = + AsmProgram( + AsmFunction( + name = "main", + body = + listOf( + AllocateStack(8), + // int a = 0; + Mov(Imm(0), Stack(-4)), + // tmp.0 = a == 0; + Cmp(Imm(0), Stack(-4)), + Mov(Imm(0), Stack(-8)), + SetCC(ConditionCode.E, Stack(-8)), + // if (tmp.0 == 0) goto .L_else_label_1 + Cmp(Imm(0), Stack(-8)), + JmpCC(ConditionCode.E, Label(".L_else_label_1")), + // return 10; + Mov(Imm(10), Register(HardwareRegister.EAX)), + Jmp(Label(".L_end_0")), + // .L_else_label_1: + Label(".L_else_label_1"), + // return 20; + Mov(Imm(20), Register(HardwareRegister.EAX)), + // .L_end_0: + Label(".L_end_0"), + // The extra return 0 + Mov(Imm(0), Register(HardwareRegister.EAX)) + ) + ) + ) + ), +// --- Test Case for GOTO and LABELS --- + ValidTestCase( + title = "Testing goto and labeled statements.", + code = + """ + int main(void) { + int a = 0; + start: + a = a + 1; + if (a < 3) + goto start; + return a; + } + """.trimIndent(), + expectedTacky = + TackyProgram( + TackyFunction( + name = "main", + body = + listOf( + // int a = 0; + TackyCopy(TackyConstant(0), TackyVar("a.0")), + // start: + TackyLabel("start"), + // tmp.0 = a + 1; + TackyBinary(TackyBinaryOP.ADD, TackyVar("a.0"), TackyConstant(1), TackyVar("tmp.0")), + // a = tmp.1 + TackyCopy(TackyVar("tmp.0"), TackyVar("a.0")), + // tmp.2 = a < 3 + TackyBinary(TackyBinaryOP.LESS, TackyVar("a.0"), TackyConstant(3), TackyVar("tmp.1")), + // if (tmp.2 == 0) goto .L_if_end_0; + JumpIfZero(TackyVar("tmp.1"), TackyLabel(".L_end_0")), + // goto start; + TackyJump(TackyLabel("start")), + // end of if + TackyLabel(".L_end_0"), + // return a; + TackyRet(TackyVar("a.0")), + TackyRet(TackyConstant(0)) + ) + ) + ), + expectedAssembly = + AsmProgram( + AsmFunction( + name = "main", + body = + listOf( + AllocateStack(12), // For a, tmp.1, tmp.2 + // a = 0 + Mov(Imm(0), Stack(-4)), + // start: + Label("start"), + // tmp.1 = a + 1 + Mov(Stack(-4), Register(HardwareRegister.R10D)), + Mov(Register(HardwareRegister.R10D), Stack(-8)), + AsmBinary(AsmBinaryOp.ADD, Imm(1), Stack(-8)), + // a = tmp.1 + Mov(Stack(-8), Register(HardwareRegister.R10D)), + Mov(Register(HardwareRegister.R10D), Stack(-4)), + // tmp.2 = a < 3 + Cmp(Imm(3), Stack(-4)), + Mov(Imm(0), Stack(-12)), + SetCC(ConditionCode.L, Stack(-12)), + // if (tmp.2 == 0) goto .L_if_end_0 + Cmp(Imm(0), Stack(-12)), + JmpCC(ConditionCode.E, Label(".L_end_0")), + // goto start + Jmp(Label("start")), + // .L_if_end_0: + Label(".L_end_0"), + // return a + Mov(Stack(-4), Register(HardwareRegister.EAX)), + Mov(src = Imm(value = 0), dest = Register(name = HardwareRegister.EAX)) + ) + ) + ) ) ) } diff --git a/src/jsTest/kotlin/parser/LabelAnalysisTest.kt b/src/jsTest/kotlin/parser/LabelAnalysisTest.kt new file mode 100644 index 0000000..5df7426 --- /dev/null +++ b/src/jsTest/kotlin/parser/LabelAnalysisTest.kt @@ -0,0 +1,142 @@ +package parser + +import compiler.parser.LabelAnalysis +import exceptions.DuplicateLabelException +import exceptions.UndeclaredLabelException +import kotlin.test.Test +import kotlin.test.assertFailsWith +import kotlin.test.assertTrue + +class LabelAnalysisTest { + private val labelAnalysis = LabelAnalysis() + + @Test + fun `test valid labels and gotos`() { + // Arrange: A program with a forward jump and a backward jump. + val ast: ASTNode = + SimpleProgram( + functionDefinition = + Function( + name = "main", + body = + listOf( + S(GotoStatement("end")), + S( + LabeledStatement( + label = "start", + statement = ExpressionStatement(IntExpression(1)) + ) + ), + S(GotoStatement("start")), + S( + LabeledStatement( + label = "end", + statement = ReturnStatement(IntExpression(0)) + ) + ) + ) + ) + ) + + // Act & Assert: This should complete successfully without throwing an exception. + // If it throws, the test will fail automatically. + labelAnalysis.analyze(ast) + assertTrue(true, "Analysis of valid labels and gotos should complete successfully.") + } + + @Test + fun `test duplicate label throws DuplicateLabelException`() { + // Arrange: A program where the same label is defined twice. + val ast: ASTNode = + SimpleProgram( + functionDefinition = + Function( + name = "main", + body = + listOf( + S( + LabeledStatement( + label = "my_label", + statement = NullStatement() + ) + ), + S( + LabeledStatement( + label = "my_label", + statement = ReturnStatement(IntExpression(0)) + ) + ) + ) + ) + ) + + // Act & Assert: Expect the analysis to fail with the specific exception. + assertFailsWith { + labelAnalysis.analyze(ast) + } + } + + @Test + fun `test undeclared label throws UndeclaredLabelException`() { + // Arrange: A program with a goto that targets a non-existent label. + val ast: ASTNode = + SimpleProgram( + functionDefinition = + Function( + name = "main", + body = + listOf( + S(GotoStatement("missing_label")), + S(ReturnStatement(IntExpression(0))) + ) + ) + ) + + // Act & Assert: Expect the analysis to fail with the specific exception. + assertFailsWith { + labelAnalysis.analyze(ast) + } + } + + @Test + fun `test nested labels are found correctly`() { + // Arrange: A program where labels are nested inside an if statement. + val ast: ASTNode = + SimpleProgram( + functionDefinition = + Function( + name = "main", + body = + listOf( + S( + IfStatement( + condition = IntExpression(1), + then = ( + LabeledStatement( + label = "then_branch", + statement = GotoStatement("end") + ) + ), + _else = ( + LabeledStatement( + label = "else_branch", + statement = GotoStatement("end") + ) + ) + ) + ), + S( + LabeledStatement( + label = "end", + statement = ReturnStatement(IntExpression(0)) + ) + ) + ) + ) + ) + + // Act & Assert: This should complete successfully. + labelAnalysis.analyze(ast) + assertTrue(true, "Analysis of nested labels should complete successfully.") + } +}