feat(lexer): add tokenizer
This commit is contained in:
1 parent
3dca55184f
commit
7a8b8dd898
1 file changed
+200
@@ -0,0 +1,200 @@
|
||||
package lumen.lexer
|
||||
|
||||
import lumen.LexError
|
||||
|
||||
private val keywords = mapOf(
|
||||
"let" to TokenType.Let,
|
||||
"var" to TokenType.Var,
|
||||
"fn" to TokenType.Fn,
|
||||
"return" to TokenType.Return,
|
||||
"if" to TokenType.If,
|
||||
"else" to TokenType.Else,
|
||||
"while" to TokenType.While,
|
||||
"for" to TokenType.For,
|
||||
"in" to TokenType.In,
|
||||
"true" to TokenType.True,
|
||||
"false" to TokenType.False,
|
||||
"nil" to TokenType.Nil,
|
||||
"and" to TokenType.And,
|
||||
"or" to TokenType.Or,
|
||||
"not" to TokenType.Not,
|
||||
"import" to TokenType.Import
|
||||
)
|
||||
|
||||
class Lexer(private val source: String) {
|
||||
private val tokens = mutableListOf<Token>()
|
||||
private var start = 0
|
||||
private var current = 0
|
||||
private var line = 1
|
||||
private var lineStart = 0
|
||||
private var groupingDepth = 0
|
||||
|
||||
fun scan(): List<Token> {
|
||||
while (!atEnd()) {
|
||||
start = current
|
||||
scanToken()
|
||||
}
|
||||
tokens.add(Token(TokenType.Eof, "", line, current - lineStart + 1))
|
||||
return tokens
|
||||
}
|
||||
|
||||
private fun scanToken() {
|
||||
val c = advance()
|
||||
when (c) {
|
||||
' ', '\t', '\r' -> {}
|
||||
'\n' -> {
|
||||
if (groupingDepth == 0) add(TokenType.Newline)
|
||||
line++
|
||||
lineStart = current
|
||||
}
|
||||
'(' -> {
|
||||
groupingDepth++
|
||||
add(TokenType.LeftParen)
|
||||
}
|
||||
')' -> {
|
||||
if (groupingDepth > 0) groupingDepth--
|
||||
add(TokenType.RightParen)
|
||||
}
|
||||
'[' -> {
|
||||
groupingDepth++
|
||||
add(TokenType.LeftBracket)
|
||||
}
|
||||
']' -> {
|
||||
if (groupingDepth > 0) groupingDepth--
|
||||
add(TokenType.RightBracket)
|
||||
}
|
||||
'{' -> add(TokenType.LeftBrace)
|
||||
'}' -> add(TokenType.RightBrace)
|
||||
',' -> add(TokenType.Comma)
|
||||
':' -> add(TokenType.Colon)
|
||||
';' -> add(TokenType.Semicolon)
|
||||
'.' -> add(TokenType.Dot)
|
||||
'+' -> add(TokenType.Plus)
|
||||
'-' -> add(TokenType.Minus)
|
||||
'*' -> add(TokenType.Star)
|
||||
'%' -> add(TokenType.Percent)
|
||||
'/' -> when {
|
||||
match('/') -> lineComment()
|
||||
match('*') -> blockComment()
|
||||
else -> add(TokenType.Slash)
|
||||
}
|
||||
'=' -> add(if (match('=')) TokenType.EqualEqual else TokenType.Assign)
|
||||
'!' -> {
|
||||
if (match('=')) add(TokenType.BangEqual)
|
||||
else throw LexError("unexpected character '!'", line, columnOf(start))
|
||||
}
|
||||
'<' -> add(if (match('=')) TokenType.LessEqual else TokenType.Less)
|
||||
'>' -> add(if (match('=')) TokenType.GreaterEqual else TokenType.Greater)
|
||||
'"' -> string()
|
||||
else -> when {
|
||||
c.isDigit() -> number()
|
||||
isIdentStart(c) -> identifier()
|
||||
else -> throw LexError("unexpected character '$c'", line, columnOf(start))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun lineComment() {
|
||||
while (!atEnd() && peek() != '\n') advance()
|
||||
}
|
||||
|
||||
private fun blockComment() {
|
||||
var depth = 1
|
||||
while (!atEnd() && depth > 0) {
|
||||
val c = advance()
|
||||
when {
|
||||
c == '\n' -> {
|
||||
line++
|
||||
lineStart = current
|
||||
}
|
||||
c == '/' && peek() == '*' -> {
|
||||
advance()
|
||||
depth++
|
||||
}
|
||||
c == '*' && peek() == '/' -> {
|
||||
advance()
|
||||
depth--
|
||||
}
|
||||
}
|
||||
}
|
||||
if (depth > 0) throw LexError("unterminated block comment", line, columnOf(start))
|
||||
}
|
||||
|
||||
private fun string() {
|
||||
val builder = StringBuilder()
|
||||
while (!atEnd() && peek() != '"') {
|
||||
val c = advance()
|
||||
when {
|
||||
c == '\n' -> {
|
||||
builder.append('\n')
|
||||
line++
|
||||
lineStart = current
|
||||
}
|
||||
c == '\\' -> builder.append(escape())
|
||||
else -> builder.append(c)
|
||||
}
|
||||
}
|
||||
if (atEnd()) throw LexError("unterminated string", line, columnOf(start))
|
||||
advance()
|
||||
tokens.add(Token(TokenType.StringLiteral, builder.toString(), line, columnOf(start), stringValue = builder.toString()))
|
||||
}
|
||||
|
||||
private fun escape(): Char {
|
||||
if (atEnd()) throw LexError("unterminated escape sequence", line, columnOf(start))
|
||||
return when (val e = advance()) {
|
||||
'n' -> '\n'
|
||||
't' -> '\t'
|
||||
'r' -> '\r'
|
||||
'"' -> '"'
|
||||
'\\' -> '\\'
|
||||
'0' -> ' '
|
||||
else -> throw LexError("invalid escape '\\$e'", line, columnOf(current - 2))
|
||||
}
|
||||
}
|
||||
|
||||
private fun number() {
|
||||
while (peek().isDigit()) advance()
|
||||
var isDouble = false
|
||||
if (peek() == '.' && peekNext().isDigit()) {
|
||||
isDouble = true
|
||||
advance()
|
||||
while (peek().isDigit()) advance()
|
||||
}
|
||||
val text = source.substring(start, current)
|
||||
if (isDouble) {
|
||||
tokens.add(Token(TokenType.DoubleLiteral, text, line, columnOf(start), doubleValue = text.toDouble()))
|
||||
} else {
|
||||
tokens.add(Token(TokenType.IntLiteral, text, line, columnOf(start), intValue = text.toLong()))
|
||||
}
|
||||
}
|
||||
|
||||
private fun identifier() {
|
||||
while (isIdentPart(peek())) advance()
|
||||
val text = source.substring(start, current)
|
||||
add(keywords[text] ?: TokenType.Identifier)
|
||||
}
|
||||
|
||||
private fun isIdentStart(c: Char) = c.isLetter() || c == '_'
|
||||
|
||||
private fun isIdentPart(c: Char) = c.isLetterOrDigit() || c == '_'
|
||||
|
||||
private fun add(type: TokenType) {
|
||||
tokens.add(Token(type, source.substring(start, current), line, columnOf(start)))
|
||||
}
|
||||
|
||||
private fun columnOf(index: Int): Int = index - lineStart + 1
|
||||
|
||||
private fun advance(): Char = source[current++]
|
||||
|
||||
private fun match(expected: Char): Boolean {
|
||||
if (atEnd() || source[current] != expected) return false
|
||||
current++
|
||||
return true
|
||||
}
|
||||
|
||||
private fun peek(): Char = if (atEnd()) ' ' else source[current]
|
||||
|
||||
private fun peekNext(): Char = if (current + 1 >= source.length) ' ' else source[current + 1]
|
||||
|
||||
private fun atEnd(): Boolean = current >= source.length
|
||||
}
|
||||
Reference in new issue
Block a user