Skip to content

Commit af50ce5

Browse files
committed
refactor: improve error handling and redability
1 parent 4274083 commit af50ce5

8 files changed

Lines changed: 253 additions & 62 deletions

File tree

build.gradle.kts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,10 @@ repositories {
1616
mavenCentral()
1717
}
1818

19+
dependencies {
20+
implementation("com.github.ajalt.mordant:mordant:2.7.2")
21+
}
22+
1923
kotlin {
2024
jvmToolchain(21)
2125
}
Lines changed: 33 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,25 @@
11
package com.aymanetech
22

3-
import com.aymanetech.TokenType.EOF
3+
import com.aymanetech.interpreter.Interpreter
4+
import com.aymanetech.runtime.errors.ErrorHandler
5+
import com.aymanetech.lexer.Scanner
6+
import com.aymanetech.parser.Parser
7+
import com.aymanetech.resolver.Resolver
8+
import com.github.ajalt.mordant.rendering.TextColors.*
9+
import com.github.ajalt.mordant.rendering.TextStyles.*
10+
import com.github.ajalt.mordant.terminal.Terminal
411
import java.io.BufferedReader
512
import java.io.InputStreamReader
6-
import java.lang.System.err
713
import java.nio.charset.Charset
814
import java.nio.file.Files
915
import java.nio.file.Paths
1016
import kotlin.system.exitProcess
1117

1218

