mirror of
https://github.com/fluencelabs/asmble
synced 2025-04-25 14:52:21 +00:00
Improvements to stack handling in unreachable cases
This commit is contained in:
parent
ab635737b7
commit
989ae25429
@ -83,7 +83,7 @@ open class AstToAsm {
|
|||||||
VarInsnNode(Opcodes.ALOAD, 1),
|
VarInsnNode(Opcodes.ALOAD, 1),
|
||||||
FieldInsnNode(Opcodes.PUTFIELD, ctx.thisRef.asmName, "memory", ctx.mem.memType.asmDesc),
|
FieldInsnNode(Opcodes.PUTFIELD, ctx.thisRef.asmName, "memory", ctx.mem.memType.asmDesc),
|
||||||
VarInsnNode(Opcodes.ALOAD, 1)
|
VarInsnNode(Opcodes.ALOAD, 1)
|
||||||
).push(ctx.mem.memType)
|
).pushBlock(Node.Instr.Block(null), null, null).push(ctx.mem.memType)
|
||||||
// Do mem init and remove it from the stack if it's still there afterwards
|
// Do mem init and remove it from the stack if it's still there afterwards
|
||||||
memCon = ctx.mem.init(memCon, ctx.mod.memories.firstOrNull()?.limits?.initial ?: 0)
|
memCon = ctx.mem.init(memCon, ctx.mod.memories.firstOrNull()?.limits?.initial ?: 0)
|
||||||
// Add all data loads
|
// Add all data loads
|
||||||
@ -128,7 +128,7 @@ open class AstToAsm {
|
|||||||
var amountCon = Func("<init>", listOf(Int::class.ref) + importTypes).addInsns(
|
var amountCon = Func("<init>", listOf(Int::class.ref) + importTypes).addInsns(
|
||||||
VarInsnNode(Opcodes.ALOAD, 0),
|
VarInsnNode(Opcodes.ALOAD, 0),
|
||||||
VarInsnNode(Opcodes.ILOAD, 1)
|
VarInsnNode(Opcodes.ILOAD, 1)
|
||||||
).push(ctx.thisRef, Int::class.ref)
|
).pushBlock(Node.Instr.Block(null), null, null).push(ctx.thisRef, Int::class.ref)
|
||||||
amountCon = ctx.mem.create(amountCon).popExpectingMulti(ctx.thisRef, ctx.mem.memType)
|
amountCon = ctx.mem.create(amountCon).popExpectingMulti(ctx.thisRef, ctx.mem.memType)
|
||||||
// In addition to this and mem on the stack, add all imports
|
// In addition to this and mem on the stack, add all imports
|
||||||
amountCon = amountCon.params.drop(1).indices.fold(amountCon) { amountCon, index ->
|
amountCon = amountCon.params.drop(1).indices.fold(amountCon) { amountCon, index ->
|
||||||
|
@ -21,16 +21,9 @@ sealed class CompileErr(message: String, cause: Throwable? = null) : RuntimeExce
|
|||||||
|
|
||||||
class BlockEndMismatch(
|
class BlockEndMismatch(
|
||||||
val expectedStack: List<TypeRef>,
|
val expectedStack: List<TypeRef>,
|
||||||
val possibleExtra: TypeRef?,
|
|
||||||
val actualStack: List<TypeRef>
|
val actualStack: List<TypeRef>
|
||||||
) : CompileErr(msgString(expectedStack, possibleExtra, actualStack)) {
|
) : CompileErr("At block end, expected stack $expectedStack, got $actualStack") {
|
||||||
override val asmErrString get() = "type mismatch"
|
override val asmErrString 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 SelectMismatch(
|
class SelectMismatch(
|
||||||
|
@ -12,18 +12,25 @@ data class Func(
|
|||||||
val insns: List<AbstractInsnNode> = emptyList(),
|
val insns: List<AbstractInsnNode> = emptyList(),
|
||||||
val stack: List<TypeRef> = emptyList(),
|
val stack: List<TypeRef> = emptyList(),
|
||||||
val blockStack: List<Block> = emptyList(),
|
val blockStack: List<Block> = emptyList(),
|
||||||
// Contains index of JumpInsnNode that has a null label
|
// Contains index of JumpInsnNode that has a null label initially
|
||||||
val ifStack: List<Int> = emptyList()
|
val ifStack: List<Int> = emptyList()
|
||||||
) {
|
) {
|
||||||
|
|
||||||
val desc: String get() = ret.asMethodRetDesc(*params.toTypedArray())
|
val desc: String get() = ret.asMethodRetDesc(*params.toTypedArray())
|
||||||
val isCurrentBlockDead get() = blockStack.lastOrNull()?.let { block ->
|
val currentBlock get() = blockAtDepth(0)
|
||||||
// It's dead if it's marked unconditional or it's an unconditional
|
val isCurrentBlockDead get() = blockStack.lastOrNull()?.unreachable ?: false
|
||||||
// if/else and we are in that if/else area
|
|
||||||
block.unconditionalBranch ||
|
fun markUnreachable() = currentBlock.let { block ->
|
||||||
(block.unconditionalBranchInIf && !block.hasElse) ||
|
if (block.insn is Node.Instr.If) {
|
||||||
(block.unconditionalBranchInElse && block.hasElse)
|
if (block.hasElse) {
|
||||||
} ?: false
|
block.unreachableInElse = true
|
||||||
|
block.unreachable = block.unreachableInElse && block.unreachableInIf
|
||||||
|
} else block.unreachableInIf = true
|
||||||
|
} else {
|
||||||
|
block.unreachable = true
|
||||||
|
}
|
||||||
|
copy(stack = currentBlock.origStack)
|
||||||
|
}
|
||||||
|
|
||||||
fun addInsns(insns: List<AbstractInsnNode>) =
|
fun addInsns(insns: List<AbstractInsnNode>) =
|
||||||
if (isCurrentBlockDead) this else copy(insns = this.insns + insns)
|
if (isCurrentBlockDead) this else copy(insns = this.insns + insns)
|
||||||
@ -31,38 +38,41 @@ data class Func(
|
|||||||
fun addInsns(vararg insns: AbstractInsnNode) =
|
fun addInsns(vararg insns: AbstractInsnNode) =
|
||||||
if (isCurrentBlockDead) this else copy(insns = this.insns + insns)
|
if (isCurrentBlockDead) this else copy(insns = this.insns + insns)
|
||||||
|
|
||||||
fun push(vararg types: TypeRef) = copy(stack = stack + types)
|
fun push(types: List<TypeRef>) = copy(stack = stack + types)
|
||||||
|
|
||||||
fun popExpectingMulti(types: List<TypeRef>) = types.reversed().fold(this, Func::popExpecting)
|
fun push(vararg types: TypeRef) = push(types.asList())
|
||||||
|
|
||||||
fun popExpectingMulti(vararg types: TypeRef) = types.reversed().fold(this, Func::popExpecting)
|
fun popExpectingMulti(types: List<TypeRef>, currBlock: Block = currentBlock) =
|
||||||
|
types.reversed().fold(this) { fn, typ -> fn.popExpecting(typ, currBlock) }
|
||||||
|
|
||||||
fun popExpecting(type: TypeRef): Func {
|
fun popExpectingMulti(vararg types: TypeRef) = popExpectingMulti(types.asList())
|
||||||
assertTopOfStack(type)
|
|
||||||
return pop().first
|
fun popExpecting(type: TypeRef, currBlock: Block = currentBlock): Func {
|
||||||
|
return pop(currBlock).let { (fn, poppedType) ->
|
||||||
|
if (poppedType != TypeRef.Unknown && type != TypeRef.Unknown && poppedType != type)
|
||||||
|
throw CompileErr.StackMismatch(arrayOf(type), poppedType)
|
||||||
|
fn
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun isStackEmptyForBlock(currBlock: Block? = blockStack.lastOrNull()): Boolean {
|
fun isStackEmptyForBlock(currBlock: Block = currentBlock): Boolean {
|
||||||
// Per https://github.com/WebAssembly/design/issues/1020, it's not whether the
|
// Per https://github.com/WebAssembly/design/issues/1020, it's not whether the
|
||||||
// stack is empty, but whether it's the same as the current block
|
// stack is empty, but whether it's the same as the current block
|
||||||
return stack.isEmpty() || (currBlock != null && stack.size <= currBlock.origStack.size)
|
return stack.isEmpty() || stack.size <= currBlock.origStack.size
|
||||||
}
|
}
|
||||||
|
|
||||||
fun pop(currBlock: Block? = blockStack.lastOrNull()): Pair<Func, TypeRef> {
|
fun pop(currBlock: Block = currentBlock): Pair<Func, TypeRef> {
|
||||||
if (isStackEmptyForBlock(currBlock)) {
|
if (isStackEmptyForBlock(currBlock)) {
|
||||||
// Just fake it if dead
|
// Just fake it if dead
|
||||||
if (isCurrentBlockDead) return this to Int::class.ref
|
if (currBlock.unreachable) return this to TypeRef.Unknown
|
||||||
throw CompileErr.StackMismatch(emptyArray(), null)
|
throw CompileErr.StackMismatch(emptyArray(), null)
|
||||||
}
|
}
|
||||||
return copy(stack = stack.dropLast(1)) to stack.last()
|
return copy(stack = stack.dropLast(1)) to stack.last()
|
||||||
}
|
}
|
||||||
|
|
||||||
fun assertTopOfStack(type: TypeRef, currBlock: Block? = blockStack.lastOrNull()): Unit {
|
fun peekExpecting(type: TypeRef, currBlock: Block = currentBlock): Unit {
|
||||||
// If it's dead, we just go with it
|
// Just pop expecting
|
||||||
if (!isCurrentBlockDead) {
|
popExpecting(type, currBlock)
|
||||||
if (isStackEmptyForBlock(currBlock)) throw CompileErr.StackMismatch(arrayOf(type), null)
|
|
||||||
if (stack.lastOrNull() != type) throw CompileErr.StackMismatch(arrayOf(type), stack.lastOrNull())
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun toMethodNode(): MethodNode {
|
fun toMethodNode(): MethodNode {
|
||||||
@ -75,7 +85,7 @@ data class Func(
|
|||||||
|
|
||||||
fun withoutAffectingStack(fn: (Func) -> Func) = fn(this).copy(stack = stack)
|
fun withoutAffectingStack(fn: (Func) -> Func) = fn(this).copy(stack = stack)
|
||||||
|
|
||||||
fun stackSwap(currBlock: Block? = blockStack.lastOrNull()) =
|
fun stackSwap(currBlock: Block = currentBlock) =
|
||||||
if (isCurrentBlockDead) this else pop(currBlock).let { (fn, refLast) ->
|
if (isCurrentBlockDead) this else pop(currBlock).let { (fn, refLast) ->
|
||||||
fn.pop(currBlock).let { (fn, refFirst) ->
|
fn.pop(currBlock).let { (fn, refFirst) ->
|
||||||
(if (refFirst.stackSize == 2) {
|
(if (refFirst.stackSize == 2) {
|
||||||
@ -96,22 +106,16 @@ data class Func(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun pushBlock(insn: Node.Instr) = copy(blockStack = blockStack + Block(insn, insns.size, stack))
|
fun pushBlock(insn: Node.Instr, labelType: Node.Type.Value?, endType: Node.Type.Value?) =
|
||||||
|
pushBlock(insn, listOfNotNull(labelType?.typeRef), listOfNotNull(endType?.typeRef))
|
||||||
|
|
||||||
|
fun pushBlock(insn: Node.Instr, labelTypes: List<TypeRef>, endTypes: List<TypeRef>) =
|
||||||
|
copy(blockStack = blockStack + Block(insn, insns.size, stack, labelTypes, endTypes))
|
||||||
|
|
||||||
fun popBlock() = copy(blockStack = blockStack.dropLast(1)) to blockStack.last()
|
fun popBlock() = copy(blockStack = blockStack.dropLast(1)) to blockStack.last()
|
||||||
|
|
||||||
fun blockAtDepth(depth: Int) = blockStack.getOrNull(blockStack.size - depth - 1).let { block ->
|
fun blockAtDepth(depth: Int): Block =
|
||||||
when (block) {
|
blockStack.getOrNull(blockStack.size - depth - 1) ?: throw CompileErr.NoBlockAtDepth(depth)
|
||||||
null -> throw CompileErr.NoBlockAtDepth(depth)
|
|
||||||
is Block.WithLabel -> this to block
|
|
||||||
// We have to lazily create it here
|
|
||||||
else -> blockStack.toMutableList().let {
|
|
||||||
val newBlock = block.withLabel(LabelNode())
|
|
||||||
it[blockStack.size - depth - 1] = newBlock
|
|
||||||
copy(blockStack = it) to newBlock
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun pushIf() = copy(ifStack = ifStack + insns.size)
|
fun pushIf() = copy(ifStack = ifStack + insns.size)
|
||||||
|
|
||||||
@ -119,34 +123,24 @@ data class Func(
|
|||||||
|
|
||||||
fun popIf() = copy(ifStack = ifStack.dropLast(1)) to peekIf()
|
fun popIf() = copy(ifStack = ifStack.dropLast(1)) to peekIf()
|
||||||
|
|
||||||
open class Block(
|
class Block(
|
||||||
val insn: Node.Instr,
|
val insn: Node.Instr,
|
||||||
val startIndex: Int,
|
val startIndex: Int,
|
||||||
val origStack: List<TypeRef>
|
val origStack: List<TypeRef>,
|
||||||
|
val labelTypes: List<TypeRef>,
|
||||||
|
val endTypes: List<TypeRef>
|
||||||
) {
|
) {
|
||||||
open val label: LabelNode? get() = null
|
var unreachable = false
|
||||||
open val requiredEndStack: List<TypeRef>? get() = null
|
var unreachableInIf = false
|
||||||
open val hasElse: Boolean get() = false
|
var unreachableInElse = false
|
||||||
open val unconditionalBranch: Boolean get() = false
|
var hasElse = false
|
||||||
open val unconditionalBranchInIf: Boolean get() = false
|
var thenStackOnIf = emptyList<TypeRef>()
|
||||||
open val unconditionalBranchInElse: Boolean get() = false
|
|
||||||
// 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
|
|
||||||
|
|
||||||
class WithLabel(
|
var _label: LabelNode? = null
|
||||||
insn: Node.Instr,
|
val label get() = _label
|
||||||
startIndex: Int,
|
val requiredLabel: LabelNode get() {
|
||||||
origStack: List<TypeRef>,
|
if (_label == null) _label = LabelNode()
|
||||||
override val label: LabelNode
|
return _label!!
|
||||||
) : Block(insn, startIndex, origStack) {
|
|
||||||
override var blockExitVals: List<Pair<Node.Instr, TypeRef?>> = emptyList()
|
|
||||||
override var requiredEndStack: List<TypeRef>? = null
|
|
||||||
override var hasElse = false
|
|
||||||
override var unconditionalBranch = false
|
|
||||||
override var unconditionalBranchInIf = false
|
|
||||||
override var unconditionalBranchInElse = false
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -22,6 +22,9 @@ open class FuncBuilder {
|
|||||||
)
|
)
|
||||||
// Rework the instructions
|
// Rework the instructions
|
||||||
val reworkedInsns = ctx.reworker.rework(ctx, f)
|
val reworkedInsns = ctx.reworker.rework(ctx, f)
|
||||||
|
// Start the implicit block
|
||||||
|
func = func.pushBlock(Node.Instr.Block(f.type.ret), f.type.ret, f.type.ret)
|
||||||
|
// Create the context
|
||||||
val funcCtx = FuncContext(
|
val funcCtx = FuncContext(
|
||||||
cls = ctx,
|
cls = ctx,
|
||||||
node = f,
|
node = f,
|
||||||
@ -47,9 +50,13 @@ open class FuncBuilder {
|
|||||||
ret
|
ret
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// End the implicit block
|
||||||
|
val implicitBlock = func.currentBlock
|
||||||
|
func = applyEnd(funcCtx, func)
|
||||||
|
f.type.ret?.typeRef?.also { func = func.popExpecting(it, implicitBlock) }
|
||||||
|
|
||||||
// If the last instruction does not terminate, add the expected return
|
// If the last instruction does not terminate, add the expected return
|
||||||
if (func.insns.isEmpty() || !func.insns.last().isTerminating) {
|
if (func.insns.isEmpty() || !func.insns.last().isTerminating) {
|
||||||
f.type.ret?.typeRef?.also { func = func.popExpecting(it) }
|
|
||||||
func = func.addInsns(InsnNode(when (f.type.ret) {
|
func = func.addInsns(InsnNode(when (f.type.ret) {
|
||||||
null -> Opcodes.RETURN
|
null -> Opcodes.RETURN
|
||||||
Node.Type.Value.I32 -> Opcodes.IRETURN
|
Node.Type.Value.I32 -> Opcodes.IRETURN
|
||||||
@ -58,7 +65,6 @@ open class FuncBuilder {
|
|||||||
Node.Type.Value.F64 -> Opcodes.DRETURN
|
Node.Type.Value.F64 -> Opcodes.DRETURN
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
|
|
||||||
return func
|
return func
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -87,19 +93,17 @@ open class FuncBuilder {
|
|||||||
|
|
||||||
fun applyNodeInsn(ctx: FuncContext, fn: Func, i: Node.Instr, index: Int) = when (i) {
|
fun applyNodeInsn(ctx: FuncContext, fn: Func, i: Node.Instr, index: Int) = when (i) {
|
||||||
is Node.Instr.Unreachable ->
|
is Node.Instr.Unreachable ->
|
||||||
fn.addInsns(UnsupportedOperationException::class.athrow("Unreachable")).let { fn ->
|
fn.addInsns(UnsupportedOperationException::class.athrow("Unreachable")).markUnreachable()
|
||||||
setAsUnconditionalBranch(ctx, fn)
|
|
||||||
}
|
|
||||||
is Node.Instr.Nop ->
|
is Node.Instr.Nop ->
|
||||||
fn.addInsns(InsnNode(Opcodes.NOP))
|
fn.addInsns(InsnNode(Opcodes.NOP))
|
||||||
is Node.Instr.Block ->
|
is Node.Instr.Block ->
|
||||||
// TODO: check last item on stack?
|
fn.pushBlock(i, i.type, i.type)
|
||||||
fn.pushBlock(i)
|
|
||||||
is Node.Instr.Loop ->
|
is Node.Instr.Loop ->
|
||||||
fn.pushBlock(i)
|
fn.pushBlock(i, null, i.type)
|
||||||
is Node.Instr.If ->
|
is Node.Instr.If ->
|
||||||
// The label is set in else or end
|
// The label is set in else or end
|
||||||
fn.popExpecting(Int::class.ref).pushBlock(i).pushIf().addInsns(JumpInsnNode(Opcodes.IFEQ, null))
|
fn.popExpecting(Int::class.ref).pushBlock(i, i.type, i.type).pushIf().
|
||||||
|
addInsns(JumpInsnNode(Opcodes.IFEQ, null))
|
||||||
is Node.Instr.Else ->
|
is Node.Instr.Else ->
|
||||||
applyElse(ctx, fn)
|
applyElse(ctx, fn)
|
||||||
is Node.Instr.End ->
|
is Node.Instr.End ->
|
||||||
@ -450,26 +454,10 @@ open class FuncBuilder {
|
|||||||
java.lang.Double::class.invokeStatic("longBitsToDouble", Double::class, Long::class))
|
java.lang.Double::class.invokeStatic("longBitsToDouble", Double::class, Long::class))
|
||||||
}
|
}
|
||||||
|
|
||||||
fun setAsUnconditionalBranch(ctx: FuncContext, fn: Func) =
|
|
||||||
fn.blockAtDepth(0).let { (fn, block) ->
|
|
||||||
// We stop at the first conditional branch that:
|
|
||||||
// * Has an else but doesn't have an initial unconditional branch
|
|
||||||
// * Doesn't have an else
|
|
||||||
if (block.insn is Node.Instr.If) {
|
|
||||||
if (block.hasElse) block.unconditionalBranchInElse = true
|
|
||||||
else block.unconditionalBranchInIf = true
|
|
||||||
|
|
||||||
if (block.hasElse && !block.unconditionalBranchInIf) return fn
|
|
||||||
if (!block.hasElse) return fn
|
|
||||||
}
|
|
||||||
block.unconditionalBranch = true
|
|
||||||
fn
|
|
||||||
}
|
|
||||||
|
|
||||||
fun applyBr(ctx: FuncContext, fn: Func, i: Node.Instr.Br) =
|
fun applyBr(ctx: FuncContext, fn: Func, i: Node.Instr.Br) =
|
||||||
fn.blockAtDepth(i.relativeDepth).let { (fn, block) ->
|
fn.blockAtDepth(i.relativeDepth).let { block ->
|
||||||
// We have to pop all unnecessary values per the spec
|
// We have to pop all unnecessary values per the spec
|
||||||
val type = block.insnType?.typeRef
|
val type = block.labelTypes.firstOrNull()
|
||||||
fun pop(fn: Func): Func {
|
fun pop(fn: Func): Func {
|
||||||
// Have to swap first if there is a type expected
|
// Have to swap first if there is a type expected
|
||||||
// Note, we check stack size because dead code is allowed to do some crazy
|
// Note, we check stack size because dead code is allowed to do some crazy
|
||||||
@ -480,38 +468,27 @@ open class FuncBuilder {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
val expectedStackSize =
|
// How many do we have to pop to get back to expected block size
|
||||||
if (type == null) block.origStack.size
|
val currBlockStackSize = fn.stack.size - block.origStack.size
|
||||||
else block.origStack.size + 1
|
val needToPop = Math.max(0, if (type == null) currBlockStackSize else currBlockStackSize - 1)
|
||||||
ctx.debug {
|
ctx.debug {
|
||||||
"Unconditional branch on ${block.insn}, curr stack ${fn.stack}, " +
|
"Unconditional branch on ${block.insn}, curr stack ${fn.stack}, " +
|
||||||
" orig stack ${block.origStack}, expected stack size $expectedStackSize" }
|
" orig stack ${block.origStack}, need to pop $needToPop"
|
||||||
val popCount = Math.max(0, fn.stack.size - expectedStackSize)
|
|
||||||
(0 until popCount).fold(fn) { fn, _ -> pop(fn) }.let { fn ->
|
|
||||||
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 ->
|
|
||||||
// Loop breaks don't have to type check
|
|
||||||
if (block.insn !is Node.Instr.Loop) fn.assertTopOfStack(typ, block)
|
|
||||||
block.blockExitVals += i to typ
|
|
||||||
}
|
|
||||||
setAsUnconditionalBranch(ctx, fn)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
(0 until needToPop).fold(fn) { fn, _ -> pop(fn) }.
|
||||||
|
popExpectingMulti(block.labelTypes, block).
|
||||||
|
addInsns(JumpInsnNode(Opcodes.GOTO, block.requiredLabel)).
|
||||||
|
markUnreachable()
|
||||||
}
|
}
|
||||||
|
|
||||||
fun applyBrIf(ctx: FuncContext, fn: Func, i: Node.Instr.BrIf) =
|
fun applyBrIf(ctx: FuncContext, fn: Func, i: Node.Instr.BrIf) =
|
||||||
fn.blockAtDepth(i.relativeDepth).let { (fn, block) ->
|
fn.blockAtDepth(i.relativeDepth).let { block ->
|
||||||
fn.popExpecting(Int::class.ref).let { fn ->
|
fn.popExpecting(Int::class.ref).let { fn ->
|
||||||
// Must at least have the item on the stack that the block expects if it expects something
|
// Must at least have the item on the stack that the block expects if it expects something
|
||||||
val needsPopBeforeJump = needsToPopBeforeJumping(ctx, fn, block)
|
val needsPopBeforeJump = needsToPopBeforeJumping(ctx, fn, block)
|
||||||
val toLabel = if (needsPopBeforeJump) LabelNode() else block.label
|
val toLabel = if (needsPopBeforeJump) LabelNode() else block.requiredLabel
|
||||||
fn.addInsns(JumpInsnNode(Opcodes.IFNE, toLabel)).let { fn ->
|
fn.addInsns(JumpInsnNode(Opcodes.IFNE, toLabel)).let { fn ->
|
||||||
block.insnType?.typeRef?.let {
|
block.endTypes.firstOrNull()?.let { fn.peekExpecting(it) }
|
||||||
fn.assertTopOfStack(it)
|
|
||||||
block.blockExitVals += i to it
|
|
||||||
}
|
|
||||||
if (needsPopBeforeJump) buildPopBeforeJump(ctx, fn, block, toLabel)
|
if (needsPopBeforeJump) buildPopBeforeJump(ctx, fn, block, toLabel)
|
||||||
else fn
|
else fn
|
||||||
}
|
}
|
||||||
@ -520,36 +497,26 @@ open class FuncBuilder {
|
|||||||
|
|
||||||
// Can compile quite cleanly as a table switch on the JVM
|
// Can compile quite cleanly as a table switch on the JVM
|
||||||
fun applyBrTable(ctx: FuncContext, fn: Func, insn: Node.Instr.BrTable) =
|
fun applyBrTable(ctx: FuncContext, fn: Func, insn: Node.Instr.BrTable) =
|
||||||
fn.blockAtDepth(insn.default).let { (fn, defaultBlock) ->
|
fn.blockAtDepth(insn.default).let { defaultBlock ->
|
||||||
defaultBlock.insnType?.typeRef?.let { defaultBlock.blockExitVals += insn to it }
|
insn.targetTable.fold(fn to emptyList<Func.Block>()) { (fn, blocks), targetDepth ->
|
||||||
insn.targetTable.fold(fn to emptyList<LabelNode>()) { (fn, labels), targetDepth ->
|
fn to (blocks + fn.blockAtDepth(targetDepth))
|
||||||
fn.blockAtDepth(targetDepth).let { (fn, targetBlock) ->
|
}.let { (fn, targetBlocks) ->
|
||||||
targetBlock.insnType?.typeRef?.let { targetBlock.blockExitVals += insn to it }
|
|
||||||
fn to (labels + targetBlock.label)
|
|
||||||
}
|
|
||||||
}.let { (fn, targetLabels) ->
|
|
||||||
// In some cases, the target labels is empty. We need to make 0 goto
|
// In some cases, the target labels is empty. We need to make 0 goto
|
||||||
// the default as well.
|
// the default as well.
|
||||||
val targetLabelsArr =
|
val targetLabelsArr =
|
||||||
if (targetLabels.isNotEmpty()) targetLabels.toTypedArray()
|
if (targetBlocks.isNotEmpty()) targetBlocks.map(Func.Block::requiredLabel).toTypedArray()
|
||||||
else arrayOf(defaultBlock.label)
|
else arrayOf(defaultBlock.requiredLabel)
|
||||||
fn.popExpecting(Int::class.ref).addInsns(TableSwitchInsnNode(0, targetLabelsArr.size - 1,
|
fn.popExpecting(Int::class.ref).addInsns(TableSwitchInsnNode(0, targetLabelsArr.size - 1,
|
||||||
defaultBlock.label, *targetLabelsArr))
|
defaultBlock.requiredLabel, *targetLabelsArr))
|
||||||
}.let { fn ->
|
}.popExpectingMulti(defaultBlock.labelTypes).markUnreachable()
|
||||||
// We only peek here, because we keep items on the stack for
|
|
||||||
// dead code that uses it
|
|
||||||
defaultBlock.insnType?.typeRef?.let { fn.assertTopOfStack(it) }
|
|
||||||
// I think we're only going to mark ourselves dead for now
|
|
||||||
setAsUnconditionalBranch(ctx, fn)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun needsToPopBeforeJumping(ctx: FuncContext, fn: Func, block: Func.Block.WithLabel): Boolean {
|
fun needsToPopBeforeJumping(ctx: FuncContext, fn: Func, block: Func.Block): Boolean {
|
||||||
val requiredStackCount = if (block.insnType == null) block.origStack.size else block.origStack.size + 1
|
val requiredStackCount = if (block.endTypes.isEmpty()) block.origStack.size else block.origStack.size + 1
|
||||||
return fn.stack.size > requiredStackCount
|
return fn.stack.size > requiredStackCount
|
||||||
}
|
}
|
||||||
|
|
||||||
fun buildPopBeforeJump(ctx: FuncContext, fn: Func, block: Func.Block.WithLabel, tempLabel: LabelNode): Func {
|
fun buildPopBeforeJump(ctx: FuncContext, fn: Func, block: Func.Block, tempLabel: LabelNode): Func {
|
||||||
// This is sad that we have to do this because we can't trust the wasm stack on nested breaks
|
// This is sad that we have to do this because we can't trust the wasm stack on nested breaks
|
||||||
// Steps:
|
// Steps:
|
||||||
// 1. Build a label, do a GOTO to it for the regular path
|
// 1. Build a label, do a GOTO to it for the regular path
|
||||||
@ -561,7 +528,7 @@ open class FuncBuilder {
|
|||||||
// TODO: make this better by moving this "pad" to the block end so we don't have
|
// TODO: make this better by moving this "pad" to the block end so we don't have
|
||||||
// to make the running code path jump also. Also consider a better approach than
|
// to make the running code path jump also. Also consider a better approach than
|
||||||
// the constant swap-and-pop we do here.
|
// the constant swap-and-pop we do here.
|
||||||
val requiredStackCount = if (block.insnType == null) block.origStack.size else block.origStack.size + 1
|
val requiredStackCount = if (block.endTypes.isEmpty()) block.origStack.size else block.origStack.size + 1
|
||||||
ctx.debug {
|
ctx.debug {
|
||||||
"Jumping to block requiring stack size $requiredStackCount but we " +
|
"Jumping to block requiring stack size $requiredStackCount but we " +
|
||||||
"have ${fn.stack.size} so we are popping all unnecessary stack items before jumping"
|
"have ${fn.stack.size} so we are popping all unnecessary stack items before jumping"
|
||||||
@ -580,33 +547,38 @@ open class FuncBuilder {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}.addInsns(
|
}.addInsns(
|
||||||
JumpInsnNode(Opcodes.GOTO, block.label),
|
JumpInsnNode(Opcodes.GOTO, block.requiredLabel),
|
||||||
resumeLabel
|
resumeLabel
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun applyElse(ctx: FuncContext, fn: Func) = fn.blockAtDepth(0).let { (fn, block) ->
|
fun applyElse(ctx: FuncContext, fn: Func) = fn.blockAtDepth(0).let { block ->
|
||||||
// Do a goto the end, and then add a fresh label to the initial "if" that jumps here
|
// Do a goto the end, and then add a fresh label to the initial "if" that jumps here
|
||||||
// Also, put the stack back at what it was pre-if and ask end to check the else stack
|
// Also, put the stack back at what it was pre-if and ask end to check the else stack
|
||||||
val label = LabelNode()
|
val label = LabelNode()
|
||||||
fn.peekIf().label = label
|
fn.peekIf().label = label
|
||||||
ctx.debug { "Else block for ${block.insn}, orig stack ${block.origStack}" }
|
ctx.debug { "Else block for ${block.insn}, orig stack ${block.origStack}" }
|
||||||
if (!block.unconditionalBranchInIf) assertValidBlockEnd(ctx, fn, block)
|
|
||||||
block.hasElse = true
|
block.hasElse = true
|
||||||
fn.addInsns(JumpInsnNode(Opcodes.GOTO, block.label), label).copy(stack = block.origStack)
|
block.thenStackOnIf = fn.stack
|
||||||
|
fn.addInsns(JumpInsnNode(Opcodes.GOTO, block.requiredLabel), label).copy(stack = block.origStack)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun applyEnd(ctx: FuncContext, fn: Func) = fn.popBlock().let { (fn, block) ->
|
fun applyEnd(ctx: FuncContext, fn: Func) = fn.popBlock().let { (fn, block) ->
|
||||||
ctx.debug { "End of block ${block.insn}, orig stack ${block.origStack}, exit vals: ${block.blockExitVals}" }
|
ctx.debug { "End of block ${block.insn}, orig stack ${block.origStack}, unreachable? " + block.unreachable }
|
||||||
// Do normal block-end validation
|
// "If" block checks
|
||||||
assertValidBlockEnd(ctx, fn, block)
|
if (block.insn is Node.Instr.If) {
|
||||||
// If the block was an typed if w/ no else, it is wrong
|
// If the block was an typed if w/ no else, it is wrong
|
||||||
if (block.insn is Node.Instr.If && block.insnType != null && !block.hasElse) {
|
if (block.endTypes.isNotEmpty() && !block.hasElse)
|
||||||
throw CompileErr.IfThenValueWithoutElse()
|
throw CompileErr.IfThenValueWithoutElse()
|
||||||
|
// If the block was an if/then w/ a stack but the else doesn't match it
|
||||||
|
if (block.hasElse && block.thenStackOnIf != fn.stack)
|
||||||
|
throw CompileErr.BlockEndMismatch(block.thenStackOnIf, fn.stack)
|
||||||
}
|
}
|
||||||
// Put the stack where it should be
|
// Put the stack where it should be
|
||||||
val newStack = block.insnType?.let { block.origStack + it.typeRef } ?: block.origStack
|
fn.popExpectingMulti(block.endTypes, block).let { fn ->
|
||||||
fn.copy(stack = newStack).let { fn ->
|
// Do normal block-end validation
|
||||||
|
assertValidBlockEnd(ctx, fn, block)
|
||||||
|
fn.push(block.endTypes).let { fn ->
|
||||||
when (block.insn) {
|
when (block.insn) {
|
||||||
is Node.Instr.Block ->
|
is Node.Instr.Block ->
|
||||||
// Add label to end of block if it's there
|
// Add label to end of block if it's there
|
||||||
@ -634,21 +606,11 @@ open class FuncBuilder {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fun assertValidBlockEnd(ctx: FuncContext, fn: Func, block: Func.Block) {
|
fun assertValidBlockEnd(ctx: FuncContext, fn: Func, block: Func.Block) {
|
||||||
// If it's dead, who cares
|
if (fn.stack != block.origStack) {
|
||||||
if (block.unconditionalBranch || (block.unconditionalBranchInIf && !block.hasElse)) return
|
throw CompileErr.BlockEndMismatch(block.origStack, fn.stack)
|
||||||
// Go over each exit and make sure it did the right thing
|
|
||||||
block.blockExitVals.forEach {
|
|
||||||
require(it.second == block.insnType?.typeRef) { "Block exit val was $it, expected ${block.insnType}" }
|
|
||||||
}
|
|
||||||
val stackShouldEqual = block.insnType?.let { block.origStack + it.typeRef } ?: block.origStack
|
|
||||||
if (fn.stack != stackShouldEqual) {
|
|
||||||
// If there was an unconditional branch, then it it only has to start with the orig
|
|
||||||
if (!block.unconditionalBranch)
|
|
||||||
throw CompileErr.BlockEndMismatch(stackShouldEqual, null, fn.stack)
|
|
||||||
if (fn.stack.take(block.origStack.size) != block.origStack)
|
|
||||||
throw CompileErr.BlockEndMismatch(block.origStack, block.insnType?.typeRef, fn.stack)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1210,7 +1172,7 @@ open class FuncBuilder {
|
|||||||
fn.popExpecting(Double::class.ref).addInsns(InsnNode(Opcodes.DRETURN))
|
fn.popExpecting(Double::class.ref).addInsns(InsnNode(Opcodes.DRETURN))
|
||||||
}.let { fn ->
|
}.let { fn ->
|
||||||
if (fn.stack.isNotEmpty()) throw CompileErr.UnusedStackOnReturn(fn.stack)
|
if (fn.stack.isNotEmpty()) throw CompileErr.UnusedStackOnReturn(fn.stack)
|
||||||
setAsUnconditionalBranch(ctx, fn)
|
fn.markUnreachable()
|
||||||
}
|
}
|
||||||
|
|
||||||
companion object : FuncBuilder()
|
companion object : FuncBuilder()
|
||||||
|
@ -6,9 +6,7 @@ open class InsnReworker {
|
|||||||
|
|
||||||
fun rework(ctx: ClsContext, func: Node.Func): List<Insn> {
|
fun rework(ctx: ClsContext, func: Node.Func): List<Insn> {
|
||||||
return injectNeededStackVars(ctx, func.instructions).let { insns ->
|
return injectNeededStackVars(ctx, func.instructions).let { insns ->
|
||||||
addEagerLocalInitializers(ctx, func, insns).let { insns ->
|
addEagerLocalInitializers(ctx, func, insns)
|
||||||
wrapWithImplicitBlock(ctx, insns, func.type.ret)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -88,9 +86,6 @@ open class InsnReworker {
|
|||||||
} + insns
|
} + insns
|
||||||
}
|
}
|
||||||
|
|
||||||
fun wrapWithImplicitBlock(ctx: ClsContext, insns: List<Insn>, retType: Node.Type.Value?) =
|
|
||||||
(listOf(Insn.Node(Node.Instr.Block(retType))) + insns) + Insn.Node(Node.Instr.End)
|
|
||||||
|
|
||||||
fun injectNeededStackVars(ctx: ClsContext, insns: List<Node.Instr>): List<Insn> {
|
fun injectNeededStackVars(ctx: ClsContext, insns: List<Node.Instr>): List<Insn> {
|
||||||
// How we do this:
|
// How we do this:
|
||||||
// We run over each insn, and keep a running list of stack
|
// We run over each insn, and keep a running list of stack
|
||||||
|
@ -9,4 +9,10 @@ data class TypeRef(val asm: Type) {
|
|||||||
fun asMethodRetDesc(vararg args: TypeRef) = Type.getMethodDescriptor(asm, *args.map { it.asm }.toTypedArray())
|
fun asMethodRetDesc(vararg args: TypeRef) = Type.getMethodDescriptor(asm, *args.map { it.asm }.toTypedArray())
|
||||||
|
|
||||||
val stackSize: Int get() = if (asm == Type.DOUBLE_TYPE || asm == Type.LONG_TYPE) 2 else 1
|
val stackSize: Int get() = if (asm == Type.DOUBLE_TYPE || asm == Type.LONG_TYPE) 2 else 1
|
||||||
|
|
||||||
|
object UnknownType
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
val Unknown = UnknownType::class.ref
|
||||||
|
}
|
||||||
}
|
}
|
11
src/main/kotlin/asmble/run/jvm/ScriptAssertionError.kt
Normal file
11
src/main/kotlin/asmble/run/jvm/ScriptAssertionError.kt
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
package asmble.run.jvm
|
||||||
|
|
||||||
|
import asmble.ast.Script
|
||||||
|
|
||||||
|
class ScriptAssertionError(
|
||||||
|
val assertion: Script.Cmd.Assertion,
|
||||||
|
msg: String,
|
||||||
|
returned: Any? = null,
|
||||||
|
expected: Any? = null,
|
||||||
|
cause: Throwable? = null
|
||||||
|
) : AssertionError(msg, cause)
|
@ -55,29 +55,32 @@ data class ScriptContext(
|
|||||||
require(ret.exprs.size < 2)
|
require(ret.exprs.size < 2)
|
||||||
val (retType, retVal) = doAction(ret.action)
|
val (retType, retVal) = doAction(ret.action)
|
||||||
when (retType) {
|
when (retType) {
|
||||||
null -> if (ret.exprs.isNotEmpty()) throw AssertionError("Got empty return, expected not empty")
|
null ->
|
||||||
|
if (ret.exprs.isNotEmpty())
|
||||||
|
throw ScriptAssertionError(ret, "Got empty return, expected not empty", retVal)
|
||||||
else -> {
|
else -> {
|
||||||
if (ret.exprs.isEmpty()) throw AssertionError("Got return, expected empty")
|
if (ret.exprs.isEmpty()) throw ScriptAssertionError(ret, "Got return, expected empty", retVal)
|
||||||
val expectedVal = runExpr(ret.exprs.first(), retType)
|
val expectedVal = runExpr(ret.exprs.first(), retType)
|
||||||
if (expectedVal is Float && expectedVal.isNaN() && retVal is Float) {
|
if (expectedVal is Float && expectedVal.isNaN() && retVal is Float && retVal.isNaN()) {
|
||||||
java.lang.Float.floatToRawIntBits(expectedVal).let { expectedBits ->
|
if (java.lang.Float.floatToRawIntBits(expectedVal) != java.lang.Float.floatToRawIntBits(retVal))
|
||||||
java.lang.Float.floatToRawIntBits(retVal).let { actualBits ->
|
throw ScriptAssertionError(
|
||||||
if (expectedBits != actualBits) throw AssertionError(
|
ret,
|
||||||
"Expected NaN ${java.lang.Integer.toHexString(expectedBits)}, " +
|
"Mismatch NaN bits, got ${java.lang.Float.floatToRawIntBits(retVal).toString(16)}, " +
|
||||||
"got ${java.lang.Integer.toHexString(actualBits)}"
|
"expected ${java.lang.Float.floatToRawIntBits(expectedVal).toString(16)}",
|
||||||
|
retVal,
|
||||||
|
expectedVal
|
||||||
)
|
)
|
||||||
}
|
} else if (expectedVal is Double && expectedVal.isNaN() && retVal is Double && retVal.isNaN()) {
|
||||||
}
|
if (java.lang.Double.doubleToRawLongBits(expectedVal) != java.lang.Double.doubleToRawLongBits(retVal))
|
||||||
} else if (expectedVal is Double && expectedVal.isNaN() && retVal is Double) {
|
throw ScriptAssertionError(
|
||||||
java.lang.Double.doubleToRawLongBits(expectedVal).let { expectedBits ->
|
ret,
|
||||||
java.lang.Double.doubleToRawLongBits(retVal).let { actualBits ->
|
"Mismatch NaN bits, got ${java.lang.Double.doubleToRawLongBits(retVal).toString(16)}, " +
|
||||||
if (expectedBits != actualBits) throw AssertionError(
|
"expected ${java.lang.Double.doubleToRawLongBits(expectedVal).toString(16)}",
|
||||||
"Expected NaN ${java.lang.Long.toHexString(expectedBits)}, " +
|
retVal,
|
||||||
"got ${java.lang.Long.toHexString(actualBits)}"
|
expectedVal
|
||||||
)
|
)
|
||||||
}
|
} else if (retVal != expectedVal)
|
||||||
}
|
throw ScriptAssertionError(ret, "Expected $expectedVal, got $retVal", retVal, expectedVal)
|
||||||
} else if (retVal != expectedVal) throw AssertionError("Expected $expectedVal, got $retVal")
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -85,15 +88,21 @@ data class ScriptContext(
|
|||||||
fun assertReturnNan(ret: Script.Cmd.Assertion.ReturnNan) {
|
fun assertReturnNan(ret: Script.Cmd.Assertion.ReturnNan) {
|
||||||
val (retType, retVal) = doAction(ret.action)
|
val (retType, retVal) = doAction(ret.action)
|
||||||
when (retType) {
|
when (retType) {
|
||||||
Node.Type.Value.F32 -> if (!(retVal as Float).isNaN()) throw AssertionError("Expected NaN, got $retVal")
|
Node.Type.Value.F32 ->
|
||||||
Node.Type.Value.F64 -> if (!(retVal as Double).isNaN()) throw AssertionError("Expected NaN, got $retVal")
|
if (!(retVal as Float).isNaN()) throw ScriptAssertionError(ret, "Expected NaN, got $retVal", retVal)
|
||||||
else -> throw AssertionError("Expected NaN, got $retVal")
|
Node.Type.Value.F64 ->
|
||||||
|
if (!(retVal as Double).isNaN()) throw ScriptAssertionError(ret, "Expected NaN, got $retVal", retVal)
|
||||||
|
else ->
|
||||||
|
throw ScriptAssertionError(ret, "Expected NaN, got $retVal", retVal)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun assertTrap(trap: Script.Cmd.Assertion.Trap) {
|
fun assertTrap(trap: Script.Cmd.Assertion.Trap) {
|
||||||
try { doAction(trap.action).also { throw AssertionError("Expected exception but completed successfully") } }
|
try {
|
||||||
catch (e: Throwable) { assertFailure(e, trap.failure) }
|
doAction(trap.action).also {
|
||||||
|
throw ScriptAssertionError(trap, "Expected exception but completed successfully")
|
||||||
|
}
|
||||||
|
} catch (e: Throwable) { assertFailure(trap, e, trap.failure) }
|
||||||
}
|
}
|
||||||
|
|
||||||
fun assertInvalid(invalid: Script.Cmd.Assertion.Invalid) {
|
fun assertInvalid(invalid: Script.Cmd.Assertion.Invalid) {
|
||||||
@ -101,22 +110,23 @@ data class ScriptContext(
|
|||||||
debug { "Compiling invalid: " + SExprToStr.Compact.fromSExpr(AstToSExpr.fromModule(invalid.module.value)) }
|
debug { "Compiling invalid: " + SExprToStr.Compact.fromSExpr(AstToSExpr.fromModule(invalid.module.value)) }
|
||||||
val className = "invalid" + UUID.randomUUID().toString().replace("-", "")
|
val className = "invalid" + UUID.randomUUID().toString().replace("-", "")
|
||||||
compileModule(invalid.module.value, className, null)
|
compileModule(invalid.module.value, className, null)
|
||||||
throw AssertionError("Expected invalid module with error '${invalid.failure}', was valid")
|
throw ScriptAssertionError(invalid, "Expected invalid module with error '${invalid.failure}', was valid")
|
||||||
} catch (e: Exception) { assertFailure(e, invalid.failure) }
|
} catch (e: Exception) { assertFailure(invalid, e, invalid.failure) }
|
||||||
}
|
}
|
||||||
|
|
||||||
fun assertExhaustion(exhaustion: Script.Cmd.Assertion.Exhaustion) {
|
fun assertExhaustion(exhaustion: Script.Cmd.Assertion.Exhaustion) {
|
||||||
try { doAction(exhaustion.action).also { throw AssertionError("Expected exception") } }
|
try { doAction(exhaustion.action).also { throw ScriptAssertionError(exhaustion, "Expected exception") } }
|
||||||
catch (e: Throwable) { assertFailure(e, exhaustion.failure) }
|
catch (e: Throwable) { assertFailure(exhaustion, e, exhaustion.failure) }
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun exceptionFromCatch(e: Throwable) =
|
private fun exceptionFromCatch(e: Throwable) =
|
||||||
e as? AssertionError ?: (e as? InvocationTargetException)?.targetException ?: e
|
e as? ScriptAssertionError ?: (e as? InvocationTargetException)?.targetException ?: e
|
||||||
|
|
||||||
private fun assertFailure(e: Throwable, expectedString: String) {
|
private fun assertFailure(a: Script.Cmd.Assertion, e: Throwable, expectedString: String) {
|
||||||
val innerEx = exceptionFromCatch(e)
|
val innerEx = exceptionFromCatch(e)
|
||||||
val msg = exceptionTranslator.translate(innerEx) ?: "<unrecognized error>"
|
val msg = exceptionTranslator.translate(innerEx) ?: "<unrecognized error>"
|
||||||
if (msg != expectedString) throw AssertionError("Expected failure '$expectedString' got '$msg'", innerEx)
|
if (msg != expectedString)
|
||||||
|
throw ScriptAssertionError(a, "Expected failure '$expectedString' got '$msg'", cause = innerEx)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun doAction(cmd: Script.Cmd.Action) = when (cmd) {
|
fun doAction(cmd: Script.Cmd.Action) = when (cmd) {
|
||||||
|
@ -6,11 +6,12 @@ interface Logger {
|
|||||||
|
|
||||||
fun log(atLevel: Level, fn: () -> String) { if (atLevel >= level) log(atLevel, fn()) }
|
fun log(atLevel: Level, fn: () -> String) { if (atLevel >= level) log(atLevel, fn()) }
|
||||||
fun error(fn: () -> String) { log(Level.ERROR, fn) }
|
fun error(fn: () -> String) { log(Level.ERROR, fn) }
|
||||||
|
fun warn(fn: () -> String) { log(Level.WARN, fn) }
|
||||||
fun info(fn: () -> String) { log(Level.INFO, fn) }
|
fun info(fn: () -> String) { log(Level.INFO, fn) }
|
||||||
fun debug(fn: () -> String) { log(Level.DEBUG, fn) }
|
fun debug(fn: () -> String) { log(Level.DEBUG, fn) }
|
||||||
fun trace(fn: () -> String) { log(Level.TRACE, fn) }
|
fun trace(fn: () -> String) { log(Level.TRACE, fn) }
|
||||||
|
|
||||||
enum class Level { TRACE, DEBUG, INFO, ERROR, OFF }
|
enum class Level { TRACE, DEBUG, INFO, WARN, ERROR, OFF }
|
||||||
|
|
||||||
data class Print(override val level: Level) : Logger {
|
data class Print(override val level: Level) : Logger {
|
||||||
override fun log(atLevel: Level, str: String) { println("[$atLevel] $str") }
|
override fun log(atLevel: Level, str: String) { println("[$atLevel] $str") }
|
||||||
|
@ -21,7 +21,10 @@ class CoreTest(val unit: CoreTestUnit) : Logger by Logger.Print(Logger.Level.INF
|
|||||||
if (unit.name.endsWith(".fail")) {
|
if (unit.name.endsWith(".fail")) {
|
||||||
assertNotNull(ex, "Expected failure, but succeeded")
|
assertNotNull(ex, "Expected failure, but succeeded")
|
||||||
debug { "Got expected failure: $ex" }
|
debug { "Got expected failure: $ex" }
|
||||||
} else if (ex != null) throw ex
|
} else if (ex != null) {
|
||||||
|
if (unit.isWarningInsteadOfError(ex)) warn { "Unexpected error on ${unit.name}, but is a warning: $ex" }
|
||||||
|
else throw ex
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun run() {
|
private fun run() {
|
||||||
|
@ -4,6 +4,7 @@ import asmble.ast.SExpr
|
|||||||
import asmble.ast.Script
|
import asmble.ast.Script
|
||||||
import asmble.io.SExprToAst
|
import asmble.io.SExprToAst
|
||||||
import asmble.io.StrToSExpr
|
import asmble.io.StrToSExpr
|
||||||
|
import asmble.run.jvm.ScriptAssertionError
|
||||||
import java.nio.file.FileSystems
|
import java.nio.file.FileSystems
|
||||||
import java.nio.file.Files
|
import java.nio.file.Files
|
||||||
import java.nio.file.Paths
|
import java.nio.file.Paths
|
||||||
@ -32,6 +33,8 @@ class CoreTestUnit(val name: String, val wast: String, val expectedOutput: Strin
|
|||||||
|
|
||||||
val script: Script by lazy { SExprToAst.toScript(SExpr.Multi(ast)) }
|
val script: Script by lazy { SExprToAst.toScript(SExpr.Multi(ast)) }
|
||||||
|
|
||||||
|
fun isWarningInsteadOfError(t: Throwable) = testsWithErrorToWarningPredicates[name]?.invoke(t) ?: false
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
|
|
||||||
/*
|
/*
|
||||||
@ -52,10 +55,12 @@ class CoreTestUnit(val name: String, val wast: String, val expectedOutput: Strin
|
|||||||
- linking.wast - Not handling tables yet
|
- linking.wast - Not handling tables yet
|
||||||
- memory.wast - Not handling mem data strings yet
|
- memory.wast - Not handling mem data strings yet
|
||||||
- return.wast - Not handling tables yet
|
- return.wast - Not handling tables yet
|
||||||
|
- start.wast - Not handling mem data strings yet
|
||||||
|
- typecheck.wast - Not handling tables yet
|
||||||
|
- unreachable.wast - Not handling tables yet
|
||||||
*/
|
*/
|
||||||
|
|
||||||
val knownGoodTests = arrayOf(
|
val knownGoodTests = arrayOf(
|
||||||
"temp.wast",
|
|
||||||
"address.wast",
|
"address.wast",
|
||||||
"address-offset-range.fail.wast",
|
"address-offset-range.fail.wast",
|
||||||
"block.wast",
|
"block.wast",
|
||||||
@ -131,7 +136,26 @@ class CoreTestUnit(val name: String, val wast: String, val expectedOutput: Strin
|
|||||||
"of_string-overflow-u32.fail.wast",
|
"of_string-overflow-u32.fail.wast",
|
||||||
"of_string-overflow-u64.fail.wast",
|
"of_string-overflow-u64.fail.wast",
|
||||||
"resizing.wast",
|
"resizing.wast",
|
||||||
"select.wast"
|
"select.wast",
|
||||||
|
"set_local.wast",
|
||||||
|
"skip-stack-guard-page.wast",
|
||||||
|
"stack.wast",
|
||||||
|
"store-align-0.fail.wast",
|
||||||
|
"store-align-odd.fail.wast",
|
||||||
|
"store_retval.wast",
|
||||||
|
// "switch.wast" TODO: we are in trouble here on the "argument switch"
|
||||||
|
"tee_local.wast",
|
||||||
|
"traps.wast"
|
||||||
|
)
|
||||||
|
|
||||||
|
val testsWithErrorToWarningPredicates: Map<String, (Throwable) -> Boolean> = mapOf(
|
||||||
|
// NaN bit patterns can be off
|
||||||
|
"float_literals" to { t ->
|
||||||
|
(((t as? ScriptAssertionError)?.
|
||||||
|
assertion as? Script.Cmd.Assertion.Return)?.
|
||||||
|
action as? Script.Cmd.Action.Invoke)?.
|
||||||
|
string?.contains("nan") ?: false
|
||||||
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
val unitsPath = "/spec/test/core"
|
val unitsPath = "/spec/test/core"
|
||||||
|
Loading…
x
Reference in New Issue
Block a user