vm: very basic virtual machine

This commit is contained in:
2023-11-14 23:44:10 -08:00
parent 8c48c93663
commit 041848c14e
92 changed files with 1652 additions and 243 deletions

11
compiler/build.gradle.kts Normal file
View File

@ -0,0 +1,11 @@
plugins {
id("gay.pizza.pork.module")
}
dependencies {
api(project(":ast"))
api(project(":bytecode"))
api(project(":parser"))
api(project(":frontend"))
implementation(project(":common"))
}

View File

@ -0,0 +1,16 @@
package gay.pizza.pork.compiler
import gay.pizza.pork.ast.gen.Symbol
import gay.pizza.pork.frontend.Slab
class CompilableSlab(val compiler: Compiler, val slab: Slab) {
val compilableSymbols: List<CompilableSymbol> by lazy {
slab.scope.internalSymbols.map { symbol ->
CompilableSymbol(this, symbol)
}
}
fun compilableSymbolOf(symbol: Symbol): CompilableSymbol? = compilableSymbols.firstOrNull {
it.scopeSymbol.symbol == symbol
}
}

View File

@ -0,0 +1,34 @@
package gay.pizza.pork.compiler
import gay.pizza.pork.ast.gen.FunctionDefinition
import gay.pizza.pork.ast.gen.LetDefinition
import gay.pizza.pork.ast.gen.visit
import gay.pizza.pork.frontend.scope.ScopeSymbol
class CompilableSymbol(val compilableSlab: CompilableSlab, val scopeSymbol: ScopeSymbol) {
val compiledStubOps: List<StubOp> by lazy { compile() }
val usedSymbols: List<ScopeSymbol>
get() = scopeSymbol.scope.usedSymbols
private fun compile(): List<StubOp> {
val emitter = StubOpEmitter(compilableSlab.compiler, this)
emitter.enter()
val what = if (scopeSymbol.definition is FunctionDefinition) {
val functionDefinition = scopeSymbol.definition as FunctionDefinition
emitter.allocateOuterScope(functionDefinition)
functionDefinition.block ?: functionDefinition.nativeFunctionDescriptor!!
} else {
val letDefinition = scopeSymbol.definition as LetDefinition
letDefinition.value
}
emitter.visit(what)
emitter.exit()
return emitter.ops()
}
val id: String
get() = "${compilableSlab.slab.location.commonFriendlyName} ${scopeSymbol.symbol.id}"
override fun toString(): String = "${compilableSlab.slab.location.commonFriendlyName} ${scopeSymbol.symbol.id}"
}

View File

@ -0,0 +1,38 @@
package gay.pizza.pork.compiler
import gay.pizza.pork.bytecode.*
class CompiledWorldLayout(val compiler: Compiler) : StubResolutionContext {
private val allStubOps = mutableListOf<StubOp>()
private val symbolTable = mutableMapOf<CompilableSymbol, SymbolInfo>()
fun add(symbol: CompilableSymbol) {
val start = allStubOps.size
val stubOps = symbol.compiledStubOps
symbolTable[symbol] = SymbolInfo(symbol.id, start.toUInt(), stubOps.size.toUInt())
allStubOps.addAll(stubOps)
}
private fun patch(): List<Op> {
val ops = mutableListOf<Op>()
for (stub in allStubOps) {
val actualArguments = stub.op.args.toMutableList()
stub.patch(this, actualArguments)
ops.add(Op(stub.op.code, actualArguments))
}
return ops
}
override fun resolveJumpTarget(symbol: CompilableSymbol): UInt {
return symbolTable[symbol]?.offset ?:
throw RuntimeException("Unable to resolve jump target: ${symbol.scopeSymbol.symbol.id}")
}
fun layoutCompiledWorld(): CompiledWorld {
val constantPool = mutableListOf<ByteArray>()
for (item in compiler.constantPool.all()) {
constantPool.add(item.value)
}
return CompiledWorld(ConstantPool(constantPool), SymbolTable(symbolTable.values.toList()), patch())
}
}

View File