1319
object Lox {
14-
private var hadError = false
15-
private var hadRuntimeError = false
16-
1720
private val interpreter = Interpreter()
21+
private val errorHandler = ErrorHandler()
22+
private val terminal = Terminal()
1823

1924
@JvmStatic
2025
fun main(args: Array<String>) {
@@ -31,57 +36,44 @@ object Lox {
3136
fun runFile(path: String) {
3237
val bytes = Files.readAllBytes(Paths.get(path))
3338
val source = String(bytes, Charset.defaultCharset())
39+
errorHandler.setSource(source, path)
3440
run(source)
35-
if (hadError) exitProcess(65)
36-
if (hadRuntimeError) exitProcess(70)
41+
if (errorHandler.hadError) exitProcess(65)
42+
if (errorHandler.hadRuntimeError) exitProcess(70)
3743
}
3844

3945
fun runPrompt() {
4046
val input = InputStreamReader(System.`in`)
4147
val reader = BufferedReader(input)
4248

49+
terminal.println(cyan(bold("\n╔════════════════════════════════════════╗")))
50+
terminal.println(cyan(bold("")) + " " + magenta(bold("Welcome to KLox REPL")) + " " + cyan(bold("")))
51+
terminal.println(cyan(bold("")) + " " + dim("Type 'exit' or Ctrl+D to quit") + " " + cyan(bold("")))
52+
terminal.println(cyan(bold("╚════════════════════════════════════════╝\n")))
53+
4354
while (true) {
44-
print(">> ")
55+
terminal.print(green(bold("klox> ")))
4556
val line: String = reader.readLine() ?: break
46-
run(line)
47-
hadError = false
48-
}
49-
}
5057

51-
fun run(source: String) {
52-
val tokens = Scanner(source).scanTokens()
53-
val statements = Parser(tokens).parse()
54-
if (hadError) return
58+
if (line.trim() == "exit") break
59+
if (line.trim().isEmpty()) continue
5560

56-
Resolver(interpreter).resolve(statements)
57-
if (hadError) return
58-
59-
interpreter.interpret(statements)
60-
}
61+
errorHandler.setSource(line, "repl")
62+
run(line)
63+
errorHandler.reset()
64+
}
6165

62-
fun error(line: Int, message: String) {
63-
report(line, "", message)
64-
hadError = true
66+
terminal.println(cyan("\n👋 Goodbye!"))
6567
}
6668

67-
fun report(line: Int, where: String, message: String) {
68-
err.println("[line $line] Error $where: $message")
69-
}
69+
fun run(source: String) {
70+
val tokens = Scanner(source, errorHandler).scanTokens()
71+
val statements = Parser(tokens, errorHandler).parse()
72+
if (errorHandler.hadError) return
7073

71-
fun error(token: Token, message: String) {
72-
if (token.type == EOF)
73-
report(token.line, "at end ", message)
74-
else
75-
report(token.line, "at '${token.lexeme}'", message)
76-
}
74+
Resolver(interpreter, errorHandler).resolve(statements)
75+
if (errorHandler.hadError) return
7776

78-
fun runtimeError(error: RuntimeError) {
79-
err.println(
80-
"""
81-
${error.message}
82-
[line ${error.token.line}]
83-
""".trimIndent()
84-
)
85-
hadRuntimeError = true
77+
interpreter.interpret(statements, errorHandler)
8678
}
8779
}

src/main/kotlin/com/aymanetech/interpreter/Interpreter.kt

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
package com.aymanetech.interpreter
22

3-
import com.aymanetech.Lox.runtimeError
3+
import com.aymanetech.runtime.errors.ErrorHandler
44
import com.aymanetech.ast.Expr
55
import com.aymanetech.ast.Expr.*
66
import com.aymanetech.ast.Stmt
@@ -26,11 +26,11 @@ class Interpreter : Expr.Visitor<Any?>, Stmt.Visitor<Unit> {
2626
}
2727
}
2828

29-
fun interpret(statements: List<Stmt>) {
29+
fun interpret(statements: List<Stmt>, errorHandler: ErrorHandler) {
3030
try {
3131
statements.forEach(::execute)
3232
} catch (error: RuntimeError) {
33-
runtimeError(error)
33+
errorHandler.reportRuntimeError(error)
3434
}
3535
}
3636

src/main/kotlin/com/aymanetech/lexer/Scanner.kt

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
11
package com.aymanetech.lexer
22

3-
import com.aymanetech.Lox.error
3+
import com.aymanetech.runtime.errors.ErrorHandler
44
import com.aymanetech.lexer.TokenType.*
55

6-
class Scanner(val source: String) {
6+
class Scanner(val source: String, private val errorHandler: ErrorHandler) {
77
private val tokens: MutableList<Token> = mutableListOf()
88
private val chars: CharArray = source.toCharArray()
99
private val keywords: Map<String, TokenType> = mapOf(
@@ -69,7 +69,7 @@ class Scanner(val source: String) {
6969
when {
7070
isDigit(c) -> consumeNumber()
7171
isAlpha(c) -> consumeIdentifier()
72-
else -> error(line, message = "Unexpected character '$c'")
72+
else -> errorHandler.reportError(line, "Unexpected character '$c'")
7373
}
7474
}
7575
}
@@ -83,7 +83,7 @@ class Scanner(val source: String) {
8383
private fun advance(): Char = chars[current++]
8484

8585
private fun peek() =
86-
if (isAtEnd()) '\u0000' // todo: check that this is equivalent to \0
86+
if (isAtEnd()) '\u0000'
8787
else chars[current]
8888

8989
private fun peekNext() =
@@ -97,7 +97,7 @@ class Scanner(val source: String) {
9797
}
9898

9999
if (isAtEnd()) {
100-
error(line, "unterminated string.")
100+
errorHandler.reportError(line, "Unterminated string.")
101101
return
102102
}
103103

src/main/kotlin/com/aymanetech/parser/Parser.kt

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,12 @@
11
package com.aymanetech.parser
22

3-
import com.aymanetech.Lox
3+
import com.aymanetech.runtime.errors.ErrorHandler
44
import com.aymanetech.ast.Expr
55
import com.aymanetech.ast.Stmt
66
import com.aymanetech.lexer.Token
77
import com.aymanetech.lexer.TokenType
88

9-
class Parser(private val tokens: List<Token>) {
9+
class Parser(private val tokens: List<Token>, private val errorHandler: ErrorHandler) {
1010
private var current = 0
1111

1212
fun parse(): List<Stmt> {
@@ -339,7 +339,7 @@ class Parser(private val tokens: List<Token>) {
339339
}
340340

341341
private fun error(token: Token, message: String): ParserError {
342-
Lox.error(token, message)
342+
errorHandler.reportError(token, message)
343343
return ParserError()
344344
}
345345

src/main/kotlin/com/aymanetech/resolver/Resolver.kt

Lines changed: 13 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,17 @@
11
package com.aymanetech.resolver
22

33
import com.aymanetech.interpreter.Interpreter
4-
import com.aymanetech.Lox
4+
import com.aymanetech.runtime.errors.ErrorHandler
55
import com.aymanetech.ast.Expr
66
import com.aymanetech.ast.Stmt
77
import com.aymanetech.lexer.Token
88
import java.util.Stack
99
import kotlin.collections.forEach
1010

11-
class Resolver(private val interpreter: Interpreter) : Expr.Visitor<Unit>, Stmt.Visitor<Unit> {
11+
class Resolver(
12+
private val interpreter: Interpreter,
13+
private val errorHandler: ErrorHandler
14+
) : Expr.Visitor<Unit>, Stmt.Visitor<Unit> {
1215
private val scopes: Stack<MutableMap<String, Boolean>> = Stack()
1316
private var currentFunction = FunctionType.NONE
1417
private var currentClass = ClassType.NONE
@@ -43,7 +46,7 @@ class Resolver(private val interpreter: Interpreter) : Expr.Visitor<Unit>, Stmt.
4346

4447
override fun visit(expr: Expr.Variable) {
4548
if (scopes.isNotEmpty() && scopes.peek()[expr.name.lexeme] == false)
46-
Lox.error(expr.name, "Can't read local variable in it's own initializer")
49+
errorHandler.reportError(expr.name, "Can't read local variable in it's own initializer")
4750

4851
resolveLocal(expr, expr.name)
4952
}
@@ -60,16 +63,16 @@ class Resolver(private val interpreter: Interpreter) : Expr.Visitor<Unit>, Stmt.
6063

6164
override fun visit(expr: Expr.Super) {
6265
if(currentClass == ClassType.NONE)
63-
Lox.error(expr.keyword, "Can't use 'super' outside of a class.")
66+
errorHandler.reportError(expr.keyword, "Can't use 'super' outside of a class.")
6467
else if (currentClass != ClassType.SUBCLASS)
65-
Lox.error(expr.keyword, "Can't use 'super' in a class without a superclass.")
68+
errorHandler.reportError(expr.keyword, "Can't use 'super' in a class without a superclass.")
6669

6770
resolveLocal(expr, expr.keyword)
6871
}
6972

7073
override fun visit(expr: Expr.This) {
7174
if (currentClass == ClassType.NONE) {
72-
Lox.error(expr.keyword, "Can't use 'this' outside of a method")
75+
errorHandler.reportError(expr.keyword, "Can't use 'this' outside of a method")
7376
return
7477
}
7578
resolveLocal(expr, expr.keyword)
@@ -128,10 +131,10 @@ class Resolver(private val interpreter: Interpreter) : Expr.Visitor<Unit>, Stmt.
128131

129132
override fun visit(stmt: Stmt.Return) {
130133
if (currentFunction == FunctionType.NONE)
131-
Lox.error(stmt.token, "Can't return from top-level code")
134+
errorHandler.reportError(stmt.token, "Can't return from top-level code")
132135

133136
if (stmt.value != null && currentFunction == FunctionType.INITIALIZER)
134-
Lox.error(stmt.token, "Can't return from a constructor")
137+
errorHandler.reportError(stmt.token, "Can't return from a constructor")
135138

136139
stmt.value?.let { resolve(it) }
137140
}
@@ -142,7 +145,7 @@ class Resolver(private val interpreter: Interpreter) : Expr.Visitor<Unit>, Stmt.
142145
declare(stmt.name)
143146
define(stmt.name)
144147
if(stmt.superClass != null && stmt.name.lexeme.equals(stmt.superClass.name.lexeme))
145-
Lox.error(stmt.superClass.name, "A class can't inherit from itself")
148+
errorHandler.reportError(stmt.superClass.name, "A class can't inherit from itself")
146149

147150
if (stmt.superClass != null){
148151
currentClass = ClassType.SUBCLASS
@@ -206,7 +209,7 @@ class Resolver(private val interpreter: Interpreter) : Expr.Visitor<Unit>, Stmt.
206209
if (scopes.isEmpty()) return
207210
val scope = scopes.peek()
208211
if (scope.containsKey(name.lexeme))
209-
Lox.error(name, "Already variable with this name in this scope")
212+
errorHandler.reportError(name, "Already variable with this name in this scope")
210213
scope[name.lexeme] = false
211214

212215
}
Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
package com.aymanetech.runtime.errors
2+
3+
import com.github.ajalt.mordant.rendering.TextColors.*
4+
import com.github.ajalt.mordant.rendering.TextStyles.*
5+
import com.github.ajalt.mordant.terminal.Terminal
6+
7+
class DiagnosticReporter(
8+
private val source: String,
9+
private val filename: String = "script.lox"
10+
) {
11+
private val terminal = Terminal()
12+
private val lines = source.lines()
13+
14+
fun reportError(
15+
line: Int,
16+
column: Int = 0,
17+
length: Int = 1,
18+
message: String,
19+
hint: String? = null,
20+
where: String = ""
21+
) {
22+
if (line < 1 || line > lines.size) {
23+
terminal.println(red(bold("error")) + ": $message")
24+
terminal.println(blue(" --> $filename:$line"))
25+
terminal.println()
26+
return
27+
}
28+
29+
val lineNumber = line.toString().padStart(4)
30+
val errorLine = lines[line - 1]
31+
32+
terminal.println(red(bold("error")) + ": $message")
33+
terminal.println(blue(" --> $filename:$line:$column"))
34+
terminal.println(blue(" |"))
35+
36+
if (line > 1 && lines.size > 1) {
37+
val prevLineNum = (line - 1).toString().padStart(4)
38+
terminal.println(blue(" $prevLineNum | ") + dim(lines[line - 2]))
39+
}
40+
41+
terminal.println(blue(" $lineNumber | ") + errorLine)
42+
43+
if (column > 0) {
44+
val padding = " ".repeat(column - 1)
45+
val underline = "^".repeat(length.coerceAtLeast(1))
46+
terminal.println(blue(" | ") + red(bold(padding + underline)))
47+
if (where.isNotEmpty()) {
48+
terminal.println(blue(" | ") + red(padding + where))
49+
}
50+
}
51+
52+
if (hint != null) {
53+
terminal.println(blue(" = ") + cyan(bold("hint: ") + hint))
54+
}
55+
56+
if (line < lines.size) {
57+
val nextLineNum = (line + 1).toString().padStart(4)
58+
terminal.println(blue(" $nextLineNum | ") + dim(lines[line]))
59+
}
60+
61+
terminal.println(blue(" |"))
62+
terminal.println()
63+
}
64+
65+
fun reportRuntimeError(
66+
line: Int,
67+
message: String,
68+
hint: String? = null
69+
) {
70+
if (line < 1 || line > lines.size) {
71+
terminal.println(red(bold("runtime error")) + ": $message")
72+
terminal.println(blue(" --> $filename:$line"))
73+
terminal.println()
74+
return
75+
}
76+
77+
val lineNumber = line.toString().padStart(4)
78+
val errorLine = lines[line - 1]
79+
80+
terminal.println(red(bold("runtime error")) + ": $message")
81+
terminal.println(blue(" --> $filename:$line"))
82+
terminal.println(blue(" |"))
83+
84+
if (line > 1 && lines.size > 1) {
85+
val prevLineNum = (line - 1).toString().padStart(4)
86+
terminal.println(blue(" $prevLineNum | ") + dim(lines[line - 2]))
87+
}
88+
89+
terminal.println(blue(" $lineNumber | ") + errorLine)
90+
91+
if (hint != null) {
92+
terminal.println(blue(" = ") + cyan(bold("hint: ") + hint))
93+
}
94+
95+
if (line < lines.size) {
96+
val nextLineNum = (line + 1).toString().padStart(4)
97+
terminal.println(blue(" $nextLineNum | ") + dim(lines[line]))
98+
}
99+
100+
terminal.println(blue(" |"))
101+
terminal.println()
102+
}
103+
}

0 commit comments

Comments
 (0)