feat(compiler): add bytecode compiler
This commit is contained in:
1 parent
3befd09cb8
commit
a7239932fb
1 file changed
+445
@@ -0,0 +1,445 @@
|
||||
package lumen.compiler
|
||||
|
||||
import lumen.CompileError
|
||||
import lumen.ast.*
|
||||
import lumen.bytecode.Chunk
|
||||
import lumen.bytecode.Opcode
|
||||
import lumen.runtime.BoolValue
|
||||
import lumen.runtime.DoubleValue
|
||||
import lumen.runtime.FunctionValue
|
||||
import lumen.runtime.IntValue
|
||||
import lumen.runtime.StringValue
|
||||
|
||||
private enum class FunctionType { Script, Function }
|
||||
|
||||
private class Local(val name: String, var depth: Int, var captured: Boolean, val mutable: Boolean)
|
||||
|
||||
private class UpvalueDesc(val index: Int, val isLocal: Boolean)
|
||||
|
||||
private class FunctionScope(
|
||||
val enclosing: FunctionScope?,
|
||||
val type: FunctionType,
|
||||
val name: String
|
||||
) {
|
||||
val chunk = Chunk()
|
||||
val locals = ArrayList<Local>()
|
||||
val upvalues = ArrayList<UpvalueDesc>()
|
||||
var scopeDepth = 0
|
||||
var arity = 0
|
||||
|
||||
init {
|
||||
locals.add(Local("", 0, false, false))
|
||||
}
|
||||
}
|
||||
|
||||
class Compiler(private val globalMutability: MutableMap<String, Boolean> = mutableMapOf()) {
|
||||
private lateinit var scope: FunctionScope
|
||||
private var currentColumn = 0
|
||||
|
||||
fun compile(program: List<Stmt>, returnLastExpr: Boolean = false): FunctionValue {
|
||||
scope = FunctionScope(null, FunctionType.Script, "script")
|
||||
if (returnLastExpr && program.isNotEmpty() && program.last() is ExprStmt) {
|
||||
for (stmt in program.dropLast(1)) statement(stmt)
|
||||
val tail = program.last() as ExprStmt
|
||||
expression(tail.expr)
|
||||
emit(Opcode.Return, tail.line)
|
||||
} else {
|
||||
for (stmt in program) statement(stmt)
|
||||
emitReturnNil(lastLine(program))
|
||||
}
|
||||
return FunctionValue("script", 0, scope.chunk, scope.upvalues.size)
|
||||
}
|
||||
|
||||
private fun lastLine(program: List<Stmt>): Int = if (program.isEmpty()) 1 else program.last().line
|
||||
|
||||
private fun statement(stmt: Stmt) {
|
||||
currentColumn = stmt.column
|
||||
when (stmt) {
|
||||
is ImportStmt -> {}
|
||||
is LetStmt -> letStatement(stmt)
|
||||
is ExprStmt -> {
|
||||
expression(stmt.expr)
|
||||
emit(Opcode.Pop, stmt.line)
|
||||
}
|
||||
is BlockStmt -> {
|
||||
beginScope()
|
||||
stmt.statements.forEach { statement(it) }
|
||||
endScope(stmt.line)
|
||||
}
|
||||
is IfStmt -> ifStatement(stmt)
|
||||
is WhileStmt -> whileStatement(stmt)
|
||||
is ForStmt -> forStatement(stmt)
|
||||
is ReturnStmt -> returnStatement(stmt)
|
||||
is FunctionStmt -> functionStatement(stmt)
|
||||
}
|
||||
}
|
||||
|
||||
private fun letStatement(stmt: LetStmt) {
|
||||
if (scope.scopeDepth == 0) {
|
||||
expression(stmt.initializer)
|
||||
emit(Opcode.DefineGlobal, stmt.line)
|
||||
emit(nameConstant(stmt.name), stmt.line)
|
||||
globalMutability[stmt.name] = stmt.mutable
|
||||
} else {
|
||||
declareLocal(stmt.name, stmt.mutable, stmt.line)
|
||||
expression(stmt.initializer)
|
||||
markInitialized()
|
||||
}
|
||||
}
|
||||
|
||||
private fun ifStatement(stmt: IfStmt) {
|
||||
expression(stmt.condition)
|
||||
val thenJump = emitJump(Opcode.JumpIfFalse, stmt.line)
|
||||
emit(Opcode.Pop, stmt.line)
|
||||
beginScope()
|
||||
stmt.thenBranch.forEach { statement(it) }
|
||||
endScope(stmt.line)
|
||||
val elseJump = emitJump(Opcode.Jump, stmt.line)
|
||||
patchJump(thenJump)
|
||||
emit(Opcode.Pop, stmt.line)
|
||||
if (stmt.elseBranch != null) {
|
||||
beginScope()
|
||||
stmt.elseBranch.forEach { statement(it) }
|
||||
endScope(stmt.line)
|
||||
}
|
||||
patchJump(elseJump)
|
||||
}
|
||||
|
||||
private fun whileStatement(stmt: WhileStmt) {
|
||||
val loopStart = scope.chunk.code.size
|
||||
expression(stmt.condition)
|
||||
val exitJump = emitJump(Opcode.JumpIfFalse, stmt.line)
|
||||
emit(Opcode.Pop, stmt.line)
|
||||
beginScope()
|
||||
stmt.body.forEach { statement(it) }
|
||||
endScope(stmt.line)
|
||||
emitLoop(loopStart, stmt.line)
|
||||
patchJump(exitJump)
|
||||
emit(Opcode.Pop, stmt.line)
|
||||
}
|
||||
|
||||
private fun forStatement(stmt: ForStmt) {
|
||||
beginScope()
|
||||
expression(stmt.iterable)
|
||||
emit(Opcode.GetIter, stmt.line)
|
||||
val iterSlot = addSyntheticLocal("\$iter")
|
||||
emit(Opcode.Nil, stmt.line)
|
||||
val varSlot = declareLocal(stmt.name, true, stmt.line)
|
||||
markInitialized()
|
||||
|
||||
val loopStart = scope.chunk.code.size
|
||||
emit(Opcode.ForIter, stmt.line)
|
||||
emit(iterSlot, stmt.line)
|
||||
val exitPatch = emit(0, stmt.line)
|
||||
emit(Opcode.SetLocal, stmt.line)
|
||||
emit(varSlot, stmt.line)
|
||||
emit(Opcode.Pop, stmt.line)
|
||||
|
||||
beginScope()
|
||||
stmt.body.forEach { statement(it) }
|
||||
endScope(stmt.line)
|
||||
|
||||
emitLoop(loopStart, stmt.line)
|
||||
patchJump(exitPatch)
|
||||
endScope(stmt.line)
|
||||
}
|
||||
|
||||
private fun returnStatement(stmt: ReturnStmt) {
|
||||
if (scope.type == FunctionType.Script) {
|
||||
throw CompileError("'return' outside of a function", stmt.line)
|
||||
}
|
||||
if (stmt.value == null) {
|
||||
emitReturnNil(stmt.line)
|
||||
} else {
|
||||
expression(stmt.value)
|
||||
emit(Opcode.Return, stmt.line)
|
||||
}
|
||||
}
|
||||
|
||||
private fun functionStatement(stmt: FunctionStmt) {
|
||||
if (scope.scopeDepth == 0) {
|
||||
compileFunction(stmt.function)
|
||||
emit(Opcode.DefineGlobal, stmt.line)
|
||||
emit(nameConstant(stmt.name), stmt.line)
|
||||
globalMutability[stmt.name] = true
|
||||
} else {
|
||||
declareLocal(stmt.name, true, stmt.line)
|
||||
markInitialized()
|
||||
compileFunction(stmt.function)
|
||||
}
|
||||
}
|
||||
|
||||
private fun expression(expr: Expr) {
|
||||
currentColumn = expr.column
|
||||
when (expr) {
|
||||
is IntLiteral -> emitConstant(IntValue(expr.value), expr.line)
|
||||
is DoubleLiteral -> emitConstant(DoubleValue(expr.value), expr.line)
|
||||
is StringLiteral -> emitConstant(StringValue(expr.value), expr.line)
|
||||
is BoolLiteral -> emit(if (expr.value) Opcode.True else Opcode.False, expr.line)
|
||||
is NilLiteral -> emit(Opcode.Nil, expr.line)
|
||||
is ListLiteral -> {
|
||||
expr.elements.forEach { expression(it) }
|
||||
emit(Opcode.BuildList, expr.line)
|
||||
emit(expr.elements.size, expr.line)
|
||||
}
|
||||
is MapLiteral -> {
|
||||
expr.entries.forEach { (key, value) ->
|
||||
expression(key)
|
||||
expression(value)
|
||||
}
|
||||
emit(Opcode.BuildMap, expr.line)
|
||||
emit(expr.entries.size, expr.line)
|
||||
}
|
||||
is Variable -> variableAccess(expr.name, expr.line)
|
||||
is Assign -> assignment(expr)
|
||||
is Index -> {
|
||||
expression(expr.target)
|
||||
expression(expr.index)
|
||||
emit(Opcode.IndexGet, expr.line)
|
||||
}
|
||||
is IndexAssign -> {
|
||||
expression(expr.target)
|
||||
expression(expr.index)
|
||||
expression(expr.value)
|
||||
emit(Opcode.IndexSet, expr.line)
|
||||
}
|
||||
is Unary -> {
|
||||
expression(expr.operand)
|
||||
emit(if (expr.op == UnaryOp.Negate) Opcode.Negate else Opcode.Not, expr.line)
|
||||
}
|
||||
is Binary -> {
|
||||
expression(expr.left)
|
||||
expression(expr.right)
|
||||
emitBinary(expr.op, expr.line)
|
||||
}
|
||||
is Logical -> logical(expr)
|
||||
is Call -> {
|
||||
expression(expr.callee)
|
||||
expr.args.forEach { expression(it) }
|
||||
currentColumn = expr.column
|
||||
emit(Opcode.Call, expr.line)
|
||||
emit(expr.args.size, expr.line)
|
||||
}
|
||||
is MethodCall -> {
|
||||
expression(expr.receiver)
|
||||
expr.args.forEach { expression(it) }
|
||||
currentColumn = expr.column
|
||||
emit(Opcode.Invoke, expr.line)
|
||||
emit(nameConstant(expr.method), expr.line)
|
||||
emit(expr.args.size, expr.line)
|
||||
}
|
||||
is FunctionExpr -> compileFunction(expr)
|
||||
}
|
||||
}
|
||||
|
||||
private fun assignment(expr: Assign) {
|
||||
expression(expr.value)
|
||||
val local = resolveLocal(scope, expr.name, expr.line)
|
||||
if (local != -1) {
|
||||
if (!scope.locals[local].mutable) {
|
||||
throw CompileError("cannot assign to immutable '${expr.name}'", expr.line)
|
||||
}
|
||||
emit(Opcode.SetLocal, expr.line)
|
||||
emit(local, expr.line)
|
||||
return
|
||||
}
|
||||
val upvalue = resolveUpvalue(scope, expr.name, expr.line)
|
||||
if (upvalue != -1) {
|
||||
emit(Opcode.SetUpvalue, expr.line)
|
||||
emit(upvalue, expr.line)
|
||||
return
|
||||
}
|
||||
if (globalMutability[expr.name] == false) {
|
||||
throw CompileError("cannot assign to immutable '${expr.name}'", expr.line)
|
||||
}
|
||||
emit(Opcode.SetGlobal, expr.line)
|
||||
emit(nameConstant(expr.name), expr.line)
|
||||
}
|
||||
|
||||
private fun variableAccess(name: String, line: Int) {
|
||||
val local = resolveLocal(scope, name, line)
|
||||
if (local != -1) {
|
||||
emit(Opcode.GetLocal, line)
|
||||
emit(local, line)
|
||||
return
|
||||
}
|
||||
val upvalue = resolveUpvalue(scope, name, line)
|
||||
if (upvalue != -1) {
|
||||
emit(Opcode.GetUpvalue, line)
|
||||
emit(upvalue, line)
|
||||
return
|
||||
}
|
||||
emit(Opcode.GetGlobal, line)
|
||||
emit(nameConstant(name), line)
|
||||
}
|
||||
|
||||
private fun logical(expr: Logical) {
|
||||
when (expr.op) {
|
||||
LogicalOp.And -> {
|
||||
expression(expr.left)
|
||||
val endJump = emitJump(Opcode.JumpIfFalse, expr.line)
|
||||
emit(Opcode.Pop, expr.line)
|
||||
expression(expr.right)
|
||||
patchJump(endJump)
|
||||
}
|
||||
LogicalOp.Or -> {
|
||||
expression(expr.left)
|
||||
val elseJump = emitJump(Opcode.JumpIfFalse, expr.line)
|
||||
val endJump = emitJump(Opcode.Jump, expr.line)
|
||||
patchJump(elseJump)
|
||||
emit(Opcode.Pop, expr.line)
|
||||
expression(expr.right)
|
||||
patchJump(endJump)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun emitBinary(op: BinaryOp, line: Int) {
|
||||
val opcode = when (op) {
|
||||
BinaryOp.Add -> Opcode.Add
|
||||
BinaryOp.Sub -> Opcode.Sub
|
||||
BinaryOp.Mul -> Opcode.Mul
|
||||
BinaryOp.Div -> Opcode.Div
|
||||
BinaryOp.Mod -> Opcode.Mod
|
||||
BinaryOp.Equal -> Opcode.Equal
|
||||
BinaryOp.NotEqual -> Opcode.NotEqual
|
||||
BinaryOp.Less -> Opcode.Less
|
||||
BinaryOp.LessEqual -> Opcode.LessEqual
|
||||
BinaryOp.Greater -> Opcode.Greater
|
||||
BinaryOp.GreaterEqual -> Opcode.GreaterEqual
|
||||
}
|
||||
emit(opcode, line)
|
||||
}
|
||||
|
||||
private fun compileFunction(fnExpr: FunctionExpr) {
|
||||
val enclosing = scope
|
||||
scope = FunctionScope(enclosing, FunctionType.Function, fnExpr.name ?: "anonymous")
|
||||
scope.arity = fnExpr.params.size
|
||||
beginScope()
|
||||
for (param in fnExpr.params) {
|
||||
declareLocal(param, true, fnExpr.line)
|
||||
markInitialized()
|
||||
}
|
||||
fnExpr.body.forEach { statement(it) }
|
||||
emitReturnNil(fnExpr.line)
|
||||
|
||||
val compiled = scope
|
||||
val function = FunctionValue(compiled.name, compiled.arity, compiled.chunk, compiled.upvalues.size)
|
||||
scope = enclosing
|
||||
|
||||
emit(Opcode.Closure, fnExpr.line)
|
||||
emit(addConstant(function), fnExpr.line)
|
||||
for (upvalue in compiled.upvalues) {
|
||||
emit(if (upvalue.isLocal) 1 else 0, fnExpr.line)
|
||||
emit(upvalue.index, fnExpr.line)
|
||||
}
|
||||
}
|
||||
|
||||
private fun beginScope() {
|
||||
scope.scopeDepth++
|
||||
}
|
||||
|
||||
private fun endScope(line: Int) {
|
||||
scope.scopeDepth--
|
||||
while (scope.locals.isNotEmpty() && scope.locals.last().depth > scope.scopeDepth) {
|
||||
if (scope.locals.last().captured) {
|
||||
emit(Opcode.CloseUpvalue, line)
|
||||
} else {
|
||||
emit(Opcode.Pop, line)
|
||||
}
|
||||
scope.locals.removeAt(scope.locals.size - 1)
|
||||
}
|
||||
}
|
||||
|
||||
private fun declareLocal(name: String, mutable: Boolean, line: Int): Int {
|
||||
for (i in scope.locals.indices.reversed()) {
|
||||
val local = scope.locals[i]
|
||||
if (local.depth != -1 && local.depth < scope.scopeDepth) break
|
||||
if (local.name == name) {
|
||||
throw CompileError("'$name' is already declared in this scope", line)
|
||||
}
|
||||
}
|
||||
scope.locals.add(Local(name, -1, false, mutable))
|
||||
return scope.locals.size - 1
|
||||
}
|
||||
|
||||
private fun addSyntheticLocal(name: String): Int {
|
||||
scope.locals.add(Local(name, scope.scopeDepth, false, false))
|
||||
return scope.locals.size - 1
|
||||
}
|
||||
|
||||
private fun markInitialized() {
|
||||
if (scope.scopeDepth == 0) return
|
||||
scope.locals.last().depth = scope.scopeDepth
|
||||
}
|
||||
|
||||
private fun resolveLocal(target: FunctionScope, name: String, line: Int): Int {
|
||||
for (i in target.locals.indices.reversed()) {
|
||||
val local = target.locals[i]
|
||||
if (local.name == name) {
|
||||
if (local.depth == -1) {
|
||||
throw CompileError("cannot read '$name' in its own initializer", line)
|
||||
}
|
||||
return i
|
||||
}
|
||||
}
|
||||
return -1
|
||||
}
|
||||
|
||||
private fun resolveUpvalue(target: FunctionScope, name: String, line: Int): Int {
|
||||
val enclosing = target.enclosing ?: return -1
|
||||
val local = resolveLocal(enclosing, name, line)
|
||||
if (local != -1) {
|
||||
enclosing.locals[local].captured = true
|
||||
return addUpvalue(target, local, true)
|
||||
}
|
||||
val upvalue = resolveUpvalue(enclosing, name, line)
|
||||
if (upvalue != -1) {
|
||||
return addUpvalue(target, upvalue, false)
|
||||
}
|
||||
return -1
|
||||
}
|
||||
|
||||
private fun addUpvalue(target: FunctionScope, index: Int, isLocal: Boolean): Int {
|
||||
for (i in target.upvalues.indices) {
|
||||
val existing = target.upvalues[i]
|
||||
if (existing.index == index && existing.isLocal == isLocal) return i
|
||||
}
|
||||
target.upvalues.add(UpvalueDesc(index, isLocal))
|
||||
return target.upvalues.size - 1
|
||||
}
|
||||
|
||||
private fun nameConstant(name: String): Int = addConstant(StringValue(name))
|
||||
|
||||
private fun addConstant(value: lumen.runtime.Value): Int = scope.chunk.addConstant(value)
|
||||
|
||||
private fun emitConstant(value: lumen.runtime.Value, line: Int) {
|
||||
emit(Opcode.Constant, line)
|
||||
emit(addConstant(value), line)
|
||||
}
|
||||
|
||||
private fun emit(op: Opcode, line: Int): Int = scope.chunk.emit(op, line, currentColumn)
|
||||
|
||||
private fun emit(byte: Int, line: Int): Int = scope.chunk.emit(byte, line, currentColumn)
|
||||
|
||||
private fun emitReturnNil(line: Int) {
|
||||
emit(Opcode.Nil, line)
|
||||
emit(Opcode.Return, line)
|
||||
}
|
||||
|
||||
private fun emitJump(op: Opcode, line: Int): Int {
|
||||
emit(op, line)
|
||||
return emit(0, line)
|
||||
}
|
||||
|
||||
private fun patchJump(offsetIndex: Int) {
|
||||
val jump = scope.chunk.code.size - (offsetIndex + 1)
|
||||
scope.chunk.patch(offsetIndex, jump)
|
||||
}
|
||||
|
||||
private fun emitLoop(loopStart: Int, line: Int) {
|
||||
emit(Opcode.Loop, line)
|
||||
val offset = scope.chunk.code.size + 1 - loopStart
|
||||
emit(offset, line)
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user