From 0097d47c4c8a4635cad3b43f1ece0b208f92f9a5 Mon Sep 17 00:00:00 2001 From: JoudiAlakkad Date: Wed, 27 Aug 2025 22:29:18 +0200 Subject: [PATCH 1/8] Add conditional tokens to lexer. Issue #27 --- src/jsMain/kotlin/lexer/Lexer.kt | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/src/jsMain/kotlin/lexer/Lexer.kt b/src/jsMain/kotlin/lexer/Lexer.kt index 9812646..4d67922 100644 --- a/src/jsMain/kotlin/lexer/Lexer.kt +++ b/src/jsMain/kotlin/lexer/Lexer.kt @@ -12,6 +12,10 @@ sealed class TokenType { object IDENTIFIER : TokenType() + object IF : TokenType() + + object ELSE : TokenType() + // literals object INT_LITERAL : TokenType() @@ -64,6 +68,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 +101,9 @@ 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 ) fun tokenize(): List { @@ -130,6 +140,8 @@ class Lexer( '%' -> addToken(TokenType.REMAINDER) '*' -> addToken(TokenType.MULTIPLY) '/' -> addToken(TokenType.DIVIDE) + '?' -> addToken(TokenType.QUESTION_MARK) + ':' -> addToken(TokenType.COLON) '~' -> addToken(TokenType.TILDE) '-' -> { From 7c04306c394ee97210750fda6e0c00068304e0d8 Mon Sep 17 00:00:00 2001 From: JoudiAlakkad Date: Wed, 27 Aug 2025 23:42:21 +0200 Subject: [PATCH 2/8] Modify parseStatement and parseExpression to support if,else, "?" and ":". Issue #27 --- src/jsMain/kotlin/export/ASTExport.kt | 10 +++++++++ src/jsMain/kotlin/parser/BlockItems.kt | 8 +++++++ src/jsMain/kotlin/parser/Expressions.kt | 8 +++++++ src/jsMain/kotlin/parser/Parser.kt | 21 ++++++++++++++++++- .../kotlin/parser/VariableResolution.kt | 10 +++++++++ src/jsMain/kotlin/parser/Visitor.kt | 4 ++++ src/jsMain/kotlin/tacky/TackyGenVisitor.kt | 10 +++++++++ 7 files changed, 70 insertions(+), 1 deletion(-) diff --git a/src/jsMain/kotlin/export/ASTExport.kt b/src/jsMain/kotlin/export/ASTExport.kt index 3f9892c..1671346 100644 --- a/src/jsMain/kotlin/export/ASTExport.kt +++ b/src/jsMain/kotlin/export/ASTExport.kt @@ -7,10 +7,12 @@ 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.IfStatement import parser.IntExpression import parser.NullStatement import parser.ReturnStatement @@ -181,6 +183,14 @@ class ASTExport : Visitor { return Json.encodeToString(jsonNode) } + override fun visit(node: IfStatement): String { + TODO("Not yet implemented") + } + + override fun visit(node: ConditionalExpression): String { + TODO("Not yet implemented") + } + override fun visit(node: AssignmentExpression): String { val children = JsonObject( diff --git a/src/jsMain/kotlin/parser/BlockItems.kt b/src/jsMain/kotlin/parser/BlockItems.kt index 1ff2a13..a1fbb4d 100644 --- a/src/jsMain/kotlin/parser/BlockItems.kt +++ b/src/jsMain/kotlin/parser/BlockItems.kt @@ -20,6 +20,14 @@ 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) +} + 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/Parser.kt b/src/jsMain/kotlin/parser/Parser.kt index 7d7af22..c38941f 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, @@ -127,7 +128,20 @@ class Parser { private fun parseStatement(tokens: MutableList): Statement { var first: Token? = null - if (!tokens.isEmpty() && tokens.first().type == TokenType.KEYWORD_RETURN) { + if (!tokens.isEmpty() && tokens.first().type == TokenType.IF) { + tokens.removeFirst() + expect(TokenType.LEFT_PAREN, tokens) + val condition = parseExpression(0, tokens) + expect(TokenType.RIGHT_PAREN, tokens) + val thenStatement = parseStatement(tokens) + + var elseStatement: Statement? = null + if (tokens.firstOrNull()?.type == TokenType.ELSE) { + tokens.removeFirst() + elseStatement = parseStatement(tokens) + } + return IfStatement(condition, thenStatement, elseStatement) + } else if (!tokens.isEmpty() && tokens.first().type == TokenType.KEYWORD_RETURN) { first = tokens.removeFirst() } else if (!tokens.isEmpty() && tokens.first().type == TokenType.SEMICOLON) { tokens.removeFirst() @@ -165,6 +179,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..819b24c 100644 --- a/src/jsMain/kotlin/parser/VariableResolution.kt +++ b/src/jsMain/kotlin/parser/VariableResolution.kt @@ -6,11 +6,13 @@ 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.IfStatement import parser.IntExpression import parser.NullStatement import parser.ReturnStatement @@ -66,6 +68,14 @@ class VariableResolution : Visitor { override fun visit(node: IntExpression): ASTNode = node + override fun visit(node: IfStatement): ASTNode { + TODO("Not yet implemented") + } + + override fun visit(node: ConditionalExpression): ASTNode { + TODO("Not yet implemented") + } + 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..5b33020 100644 --- a/src/jsMain/kotlin/parser/Visitor.kt +++ b/src/jsMain/kotlin/parser/Visitor.kt @@ -19,6 +19,10 @@ interface Visitor { fun visit(node: IntExpression): T + fun visit(node: IfStatement): T + + fun visit(node: ConditionalExpression): 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..d5a9323 100644 --- a/src/jsMain/kotlin/tacky/TackyGenVisitor.kt +++ b/src/jsMain/kotlin/tacky/TackyGenVisitor.kt @@ -4,10 +4,12 @@ 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.IfStatement import parser.IntExpression import parser.NullStatement import parser.ReturnStatement @@ -160,6 +162,14 @@ class TackyGenVisitor : Visitor { override fun visit(node: IntExpression): TackyConstruct = TackyConstant(node.value) + override fun visit(node: IfStatement): TackyConstruct? { + TODO("Not yet implemented") + } + + override fun visit(node: ConditionalExpression): TackyConstruct? { + TODO("Not yet implemented") + } + override fun visit(node: AssignmentExpression): TackyConstruct { val rvalue = node.rvalue.accept(this) as TackyVal val dest = TackyVar(node.lvalue.name) From 0d47e0e352e8903e477f6cc3b74ba5f955e9e3e1 Mon Sep 17 00:00:00 2001 From: JoudiAlakkad Date: Thu, 28 Aug 2025 00:25:12 +0200 Subject: [PATCH 3/8] Update VariableResolution. Issue #27 --- src/jsMain/kotlin/parser/VariableResolution.kt | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/src/jsMain/kotlin/parser/VariableResolution.kt b/src/jsMain/kotlin/parser/VariableResolution.kt index 819b24c..f6e36cf 100644 --- a/src/jsMain/kotlin/parser/VariableResolution.kt +++ b/src/jsMain/kotlin/parser/VariableResolution.kt @@ -69,11 +69,19 @@ class VariableResolution : Visitor { override fun visit(node: IntExpression): ASTNode = node override fun visit(node: IfStatement): ASTNode { - TODO("Not yet implemented") + val condition = node.condition.accept(this) as Expression + val thenStatement = node.then.accept(this) as Statement + var elseStatement = node._else?.accept(this) as Statement? + TODO("Manage the scope in Compound statements") + return IfStatement(condition, thenStatement, elseStatement) } override fun visit(node: ConditionalExpression): ASTNode { - TODO("Not yet implemented") + val condition = node.codition.accept(this) as Expression + val thenExpression = node.thenExpression.accept(this) as Expression + val elseExpression = node.elseExpression.accept(this) as Expression + TODO("Manage the scope in Compound statements") + return ConditionalExpression(condition, thenExpression, elseExpression) } override fun visit(node: AssignmentExpression): ASTNode { From 173a8fb8c906a2f24edca119fede47b2190ea041 Mon Sep 17 00:00:00 2001 From: JoudiAlakkad Date: Thu, 28 Aug 2025 01:07:09 +0200 Subject: [PATCH 4/8] Update Tacky. Issue #27 --- src/jsMain/kotlin/tacky/TackyGenVisitor.kt | 36 ++++++++++++++++++++-- 1 file changed, 34 insertions(+), 2 deletions(-) diff --git a/src/jsMain/kotlin/tacky/TackyGenVisitor.kt b/src/jsMain/kotlin/tacky/TackyGenVisitor.kt index d5a9323..dcde137 100644 --- a/src/jsMain/kotlin/tacky/TackyGenVisitor.kt +++ b/src/jsMain/kotlin/tacky/TackyGenVisitor.kt @@ -163,11 +163,43 @@ class TackyGenVisitor : Visitor { override fun visit(node: IntExpression): TackyConstruct = TackyConstant(node.value) override fun visit(node: IfStatement): TackyConstruct? { - TODO("Not yet implemented") + 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? { - TODO("Not yet implemented") + 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: AssignmentExpression): TackyConstruct { From 6d09e8e88d425d9905142889b425f475b1f0f770 Mon Sep 17 00:00:00 2001 From: JoudiAlakkad Date: Thu, 28 Aug 2025 02:15:16 +0200 Subject: [PATCH 5/8] Add GOTO and LabeledStatement. Issue #27 --- .../exceptions/CompilationExceptions.kt | 12 ++ src/jsMain/kotlin/export/ASTExport.kt | 80 ++++++++- src/jsMain/kotlin/lexer/Lexer.kt | 5 +- src/jsMain/kotlin/parser/BlockItems.kt | 13 ++ src/jsMain/kotlin/parser/LabelAnalysis.kt | 153 ++++++++++++++++++ src/jsMain/kotlin/parser/Parser.kt | 11 ++ .../kotlin/parser/VariableResolution.kt | 9 ++ src/jsMain/kotlin/parser/Visitor.kt | 4 + src/jsMain/kotlin/tacky/TackyGenVisitor.kt | 14 ++ 9 files changed, 298 insertions(+), 3 deletions(-) create mode 100644 src/jsMain/kotlin/parser/LabelAnalysis.kt 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 1671346..018da94 100644 --- a/src/jsMain/kotlin/export/ASTExport.kt +++ b/src/jsMain/kotlin/export/ASTExport.kt @@ -12,8 +12,10 @@ 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 @@ -184,11 +186,85 @@ class ASTExport : Visitor { } override fun visit(node: IfStatement): String { - TODO("Not yet implemented") + 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 { - TODO("Not yet implemented") + 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 { diff --git a/src/jsMain/kotlin/lexer/Lexer.kt b/src/jsMain/kotlin/lexer/Lexer.kt index 4d67922..d1a444d 100644 --- a/src/jsMain/kotlin/lexer/Lexer.kt +++ b/src/jsMain/kotlin/lexer/Lexer.kt @@ -16,6 +16,8 @@ sealed class TokenType { object ELSE : TokenType() + object GOTO : TokenType() + // literals object INT_LITERAL : TokenType() @@ -103,7 +105,8 @@ class Lexer( "void" to TokenType.KEYWORD_VOID, "return" to TokenType.KEYWORD_RETURN, "if" to TokenType.IF, - "else" to TokenType.ELSE + "else" to TokenType.ELSE, + "goto" to TokenType.GOTO ) fun tokenize(): List { diff --git a/src/jsMain/kotlin/parser/BlockItems.kt b/src/jsMain/kotlin/parser/BlockItems.kt index a1fbb4d..fc8c259 100644 --- a/src/jsMain/kotlin/parser/BlockItems.kt +++ b/src/jsMain/kotlin/parser/BlockItems.kt @@ -28,6 +28,19 @@ class IfStatement( 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/LabelAnalysis.kt b/src/jsMain/kotlin/parser/LabelAnalysis.kt new file mode 100644 index 0000000..f59226f --- /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 = 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 c38941f..9aafcef 100644 --- a/src/jsMain/kotlin/parser/Parser.kt +++ b/src/jsMain/kotlin/parser/Parser.kt @@ -128,6 +128,7 @@ class Parser { private fun parseStatement(tokens: MutableList): Statement { var first: Token? = null + val secondToken = if (tokens.size > 1) tokens[1] else null if (!tokens.isEmpty() && tokens.first().type == TokenType.IF) { tokens.removeFirst() expect(TokenType.LEFT_PAREN, tokens) @@ -146,6 +147,16 @@ class Parser { } else if (!tokens.isEmpty() && tokens.first().type == TokenType.SEMICOLON) { tokens.removeFirst() return NullStatement() + } else if (!tokens.isEmpty() && tokens.first().type == TokenType.GOTO) { + tokens.removeFirst() + val label = parseIdentifier(tokens) + expect(TokenType.SEMICOLON, tokens) + return GotoStatement(label) + } else if (first?.type == TokenType.IDENTIFIER && secondToken?.type == TokenType.COLON) { + val labelName = parseIdentifier(tokens) + expect(TokenType.COLON, tokens) + val statement = parseStatement(tokens) + return LabeledStatement(labelName, statement) } val expression = parseExpression(tokens = tokens) expect(TokenType.SEMICOLON, tokens) diff --git a/src/jsMain/kotlin/parser/VariableResolution.kt b/src/jsMain/kotlin/parser/VariableResolution.kt index f6e36cf..7b23f96 100644 --- a/src/jsMain/kotlin/parser/VariableResolution.kt +++ b/src/jsMain/kotlin/parser/VariableResolution.kt @@ -12,8 +12,10 @@ 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 @@ -84,6 +86,13 @@ class VariableResolution : Visitor { 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 5b33020..11cbd23 100644 --- a/src/jsMain/kotlin/parser/Visitor.kt +++ b/src/jsMain/kotlin/parser/Visitor.kt @@ -23,6 +23,10 @@ interface Visitor { 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 dcde137..680302e 100644 --- a/src/jsMain/kotlin/tacky/TackyGenVisitor.kt +++ b/src/jsMain/kotlin/tacky/TackyGenVisitor.kt @@ -9,8 +9,10 @@ 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 @@ -202,6 +204,18 @@ class TackyGenVisitor : Visitor { 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) + node.statement.accept(this) + currentInstructions += TackyLabel(label.name) + return null + } + override fun visit(node: AssignmentExpression): TackyConstruct { val rvalue = node.rvalue.accept(this) as TackyVal val dest = TackyVar(node.lvalue.name) From 42c8bad97f14ddd4d35ee1de8e8dfd809db900e2 Mon Sep 17 00:00:00 2001 From: JoudiAlakkad Date: Fri, 29 Aug 2025 00:46:06 +0200 Subject: [PATCH 6/8] add test cases. Issue #27 --- src/jsMain/kotlin/export/CompilerExport.kt | 3 + src/jsMain/kotlin/parser/LabelAnalysis.kt | 2 +- src/jsMain/kotlin/parser/Parser.kt | 91 ++++++----- .../kotlin/parser/VariableResolution.kt | 2 - src/jsMain/kotlin/tacky/TackyGenVisitor.kt | 5 +- .../kotlin/integration/CompilerTestSuite.kt | 6 +- .../kotlin/integration/InvalidTestCases.kt | 60 +++++++ .../kotlin/integration/ValidTestCases.kt | 152 +++++++++++++++++- 8 files changed, 276 insertions(+), 45 deletions(-) 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/parser/LabelAnalysis.kt b/src/jsMain/kotlin/parser/LabelAnalysis.kt index f59226f..a43b71a 100644 --- a/src/jsMain/kotlin/parser/LabelAnalysis.kt +++ b/src/jsMain/kotlin/parser/LabelAnalysis.kt @@ -23,7 +23,7 @@ import parser.VariableExpression import parser.Visitor class LabelCollector : Visitor { - val definedLabels = mutableSetOf() + val definedLabels: MutableSet = mutableSetOf() override fun visit(node: LabeledStatement) { if (!definedLabels.add(node.label)) { diff --git a/src/jsMain/kotlin/parser/Parser.kt b/src/jsMain/kotlin/parser/Parser.kt index 9aafcef..fb3d99b 100644 --- a/src/jsMain/kotlin/parser/Parser.kt +++ b/src/jsMain/kotlin/parser/Parser.kt @@ -127,46 +127,65 @@ class Parser { } private fun parseStatement(tokens: MutableList): Statement { - var first: Token? = null - val secondToken = if (tokens.size > 1) tokens[1] else null - if (!tokens.isEmpty() && tokens.first().type == TokenType.IF) { - tokens.removeFirst() - expect(TokenType.LEFT_PAREN, tokens) - val condition = parseExpression(0, tokens) - expect(TokenType.RIGHT_PAREN, tokens) - val thenStatement = parseStatement(tokens) + val first = tokens.firstOrNull() + val second = if (tokens.size > 1) tokens[1] else null + + 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 + } - var elseStatement: Statement? = null - if (tokens.firstOrNull()?.type == TokenType.ELSE) { + 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() - elseStatement = parseStatement(tokens) + NullStatement() } - return IfStatement(condition, thenStatement, elseStatement) - } else 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() - } else if (!tokens.isEmpty() && tokens.first().type == TokenType.GOTO) { - tokens.removeFirst() - val label = parseIdentifier(tokens) - expect(TokenType.SEMICOLON, tokens) - return GotoStatement(label) - } else if (first?.type == TokenType.IDENTIFIER && secondToken?.type == TokenType.COLON) { - val labelName = parseIdentifier(tokens) - expect(TokenType.COLON, tokens) - val statement = parseStatement(tokens) - return LabeledStatement(labelName, statement) - } - val expression = parseExpression(tokens = tokens) - expect(TokenType.SEMICOLON, tokens) - return if (first != null) { - ReturnStatement( - expression = expression - ) - } else { - ExpressionStatement(expression) + // 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) + } } } diff --git a/src/jsMain/kotlin/parser/VariableResolution.kt b/src/jsMain/kotlin/parser/VariableResolution.kt index 7b23f96..a48b896 100644 --- a/src/jsMain/kotlin/parser/VariableResolution.kt +++ b/src/jsMain/kotlin/parser/VariableResolution.kt @@ -74,7 +74,6 @@ class VariableResolution : Visitor { val condition = node.condition.accept(this) as Expression val thenStatement = node.then.accept(this) as Statement var elseStatement = node._else?.accept(this) as Statement? - TODO("Manage the scope in Compound statements") return IfStatement(condition, thenStatement, elseStatement) } @@ -82,7 +81,6 @@ class VariableResolution : Visitor { val condition = node.codition.accept(this) as Expression val thenExpression = node.thenExpression.accept(this) as Expression val elseExpression = node.elseExpression.accept(this) as Expression - TODO("Manage the scope in Compound statements") return ConditionalExpression(condition, thenExpression, elseExpression) } diff --git a/src/jsMain/kotlin/tacky/TackyGenVisitor.kt b/src/jsMain/kotlin/tacky/TackyGenVisitor.kt index 680302e..df343ea 100644 --- a/src/jsMain/kotlin/tacky/TackyGenVisitor.kt +++ b/src/jsMain/kotlin/tacky/TackyGenVisitor.kt @@ -74,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) } @@ -210,9 +211,9 @@ class TackyGenVisitor : Visitor { } override fun visit(node: LabeledStatement): TackyConstruct? { - val label = newLabel(node.label) + // val label = newLabel(node.label) + currentInstructions += TackyLabel(node.label) node.statement.accept(this) - currentInstructions += TackyLabel(label.name) return null } diff --git a/src/jsTest/kotlin/integration/CompilerTestSuite.kt b/src/jsTest/kotlin/integration/CompilerTestSuite.kt index b1270d8..265551a 100644 --- a/src/jsTest/kotlin/integration/CompilerTestSuite.kt +++ b/src/jsTest/kotlin/integration/CompilerTestSuite.kt @@ -18,7 +18,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 +45,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 +109,14 @@ class CompilerTestSuite { // Parser stage if (testCase.failingStage == CompilerStage.PARSER) { assertFailsWith(testCase.expectedException) { + val variableResolution = VariableResolution() val ast = parser.parseTokens(tokens) as SimpleProgram 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..56c089d 100644 --- a/src/jsTest/kotlin/integration/InvalidTestCases.kt +++ b/src/jsTest/kotlin/integration/InvalidTestCases.kt @@ -1,9 +1,11 @@ package integration import compiler.CompilerStage +import exceptions.DuplicateLabelException import exceptions.DuplicateVariableDeclaration import exceptions.InvalidLValueException import exceptions.LexicalException +import exceptions.UndeclaredLabelException import exceptions.UndeclaredVariableException import exceptions.UnexpectedTokenException import kotlin.reflect.KClass @@ -110,6 +112,64 @@ 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, // Thrown by VariableResolution, caught in the Parser stage + 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 + ), + InvalidTestCase( + code = "int main(void) { my_label: return 0; my_label: return 1; }", // Duplicate label + failingStage = CompilerStage.PARSER, + expectedException = DuplicateLabelException::class + ), + InvalidTestCase( + code = "int main(void) { goto missing_label; }", // Undeclared label + failingStage = CompilerStage.PARSER, + expectedException = UndeclaredLabelException::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)) + ) + ) + ) ) ) } From 43f56062ed3831959df216cb7bea9ef452dc20e9 Mon Sep 17 00:00:00 2001 From: JoudiAlakkad Date: Fri, 29 Aug 2025 01:54:32 +0200 Subject: [PATCH 7/8] add test cases. Issue #27 --- src/jsMain/kotlin/export/CompilerExport.kt | 6 +- src/jsMain/kotlin/parser/LabelAnalysis.kt | 2 + .../kotlin/integration/InvalidTestCases.kt | 12 -- src/jsTest/kotlin/parser/LabelAnalysisTest.kt | 142 ++++++++++++++++++ 4 files changed, 147 insertions(+), 15 deletions(-) create mode 100644 src/jsTest/kotlin/parser/LabelAnalysisTest.kt diff --git a/src/jsMain/kotlin/export/CompilerExport.kt b/src/jsMain/kotlin/export/CompilerExport.kt index 75e1763..8b4e543 100644 --- a/src/jsMain/kotlin/export/CompilerExport.kt +++ b/src/jsMain/kotlin/export/CompilerExport.kt @@ -76,13 +76,13 @@ class CompilerExport { errors = emptyArray(), ast = ast.accept(ASTExport()) ) - } catch (e: CompilationExceptions) { + } catch (e: Exception) { val error = CompilationError( type = ErrorType.SYNTAX, message = e.message ?: "Unknown syntax error", - line = e.line ?: -1, - column = e.column ?: -1 + line = (e as? CompilationExceptions)?.line ?: -1, + column = (e as? CompilationExceptions)?.column ?: -1 ) overallErrors.add(error) ParserOutput( diff --git a/src/jsMain/kotlin/parser/LabelAnalysis.kt b/src/jsMain/kotlin/parser/LabelAnalysis.kt index a43b71a..1489463 100644 --- a/src/jsMain/kotlin/parser/LabelAnalysis.kt +++ b/src/jsMain/kotlin/parser/LabelAnalysis.kt @@ -144,6 +144,8 @@ private class GotoValidator( class LabelAnalysis { fun analyze(ast: ASTNode) { + console.log("[LabelAnalysis] Starting analysis on AST: $ast") + val collector = LabelCollector() ast.accept(collector) diff --git a/src/jsTest/kotlin/integration/InvalidTestCases.kt b/src/jsTest/kotlin/integration/InvalidTestCases.kt index 56c089d..ef81d94 100644 --- a/src/jsTest/kotlin/integration/InvalidTestCases.kt +++ b/src/jsTest/kotlin/integration/InvalidTestCases.kt @@ -1,11 +1,9 @@ package integration import compiler.CompilerStage -import exceptions.DuplicateLabelException import exceptions.DuplicateVariableDeclaration import exceptions.InvalidLValueException import exceptions.LexicalException -import exceptions.UndeclaredLabelException import exceptions.UndeclaredVariableException import exceptions.UnexpectedTokenException import kotlin.reflect.KClass @@ -160,16 +158,6 @@ object InvalidTestCases { code = "int main(void) { 1 = 2; return 0; }", // Invalid L-value failingStage = CompilerStage.PARSER, expectedException = InvalidLValueException::class - ), - InvalidTestCase( - code = "int main(void) { my_label: return 0; my_label: return 1; }", // Duplicate label - failingStage = CompilerStage.PARSER, - expectedException = DuplicateLabelException::class - ), - InvalidTestCase( - code = "int main(void) { goto missing_label; }", // Undeclared label - failingStage = CompilerStage.PARSER, - expectedException = UndeclaredLabelException::class ) ) } diff --git a/src/jsTest/kotlin/parser/LabelAnalysisTest.kt b/src/jsTest/kotlin/parser/LabelAnalysisTest.kt new file mode 100644 index 0000000..fbb7f45 --- /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.") + } +} From 45c7976f5c1bae372ff85aa7f343fde236f9257e Mon Sep 17 00:00:00 2001 From: JoudiAlakkad Date: Fri, 29 Aug 2025 13:11:45 +0200 Subject: [PATCH 8/8] add test cases (update CompilerTestSuite, build gradle). Issue #27 --- build.gradle.kts | 46 ++++--- src/jsMain/kotlin/export/CompilerExport.kt | 6 +- src/jsMain/kotlin/parser/LabelAnalysis.kt | 2 - .../kotlin/integration/CompilerTestSuite.kt | 3 + .../kotlin/integration/InvalidTestCases.kt | 2 +- src/jsTest/kotlin/parser/LabelAnalysisTest.kt | 130 +++++++++--------- 6 files changed, 100 insertions(+), 89 deletions(-) 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/export/CompilerExport.kt b/src/jsMain/kotlin/export/CompilerExport.kt index 8b4e543..75e1763 100644 --- a/src/jsMain/kotlin/export/CompilerExport.kt +++ b/src/jsMain/kotlin/export/CompilerExport.kt @@ -76,13 +76,13 @@ class CompilerExport { errors = emptyArray(), ast = ast.accept(ASTExport()) ) - } catch (e: Exception) { + } catch (e: CompilationExceptions) { val error = CompilationError( type = ErrorType.SYNTAX, message = e.message ?: "Unknown syntax error", - line = (e as? CompilationExceptions)?.line ?: -1, - column = (e as? CompilationExceptions)?.column ?: -1 + line = e.line ?: -1, + column = e.column ?: -1 ) overallErrors.add(error) ParserOutput( diff --git a/src/jsMain/kotlin/parser/LabelAnalysis.kt b/src/jsMain/kotlin/parser/LabelAnalysis.kt index 1489463..a43b71a 100644 --- a/src/jsMain/kotlin/parser/LabelAnalysis.kt +++ b/src/jsMain/kotlin/parser/LabelAnalysis.kt @@ -144,8 +144,6 @@ private class GotoValidator( class LabelAnalysis { fun analyze(ast: ASTNode) { - console.log("[LabelAnalysis] Starting analysis on AST: $ast") - val collector = LabelCollector() ast.accept(collector) diff --git a/src/jsTest/kotlin/integration/CompilerTestSuite.kt b/src/jsTest/kotlin/integration/CompilerTestSuite.kt index 265551a..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 @@ -109,8 +110,10 @@ 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 diff --git a/src/jsTest/kotlin/integration/InvalidTestCases.kt b/src/jsTest/kotlin/integration/InvalidTestCases.kt index ef81d94..0cee321 100644 --- a/src/jsTest/kotlin/integration/InvalidTestCases.kt +++ b/src/jsTest/kotlin/integration/InvalidTestCases.kt @@ -146,7 +146,7 @@ object InvalidTestCases { // Semantic Errors (caught after parsing) InvalidTestCase( code = "int main(void) { int a; int a; return a; }", // Duplicate variable - failingStage = CompilerStage.PARSER, // Thrown by VariableResolution, caught in the Parser stage + failingStage = CompilerStage.PARSER, expectedException = DuplicateVariableDeclaration::class ), InvalidTestCase( diff --git a/src/jsTest/kotlin/parser/LabelAnalysisTest.kt b/src/jsTest/kotlin/parser/LabelAnalysisTest.kt index fbb7f45..5df7426 100644 --- a/src/jsTest/kotlin/parser/LabelAnalysisTest.kt +++ b/src/jsTest/kotlin/parser/LabelAnalysisTest.kt @@ -16,26 +16,26 @@ class LabelAnalysisTest { 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)) - ) - ) + 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. @@ -50,24 +50,24 @@ class LabelAnalysisTest { 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)) - ) - ) + 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. @@ -82,14 +82,14 @@ class LabelAnalysisTest { val ast: ASTNode = SimpleProgram( functionDefinition = - Function( - name = "main", - body = - listOf( - S(GotoStatement("missing_label")), - S(ReturnStatement(IntExpression(0))) - ) + Function( + name = "main", + body = + listOf( + S(GotoStatement("missing_label")), + S(ReturnStatement(IntExpression(0))) ) + ) ) // Act & Assert: Expect the analysis to fail with the specific exception. @@ -104,35 +104,35 @@ class LabelAnalysisTest { 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") - ) - ) + Function( + name = "main", + body = + listOf( + S( + IfStatement( + condition = IntExpression(1), + then = ( + LabeledStatement( + label = "then_branch", + statement = GotoStatement("end") ) - ), - S( + ), + _else = ( LabeledStatement( - label = "end", - statement = ReturnStatement(IntExpression(0)) + label = "else_branch", + statement = GotoStatement("end") + ) ) - ) ) + ), + S( + LabeledStatement( + label = "end", + statement = ReturnStatement(IntExpression(0)) + ) + ) ) + ) ) // Act & Assert: This should complete successfully.