Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 8 additions & 3 deletions .github/workflows/kotlin-checks.yml
Original file line number Diff line number Diff line change
Expand Up @@ -19,14 +19,19 @@ jobs:
distribution: 'temurin'
cache: gradle

- name: Cache Gradle dependencies
uses: actions/cache@v3
with:
path: ~/.gradle/caches
key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle*', '**/gradle-wrapper.properties') }}
restore-keys: |
${{ runner.os }}-gradle-

- name: Grant execute permission for gradlew
run: chmod +x gradlew

- name: Build with Gradle
run: ./gradlew build

- name: Run ktlint
run: ./gradlew ktlintCheck

- name: Kotlin Kover
run: ./gradlew koverXmlReport
1 change: 1 addition & 0 deletions .idea/.name

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions .idea/gradle.xml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions .idea/inspectionProfiles/Project_Default.xml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

7 changes: 7 additions & 0 deletions .idea/ktlint-plugin.xml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

20 changes: 20 additions & 0 deletions src/main/kotlin/Exceptions/SyntaxError.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
package org.example.Exceptions

class SyntaxError(
message: String,
val line: Int? = null,
val column: Int? = null
) : Exception(buildMessage(message, line, column)) {
companion object {
private fun buildMessage(
message: String,
line: Int?,
column: Int?
): String =
when {
line != null && column != null -> "Syntax error at line $line, column $column: $message"
line != null -> "Syntax error at line $line: $message"
else -> "Syntax error: $message"
}
}
}
20 changes: 12 additions & 8 deletions src/main/kotlin/Main.kt
Original file line number Diff line number Diff line change
@@ -1,15 +1,15 @@
package org.example

import Lexer
import lexer.Lexer
import org.example.parser.Parser

fun main() {
val code = """
int main(void){
int x = 5
int y = 3
int z = 5+3
}
""".trimIndent()
val code =
"""
int main(void){
return 5;
}
""".trimIndent()
val lexer = Lexer(code)
val tokens = lexer.tokenize()
tokens.forEach { token ->
Expand All @@ -19,4 +19,8 @@ fun main() {
"Position: [L:${token.line}, C:${token.column}]"
)
}

val parser = Parser()
val ast = parser.parseTokens(tokens)
println(ast.prettyPrint())
}
8 changes: 5 additions & 3 deletions src/main/kotlin/Lexer.kt → src/main/kotlin/lexer/Lexer.kt
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
package lexer

// a sealed class that can't be inherited and includes distinct objects for each category
// instead of instantiating objects, we use them directly

