From 2f1e8ee0891944f02df9c904f5a2dc5b35b935b9 Mon Sep 17 00:00:00 2001 From: Chad Retz Date: Mon, 27 Mar 2017 13:40:37 -0500 Subject: [PATCH] Successful blocks test --- build.gradle | 1 + src/main/kotlin/asmble/compile/jvm/AsmExt.kt | 11 +++- .../kotlin/asmble/compile/jvm/AstToAsm.kt | 21 ++++--- .../kotlin/asmble/compile/jvm/CompileErr.kt | 35 +++++++++++ src/main/kotlin/asmble/compile/jvm/Func.kt | 26 +++++--- .../kotlin/asmble/compile/jvm/FuncBuilder.kt | 61 +++++++++++++------ .../asmble/run/jvm/ExceptionTranslator.kt | 5 +- .../kotlin/asmble/run/jvm/ScriptContext.kt | 30 ++++----- src/test/kotlin/asmble/CoreTest.kt | 2 +- 9 files changed, 132 insertions(+), 60 deletions(-) create mode 100644 src/main/kotlin/asmble/compile/jvm/CompileErr.kt diff --git a/build.gradle b/build.gradle index 17fbfe6..a2368dd 100644 --- a/build.gradle +++ b/build.gradle @@ -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" } diff --git a/src/main/kotlin/asmble/compile/jvm/AsmExt.kt b/src/main/kotlin/asmble/compile/jvm/AsmExt.kt index 1be824a..4b72afe 100644 --- a/src/main/kotlin/asmble/compile/jvm/AsmExt.kt +++ b/src/main/kotlin/asmble/compile/jvm/AsmExt.kt @@ -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 KFunction.asmDesc: String get() = Type.getMethodDescriptor(this.javaMethod) @@ -35,7 +36,8 @@ fun KClass.athrow(msg: String) = listOf( InsnNode(Opcodes.DUP), msg.const, MethodInsnNode(Opcodes.INVOKESPECIAL, this.ref.asmName, "", - 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()) diff --git a/src/main/kotlin/asmble/compile/jvm/AstToAsm.kt b/src/main/kotlin/asmble/compile/jvm/AstToAsm.kt index c485189..2a0527e 100644 --- a/src/main/kotlin/asmble/compile/jvm/AstToAsm.kt +++ b/src/main/kotlin/asmble/compile/jvm/AstToAsm.kt @@ -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() diff --git a/src/main/kotlin/asmble/compile/jvm/CompileErr.kt b/src/main/kotlin/asmble/compile/jvm/CompileErr.kt new file mode 100644 index 0000000..2e4794c --- /dev/null +++ b/src/main/kotlin/asmble/compile/jvm/CompileErr.kt @@ -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, + 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, + val possibleExtra: TypeRef?, + val actualStack: List + ) : CompileErr(msgString(expectedStack, possibleExtra, actualStack)) { + + override val asmErrString: String get() = "type mismatch" + + companion object { + fun msgString(expectedStack: List, possibleExtra: TypeRef?, actualStack: List) = + 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 + ) : CompileErr("Expected empty stack on return, still leftover with: $leftover") { + override val asmErrString: String get() = "type mismatch" + } +} \ No newline at end of file diff --git a/src/main/kotlin/asmble/compile/jvm/Func.kt b/src/main/kotlin/asmble/compile/jvm/Func.kt index 1cf836a..9a60c33 100644 --- a/src/main/kotlin/asmble/compile/jvm/Func.kt +++ b/src/main/kotlin/asmble/compile/jvm/Func.kt @@ -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 { - 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 ) { open val label: LabelNode? get() = null - open val blockExitVals: List = emptyList() + // First val is the insn, second is the type + open val blockExitVals: List> = 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, override val label: LabelNode ) : Block(insn, startIndex, origStack) { - override var blockExitVals: List = emptyList() + override var blockExitVals: List> = emptyList() } } } \ No newline at end of file diff --git a/src/main/kotlin/asmble/compile/jvm/FuncBuilder.kt b/src/main/kotlin/asmble/compile/jvm/FuncBuilder.kt index 13dc5b9..232b334 100644 --- a/src/main/kotlin/asmble/compile/jvm/FuncBuilder.kt +++ b/src/main/kotlin/asmble/compile/jvm/FuncBuilder.kt @@ -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()) { (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 } diff --git a/src/main/kotlin/asmble/run/jvm/ExceptionTranslator.kt b/src/main/kotlin/asmble/run/jvm/ExceptionTranslator.kt index e5272ec..92202d5 100644 --- a/src/main/kotlin/asmble/run/jvm/ExceptionTranslator.kt +++ b/src/main/kotlin/asmble/run/jvm/ExceptionTranslator.kt @@ -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 } diff --git a/src/main/kotlin/asmble/run/jvm/ScriptContext.kt b/src/main/kotlin/asmble/run/jvm/ScriptContext.kt index 65d858a..7baba0b 100644 --- a/src/main/kotlin/asmble/run/jvm/ScriptContext.kt +++ b/src/main/kotlin/asmble/run/jvm/ScriptContext.kt @@ -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, 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) } } diff --git a/src/test/kotlin/asmble/CoreTest.kt b/src/test/kotlin/asmble/CoreTest.kt index a65d7a5..baae36c 100644 --- a/src/test/kotlin/asmble/CoreTest.kt +++ b/src/test/kotlin/asmble/CoreTest.kt @@ -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() {