diff --git a/src/main/kotlin/asmble/ast/SExpr.kt b/src/main/kotlin/asmble/ast/SExpr.kt index 10cb25a..2f7199d 100644 --- a/src/main/kotlin/asmble/ast/SExpr.kt +++ b/src/main/kotlin/asmble/ast/SExpr.kt @@ -8,6 +8,7 @@ sealed class SExpr { } data class Symbol(val contents: String = "", val quoted: Boolean = false) : SExpr() { override fun toString() = SExprToStr.Compact.fromSExpr(this) + // This is basically the same as the deprecated java.lang.String#getBytes fun rawContentCharsToBytes() = contents.toCharArray().map(Char::toByte) } } \ No newline at end of file diff --git a/src/main/kotlin/asmble/compile/jvm/AsmExt.kt b/src/main/kotlin/asmble/compile/jvm/AsmExt.kt index eb6486c..6ae2119 100644 --- a/src/main/kotlin/asmble/compile/jvm/AsmExt.kt +++ b/src/main/kotlin/asmble/compile/jvm/AsmExt.kt @@ -161,13 +161,17 @@ fun MethodNode.addInsns(vararg insn: AbstractInsnNode): MethodNode { return this } +fun MethodNode.toAsmString(): String { + val stringWriter = StringWriter() + val cv = TraceClassVisitor(PrintWriter(stringWriter)) + this.accept(cv) + cv.p.print(PrintWriter(stringWriter)) + return stringWriter.toString() +} + val Node.Type.Func.asmDesc: String get() = (this.ret?.typeRef ?: Void::class.ref).asMethodRetDesc(*this.params.map { it.typeRef }.toTypedArray()) -fun ClassNode.addMethodIfNotPresent(m: MethodNode) { - this.methods.find { (it as MethodNode).let { it.name == m.name && it.desc == it.desc } } ?: this.methods.add(m) -} - fun ClassNode.withComputedFramesAndMaxs(): ByteArray { // TODO: compute maxs adds a bunch of NOPs for unreachable code // See $func12 of block.wast. Is removing these worth the extra visit cycle? diff --git a/src/main/kotlin/asmble/compile/jvm/ClsContext.kt b/src/main/kotlin/asmble/compile/jvm/ClsContext.kt index 02e631d..3b5818d 100644 --- a/src/main/kotlin/asmble/compile/jvm/ClsContext.kt +++ b/src/main/kotlin/asmble/compile/jvm/ClsContext.kt @@ -59,20 +59,25 @@ data class ClsContext( fun globalName(index: Int) = "\$global$index" fun funcName(index: Int) = "\$func$index" - private fun syntheticAssertion(fn: SyntheticAssertionBuilder.(ClsContext) -> MethodNode) = - fn(syntheticAssertionBuilder, this).let { method -> - cls.addMethodIfNotPresent(method) - MethodInsnNode(Opcodes.INVOKESTATIC, thisRef.asmName, method.name, method.desc, false) - } + private fun syntheticAssertion( + nameSuffix: String, + fn: SyntheticAssertionBuilder.(ClsContext, String) -> MethodNode + ): MethodInsnNode { + val name = "\$\$$nameSuffix" + val method = + cls.methods.find { (it as MethodNode).name == name }?.let { it as MethodNode } ?: + fn(syntheticAssertionBuilder, this, name).also { cls.methods.add(it) } + return MethodInsnNode(Opcodes.INVOKESTATIC, thisRef.asmName, method.name, method.desc, false) + } - val truncAssertF2SI by lazy { syntheticAssertion(SyntheticAssertionBuilder::buildF2SIAssertion) } - val truncAssertF2UI by lazy { syntheticAssertion(SyntheticAssertionBuilder::buildF2UIAssertion) } - val truncAssertF2SL by lazy { syntheticAssertion(SyntheticAssertionBuilder::buildF2SLAssertion) } - val truncAssertF2UL by lazy { syntheticAssertion(SyntheticAssertionBuilder::buildF2ULAssertion) } - val truncAssertD2SI by lazy { syntheticAssertion(SyntheticAssertionBuilder::buildD2SIAssertion) } - val truncAssertD2UI by lazy { syntheticAssertion(SyntheticAssertionBuilder::buildD2UIAssertion) } - val truncAssertD2SL by lazy { syntheticAssertion(SyntheticAssertionBuilder::buildD2SLAssertion) } - val truncAssertD2UL by lazy { syntheticAssertion(SyntheticAssertionBuilder::buildD2ULAssertion) } - val divAssertI by lazy { syntheticAssertion(SyntheticAssertionBuilder::buildIDivAssertion) } - val divAssertL by lazy { syntheticAssertion(SyntheticAssertionBuilder::buildLDivAssertion) } + val truncAssertF2SI get() = syntheticAssertion("assertF2SI", SyntheticAssertionBuilder::buildF2SIAssertion) + val truncAssertF2UI get() = syntheticAssertion("assertF2UI", SyntheticAssertionBuilder::buildF2UIAssertion) + val truncAssertF2SL get() = syntheticAssertion("assertF2SL", SyntheticAssertionBuilder::buildF2SLAssertion) + val truncAssertF2UL get() = syntheticAssertion("assertF2UL", SyntheticAssertionBuilder::buildF2ULAssertion) + val truncAssertD2SI get() = syntheticAssertion("assertD2SI", SyntheticAssertionBuilder::buildD2SIAssertion) + val truncAssertD2UI get() = syntheticAssertion("assertD2UI", SyntheticAssertionBuilder::buildD2UIAssertion) + val truncAssertD2SL get() = syntheticAssertion("assertD2SL", SyntheticAssertionBuilder::buildD2SLAssertion) + val truncAssertD2UL get() = syntheticAssertion("assertD2UL", SyntheticAssertionBuilder::buildD2ULAssertion) + val divAssertI get() = syntheticAssertion("assertIDiv", SyntheticAssertionBuilder::buildIDivAssertion) + val divAssertL get() = syntheticAssertion("assertLDiv", SyntheticAssertionBuilder::buildLDivAssertion) } \ 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 5d1d5de..3cab64d 100644 --- a/src/main/kotlin/asmble/compile/jvm/Func.kt +++ b/src/main/kotlin/asmble/compile/jvm/Func.kt @@ -77,7 +77,7 @@ data class Func( fun toMethodNode(): MethodNode { if (stack.isNotEmpty()) throw CompileErr.UnusedStackOnReturn(stack) - require(insns.lastOrNull()?.isTerminating ?: false, { "Last insn for $name$desc is not terminating" }) + 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) return ret diff --git a/src/main/kotlin/asmble/compile/jvm/SyntheticAssertionBuilder.kt b/src/main/kotlin/asmble/compile/jvm/SyntheticAssertionBuilder.kt index 79d6528..b55c335 100644 --- a/src/main/kotlin/asmble/compile/jvm/SyntheticAssertionBuilder.kt +++ b/src/main/kotlin/asmble/compile/jvm/SyntheticAssertionBuilder.kt @@ -5,12 +5,12 @@ import org.objectweb.asm.tree.* open class SyntheticAssertionBuilder { - fun buildIDivAssertion(ctx: ClsContext) = + fun buildIDivAssertion(ctx: ClsContext, name: String) = LabelNode().let { safeLabel -> LabelNode().let { overflowLabel -> MethodNode( Opcodes.ACC_PRIVATE + Opcodes.ACC_STATIC + Opcodes.ACC_SYNTHETIC, - "\$\$assertIDiv", "(II)V", null, null + name, "(II)V", null, null ).addInsns( VarInsnNode(Opcodes.ILOAD, 0), Int.MIN_VALUE.const, @@ -25,12 +25,12 @@ open class SyntheticAssertionBuilder { } } - fun buildLDivAssertion(ctx: ClsContext) = + fun buildLDivAssertion(ctx: ClsContext, name: String) = LabelNode().let { safeLabel -> LabelNode().let { overflowLabel -> MethodNode( Opcodes.ACC_PRIVATE + Opcodes.ACC_STATIC + Opcodes.ACC_SYNTHETIC, - "\$\$assertLDiv", "(JJ)V", null, null + name, "(JJ)V", null, null ).addInsns( VarInsnNode(Opcodes.LLOAD, 0), Long.MIN_VALUE.const, @@ -49,54 +49,54 @@ open class SyntheticAssertionBuilder { // TODO: add tests for +- 4 near overflow for each combo compared with spec - fun buildF2SIAssertion(ctx: ClsContext) = + fun buildF2SIAssertion(ctx: ClsContext, name: String) = MethodNode( Opcodes.ACC_PRIVATE + Opcodes.ACC_STATIC + Opcodes.ACC_SYNTHETIC, - "\$\$assertF2SI", "(F)V", null, null + name, "(F)V", null, null ).floatNanCheck().floatRangeCheck(2147483648f, -2147483648f).addInsns(InsnNode(Opcodes.RETURN)) - fun buildF2UIAssertion(ctx: ClsContext) = + fun buildF2UIAssertion(ctx: ClsContext, name: String) = MethodNode( Opcodes.ACC_PRIVATE + Opcodes.ACC_STATIC + Opcodes.ACC_SYNTHETIC, - "\$\$assertF2UI", "(F)V", null, null + name, "(F)V", null, null ).floatNanCheck().floatUnsignedRangeCheck(4294967296f).addInsns(InsnNode(Opcodes.RETURN)) - fun buildF2SLAssertion(ctx: ClsContext) = + fun buildF2SLAssertion(ctx: ClsContext, name: String) = MethodNode( Opcodes.ACC_PRIVATE + Opcodes.ACC_STATIC + Opcodes.ACC_SYNTHETIC, - "\$\$assertF2SL", "(F)V", null, null + name, "(F)V", null, null ).floatNanCheck().floatRangeCheck(9223372036854775807f, -9223372036854775807f). addInsns(InsnNode(Opcodes.RETURN)) - fun buildF2ULAssertion(ctx: ClsContext) = + fun buildF2ULAssertion(ctx: ClsContext, name: String) = MethodNode( Opcodes.ACC_PRIVATE + Opcodes.ACC_STATIC + Opcodes.ACC_SYNTHETIC, - "\$\$assertF2UL", "(F)V", null, null + name, "(F)V", null, null ).floatNanCheck().floatUnsignedRangeCheck(18446744073709551616f).addInsns(InsnNode(Opcodes.RETURN)) - fun buildD2SIAssertion(ctx: ClsContext) = + fun buildD2SIAssertion(ctx: ClsContext, name: String) = MethodNode( Opcodes.ACC_PRIVATE + Opcodes.ACC_STATIC + Opcodes.ACC_SYNTHETIC, - "\$\$assertD2SI", "(D)V", null, null + name, "(D)V", null, null ).doubleNanCheck().doubleRangeCheck(2147483648.0, -2147483648.0).addInsns(InsnNode(Opcodes.RETURN)) - fun buildD2UIAssertion(ctx: ClsContext) = + fun buildD2UIAssertion(ctx: ClsContext, name: String) = MethodNode( Opcodes.ACC_PRIVATE + Opcodes.ACC_STATIC + Opcodes.ACC_SYNTHETIC, - "\$\$assertD2UI", "(D)V", null, null + name, "(D)V", null, null ).doubleNanCheck().doubleUnsignedRangeCheck(4294967296.0).addInsns(InsnNode(Opcodes.RETURN)) - fun buildD2SLAssertion(ctx: ClsContext) = + fun buildD2SLAssertion(ctx: ClsContext, name: String) = MethodNode( Opcodes.ACC_PRIVATE + Opcodes.ACC_STATIC + Opcodes.ACC_SYNTHETIC, - "\$\$assertD2SL", "(D)V", null, null + name, "(D)V", null, null ).doubleNanCheck().doubleRangeCheck(9223372036854775807.0, -9223372036854775807.0). addInsns(InsnNode(Opcodes.RETURN)) - fun buildD2ULAssertion(ctx: ClsContext) = + fun buildD2ULAssertion(ctx: ClsContext, name: String) = MethodNode( Opcodes.ACC_PRIVATE + Opcodes.ACC_STATIC + Opcodes.ACC_SYNTHETIC, - "\$\$assertD2UL", "(D)V", null, null + name, "(D)V", null, null ).doubleNanCheck().doubleUnsignedRangeCheck(18446744073709551616.0).addInsns(InsnNode(Opcodes.RETURN)) fun MethodNode.floatNanCheck() = LabelNode().let { okLabel -> diff --git a/src/main/kotlin/asmble/io/SExprToAst.kt b/src/main/kotlin/asmble/io/SExprToAst.kt index a99a786..96b127b 100644 --- a/src/main/kotlin/asmble/io/SExprToAst.kt +++ b/src/main/kotlin/asmble/io/SExprToAst.kt @@ -7,7 +7,6 @@ import asmble.ast.Script import asmble.util.* import java.io.ByteArrayInputStream import java.math.BigInteger -import java.nio.ByteBuffer typealias NameMap = Map @@ -101,8 +100,10 @@ open class SExprToAst { toInstrs(offsetMulti, 1, ExprContext(nameMap)).first } else toExprMaybe(offsetMulti, ExprContext(nameMap)) currIndex++ - val strs = exp.vals.drop(currIndex).fold("") { str, sym -> str + (sym as SExpr.Symbol).contents } - return Node.Data(index ?: 0, instrs, strs.toByteArray(Charsets.UTF_8)) + val bytes = exp.vals.drop(currIndex).fold(byteArrayOf()) { bytes, sym -> + bytes + (sym as SExpr.Symbol).rawContentCharsToBytes() + } + return Node.Data(index ?: 0, instrs, bytes) } fun toElem(exp: SExpr.Multi, nameMap: NameMap): Node.Elem { @@ -386,16 +387,28 @@ open class SExprToAst { return Pair(null, exp.vals.drop(1).map { toType(it.symbol()!!) }) } - fun toMemory(exp: SExpr.Multi): Triple { + fun toMemory(exp: SExpr.Multi): Triple, ImportOrExport?> { exp.requireFirstSymbol("memory") var currIndex = 1 val name = exp.maybeName(currIndex) if (name != null) currIndex++ val maybeImpExp = toImportOrExportMaybe(exp, currIndex) if (maybeImpExp != null) currIndex++ - // Try data approach - if (exp.vals[currIndex] is SExpr.Multi) throw Exception("Data string not yet supported for memory") - return Triple(name, toMemorySig(exp, currIndex), maybeImpExp) + // If it's a multi we assume "data", otherwise assume sig + val memOrData = exp.vals[currIndex].let { + when (it) { + is SExpr.Multi -> { + it.requireFirstSymbol("data") + Either.Right(Node.Data( + 0, + listOf(Node.Instr.I32Const(0)), + it.vals.drop(1).fold(byteArrayOf()) { b, exp -> b + exp.symbol()!!.rawContentCharsToBytes() } + )) + } + else -> Either.Left(toMemorySig(exp, currIndex)) + } + } + return Triple(name, memOrData, maybeImpExp) } fun toMemorySig(exp: SExpr.Multi, offset: Int): Node.Type.Memory { @@ -465,7 +478,8 @@ open class SExprToAst { Triple(impExp!!.importModule!!, impExp.field, tbl) } "memory" -> toMemory(it).let { (_, mem, impExp) -> - Triple(impExp!!.importModule!!, impExp.field, mem) + if (mem !is Either.Left) error("Data segment on import mem") + Triple(impExp!!.importModule!!, impExp.field, mem.v) } else -> throw Exception("Unknown import exp: $it") } @@ -503,7 +517,15 @@ open class SExprToAst { } "memory" -> toMemory(it).also { (_, mem, impExp) -> addMaybeExport(impExp, Node.ExternalKind.MEMORY, memoryCount++) - mod = mod.copy(memories = mod.memories + mem) + when (mem) { + is Either.Left -> mod = mod.copy(memories = mod.memories + mem.v) + is Either.Right -> mod = mod.copy( + memories = mod.memories + Node.Type.Memory( + Node.ResizableLimits(mem.v.data.size, mem.v.data.size) + ), + data = mod.data + mem.v + ) + } } "elem" -> mod = mod.copy(elems = mod.elems + toElem(it, nameMap)) "data" -> mod = mod.copy(data = mod.data + toData(it, nameMap)) @@ -511,6 +533,7 @@ open class SExprToAst { else -> throw Exception("Unknown non-import exp $exp") } } + require(mod.memories.size <= 1) { "Only single memory allowed" } return name to mod } diff --git a/src/test/kotlin/asmble/SpecTestUnit.kt b/src/test/kotlin/asmble/SpecTestUnit.kt index d2f503a..e063d89 100644 --- a/src/test/kotlin/asmble/SpecTestUnit.kt +++ b/src/test/kotlin/asmble/SpecTestUnit.kt @@ -45,8 +45,6 @@ class SpecTestUnit(val name: String, val wast: String, val expectedOutput: Strin - br_table.wast - Not handling tables yet - call_indirect.wast - Not handling tables yet - exports.wast - Not handling tables yet - - float_exprs.wast - Not handling mem data strings yet - - float_memory.wast - Not handling mem data strings yet - func.wast - Not handling tables yet - func_ptrs.wast - Not handling tables yet - imports.wast - No memory exports yet @@ -86,7 +84,9 @@ class SpecTestUnit(val name: String, val wast: String, val expectedOutput: Strin "f64.wast", "f64_cmp.wast", "fac.wast", + "float_exprs.wast", "float_literals.wast", + "float_memory.wast", "float_misc.wast", "forward.wast", "func-local-after-body.fail.wast",