Work on globals and binary oddities

This commit is contained in:
Chad Retz 2017-04-11 16:03:12 -05:00
parent 5a6a68aa61
commit e60c344a01
12 changed files with 181 additions and 49 deletions

View File

@ -8,5 +8,6 @@ sealed class SExpr {
}
data class Symbol(val contents: String = "", val quoted: Boolean = false) : SExpr() {
override fun toString() = SExprToStr.Compact.fromSExpr(this)
fun rawContentCharsToBytes() = contents.toCharArray().map(Char::toByte)
}
}

View File

@ -31,33 +31,16 @@ open class AstToAsm {
MethodHandle::class.ref.asmDesc, null, null)
})
// Now all import globals as getter (and maybe setter) method handles
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)
}
ctx.cls.fields.addAll(ctx.importGlobals.mapIndexed { index, import ->
if ((import.kind as Node.Import.Kind.Global).type.mutable) throw CompileErr.MutableGlobalImport(index)
FieldNode(Opcodes.ACC_PRIVATE + Opcodes.ACC_FINAL, ctx.importGlobalGetterFieldName(index),
MethodHandle::class.ref.asmDesc, null, null)
})
// Now all non-import globals
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 {
when (it) {
is Node.Instr.Args.Const<*> -> it.value
null -> when (global.type.contentType) {
is Node.Type.Value.I32 -> 0
is Node.Type.Value.I64 -> 0L
is Node.Type.Value.F32 -> 0F
is Node.Type.Value.F64 -> 0.0
}
else -> throw RuntimeException("Unsupported init insn: $it")
}
}
ctx.cls.fields.addAll(ctx.mod.globals.mapIndexed { index, global ->
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)
global.type.contentType.typeRef.asmDesc, null, null)
})
}
@ -69,14 +52,13 @@ open class AstToAsm {
// <init>(imports...)
// TODO: what happens w/ more than 254 imports?
val importTypes = ctx.mod.imports.map {
when (it.kind) {
is Node.Import.Kind.Func -> MethodHandle::class.ref
else -> TODO()
}
}
val importTypes =
ctx.importFuncs.map { MethodHandle::class.ref } +
// We know it's only getters
ctx.importGlobals.map { MethodHandle::class.ref }
// <init>(MemClass maxMemory, imports...)
// This is the one that actually does everything and is always deferred to
// Set the mem field then call init then put it back on the stack
var memCon = Func("<init>", listOf(ctx.mem.memType) + importTypes).addInsns(
VarInsnNode(Opcodes.ALOAD, 0),
@ -116,7 +98,78 @@ open class AstToAsm {
FieldInsnNode(Opcodes.PUTFIELD, ctx.thisRef.asmName,
ctx.funcName(importIndex), MethodHandle::class.ref.asmDesc)
)
}.addInsns(
}
// Set all import globals
memCon = ctx.importGlobals.indices.fold(memCon) { memCon, importIndex ->
memCon.addInsns(
VarInsnNode(Opcodes.ALOAD, 0),
VarInsnNode(Opcodes.ALOAD, ctx.importFuncs.size + importIndex + 2),
FieldInsnNode(Opcodes.PUTFIELD, ctx.thisRef.asmName,
ctx.globalName(importIndex), MethodHandle::class.ref.asmDesc)
)
}
// Initialize all globals
memCon = ctx.mod.globals.foldIndexed(memCon) { index, memCon, global ->
// In the MVP, we can trust the init is constant stuff and a single instr
if (global.init.size > 1) throw CompileErr.GlobalInitNonConstant(index)
memCon.addInsns(VarInsnNode(Opcodes.ALOAD, 0)).
addInsns(
global.init.firstOrNull().let {
when (it) {
is Node.Instr.Args.Const<*> -> {
if (it.value::class.javaPrimitiveType?.ref != global.type.contentType.typeRef)
throw CompileErr.GlobalConstantMismatch(
index,
global.type.contentType.typeRef,
it.value::class.ref
)
listOf(LdcInsnNode(it.value))
}
// Initialize from an import global means we'll just grab the MH as from the param and call
is Node.Instr.GetGlobal -> {
val refGlobal = ctx.globalAtIndex(it.index)
when (refGlobal) {
is Either.Right -> throw CompileErr.UnknownGlobal(it.index)
is Either.Left -> (refGlobal.v.kind as Node.Import.Kind.Global).let { refGlobalKind ->
if (refGlobalKind.type.contentType != global.type.contentType)
throw CompileErr.GlobalConstantMismatch(
index,
global.type.contentType.typeRef,
refGlobalKind.type.contentType.typeRef
)
listOf(
VarInsnNode(Opcodes.ALOAD, ctx.importFuncs.size + it.index + 2),
MethodInsnNode(
Opcodes.INVOKEVIRTUAL,
MethodHandle::class.ref.asmName,
"invokeExact",
"()" + refGlobalKind.type.contentType.typeRef.asmDesc,
false
)
)
}
}
}
// Otherwise, global constant
null -> listOf(when (global.type.contentType) {
is Node.Type.Value.I32 -> 0.const
is Node.Type.Value.I64 -> 0L.const
is Node.Type.Value.F32 -> 0F.const
is Node.Type.Value.F64 -> 0.0.const
})
else -> throw CompileErr.GlobalInitNonConstant(index)
}
}
).addInsns(
FieldInsnNode(Opcodes.PUTFIELD, ctx.thisRef.asmName,
ctx.globalName(ctx.importGlobals.size + index), global.type.contentType.typeRef.asmDesc)
)
}
// Call object init
memCon = memCon.addInsns(
// Gotta call super()
VarInsnNode(Opcodes.ALOAD, 0),
MethodInsnNode(Opcodes.INVOKESPECIAL, Object::class.ref.asmName, "<init>", "()V", false),
@ -165,6 +218,7 @@ open class AstToAsm {
ctx.mod.exports.forEach {
when (it.kind) {
Node.ExternalKind.FUNCTION -> addExportFunc(ctx, it)
Node.ExternalKind.GLOBAL -> addExportGlobal(ctx, it)
else -> TODO()
}
}
@ -215,6 +269,21 @@ open class AstToAsm {
ctx.cls.methods.plusAssign(method)
}
fun addExportGlobal(ctx: ClsContext, export: Node.Export) {
val global = ctx.globalAtIndex(export.index)
when (global) {
is Either.Left -> {
if ((global.v.kind as Node.Import.Kind.Global).type.mutable)
throw CompileErr.MutableGlobalExport(export.index)
}
is Either.Right -> {
if (global.v.type.mutable)
throw CompileErr.MutableGlobalExport(export.index)
}
}
TODO()
}
fun addFuncs(ctx: ClsContext) {
ctx.cls.methods.addAll(ctx.mod.funcs.mapIndexed { index, func ->
ctx.funcBuilder.fromFunc(ctx, func, ctx.importFuncs.size + index).toMethodNode()

View File

@ -47,7 +47,7 @@ data class ClsContext(
fun globalAtIndex(index: Int) = importGlobals.getOrNull(index).let {
when (it) {
null ->
Either.Right(mod.globals.getOrNull(importGlobals.size - index) ?:
Either.Right(mod.globals.getOrNull(index - importGlobals.size) ?:
throw CompileErr.UnknownGlobal(index))
else ->
Either.Left(it)

View File

@ -40,6 +40,14 @@ sealed class CompileErr(message: String, cause: Throwable? = null) : RuntimeExce
override val asmErrString get() = "type mismatch"
}
class GlobalConstantMismatch(
val index: Int,
val expected: TypeRef,
val actual: TypeRef
) : CompileErr("Global $index expected const of type $expected, got $actual") {
override val asmErrString get() = "type mismatch"
}
class IfThenValueWithoutElse() : CompileErr("If has value but no else clause") {
override val asmErrString get() = "type mismatch"
}
@ -73,4 +81,28 @@ sealed class CompileErr(message: String, cause: Throwable? = null) : RuntimeExce
) : CompileErr("Unknown global at index $index") {
override val asmErrString get() = "unknown global"
}
class SetImmutableGlobal(
val index: Int
) : CompileErr("Attempting to set global $index which is immutable") {
override val asmErrString get() = "global is immutable"
}
class MutableGlobalImport(
val index: Int
) : CompileErr("Attempted to import mutable global at index $index") {
override val asmErrString get() = "mutable globals cannot be imported"
}
class MutableGlobalExport(
val index: Int
) : CompileErr("Attempted to export global $index which is mutable") {
override val asmErrString get() = "mutable globals cannot be exported"
}
class GlobalInitNonConstant(
val index: Int
) : CompileErr("Expected init for global $index to be constant") {
override val asmErrString get() = "constant expression required"
}
}

View File

@ -1054,25 +1054,29 @@ open class FuncBuilder {
}
}
fun applySelfSetGlobal(ctx: FuncContext, fn: Func, index: Int, global: Node.Global) =
fun applySelfSetGlobal(ctx: FuncContext, fn: Func, index: Int, global: Node.Global): Func {
if (!global.type.mutable) throw CompileErr.SetImmutableGlobal(index)
// Just call putfield
// Note, this is special and "this" has already been injected on the stack for us
fn.popExpecting(global.type.contentType.typeRef).
popExpecting(MethodHandle::class.ref).
return fn.popExpecting(global.type.contentType.typeRef).
popExpecting(ctx.cls.thisRef).
addInsns(
FieldInsnNode(Opcodes.PUTFIELD, ctx.cls.thisRef.asmName, ctx.cls.globalName(index),
global.type.contentType.typeRef.asmDesc)
)
}
fun applyImportSetGlobal(ctx: FuncContext, fn: Func, index: Int, import: Node.Import.Kind.Global) =
fun applyImportSetGlobal(ctx: FuncContext, fn: Func, index: Int, import: Node.Import.Kind.Global): Func {
if (!import.type.mutable) throw CompileErr.SetImmutableGlobal(index)
// Load the setter method handle field, then invoke it with stack val
// Note, this is special and the method handle has already been injected on the stack for us
fn.popExpecting(import.type.contentType.typeRef).
return fn.popExpecting(import.type.contentType.typeRef).
popExpecting(MethodHandle::class.ref).
addInsns(
MethodInsnNode(Opcodes.INVOKEVIRTUAL, MethodHandle::class.ref.asmName, "invokeExact",
"(${import.type.contentType.typeRef.asmDesc})V", false)
)
}
fun applyGetGlobal(ctx: FuncContext, fn: Func, index: Int) = ctx.cls.globalAtIndex(index).let {
when (it) {

View File

@ -75,7 +75,7 @@ open class BinaryToAst(
fun toGlobalType(b: ByteReader) = Node.Type.Global(
contentType = toValueType(b),
mutable = b.readVarUInt1()
mutable = try { b.readVarUInt1() } catch (_: Exception) { throw IoErr.InvalidMutability() }
)
fun toImport(b: ByteReader) = Node.Import(

View File

@ -5,7 +5,6 @@ import asmble.util.toUnsignedBigInt
import asmble.util.toUnsignedLong
import java.io.ByteArrayInputStream
import java.io.ByteArrayOutputStream
import java.io.EOFException
import java.math.BigInteger
abstract class ByteReader {
@ -58,7 +57,7 @@ abstract class ByteReader {
protected fun readUnsignedLeb128(): Int {
// Taken from Android source, Apache licensed
var result = 0
var cur = 0
var cur: Int
var count = 0
do {
cur = readByte().toInt() and 0xff
@ -72,7 +71,7 @@ abstract class ByteReader {
private fun readSignedLeb128(): Long {
// Taken from Android source, Apache licensed
var result = 0L
var cur = 0
var cur: Int
var count = 0
var signBits = -1L
do {

View File

@ -22,4 +22,8 @@ sealed class IoErr(message: String, cause: Throwable? = null) : RuntimeException
class InvalidCodeLength(funcLen: Int, codeLen: Int) : IoErr("Got $funcLen funcs but only $codeLen bodies") {
override val asmErrString get() = "function and code section have inconsistent lengths"
}
class InvalidMutability : IoErr("Invalid mutability boolean") {
override val asmErrString = "invalid mutability"
}
}

View File

@ -422,15 +422,16 @@ open class SExprToAst {
exp.vals.drop(if (name == null) 1 else 2).also { otherVals ->
if (otherVals.isNotEmpty() && otherVals.find { it !is SExpr.Symbol || !it.quoted } == null)
return name to toModuleFromBytes(otherVals.fold(byteArrayOf()) { bytes, strVal ->
bytes + (strVal as SExpr.Symbol).contents.toByteArray()
bytes + (strVal as SExpr.Symbol).rawContentCharsToBytes()
})
}
var mod = Node.Module()
// Go over each module element and apply to the module
// But we have to do all imports first before anything else, so we separate them
var foundNonImport = false
val (importExps, nonImportExps) = exp.vals.mapNotNull { it as? SExpr.Multi }.partition {
when(it.vals.firstOrNull()?.symbolStr()) {
val isImport = when(it.vals.firstOrNull()?.symbolStr()) {
"import" -> true
"func", "global", "table", "memory" -> {
val possibleImportIndex = if (it.maybeName(1) == null) 1 else 2
@ -438,6 +439,9 @@ open class SExprToAst {
}
else -> false
}
if (!isImport && !foundNonImport) foundNonImport = true
if (isImport && foundNonImport) error("Import after non-import")
isImport
}
// Eagerly build the names (for forward decls)

View File

@ -4,6 +4,10 @@ import java.io.PrintWriter
open class Harness(val out: PrintWriter) {
// We're not as evil as WASM:
// https://github.com/WebAssembly/spec/blob/7c62b17f547b80c9c717cc6ef3a8aba1e04e4bcb/test/harness/index.js#L84
val global get() = 555
fun print(arg0: Int) { out.println("$arg0 : i32") }
companion object : Harness(PrintWriter(System.out, true))

View File

@ -225,6 +225,16 @@ data class ScriptContext(
)
}
fun resolveImportGlobal(import: Node.Import, kind: Node.Type.Global): MethodHandle {
// Find a getter that matches the name
val module = registrations[import.module] ?: error("Unable to find module ${import.module}")
return MethodHandles.lookup().bind(
module.instance,
"get" + import.field.javaIdent.capitalize(),
MethodType.methodType(kind.contentType.jclass)
)
}
interface Module {
val cls: Class<*>
val instance: Any
@ -251,14 +261,19 @@ data class ScriptContext(
it.parameterTypes.firstOrNull() == Int::class.java
} ?: error("Unable to find no-arg or mem-accepting construtor")
}
// Now resolve the imports
constructorParams += mod.imports.map {
val kind = it.kind
when (kind) {
is Node.Import.Kind.Func -> resolveImportFunc(it, mod.types[kind.typeIndex])
else -> TODO()
}
// First, the function imports
constructorParams += mod.imports.mapNotNull {
if (it.kind is Node.Import.Kind.Func) resolveImportFunc(it, mod.types[it.kind.typeIndex])
else null
}
// Then the global imports
constructorParams += mod.imports.mapNotNull {
if (it.kind is Node.Import.Kind.Global) resolveImportGlobal(it, it.kind.type)
else null
}
// Construct
constructor!!.newInstance(*constructorParams.toTypedArray())
}

View File

@ -49,7 +49,6 @@ class SpecTestUnit(val name: String, val wast: String, val expectedOutput: Strin
- float_memory.wast - Not handling mem data strings yet
- func.wast - Not handling tables yet
- func_ptrs.wast - Not handling tables yet
- globals.wast - No binary yet
- imports.wast - No memory exports yet
- left-to-right.wast - Not handling tables yet
- linking.wast - Not handling tables yet
@ -97,6 +96,7 @@ class SpecTestUnit(val name: String, val wast: String, val expectedOutput: Strin
"func-result-after-body.fail.wast",
"func-result-before-param.fail.wast",
"get_local.wast",
"globals.wast",
"i32.load32_s.fail.wast",
"i32.load32_u.fail.wast",
"i32.load64_s.fail.wast",