Expand Down Expand Up @@ -60,10 +62,10 @@ class Lexer(val source: String) {

// check if we are at end of the source code
private fun isAtEnd(): Boolean {
if (current >= source.length) {
return true
return if (current >= source.length) {
true
} else {
return false
false
}
}

Expand Down
6 changes: 6 additions & 0 deletions src/main/kotlin/parser/ASTNode.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
package org.example.parser

sealed class ASTNode {
abstract fun prettyPrint(indent: Int = 0): String
fun indent(level: Int): String = " ".repeat(level) // 4 spaces per level
}
13 changes: 13 additions & 0 deletions src/main/kotlin/parser/Expression.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package org.example.parser

import lexer.Token

sealed class Expression : ASTNode()

data class IntExpression(
val value: Token
) : Expression() {
override fun prettyPrint(indent: Int): String {
return "${indent(indent)}Int(${value.lexeme})"
}
}
18 changes: 18 additions & 0 deletions src/main/kotlin/parser/Functions.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
package org.example.parser

sealed class FunctionDefinition : ASTNode()

data class SimpleFunction(
val name: Identifier,
val body: Statement
) : FunctionDefinition() {
override fun prettyPrint(indent: Int): String {
return buildString {
appendLine("${indent(indent)}SimpleFunction(")
append("${indent(indent + 1)}name=${name.prettyPrint(0)}")
appendLine("${indent(indent + 1)}body=")
append("${indent(indent)}${body.prettyPrint(indent + 1)}")
appendLine("${indent(indent)})")
}
}
}
11 changes: 11 additions & 0 deletions src/main/kotlin/parser/Identifier.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package org.example.parser

import lexer.Token

data class Identifier(
val token: Token
) : ASTNode() {
override fun prettyPrint(indent: Int): String {
return buildString { appendLine("${indent(indent)}\"${token.lexeme}\"") }
}
}
91 changes: 91 additions & 0 deletions src/main/kotlin/parser/Parser.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
package org.example.parser

import lexer.Token
import lexer.TokenType
import org.example.Exceptions.SyntaxError

class Parser {
fun parseTokens(tokens: List<Token>): ASTNode {
val tokens = tokens.toMutableList()
val ast = parseProgram(tokens)

val lastToken = tokens.removeFirst()
if (lastToken.type != TokenType.EOF) {
throw SyntaxError(
line = lastToken.line,
column = lastToken.column,
message = "Expected end of file"
)
}
return ast
}

private fun parseProgram(tokens: MutableList<Token>): SimpleProgram {
val function = parseFunction(tokens)

return SimpleProgram(functionDefinition = function)
}

private fun parseFunction(tokens: MutableList<Token>): FunctionDefinition {
expect(TokenType.KEYWORD_INT, tokens)
val name = parseIdentifier(tokens)
expect(TokenType.LEFT_PAREN, tokens)
expect(TokenType.KEYWORD_VOID, tokens)
expect(TokenType.RIGHT_PAREN, tokens)
expect(TokenType.LEFT_BRACK, tokens)
val body = parseStatement(tokens)
expect(TokenType.RIGHT_BRACK, tokens)

return SimpleFunction(name = name, body = body)
}

private fun expect(
expected: TokenType,
tokens: MutableList<Token>
): Token {
val token = tokens.removeFirst()

if (token.type != expected) {
throw SyntaxError(
line = token.line,
column = token.column,
message = "Expected token: $expected, got ${token.type}"
)
}

return token
}

private fun parseIdentifier(tokens: MutableList<Token>): Identifier {
val token = tokens.removeFirst()
if (token.type != TokenType.IDENTIFIER) {
throw SyntaxError(
line = token.line,
column = token.column,
message = "Expected token: ${TokenType.IDENTIFIER}, got ${token.type}"
)
}

return Identifier(token = token)
}

private fun parseStatement(tokens: MutableList<Token>): Statement {
expect(TokenType.KEYWORD_RETURN, tokens)
val expression = parseExpression(tokens)
expect(TokenType.SEMICOLON, tokens)

return ReturnStatement(expression = expression)
}

private fun parseExpression(tokens: MutableList<Token>): Expression {
val token = tokens.removeFirst()
if (token.type != TokenType.INT_LITERAL) {
throw SyntaxError(
line = token.line,
column = token.column,
message = "Expected token: ${TokenType.INT_LITERAL}, got ${token.type}"
)
}
return IntExpression(value = token)
}
}
15 changes: 15 additions & 0 deletions src/main/kotlin/parser/Programs.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package org.example.parser

sealed class Program : ASTNode()

data class SimpleProgram(
val functionDefinition: FunctionDefinition
) : Program() {
override fun prettyPrint(indent: Int): String {
return buildString {
appendLine("${indent(indent)}SimpleProgram(")
append(functionDefinition.prettyPrint(indent + 1))
appendLine("${indent(indent)})")
}
}
}
15 changes: 15 additions & 0 deletions src/main/kotlin/parser/Statement.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package org.example.parser

sealed class Statement : ASTNode()

data class ReturnStatement(
val expression: Expression
) : Statement() {
override fun prettyPrint(indent: Int): String {
return buildString {
appendLine("${indent(indent)}ReturnStatement(")
appendLine(expression.prettyPrint(indent + 1))
appendLine("${indent(indent)})")
}
}
}
2 changes: 2 additions & 0 deletions src/test/kotlin/LexerTest.kt
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@

import lexer.Lexer
import lexer.TokenType
import org.junit.jupiter.api.Test
import kotlin.test.assertEquals

Expand Down
48 changes: 48 additions & 0 deletions src/test/kotlin/integration/LexerParserIntegrationTest.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
package integration

import lexer.Lexer
import org.example.parser.IntExpression
import org.example.parser.Parser
import org.example.parser.ReturnStatement
import org.example.parser.SimpleFunction
import org.example.parser.SimpleProgram
import org.junit.jupiter.api.Test
import kotlin.test.assertEquals
import kotlin.test.assertIs

class LexerParserIntegrationTest {

@Test
fun `test lexer and parser integration`() {
// Source code for a simple program
val source = "int main(void) { return 42; }"

// Use the lexer to tokenize the source
val lexer = Lexer(source)
val tokens = lexer.tokenize()

// Parse the tokens
val parser = Parser()
val ast = parser.parseTokens(tokens)

// Verify the AST structure
assertIs<SimpleProgram>(ast)
val program = ast

// Check function name
val function = program.functionDefinition
assertIs<SimpleFunction>(function)
val simpleFunction = function
assertEquals("main", simpleFunction.name.token.lexeme)

// Check return value
val returnStatement = simpleFunction.body
assertIs<ReturnStatement>(returnStatement)
val returnStmt = returnStatement

val expression = returnStmt.expression
assertIs<IntExpression>(expression)
val intExpr = expression
assertEquals("42", intExpr.value.lexeme)
}
}
Loading