mirror of
https://github.com/fluencelabs/asmble
synced 2025-04-24 22:32:19 +00:00
Successful blocks test
This commit is contained in:
parent
d4ca5885d3
commit
2f1e8ee089
@ -30,4 +30,5 @@ dependencies {
|
||||
compile "org.ow2.asm:asm-util:$asm_version"
|
||||
testCompile 'junit:junit:4.12'
|
||||
testCompile "org.jetbrains.kotlin:kotlin-test-junit:$kotlin_version"
|
||||
testCompile "org.ow2.asm:asm-debug-all:$asm_version"
|
||||
}
|
||||
|
@ -14,7 +14,8 @@ import kotlin.reflect.KFunction
|
||||
import kotlin.reflect.KProperty
|
||||
import kotlin.reflect.jvm.javaField
|
||||
import kotlin.reflect.jvm.javaMethod
|
||||
import kotlin.reflect.jvm.reflect
|
||||
|
||||
|
||||
|
||||
val <R> KFunction<R>.asmDesc: String get() = Type.getMethodDescriptor(this.javaMethod)
|
||||
|
||||
@ -35,7 +36,8 @@ fun <T : Exception> KClass<T>.athrow(msg: String) = listOf(
|
||||
InsnNode(Opcodes.DUP),
|
||||
msg.const,
|
||||
MethodInsnNode(Opcodes.INVOKESPECIAL, this.ref.asmName, "<init>",
|
||||
Void::class.ref.asMethodRetDesc(String::class.ref), false)
|
||||
Void::class.ref.asMethodRetDesc(String::class.ref), false),
|
||||
InsnNode(Opcodes.ATHROW)
|
||||
)
|
||||
|
||||
// Ug: https://youtrack.jetbrains.com/issue/KT-17064
|
||||
@ -117,6 +119,11 @@ val AbstractInsnNode.isTerminating: Boolean get() = when (this.opcode) {
|
||||
else -> false
|
||||
}
|
||||
|
||||
val AbstractInsnNode.isUnconditionalJump: Boolean get() = when (this.opcode) {
|
||||
Opcodes.GOTO, Opcodes.JSR -> true
|
||||
else -> false
|
||||
}
|
||||
|
||||
val Node.Type.Func.asmDesc: String get() =
|
||||
(this.ret?.typeRef ?: Void::class.ref).asMethodRetDesc(*this.params.map { it.typeRef }.toTypedArray())
|
||||
|
||||
|
@ -22,24 +22,25 @@ open class AstToAsm {
|
||||
fun addFields(ctx: ClsContext) {
|
||||
// First field is always a private final memory field
|
||||
// Ug, ambiguity on List<?> +=
|
||||
ctx.cls.fields.plusAssign(FieldNode(Opcodes.ACC_PRIVATE + Opcodes.ACC_FINAL, "memory",
|
||||
ctx.cls.fields.add(FieldNode(Opcodes.ACC_PRIVATE + Opcodes.ACC_FINAL, "memory",
|
||||
ctx.mem.memType.asmDesc, null, null))
|
||||
// Now all method imports as method handles
|
||||
ctx.cls.fields += ctx.importFuncs.indices.map {
|
||||
// TODO: why does this fail with asm-debug-all but not with just regular asm?
|
||||
ctx.cls.fields.addAll(ctx.importFuncs.indices.map {
|
||||
FieldNode(Opcodes.ACC_PRIVATE + Opcodes.ACC_FINAL, ctx.funcName(it),
|
||||
MethodHandle::class.ref.asmDesc, null, null)
|
||||
}
|
||||
})
|
||||
// Now all import globals as getter (and maybe setter) method handles
|
||||
ctx.cls.fields += ctx.importGlobals.withIndex().flatMap { (index, import) ->
|
||||
ctx.cls.fields.addAll(ctx.importGlobals.withIndex().flatMap { (index, import) ->
|
||||
val ret = listOf(FieldNode(Opcodes.ACC_PRIVATE + Opcodes.ACC_FINAL, ctx.importGlobalGetterFieldName(index),
|
||||
MethodHandle::class.ref.asmDesc, null, null))
|
||||
if (!(import.kind as Node.Import.Kind.Global).type.mutable) ret else {
|
||||
ret + FieldNode(Opcodes.ACC_PRIVATE + Opcodes.ACC_FINAL, ctx.importGlobalSetterFieldName(index),
|
||||
MethodHandle::class.ref.asmDesc, null, null)
|
||||
}
|
||||
}
|
||||
})
|
||||
// Now all non-import globals
|
||||
ctx.cls.fields += ctx.mod.globals.withIndex().map { (index, global) ->
|
||||
ctx.cls.fields.addAll(ctx.mod.globals.withIndex().map { (index, global) ->
|
||||
// In the MVP, we can trust the init is constant stuff and a single instr
|
||||
require(global.init.size <= 1) { "Global init has more than 1 insn" }
|
||||
val init: Number = global.init.firstOrNull().let {
|
||||
@ -57,7 +58,7 @@ open class AstToAsm {
|
||||
val access = Opcodes.ACC_PRIVATE + if (!global.type.mutable) Opcodes.ACC_FINAL else 0
|
||||
FieldNode(access, ctx.globalName(ctx.importGlobals.size + index),
|
||||
global.type.contentType.typeRef.asmDesc, null, init)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
fun addConstructors(ctx: ClsContext) {
|
||||
@ -153,7 +154,7 @@ open class AstToAsm {
|
||||
constructors = listOf(regCon) + constructors
|
||||
}
|
||||
|
||||
ctx.cls.methods += constructors.map(Func::toMethodNode)
|
||||
ctx.cls.methods.addAll(constructors.map(Func::toMethodNode))
|
||||
}
|
||||
|
||||
fun addExports(ctx: ClsContext) {
|
||||
@ -212,9 +213,9 @@ open class AstToAsm {
|
||||
}
|
||||
|
||||
fun addFuncs(ctx: ClsContext) {
|
||||
ctx.cls.methods += ctx.mod.funcs.mapIndexed { index, func ->
|
||||
ctx.cls.methods.addAll(ctx.mod.funcs.mapIndexed { index, func ->
|
||||
ctx.funcBuilder.fromFunc(ctx, func, ctx.importFuncs.size + index).toMethodNode()
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
companion object : AstToAsm()
|
||||
|
35
src/main/kotlin/asmble/compile/jvm/CompileErr.kt
Normal file
35
src/main/kotlin/asmble/compile/jvm/CompileErr.kt
Normal file
@ -0,0 +1,35 @@
|
||||
package asmble.compile.jvm
|
||||
|
||||
import java.util.*
|
||||
|
||||
sealed class CompileErr(message: String, cause: Throwable? = null) : RuntimeException(message, cause) {
|
||||
abstract val asmErrString: String
|
||||
|
||||
class StackMismatch(
|
||||
val expected: Array<out TypeRef>,
|
||||
val actual: TypeRef?
|
||||
) : CompileErr("Expected any type of ${Arrays.toString(expected)}, got $actual") {
|
||||
override val asmErrString: String get() = "type mismatch"
|
||||
}
|
||||
|
||||
class BlockEndMismatch(
|
||||
val expectedStack: List<TypeRef>,
|
||||
val possibleExtra: TypeRef?,
|
||||
val actualStack: List<TypeRef>
|
||||
) : CompileErr(msgString(expectedStack, possibleExtra, actualStack)) {
|
||||
|
||||
override val asmErrString: String get() = "type mismatch"
|
||||
|
||||
companion object {
|
||||
fun msgString(expectedStack: List<TypeRef>, possibleExtra: TypeRef?, actualStack: List<TypeRef>) =
|
||||
if (possibleExtra == null) "At block end, expected stack $expectedStack, got $actualStack"
|
||||
else "At block end, expected stack $expectedStack and maybe $possibleExtra, got $actualStack"
|
||||
}
|
||||
}
|
||||
|
||||
class UnusedStackOnReturn(
|
||||
val leftover: List<TypeRef>
|
||||
) : CompileErr("Expected empty stack on return, still leftover with: $leftover") {
|
||||
override val asmErrString: String get() = "type mismatch"
|
||||
}
|
||||
}
|
@ -3,6 +3,7 @@ package asmble.compile.jvm
|
||||
import asmble.ast.Node
|
||||
import org.objectweb.asm.Opcodes
|
||||
import org.objectweb.asm.tree.*
|
||||
import java.util.*
|
||||
|
||||
data class Func(
|
||||
val name: String,
|
||||
@ -32,22 +33,26 @@ data class Func(
|
||||
|
||||
fun popExpecting(type: TypeRef) = popExpectingAny(type)
|
||||
|
||||
fun popExpectingNum() = popExpectingAny(Int::class.ref, Long::class.ref, Float::class.ref, Double::class.ref)
|
||||
|
||||
fun popExpectingAny(vararg types: TypeRef) = popExpectingAny(types::contains)
|
||||
|
||||
fun popExpectingAny(pred: (TypeRef) -> Boolean): Func {
|
||||
stack.lastOrNull()?.let { require(pred(it)) { "Stack var type ${stack.last()} unexpected" } }
|
||||
fun popExpectingAny(vararg types: TypeRef): Func {
|
||||
peekExpectingAny(*types)
|
||||
return pop().first
|
||||
}
|
||||
|
||||
fun pop(): Pair<Func, TypeRef> {
|
||||
require(stack.isNotEmpty(), { "Stack is empty" })
|
||||
if (stack.isEmpty()) throw CompileErr.StackMismatch(emptyArray(), null)
|
||||
return copy(stack = stack.dropLast(1)) to stack.last()
|
||||
}
|
||||
|
||||
fun peekExpecting(type: TypeRef) = peekExpectingAny(type)
|
||||
|
||||
fun peekExpectingAny(vararg types: TypeRef): TypeRef {
|
||||
val hasExpected = stack.lastOrNull()?.let(types::contains) ?: false
|
||||
if (!hasExpected) throw CompileErr.StackMismatch(types, stack.lastOrNull())
|
||||
return stack.last()
|
||||
}
|
||||
|
||||
fun toMethodNode(): MethodNode {
|
||||
require(stack.isEmpty(), { "Stack not empty for $name when compiling" })
|
||||
if (stack.isNotEmpty()) throw CompileErr.UnusedStackOnReturn(stack)
|
||||
require(insns.lastOrNull()?.isTerminating ?: false, { "Last insn for $name$desc is not terminating" })
|
||||
val ret = MethodNode(access, name, desc, null, null)
|
||||
insns.forEach(ret.instructions::add)
|
||||
@ -102,7 +107,8 @@ data class Func(
|
||||
val origStack: List<TypeRef>
|
||||
) {
|
||||
open val label: LabelNode? get() = null
|
||||
open val blockExitVals: List<TypeRef?> = emptyList()
|
||||
// First val is the insn, second is the type
|
||||
open val blockExitVals: List<Pair<Node.Instr, TypeRef?>> = emptyList()
|
||||
fun withLabel(label: LabelNode) = WithLabel(insn, startIndex, origStack, label)
|
||||
val insnType: Node.Type.Value? get() = (insn as? Node.Instr.Args.Type)?.type
|
||||
|
||||
@ -112,7 +118,7 @@ data class Func(
|
||||
origStack: List<TypeRef>,
|
||||
override val label: LabelNode
|
||||
) : Block(insn, startIndex, origStack) {
|
||||
override var blockExitVals: List<TypeRef?> = emptyList()
|
||||
override var blockExitVals: List<Pair<Node.Instr, TypeRef?>> = emptyList()
|
||||
}
|
||||
}
|
||||
}
|
@ -424,17 +424,23 @@ open class FuncBuilder {
|
||||
fun applyBr(ctx: FuncContext, fn: Func, i: Node.Instr.Br) =
|
||||
fn.blockAtDepth(i.relativeDepth).let { (fn, block) ->
|
||||
fn.addInsns(JumpInsnNode(Opcodes.GOTO, block.label)).let { fn ->
|
||||
// We only peek here, because we keep items on the stack for
|
||||
// dead code that uses it
|
||||
block.insnType?.typeRef?.let { typ ->
|
||||
fn.popExpecting(typ).also { block.blockExitVals += typ }
|
||||
} ?: fn
|
||||
fn.peekExpecting(typ)
|
||||
block.blockExitVals += i to typ
|
||||
}
|
||||
fn
|
||||
}
|
||||
}
|
||||
|
||||
fun applyBrIf(ctx: FuncContext, fn: Func, i: Node.Instr.BrIf) =
|
||||
fn.blockAtDepth(i.relativeDepth).let { (fn, block) ->
|
||||
fn.popExpecting(Int::class.ref).addInsns(JumpInsnNode(Opcodes.IFNE, block.label)).let { fn ->
|
||||
// We don't have to pop this like we do with br, because it's conditional
|
||||
block.insnType?.typeRef?.let { block.blockExitVals += it }
|
||||
block.insnType?.typeRef?.let {
|
||||
fn.peekExpecting(it)
|
||||
block.blockExitVals += i to it
|
||||
}
|
||||
fn
|
||||
}
|
||||
}
|
||||
@ -442,17 +448,25 @@ open class FuncBuilder {
|
||||
// Can compile quite cleanly as a table switch on the JVM
|
||||
fun applyBrTable(ctx: FuncContext, fn: Func, insn: Node.Instr.BrTable) =
|
||||
fn.blockAtDepth(insn.default).let { (fn, defaultBlock) ->
|
||||
defaultBlock.insnType?.typeRef?.let { defaultBlock.blockExitVals += it }
|
||||
defaultBlock.insnType?.typeRef?.let { defaultBlock.blockExitVals += insn to it }
|
||||
insn.targetTable.fold(fn to emptyList<LabelNode>()) { (fn, labels), targetDepth ->
|
||||
fn.blockAtDepth(targetDepth).let { (fn, targetBlock) ->
|
||||
targetBlock.insnType?.typeRef?.let { targetBlock.blockExitVals += it }
|
||||
targetBlock.insnType?.typeRef?.let { targetBlock.blockExitVals += insn to it }
|
||||
fn to (labels + targetBlock.label)
|
||||
}
|
||||
}.let { (fn, targetLabels) ->
|
||||
fn.popExpecting(Int::class.ref).addInsns(TableSwitchInsnNode(0, targetLabels.size - 1,
|
||||
defaultBlock.label, *targetLabels.toTypedArray()))
|
||||
// In some cases, the target labels is empty. We need to make 0 goto
|
||||
// the default as well.
|
||||
val targetLabelsArr =
|
||||
if (targetLabels.isNotEmpty()) targetLabels.toTypedArray()
|
||||
else arrayOf(defaultBlock.label)
|
||||
fn.popExpecting(Int::class.ref).addInsns(TableSwitchInsnNode(0, targetLabelsArr.size - 1,
|
||||
defaultBlock.label, *targetLabelsArr))
|
||||
}.let { fn ->
|
||||
defaultBlock.insnType?.typeRef?.let { fn.popExpecting(it) } ?: fn
|
||||
// We only peek here, because we keep items on the stack for
|
||||
// dead code that uses it
|
||||
defaultBlock.insnType?.typeRef?.let { fn.peekExpecting(it) }
|
||||
fn
|
||||
}
|
||||
}
|
||||
|
||||
@ -466,23 +480,30 @@ open class FuncBuilder {
|
||||
fun applyEnd(ctx: FuncContext, fn: Func) = fn.popBlock().let { (fn, block) ->
|
||||
// Go over each exit and make sure it did the right thing
|
||||
block.blockExitVals.forEach {
|
||||
require(it == block.insnType?.typeRef) { "Block exit val was $it, expected ${block.insnType}" }
|
||||
require(it.second == block.insnType?.typeRef) { "Block exit val was $it, expected ${block.insnType}" }
|
||||
}
|
||||
// We need to check the current stack
|
||||
when (block.insnType) {
|
||||
null -> {
|
||||
require(fn.stack == block.origStack) {
|
||||
"At block end, expected stack ${block.origStack}, got ${fn.stack}"
|
||||
}
|
||||
if (fn.stack != block.origStack) throw CompileErr.BlockEndMismatch(block.origStack, null, fn.stack)
|
||||
fn
|
||||
}
|
||||
else -> {
|
||||
val typ = block.insnType!!.typeRef
|
||||
require(fn.stack == block.origStack || fn.stack == block.origStack + typ) {
|
||||
"At block end, expected stack ${block.origStack} and maybe $typ, got ${fn.stack}"
|
||||
}
|
||||
// We have to add the expected type ourselves, there wasn't a fall through...
|
||||
if (fn.stack.size == block.origStack.size) fn.push(typ) else fn
|
||||
val hasOrig = fn.stack == block.origStack
|
||||
val hasExpectedStart = !hasOrig && fn.stack.take(block.origStack.size + 1) == block.origStack + typ
|
||||
if (hasExpectedStart) {
|
||||
// Extra stack items are always ok, because sometimes dead code
|
||||
// works with them. Just pop off everything to get back down to
|
||||
// size.
|
||||
fn.stack.drop(block.origStack.size + 1).fold(fn) { fn, _ -> fn.pop().first }
|
||||
} else if (hasOrig && block.blockExitVals.isNotEmpty()) {
|
||||
// Exact orig stack is ok so long as there was at least one exit.
|
||||
// This means that there was likely something internally that
|
||||
// did an unconditional jump and some dead code after it consumed
|
||||
// the stack.
|
||||
fn.push(typ)
|
||||
} else throw CompileErr.BlockEndMismatch(block.origStack, typ, fn.stack)
|
||||
}
|
||||
}.let { fn ->
|
||||
when (block.insn) {
|
||||
@ -849,7 +870,7 @@ open class FuncBuilder {
|
||||
"invokeExact", funcType.asmDesc, false)
|
||||
)
|
||||
is Either.Right -> fn.popExpecting(ctx.cls.thisRef).addInsns(
|
||||
MethodInsnNode(Opcodes.INVOKESTATIC, ctx.cls.thisRef.asmName,
|
||||
MethodInsnNode(Opcodes.INVOKEVIRTUAL, ctx.cls.thisRef.asmName,
|
||||
ctx.cls.funcName(index), funcType.asmDesc, false)
|
||||
)
|
||||
}.let { fn -> funcType.ret?.let { fn.push(it.typeRef) } ?: fn }
|
||||
@ -868,7 +889,7 @@ open class FuncBuilder {
|
||||
Node.Type.Value.F64 ->
|
||||
fn.popExpecting(Double::class.ref).addInsns(InsnNode(Opcodes.DRETURN))
|
||||
}.let {
|
||||
require(it.stack.isEmpty()) { "Stack not empty on return" }
|
||||
if (it.stack.isNotEmpty()) throw CompileErr.UnusedStackOnReturn(it.stack)
|
||||
it
|
||||
}
|
||||
|
||||
|
@ -1,10 +1,11 @@
|
||||
package asmble.run.jvm
|
||||
|
||||
open class ExceptionTranslator {
|
||||
fun translateOrRethrow(ex: Throwable) = translate(ex) ?: throw ex
|
||||
import asmble.compile.jvm.CompileErr
|
||||
|
||||
open class ExceptionTranslator {
|
||||
fun translate(ex: Throwable): String? = when (ex) {
|
||||
is IndexOutOfBoundsException -> "out of bounds memory access"
|
||||
is CompileErr -> ex.asmErrString
|
||||
else -> null
|
||||
}
|
||||
|
||||
|
@ -3,6 +3,8 @@ package asmble.run.jvm
|
||||
import asmble.ast.Node
|
||||
import asmble.ast.Script
|
||||
import asmble.compile.jvm.*
|
||||
import asmble.io.AstToSExpr
|
||||
import asmble.io.SExprToStr
|
||||
import asmble.util.Logger
|
||||
import java.io.PrintWriter
|
||||
import java.lang.invoke.MethodHandle
|
||||
@ -20,7 +22,7 @@ data class ScriptContext(
|
||||
val classLoader: SimpleClassLoader =
|
||||
ScriptContext.SimpleClassLoader(ScriptContext::class.java.classLoader, logger),
|
||||
val exceptionTranslator: ExceptionTranslator = ExceptionTranslator
|
||||
) {
|
||||
) : Logger by logger {
|
||||
fun withHarnessRegistered(out: PrintWriter = PrintWriter(System.out, true)) =
|
||||
copy(registrations = registrations + (
|
||||
"spectest" to NativeModule(Harness::class.java, Harness(out))
|
||||
@ -60,25 +62,22 @@ data class ScriptContext(
|
||||
|
||||
fun assertTrap(trap: Script.Cmd.Assertion.Trap) {
|
||||
try { doAction(trap.action).also { throw AssertionError("Expected exception") } }
|
||||
catch (e: Throwable) {
|
||||
val innerEx = if (e is InvocationTargetException) e.targetException else e
|
||||
exceptionTranslator.translateOrRethrow(innerEx).let {
|
||||
if (it != trap.failure) throw AssertionError("Expected failure '${trap.failure}' got '$it'")
|
||||
}
|
||||
}
|
||||
catch (e: Throwable) { assertFailure(e, trap.failure) }
|
||||
}
|
||||
|
||||
fun assertInvalid(invalid: Script.Cmd.Assertion.Invalid) {
|
||||
try {
|
||||
debug { "Compiling invalid: " + SExprToStr.Compact.fromSExpr(AstToSExpr.fromModule(invalid.module)) }
|
||||
val className = "invalid" + UUID.randomUUID().toString().replace("-", "")
|
||||
compileModule(invalid.module, className, null)
|
||||
throw AssertionError("Expected invalid module with error '${invalid.failure}', was valid")
|
||||
} catch (e: Throwable) {
|
||||
val innerEx = if (e is InvocationTargetException) e.targetException else e
|
||||
exceptionTranslator.translateOrRethrow(innerEx).let {
|
||||
if (it != invalid.failure) throw AssertionError("Expected invalid '${invalid.failure}' got '$it'")
|
||||
}
|
||||
}
|
||||
} catch (e: Throwable) { assertFailure(e, invalid.failure) }
|
||||
}
|
||||
|
||||
private fun assertFailure(e: Throwable, expectedString: String) {
|
||||
val innerEx = if (e is InvocationTargetException) e.targetException else e
|
||||
val msg = exceptionTranslator.translate(innerEx) ?: "unknown"
|
||||
if (msg != expectedString) throw AssertionError("Expected failure '$expectedString' got '$msg'", innerEx)
|
||||
}
|
||||
|
||||
fun doAction(cmd: Script.Cmd.Action) = when (cmd) {
|
||||
@ -120,6 +119,7 @@ data class ScriptContext(
|
||||
}
|
||||
|
||||
fun compileExpr(insns: List<Node.Instr>, retType: Node.Type.Value?): MethodHandle {
|
||||
debug { "Compiling expression: $insns" }
|
||||
val mod = Node.Module(
|
||||
exports = listOf(Node.Export("expr", Node.ExternalKind.FUNCTION, 0)),
|
||||
funcs = listOf(Node.Func(
|
||||
@ -194,9 +194,9 @@ data class ScriptContext(
|
||||
|
||||
open class SimpleClassLoader(parent: ClassLoader, logger: Logger) : ClassLoader(parent), Logger by logger {
|
||||
fun fromBuiltContext(ctx: ClsContext): Class<*> {
|
||||
ctx.trace { "Computing frames for ASM class:\n" + ctx.cls.toAsmString() }
|
||||
trace { "Computing frames for ASM class:\n" + ctx.cls.toAsmString() }
|
||||
return ctx.cls.withComputedFramesAndMaxs().let { bytes ->
|
||||
ctx.debug { "ASM class:\n" + bytes.asClassNode().toAsmString() }
|
||||
debug { "ASM class:\n" + bytes.asClassNode().toAsmString() }
|
||||
defineClass("${ctx.packageName}.${ctx.className}", bytes, 0, bytes.size)
|
||||
}
|
||||
}
|
||||
|
@ -13,7 +13,7 @@ import java.io.StringWriter
|
||||
import kotlin.test.assertEquals
|
||||
|
||||
@RunWith(Parameterized::class)
|
||||
class CoreTest(val unit: CoreTestUnit) : Logger by Logger.Print(Logger.Level.TRACE) {
|
||||
class CoreTest(val unit: CoreTestUnit) : Logger by Logger.Print(Logger.Level.INFO) {
|
||||
|
||||
@Test
|
||||
fun testName() {
|
||||
|
Loading…
x
Reference in New Issue
Block a user