@ -0,0 +1,47 @@
package gay.pizza.pork.compiler
import gay.pizza.pork.bytecode.CompiledWorld
import gay.pizza.pork.bytecode.MutableConstantPool
import gay.pizza.pork.frontend.Slab
import gay.pizza.pork.frontend.scope.ScopeSymbol
class Compiler {
val constantPool: MutableConstantPool = MutableConstantPool()
val compilableSlabs: ComputableState<Slab, CompilableSlab> = ComputableState { slab ->
CompilableSlab(this, slab)
}
fun resolveOrNull(scopeSymbol: ScopeSymbol): CompilableSymbol? {
val compiledSlab = compilableSlabs.of(scopeSymbol.slabScope.slab)
return compiledSlab.compilableSymbolOf(scopeSymbol.symbol)
}
fun resolve(scopeSymbol: ScopeSymbol): CompilableSymbol = resolveOrNull(scopeSymbol) ?:
throw RuntimeException(
"Unable to resolve scope symbol: " +
"${scopeSymbol.slabScope.slab.location.commonFriendlyName} ${scopeSymbol.symbol.id}")
fun contributeCompiledSymbols(
into: MutableSet<CompilableSymbol>,
symbol: ScopeSymbol,
resolved: CompilableSymbol = resolve(symbol)
) {
if (!into.add(resolved)) {
return
}
for (used in resolved.usedSymbols) {
contributeCompiledSymbols(into, used)
}
}
fun compile(entryPointSymbol: CompilableSymbol): CompiledWorld {
val usedSymbolSet = mutableSetOf<CompilableSymbol>()
contributeCompiledSymbols(usedSymbolSet, entryPointSymbol.scopeSymbol, entryPointSymbol)
val layout = CompiledWorldLayout(this)
for (used in usedSymbolSet) {
layout.add(used)
}
return layout.layoutCompiledWorld()
}
}

View File

@ -0,0 +1,7 @@
package gay.pizza.pork.compiler
class ComputableState<X, T>(val computation: (X) -> T) {
private val state = StoredState<X, T>()
fun of(key: X): T = state.computeIfAbsent(key, computation)
}

View File

@ -0,0 +1,11 @@
package gay.pizza.pork.compiler
import gay.pizza.pork.bytecode.MutableRel
class LoopState(
val startOfLoop: UInt,
val exitJumpTarget: MutableRel,
val body: MutableRel,
val scopeDepth: Int,
val enclosing: LoopState? = null
)

View File

@ -0,0 +1,12 @@
package gay.pizza.pork.compiler
import gay.pizza.pork.bytecode.MutableRel
import gay.pizza.pork.bytecode.Op
class PatchRelOp(op: Op, val index: Int, val symbol: CompilableSymbol, val rel: MutableRel) : StubOp(op) {
override fun patch(context: StubResolutionContext, arguments: MutableList<UInt>) {
arguments[index] = context.resolveJumpTarget(symbol) + rel.rel
}
override fun toString(): String = "PatchRelOp(${op}, ${index}, ${symbol}, ${rel})"
}

View File

@ -0,0 +1,13 @@
package gay.pizza.pork.compiler
import gay.pizza.pork.bytecode.Op
class PatchSymOp(op: Op, val patches: Map<Int, CompilableSymbol>) : StubOp(op) {
override fun patch(context: StubResolutionContext, arguments: MutableList<UInt>) {
for ((index, symbol) in patches) {
arguments[index] = context.resolveJumpTarget(symbol)
}
}
override fun toString(): String = "PatchSymOp(${op}, ${patches})"
}

View File

@ -0,0 +1,7 @@
package gay.pizza.pork.compiler
import gay.pizza.pork.bytecode.Op
class StaticOp(op: Op) : StubOp(op) {
override fun toString(): String = "StaticOp(${op})"
}

View File

@ -0,0 +1,11 @@
package gay.pizza.pork.compiler
class StoredState<X, T> {
private val state = mutableMapOf<X, T>()
fun computeIfAbsent(key: X, computation: (X) -> T): T = state.computeIfAbsent(key, computation)
fun of(key: X): T? = state[key]
fun put(key: X, value: T) {
state[key] = value
}
}

View File

@ -0,0 +1,7 @@
package gay.pizza.pork.compiler
import gay.pizza.pork.bytecode.Op
abstract class StubOp(val op: Op) {
open fun patch(context: StubResolutionContext, arguments: MutableList<UInt>) {}
}

