Skip to content

Commit

Permalink
Interpreter (#25)
Browse files Browse the repository at this point in the history
Fixes #23
  • Loading branch information
cretz authored Oct 15, 2018
1 parent 890cfd8 commit 6102ea5
Show file tree
Hide file tree
Showing 23 changed files with 1,243 additions and 277 deletions.
16 changes: 12 additions & 4 deletions compiler/src/main/kotlin/asmble/ast/Node.kt
Original file line number Diff line number Diff line change
Expand Up @@ -30,10 +30,18 @@ sealed class Node {
sealed class Type : Node() {

sealed class Value : Type() {
object I32 : Value()
object I64 : Value()
object F32 : Value()
object F64 : Value()
object I32 : Value() {
override fun toString() = "I32"
}
object I64 : Value() {
override fun toString() = "I64"
}
object F32 : Value() {
override fun toString() = "F32"
}
object F64 : Value() {
override fun toString() = "F64"
}
}

data class Func(
Expand Down
5 changes: 2 additions & 3 deletions compiler/src/main/kotlin/asmble/cli/Invoke.kt
Original file line number Diff line number Diff line change
Expand Up @@ -40,8 +40,7 @@ open class Invoke : ScriptCommand<Invoke.Args>() {
if (args.module == "<last-in-entry>") ctx.modules.lastOrNull() ?: error("No modules available")
else ctx.registrations[args.module] as? Module.Instance ?:
error("Unable to find module registered as ${args.module}")
// Just make sure the module is instantiated here...
module.instance(ctx)
module as Module.Compiled
// If an export is provided, call it
if (args.export != "<start-func>") args.export.javaIdent.let { javaName ->
val method = module.cls.declaredMethods.find { it.name == javaName } ?:
Expand All @@ -59,7 +58,7 @@ open class Invoke : ScriptCommand<Invoke.Args>() {
else -> error("Unrecognized type for param ${index + 1}: $paramType")
}
}
val result = method.invoke(module.instance(ctx), *params.toTypedArray())
val result = method.invoke(module.inst, *params.toTypedArray())
if (args.resultToStdout && method.returnType != Void.TYPE) println(result)
}
}
Expand Down
13 changes: 8 additions & 5 deletions compiler/src/main/kotlin/asmble/cli/ScriptCommand.kt
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package asmble.cli
import asmble.ast.Script
import asmble.compile.jvm.javaIdent
import asmble.run.jvm.Module
import asmble.run.jvm.ModuleBuilder
import asmble.run.jvm.ScriptContext
import java.io.File
import java.util.*
Expand Down Expand Up @@ -45,21 +46,23 @@ abstract class ScriptCommand<T> : Command<T>() {
)

fun prepareContext(args: ScriptArgs): ScriptContext {
var ctx = ScriptContext(
val builder = ModuleBuilder.Compiled(
packageName = "asmble.temp" + UUID.randomUUID().toString().replace("-", ""),
logger = logger,
defaultMaxMemPages = args.defaultMaxMemPages
)
var ctx = ScriptContext(logger = logger, builder = builder)
// Compile everything
ctx = args.inFiles.foldIndexed(ctx) { index, ctx, inFile ->
try {
when (inFile.substringAfterLast('.')) {
"class" -> ctx.classLoader.addClass(File(inFile).readBytes()).let { ctx }
"class" -> builder.classLoader.addClass(File(inFile).readBytes()).let { ctx }
else -> Translate.inToAst(inFile, inFile.substringAfterLast('.')).let { inAst ->
val (mod, name) = (inAst.commands.singleOrNull() as? Script.Cmd.Module) ?:
error("Input file must only contain a single module")
val className = name?.javaIdent?.capitalize() ?:
"Temp" + UUID.randomUUID().toString().replace("-", "")
ctx.withCompiledModule(mod, className, name).let { ctx ->
ctx.withBuiltModule(mod, className, name).let { ctx ->
if (name == null && index != args.inFiles.size - 1)
logger.warn { "File '$inFile' not last and has no name so will be unused" }
if (name == null || args.disableAutoRegister) ctx
Expand All @@ -71,8 +74,8 @@ abstract class ScriptCommand<T> : Command<T>() {
}
// Do registrations
ctx = args.registrations.fold(ctx) { ctx, (moduleName, className) ->
ctx.withModuleRegistered(moduleName,
Module.Native(Class.forName(className, true, ctx.classLoader).newInstance()))
ctx.withModuleRegistered(
Module.Native(moduleName, Class.forName(className, true, builder.classLoader).newInstance()))
}
if (args.specTestRegister) ctx = ctx.withHarnessRegistered()
return ctx
Expand Down
18 changes: 13 additions & 5 deletions compiler/src/main/kotlin/asmble/compile/jvm/AsmExt.kt
Original file line number Diff line number Diff line change
Expand Up @@ -56,10 +56,10 @@ val Class<*>.ref: TypeRef get() = TypeRef(this.asmType)

val Class<*>.valueType: Node.Type.Value? get() = when (this) {
Void.TYPE -> null
Int::class.java -> Node.Type.Value.I32
Long::class.java -> Node.Type.Value.I64
Float::class.java -> Node.Type.Value.F32
Double::class.java -> Node.Type.Value.F64
Int::class.java, java.lang.Integer::class.java -> Node.Type.Value.I32
Long::class.java, java.lang.Long::class.java -> Node.Type.Value.I64
Float::class.java, java.lang.Float::class.java -> Node.Type.Value.F32
Double::class.java, java.lang.Double::class.java -> Node.Type.Value.F64
else -> error("Unrecognized value type class: $this")
}

Expand Down Expand Up @@ -113,6 +113,15 @@ val Double.const: AbstractInsnNode get() = when (this) {
else -> LdcInsnNode(this)
}

val Number?.valueType get() = when (this) {
null -> null
is Int -> Node.Type.Value.I32
is Long-> Node.Type.Value.I64
is Float -> Node.Type.Value.F32
is Double -> Node.Type.Value.F64
else -> error("Unrecognized value type class: $this")
}

val String.const: AbstractInsnNode get() = LdcInsnNode(this)

val javaKeywords = setOf("abstract", "assert", "boolean",
Expand Down Expand Up @@ -177,7 +186,6 @@ fun MethodNode.addInsns(vararg insn: AbstractInsnNode): MethodNode {
return this
}


fun MethodNode.cloneWithInsnRange(range: IntRange) =
MethodNode(access, name, desc, signature, exceptions.toTypedArray()).also { new ->
accept(new)
Expand Down
2 changes: 1 addition & 1 deletion compiler/src/main/kotlin/asmble/compile/jvm/AstToAsm.kt
Original file line number Diff line number Diff line change
Expand Up @@ -385,7 +385,7 @@ open class AstToAsm {
}
// Otherwise, it was imported and we can set the elems on the imported one
// from the parameter
// TODO: I think this is a security concern and bad practice, may revisit
// TODO: I think this is a security concern and bad practice, may revisit (TODO: consider cloning the array)
val importIndex = ctx.importFuncs.size + ctx.importGlobals.sumBy {
// Immutable is 1, mutable is 2
if ((it.kind as? Node.Import.Kind.Global)?.type?.mutable == false) 1 else 2
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,8 @@ open class ExceptionTranslator {
"/ by zero", "BigInteger divide by zero" -> listOf("integer divide by zero")
else -> listOf(ex.message!!.decapitalize())
}
is ArrayIndexOutOfBoundsException -> listOf("undefined element", "elements segment does not fit")
is ArrayIndexOutOfBoundsException ->
listOf("out of bounds memory access", "undefined element", "elements segment does not fit")
is AsmErr -> ex.asmErrStrings
is IndexOutOfBoundsException -> listOf("out of bounds memory access")
is MalformedInputException -> listOf("invalid UTF-8 encoding")
Expand Down
97 changes: 54 additions & 43 deletions compiler/src/main/kotlin/asmble/run/jvm/Module.kt
Original file line number Diff line number Diff line change
Expand Up @@ -4,72 +4,81 @@ import asmble.annotation.WasmExport
import asmble.annotation.WasmExternalKind
import asmble.ast.Node
import asmble.compile.jvm.Mem
import asmble.compile.jvm.javaIdent
import asmble.compile.jvm.ref
import java.lang.invoke.MethodHandle
import java.lang.invoke.MethodHandles
import java.lang.invoke.MethodType
import java.lang.reflect.Constructor
import java.lang.reflect.Modifier

interface Module {
fun bindMethod(
ctx: ScriptContext,
wasmName: String,
wasmKind: WasmExternalKind,
javaName: String,
type: MethodType
): MethodHandle?

data class Composite(val modules: List<Module>) : Module {
override fun bindMethod(
ctx: ScriptContext,
wasmName: String,
wasmKind: WasmExternalKind,
javaName: String,
type: MethodType
) = modules.asSequence().mapNotNull { it.bindMethod(ctx, wasmName, wasmKind, javaName, type) }.singleOrNull()
val name: String?

fun exportedFunc(field: String): MethodHandle?
fun exportedGlobal(field: String): Pair<MethodHandle, MethodHandle?>?
fun <T> exportedMemory(field: String, memClass: Class<T>): T?
fun exportedTable(field: String): Array<MethodHandle?>?

interface ImportResolver {
fun resolveImportFunc(module: String, field: String, type: Node.Type.Func): MethodHandle
fun resolveImportGlobal(
module: String,
field: String,
type: Node.Type.Global
): Pair<MethodHandle, MethodHandle?>
fun <T> resolveImportMemory(module: String, field: String, type: Node.Type.Memory, memClass: Class<T>): T
fun resolveImportTable(module: String, field: String, type: Node.Type.Table): Array<MethodHandle?>
}

interface Instance : Module {
val cls: Class<*>
// Guaranteed to be the same instance when there is no error
fun instance(ctx: ScriptContext): Any
val inst: Any

override fun bindMethod(
ctx: ScriptContext,
fun bindMethod(
wasmName: String,
wasmKind: WasmExternalKind,
javaName: String,
type: MethodType
javaName: String = wasmName.javaIdent,
paramCountRequired: Int? = null
) = cls.methods.filter {
// @WasmExport match or just javaName match
Modifier.isPublic(it.modifiers) &&
!Modifier.isStatic(it.modifiers) &&
(paramCountRequired == null || it.parameterCount == paramCountRequired) &&
it.getDeclaredAnnotation(WasmExport::class.java).let { ann ->
if (ann == null) it.name == javaName else ann.value == wasmName && ann.kind == wasmKind
}
}.mapNotNull {
MethodHandles.lookup().unreflect(it).bindTo(instance(ctx)).takeIf { it.type() == type }
}.singleOrNull()
}
}.mapNotNull { MethodHandles.lookup().unreflect(it).bindTo(inst) }.singleOrNull()

data class Native(override val cls: Class<*>, val inst: Any) : Instance {
constructor(inst: Any) : this(inst::class.java, inst)
override fun exportedFunc(field: String) = bindMethod(field, WasmExternalKind.FUNCTION, field.javaIdent)
override fun exportedGlobal(field: String) =
bindMethod(field, WasmExternalKind.GLOBAL, "get" + field.javaIdent.capitalize(), 0)?.let {
it to bindMethod(field, WasmExternalKind.GLOBAL, "set" + field.javaIdent.capitalize(), 1)
}
@SuppressWarnings("UNCHECKED_CAST")
override fun <T> exportedMemory(field: String, memClass: Class<T>) =
bindMethod(field, WasmExternalKind.MEMORY, "get" + field.javaIdent.capitalize(), 0)?.
takeIf { it.type().returnType() == memClass }?.let { it.invokeWithArguments() as? T }
@SuppressWarnings("UNCHECKED_CAST")
override fun exportedTable(field: String) =
bindMethod(field, WasmExternalKind.TABLE, "get" + field.javaIdent.capitalize(), 0)?.
let { it.invokeWithArguments() as? Array<MethodHandle?> }
}

override fun instance(ctx: ScriptContext) = inst
data class Native(override val cls: Class<*>, override val name: String?, override val inst: Any) : Instance {
constructor(name: String?, inst: Any) : this(inst::class.java, name, inst)
}

class Compiled(
val mod: Node.Module,
override val cls: Class<*>,
val name: String?,
val mem: Mem
override val name: String?,
val mem: Mem,
imports: ImportResolver,
val defaultMaxMemPages: Int = 1
) : Instance {
private var inst: Any? = null
override fun instance(ctx: ScriptContext) =
synchronized(this) { inst ?: createInstance(ctx).also { inst = it } }
override val inst = createInstance(imports)

private fun createInstance(ctx: ScriptContext): Any {
private fun createInstance(imports: ImportResolver): Any {
// Find the constructor
var constructorParams = emptyList<Any>()
var constructor: Constructor<*>?
Expand All @@ -79,7 +88,8 @@ interface Module {
val memLimit = if (memImport != null) {
constructor = cls.declaredConstructors.find { it.parameterTypes.firstOrNull()?.ref == mem.memType }
val memImportKind = memImport.kind as Node.Import.Kind.Memory
val memInst = ctx.resolveImportMemory(memImport, memImportKind.type, mem)
val memInst = imports.resolveImportMemory(memImport.module, memImport.field,
memImportKind.type, Class.forName(mem.memType.asm.className))
constructorParams += memInst
val (memLimit, memCap) = mem.limitAndCapacity(memInst)
if (memLimit < memImportKind.type.limits.initial * Mem.PAGE_SIZE)
Expand All @@ -101,7 +111,7 @@ interface Module {
// If it is not there, find the one w/ the max mem amount
val maybeMem = mod.memories.firstOrNull()
if (constructor == null) {
val maxMem = Math.max(maybeMem?.limits?.initial ?: 0, ctx.defaultMaxMemPages)
val maxMem = Math.max(maybeMem?.limits?.initial ?: 0, defaultMaxMemPages)
constructor = cls.declaredConstructors.find { it.parameterTypes.firstOrNull() == Int::class.java }
constructorParams += maxMem * Mem.PAGE_SIZE
}
Expand All @@ -111,22 +121,24 @@ interface Module {

// Function imports
constructorParams += mod.imports.mapNotNull {
if (it.kind is Node.Import.Kind.Func) ctx.resolveImportFunc(it, mod.types[it.kind.typeIndex])
if (it.kind is Node.Import.Kind.Func)
imports.resolveImportFunc(it.module, it.field, mod.types[it.kind.typeIndex])
else null
}

// Global imports
val globalImports = mod.imports.flatMap {
if (it.kind is Node.Import.Kind.Global) ctx.resolveImportGlobals(it, it.kind.type)
else emptyList()
if (it.kind is Node.Import.Kind.Global) {
imports.resolveImportGlobal(it.module, it.field, it.kind.type).toList().mapNotNull { it }
} else emptyList()
}
constructorParams += globalImports

// Table imports
val tableImport = mod.imports.find { it.kind is Node.Import.Kind.Table }
val tableSize = if (tableImport != null) {
val tableImportKind = tableImport.kind as Node.Import.Kind.Table
val table = ctx.resolveImportTable(tableImport, tableImportKind.type)
val table = imports.resolveImportTable(tableImport.module, tableImport.field, tableImportKind.type)
if (table.size < tableImportKind.type.limits.initial)
throw RunErr.ImportTableTooSmall(tableImportKind.type.limits.initial, table.size)
tableImportKind.type.limits.maximum?.let {
Expand Down Expand Up @@ -164,7 +176,6 @@ interface Module {
}

// Construct
ctx.debug { "Instantiating $cls using $constructor with params $constructorParams" }
return constructor.newInstance(*constructorParams.toTypedArray())
}
}
Expand Down
65 changes: 65 additions & 0 deletions compiler/src/main/kotlin/asmble/run/jvm/ModuleBuilder.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
package asmble.run.jvm

import asmble.ast.Node
import asmble.compile.jvm.*
import asmble.util.Logger
import org.objectweb.asm.ClassReader
import org.objectweb.asm.ClassVisitor
import org.objectweb.asm.Opcodes

interface ModuleBuilder<T : Module> {
fun build(imports: Module.ImportResolver, mod: Node.Module, className: String, name: String?): T

class Compiled(
val packageName: String = "",
val logger: Logger = Logger.Print(Logger.Level.OFF),
val classLoader: SimpleClassLoader = SimpleClassLoader(Compiled::class.java.classLoader, logger),
val adjustContext: (ClsContext) -> ClsContext = { it },
val includeBinaryInCompiledClass: Boolean = false,
val defaultMaxMemPages: Int = 1
) : ModuleBuilder<Module.Compiled> {
override fun build(
imports: Module.ImportResolver,
mod: Node.Module,
className: String,
name: String?
): Module.Compiled {
val ctx = ClsContext(
packageName = packageName,
className = className,
mod = mod,
logger = logger,
includeBinary = includeBinaryInCompiledClass
).let(adjustContext)
AstToAsm.fromModule(ctx)
return Module.Compiled(mod, classLoader.fromBuiltContext(ctx), name, ctx.mem, imports, defaultMaxMemPages)
}

open class SimpleClassLoader(
parent: ClassLoader,
logger: Logger,
val splitWhenTooLarge: Boolean = true
) : ClassLoader(parent), Logger by logger {
fun fromBuiltContext(ctx: ClsContext): Class<*> {
trace { "Computing frames for ASM class:\n" + ctx.cls.toAsmString() }
val writer = if (splitWhenTooLarge) AsmToBinary else AsmToBinary.noSplit
return writer.fromClassNode(ctx.cls).let { bytes ->
debug { "ASM class:\n" + bytes.asClassNode().toAsmString() }
val prefix = if (ctx.packageName.isNotEmpty()) ctx.packageName + "." else ""
defineClass("$prefix${ctx.className}", bytes, 0, bytes.size)
}
}

fun addClass(bytes: ByteArray) {
// Just get the name
var className = ""
ClassReader(bytes).accept(object : ClassVisitor(Opcodes.ASM5) {
override fun visit(a: Int, b: Int, name: String, c: String?, d: String?, e: Array<out String>?) {
className = name.replace('/', '.')
}
}, ClassReader.SKIP_CODE)
defineClass(className, bytes, 0, bytes.size)
}
}
}
}
Loading

0 comments on commit 6102ea5

Please sign in to comment.