Successful blocks test

This commit is contained in:
Chad Retz 2017-03-27 13:40:37 -05:00
parent d4ca5885d3
commit 2f1e8ee089
9 changed files with 132 additions and 60 deletions

View File

@ -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"
}

View File

@ -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())

View File

@ -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()

View 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"
}
}

View File

@ -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()
}
}
}

View File

@ -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
}

View File

@ -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
}

View File

@ -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)
}
}

View File

@ -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() {