View File

@ -0,0 +1,336 @@
package gay.pizza.pork.compiler
import gay.pizza.pork.ast.FunctionLevelVisitor
import gay.pizza.pork.ast.gen.*
import gay.pizza.pork.bytecode.MutableRel
import gay.pizza.pork.bytecode.Op
import gay.pizza.pork.bytecode.Opcode
class StubOpEmitter(val compiler: Compiler, val symbol: CompilableSymbol) : FunctionLevelVisitor<Unit>() {
private val ops = mutableListOf<StubOp>()
private var loopState: LoopState? = null
private val localVariables = mutableListOf<MutableList<StubVar>>()
private var localVarIndex = 0u
private val requiredLoopState: LoopState
get() {
if (loopState != null) {
return loopState!!
}
throw RuntimeException("loopState expected but was not found.")
}
private fun allocateLocalVariable(symbol: Symbol): StubVar {
val scope = localVariables.last()
val variable = StubVar(localVarIndex++, symbol)
scope.add(variable)
return variable
}
private fun resolveSymbol(symbol: Symbol): CallOrStubVar {
for (scope in localVariables.reversed()) {
val found = scope.firstOrNull { it.symbol == symbol }
if (found != null) {
return CallOrStubVar(stubVar = found)
}
}
val found = this.symbol.compilableSlab.compilableSymbolOf(symbol)
if (found != null) {
return CallOrStubVar(call = found)
}
throw RuntimeException("Unable to resolve symbol: ${symbol.id}")
}
private fun pushScope() {
emit(Opcode.ScopeIn)
localVariables.add(mutableListOf())
}
private fun popScope() {
emit(Opcode.ScopeOut)
localVariables.removeLast()
}
fun enter() {
pushScope()
}
fun allocateOuterScope(definition: FunctionDefinition) {
val allNormalArguments = definition.arguments.takeWhile { !it.multiple }
val varArgument = definition.arguments.firstOrNull { it.multiple }
for (arg in allNormalArguments.reversed()) {
val functionLocal = allocateLocalVariable(arg.symbol)
emit(Opcode.StoreLocal, listOf(functionLocal.index))
}
if (varArgument != null) {
val functionLocal = allocateLocalVariable(varArgument.symbol)
emit(Opcode.StoreLocal, listOf(functionLocal.index))
}
}
fun exit() {
popScope()
emit(Opcode.Return)
}
override fun visitBlock(node: Block) {
pushScope()
node.visitChildren(this)
popScope()
}
override fun visitBooleanLiteral(node: BooleanLiteral) {
emit(if (node.value) Opcode.True else Opcode.False)
}
override fun visitBreak(node: Break) {
patch(Opcode.Jump, listOf(0u), 0, symbol, requiredLoopState.exitJumpTarget)
}
override fun visitContinue(node: Continue) {
patch(Opcode.Jump, listOf(0u), 0, symbol, requiredLoopState.startOfLoop)
}
override fun visitDoubleLiteral(node: DoubleLiteral) {
emit(Opcode.Integer, listOf(node.value.toUInt()))
}
override fun visitForIn(node: ForIn) {
TODO("ForIn is currently unsupported")
}
override fun visitFunctionCall(node: FunctionCall) {
val targetScopeSymbol = symbol.scopeSymbol.scope.resolve(node.symbol) ?:
throw RuntimeException("Unable to resolve symbol: ${node.symbol.id}")
val targetSymbol = compiler.resolve(targetScopeSymbol)
val functionDefinition = targetSymbol.scopeSymbol.definition as FunctionDefinition
val retRel = MutableRel(0u)
patch(Opcode.Integer, listOf(0u), 0, symbol, retRel)
val normalArguments = mutableListOf<Expression>()
var variableArguments: List<Expression>? = null
for ((index, item) in functionDefinition.arguments.zip(node.arguments).withIndex()) {
val (spec, value) = item
if (spec.multiple) {
val remaining = node.arguments.drop(index)
variableArguments = remaining
break
} else {
normalArguments.add(value)
}
}
if (variableArguments != null) {
for (item in variableArguments.reversed()) {
item.visit(this)
}
emit(Opcode.List, listOf(variableArguments.size.toUInt()))
}
for (item in normalArguments.reversed()) {
visit(item)
}
retRel.rel = (ops.size + 1).toUInt()
patch(Opcode.Call, listOf(0u), mapOf(0 to targetSymbol))
}
override fun visitIf(node: If) {
val thenRel = MutableRel(0u)
val endRel = MutableRel(0u)
node.condition.visit(this)
patch(Opcode.JumpIf, listOf(0u), 0, symbol, thenRel)
node.elseBlock?.visit(this)
patch(Opcode.Jump, listOf(0u), 0, symbol, endRel)
thenRel.rel = ops.size.toUInt()
node.thenBlock.visit(this)
endRel.rel = ops.size.toUInt()
}
override fun visitIndexedBy(node: IndexedBy) {
node.expression.visit(this)
node.index.visit(this)
emit(Opcode.Index)
}
override fun visitInfixOperation(node: InfixOperation) {
node.left.visit(this)
node.right.visit(this)
when (node.op) {
InfixOperator.Plus -> emit(Opcode.Add)
InfixOperator.Minus -> emit(Opcode.Subtract)
InfixOperator.Multiply -> emit(Opcode.Multiply)
InfixOperator.Divide -> emit(Opcode.Divide)
InfixOperator.Equals -> emit(Opcode.CompareEqual)
InfixOperator.NotEquals -> {
emit(Opcode.CompareEqual)
emit(Opcode.Not)
}
InfixOperator.EuclideanModulo -> emit(Opcode.EuclideanModulo)
InfixOperator.Remainder -> emit(Opcode.Remainder)
InfixOperator.Lesser -> emit(Opcode.CompareLesser)
InfixOperator.Greater -> emit(Opcode.CompareGreater)
InfixOperator.GreaterEqual -> emit(Opcode.CompareGreaterEqual)
InfixOperator.LesserEqual -> emit(Opcode.CompareLesserEqual)
InfixOperator.BooleanAnd -> emit(Opcode.And)
InfixOperator.BooleanOr -> emit(Opcode.Or)
InfixOperator.BinaryAnd -> emit(Opcode.BinaryAnd)
InfixOperator.BinaryOr -> emit(Opcode.BinaryOr)
InfixOperator.BinaryExclusiveOr -> emit(Opcode.BinaryXor)
}
}
override fun visitIntegerLiteral(node: IntegerLiteral) {
emit(Opcode.Integer, listOf(node.value.toUInt()))
}
override fun visitLetAssignment(node: LetAssignment) {
val variable = allocateLocalVariable(node.symbol)
node.value.visit(this)
emit(Opcode.StoreLocal, listOf(variable.index))
}
override fun visitListLiteral(node: ListLiteral) {
val count = node.items.size
for (item in node.items) {
item.visit(this)
}
emit(Opcode.List, listOf(count.toUInt()))
}
override fun visitLongLiteral(node: LongLiteral) {
emit(Opcode.Integer, listOf(node.value.toUInt()))
}
override fun visitNoneLiteral(node: NoneLiteral) {
emit(Opcode.None)
}
override fun visitParentheses(node: Parentheses) {
node.expression.visit(this)
}
override fun visitPrefixOperation(node: PrefixOperation) {
node.expression.visit(this)
when (node.op) {
PrefixOperator.BooleanNot -> emit(Opcode.Not)
PrefixOperator.UnaryPlus -> emit(Opcode.UnaryPlus)
PrefixOperator.UnaryMinus -> emit(Opcode.UnaryMinus)
PrefixOperator.BinaryNot -> emit(Opcode.BinaryNot)
}
}
override fun visitSetAssignment(node: SetAssignment) {
val stubVarOrCall = resolveSymbol(node.symbol)
if (stubVarOrCall.stubVar == null) {
throw RuntimeException("Invalid set assignment.")
}
node.value.visit(this)
emit(Opcode.StoreLocal, listOf(stubVarOrCall.stubVar.index))
}
override fun visitStringLiteral(node: StringLiteral) {
val bytes = node.text.toByteArray()
val constant = compiler.constantPool.assign(bytes)
emit(Opcode.Constant, listOf(constant))
}
override fun visitSuffixOperation(node: SuffixOperation) {
val stubVarOrCall = resolveSymbol(node.reference.symbol)
if (stubVarOrCall.stubVar == null) {
throw RuntimeException("Invalid suffix operation.")
}
load(stubVarOrCall)
when (node.op) {
SuffixOperator.Increment -> {
emit(Opcode.Integer, listOf(1u))
emit(Opcode.Add, emptyList())
emit(Opcode.StoreLocal, listOf(stubVarOrCall.stubVar.index))
}
SuffixOperator.Decrement -> {
emit(Opcode.Integer, listOf(1u))
emit(Opcode.Subtract, emptyList())
emit(Opcode.StoreLocal, listOf(stubVarOrCall.stubVar.index))
}
}
}
override fun visitSymbolReference(node: SymbolReference) {
val variable = resolveSymbol(node.symbol)
load(variable)
}
override fun visitVarAssignment(node: VarAssignment) {
val variable = allocateLocalVariable(node.symbol)
node.value.visit(this)
emit(Opcode.StoreLocal, listOf(variable.index))
}
override fun visitWhile(node: While) {
val startOfBody = MutableRel(0u)
val endOfLoop = MutableRel(0u)
val currentLoopState = LoopState(
startOfLoop = ops.size.toUInt(),
exitJumpTarget = endOfLoop,
body = startOfBody,
scopeDepth = (loopState?.scopeDepth ?: 0) + 1,
enclosing = loopState
)
loopState = currentLoopState
node.condition.visit(this)
patch(Opcode.JumpIf, listOf(0u), 0, symbol, startOfBody)
patch(Opcode.Jump, listOf(0u), 0, symbol, endOfLoop)
startOfBody.rel = ops.size.toUInt()
node.block.visit(this)
patch(Opcode.Jump, listOf(0u), 0, symbol, currentLoopState.startOfLoop)
endOfLoop.rel = ops.size.toUInt()
}
override fun visitNativeFunctionDescriptor(node: NativeFunctionDescriptor) {
for (def in node.definitions) {
val defConstant = compiler.constantPool.assign(def.text.toByteArray())
emit(Opcode.Constant, listOf(defConstant))
}
val formConstant = compiler.constantPool.assign(node.form.id.toByteArray())
emit(Opcode.Native, listOf(formConstant, node.definitions.size.toUInt()))
}
private fun emit(code: Opcode) {
emit(code, emptyList())
}
private fun emit(code: Opcode, arguments: List<UInt>) {
ops.add(StaticOp(Op(code, arguments)))
}
private fun patch(code: Opcode, arguments: List<UInt>, index: Int, symbol: CompilableSymbol, rel: MutableRel) {
ops.add(PatchRelOp(Op(code, arguments), index, symbol, rel))
}
private fun patch(code: Opcode, arguments: List<UInt>, index: Int, symbol: CompilableSymbol, rel: UInt) {
ops.add(PatchRelOp(Op(code, arguments), index, symbol, MutableRel(rel)))
}
private fun patch(code: Opcode, arguments: List<UInt>, patches: Map<Int, CompilableSymbol>) {
ops.add(PatchSymOp(Op(code, arguments), patches))
}
fun ops(): List<StubOp> = ops
private fun load(callOrStubVar: CallOrStubVar) {
if (callOrStubVar.stubVar != null) {
emit(Opcode.LoadLocal, listOf(callOrStubVar.stubVar.index))
} else {
emit(Opcode.Integer, listOf(ops.size.toUInt() + 2u))
patch(Opcode.Call, listOf(0u), mapOf(0 to callOrStubVar.call!!))
}
}
private class CallOrStubVar(
val call: CompilableSymbol? = null,
val stubVar: StubVar? = null
)
}

View File

@ -0,0 +1,5 @@
package gay.pizza.pork.compiler
interface StubResolutionContext {
fun resolveJumpTarget(symbol: CompilableSymbol): UInt
}

View File

@ -0,0 +1,8 @@
package gay.pizza.pork.compiler
import gay.pizza.pork.ast.gen.Symbol
class StubVar(
val index: UInt,
val symbol: Symbol
)