mirror of
https://github.com/fluencelabs/asmble
synced 2025-04-24 14:22:20 +00:00
Support mutable globals. Fixes #16
This commit is contained in:
parent
9d87ce440f
commit
73e6b5769a
@ -13,4 +13,5 @@ public @interface WasmImport {
|
||||
WasmExternalKind kind();
|
||||
int resizableLimitInitial() default -1;
|
||||
int resizableLimitMaximum() default -1;
|
||||
boolean globalSetter() default false;
|
||||
}
|
||||
|
@ -47,10 +47,13 @@ open class AstToAsm {
|
||||
})
|
||||
// Now all import globals as getter (and maybe setter) method handles
|
||||
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),
|
||||
val getter = 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) listOf(getter)
|
||||
else listOf(getter, FieldNode(
|
||||
Opcodes.ACC_PRIVATE + Opcodes.ACC_FINAL, ctx.importGlobalSetterFieldName(index),
|
||||
MethodHandle::class.ref.asmDesc, null, null))
|
||||
}.flatten())
|
||||
// Now all non-import globals
|
||||
ctx.cls.fields.addAll(ctx.mod.globals.mapIndexed { index, global ->
|
||||
val access = Opcodes.ACC_PRIVATE + if (!global.type.mutable) Opcodes.ACC_FINAL else 0
|
||||
@ -180,9 +183,11 @@ open class AstToAsm {
|
||||
|
||||
fun constructorImportTypes(ctx: ClsContext) =
|
||||
ctx.importFuncs.map { MethodHandle::class.ref } +
|
||||
// We know it's only getters
|
||||
ctx.importGlobals.map { MethodHandle::class.ref } +
|
||||
ctx.mod.imports.filter { it.kind is Node.Import.Kind.Table }.map { Array<MethodHandle>::class.ref }
|
||||
ctx.importGlobals.flatMap {
|
||||
// If it's mutable, it also comes with a setter
|
||||
if ((it.kind as? Node.Import.Kind.Global)?.type?.mutable == false) listOf(MethodHandle::class.ref)
|
||||
else listOf(MethodHandle::class.ref, MethodHandle::class.ref)
|
||||
} + ctx.mod.imports.filter { it.kind is Node.Import.Kind.Table }.map { Array<MethodHandle>::class.ref }
|
||||
|
||||
fun toConstructorNode(ctx: ClsContext, func: Func) = mutableListOf<List<AnnotationNode>>().let { paramAnns ->
|
||||
// If the first param is a mem class and imported, add annotation
|
||||
@ -199,7 +204,15 @@ open class AstToAsm {
|
||||
}
|
||||
// All non-mem imports one after another
|
||||
ctx.importFuncs.forEach { paramAnns.add(listOf(importAnnotation(ctx, it))) }
|
||||
ctx.importGlobals.forEach { paramAnns.add(listOf(importAnnotation(ctx, it))) }
|
||||
ctx.importGlobals.forEach {
|
||||
paramAnns.add(listOf(importAnnotation(ctx, it)))
|
||||
// There are two annotations here if it's mutable
|
||||
if ((it.kind as? Node.Import.Kind.Global)?.type?.mutable == true)
|
||||
paramAnns.add(listOf(importAnnotation(ctx, it).also {
|
||||
it.values.add("globalSetter")
|
||||
it.values.add(true)
|
||||
}))
|
||||
}
|
||||
ctx.mod.imports.forEach {
|
||||
if (it.kind is Node.Import.Kind.Table) paramAnns.add(listOf(importAnnotation(ctx, it)))
|
||||
}
|
||||
@ -240,14 +253,25 @@ open class AstToAsm {
|
||||
}
|
||||
|
||||
fun setConstructorGlobalImports(ctx: ClsContext, func: Func, paramsBeforeImports: Int) =
|
||||
ctx.importGlobals.indices.fold(func) { func, importIndex ->
|
||||
ctx.importGlobals.foldIndexed(func to ctx.importFuncs.size + paramsBeforeImports) {
|
||||
importIndex, (func, importParamOffset), import ->
|
||||
// Always a getter handle
|
||||
func.addInsns(
|
||||
VarInsnNode(Opcodes.ALOAD, 0),
|
||||
VarInsnNode(Opcodes.ALOAD, ctx.importFuncs.size + importIndex + paramsBeforeImports + 1),
|
||||
VarInsnNode(Opcodes.ALOAD, importParamOffset + 1),
|
||||
FieldInsnNode(Opcodes.PUTFIELD, ctx.thisRef.asmName,
|
||||
ctx.importGlobalGetterFieldName(importIndex), MethodHandle::class.ref.asmDesc)
|
||||
)
|
||||
}
|
||||
).let { func ->
|
||||
// If it's mutable, it has a second setter handle
|
||||
if ((import.kind as? Node.Import.Kind.Global)?.type?.mutable == false) func to importParamOffset + 1
|
||||
else func.addInsns(
|
||||
VarInsnNode(Opcodes.ALOAD, 0),
|
||||
VarInsnNode(Opcodes.ALOAD, importParamOffset + 2),
|
||||
FieldInsnNode(Opcodes.PUTFIELD, ctx.thisRef.asmName,
|
||||
ctx.importGlobalSetterFieldName(importIndex), MethodHandle::class.ref.asmDesc)
|
||||
) to importParamOffset + 2
|
||||
}
|
||||
}.first
|
||||
|
||||
fun setConstructorFunctionImports(ctx: ClsContext, func: Func, paramsBeforeImports: Int) =
|
||||
ctx.importFuncs.indices.fold(func) { func, importIndex ->
|
||||
@ -261,7 +285,10 @@ open class AstToAsm {
|
||||
|
||||
fun setConstructorTableImports(ctx: ClsContext, func: Func, paramsBeforeImports: Int) =
|
||||
if (ctx.mod.imports.none { it.kind is Node.Import.Kind.Table }) func else {
|
||||
val importIndex = ctx.importFuncs.size + ctx.importGlobals.size + paramsBeforeImports + 1
|
||||
val importIndex = ctx.importFuncs.size +
|
||||
// Mutable global imports have setters and take up two spots
|
||||
ctx.importGlobals.sumBy { if ((it.kind as? Node.Import.Kind.Global)?.type?.mutable == true) 2 else 1 } +
|
||||
paramsBeforeImports + 1
|
||||
func.addInsns(
|
||||
VarInsnNode(Opcodes.ALOAD, 0),
|
||||
VarInsnNode(Opcodes.ALOAD, importIndex),
|
||||
@ -299,11 +326,14 @@ open class AstToAsm {
|
||||
global.type.contentType.typeRef,
|
||||
refGlobalKind.type.contentType.typeRef
|
||||
)
|
||||
val paramOffset = ctx.importFuncs.size + paramsBeforeImports + 1 +
|
||||
ctx.importGlobals.take(it.index).sumBy {
|
||||
// Immutable jumps 1, mutable jumps 2
|
||||
if ((it.kind as? Node.Import.Kind.Global)?.type?.mutable == false) 1
|
||||
else 2
|
||||
}
|
||||
listOf(
|
||||
VarInsnNode(
|
||||
Opcodes.ALOAD,
|
||||
ctx.importFuncs.size + it.index + paramsBeforeImports + 1
|
||||
),
|
||||
VarInsnNode(Opcodes.ALOAD, paramOffset),
|
||||
MethodInsnNode(
|
||||
Opcodes.INVOKEVIRTUAL,
|
||||
MethodHandle::class.ref.asmName,
|
||||
@ -356,7 +386,10 @@ open class AstToAsm {
|
||||
// Otherwise, it was imported and we can set the elems on the imported one
|
||||
// from the parameter
|
||||
// TODO: I think this is a security concern and bad practice, may revisit
|
||||
val importIndex = ctx.importFuncs.size + ctx.importGlobals.size + paramsBeforeImports + 1
|
||||
val importIndex = ctx.importFuncs.size + ctx.importGlobals.sumBy {
|
||||
// Immutable is 1, mutable is 2
|
||||
if ((it.kind as? Node.Import.Kind.Global)?.type?.mutable == false) 1 else 2
|
||||
} + paramsBeforeImports + 1
|
||||
return func.addInsns(VarInsnNode(Opcodes.ALOAD, importIndex)).
|
||||
let { func -> addElemsToTable(ctx, func, paramsBeforeImports) }.
|
||||
// Remove the array that's still there
|
||||
@ -532,28 +565,58 @@ open class AstToAsm {
|
||||
is Either.Left -> (global.v.kind as Node.Import.Kind.Global).type
|
||||
is Either.Right -> global.v.type
|
||||
}
|
||||
if (type.mutable) throw CompileErr.MutableGlobalExport(export.index)
|
||||
// Create a simple getter
|
||||
val method = MethodNode(Opcodes.ACC_PUBLIC, "get" + export.field.javaIdent.capitalize(),
|
||||
val getter = MethodNode(Opcodes.ACC_PUBLIC, "get" + export.field.javaIdent.capitalize(),
|
||||
"()" + type.contentType.typeRef.asmDesc, null, null)
|
||||
method.addInsns(VarInsnNode(Opcodes.ALOAD, 0))
|
||||
if (global is Either.Left) method.addInsns(
|
||||
getter.addInsns(VarInsnNode(Opcodes.ALOAD, 0))
|
||||
if (global is Either.Left) getter.addInsns(
|
||||
FieldInsnNode(Opcodes.GETFIELD, ctx.thisRef.asmName,
|
||||
ctx.importGlobalGetterFieldName(export.index), MethodHandle::class.ref.asmDesc),
|
||||
MethodInsnNode(Opcodes.INVOKEVIRTUAL, MethodHandle::class.ref.asmName, "invokeExact",
|
||||
"()" + type.contentType.typeRef.asmDesc, false)
|
||||
) else method.addInsns(
|
||||
) else getter.addInsns(
|
||||
FieldInsnNode(Opcodes.GETFIELD, ctx.thisRef.asmName, ctx.globalName(export.index),
|
||||
type.contentType.typeRef.asmDesc)
|
||||
)
|
||||
method.addInsns(InsnNode(when (type.contentType) {
|
||||
getter.addInsns(InsnNode(when (type.contentType) {
|
||||
Node.Type.Value.I32 -> Opcodes.IRETURN
|
||||
Node.Type.Value.I64 -> Opcodes.LRETURN
|
||||
Node.Type.Value.F32 -> Opcodes.FRETURN
|
||||
Node.Type.Value.F64 -> Opcodes.DRETURN
|
||||
}))
|
||||
method.visibleAnnotations = listOf(exportAnnotation(export))
|
||||
ctx.cls.methods.plusAssign(method)
|
||||
getter.visibleAnnotations = listOf(exportAnnotation(export))
|
||||
ctx.cls.methods.plusAssign(getter)
|
||||
// If mutable, create simple setter
|
||||
if (type.mutable) {
|
||||
val setter = MethodNode(Opcodes.ACC_PUBLIC, "set" + export.field.javaIdent.capitalize(),
|
||||
"(${type.contentType.typeRef.asmDesc})V", null, null)
|
||||
setter.addInsns(VarInsnNode(Opcodes.ALOAD, 0))
|
||||
if (global is Either.Left) setter.addInsns(
|
||||
FieldInsnNode(Opcodes.GETFIELD, ctx.thisRef.asmName,
|
||||
ctx.importGlobalSetterFieldName(export.index), MethodHandle::class.ref.asmDesc),
|
||||
VarInsnNode(when (type.contentType) {
|
||||
Node.Type.Value.I32 -> Opcodes.ILOAD
|
||||
Node.Type.Value.I64 -> Opcodes.LLOAD
|
||||
Node.Type.Value.F32 -> Opcodes.FLOAD
|
||||
Node.Type.Value.F64 -> Opcodes.DLOAD
|
||||
}, 1),
|
||||
MethodInsnNode(Opcodes.INVOKEVIRTUAL, MethodHandle::class.ref.asmName, "invokeExact",
|
||||
"(${type.contentType.typeRef.asmDesc})V", false),
|
||||
InsnNode(Opcodes.RETURN)
|
||||
) else setter.addInsns(
|
||||
VarInsnNode(when (type.contentType) {
|
||||
Node.Type.Value.I32 -> Opcodes.ILOAD
|
||||
Node.Type.Value.I64 -> Opcodes.LLOAD
|
||||
Node.Type.Value.F32 -> Opcodes.FLOAD
|
||||
Node.Type.Value.F64 -> Opcodes.DLOAD
|
||||
}, 1),
|
||||
FieldInsnNode(Opcodes.PUTFIELD, ctx.thisRef.asmName, ctx.globalName(export.index),
|
||||
type.contentType.typeRef.asmDesc),
|
||||
InsnNode(Opcodes.RETURN)
|
||||
)
|
||||
setter.visibleAnnotations = listOf(exportAnnotation(export))
|
||||
ctx.cls.methods.plusAssign(setter)
|
||||
}
|
||||
}
|
||||
|
||||
fun addExportMemory(ctx: ClsContext, export: Node.Export) {
|
||||
|
@ -102,18 +102,6 @@ sealed class CompileErr(message: String, cause: Throwable? = null) : RuntimeExce
|
||||
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 GlobalInitNotConstant(
|
||||
val index: Int
|
||||
) : CompileErr("Expected init for global $index to be single constant value") {
|
||||
|
@ -116,9 +116,9 @@ interface Module {
|
||||
}
|
||||
|
||||
// Global imports
|
||||
val globalImports = mod.imports.mapNotNull {
|
||||
if (it.kind is Node.Import.Kind.Global) ctx.resolveImportGlobal(it, it.kind.type)
|
||||
else null
|
||||
val globalImports = mod.imports.flatMap {
|
||||
if (it.kind is Node.Import.Kind.Global) ctx.resolveImportGlobals(it, it.kind.type)
|
||||
else emptyList()
|
||||
}
|
||||
constructorParams += globalImports
|
||||
|
||||
|
@ -55,4 +55,12 @@ sealed class RunErr(message: String, cause: Throwable? = null) : RuntimeExceptio
|
||||
override val asmErrString get() = "unknown import"
|
||||
override val asmErrStrings get() = listOf(asmErrString, "incompatible import type")
|
||||
}
|
||||
|
||||
class ImportGlobalInvalidMutability(
|
||||
val module: String,
|
||||
val field: String,
|
||||
val expected: Boolean
|
||||
) : RunErr("Expected imported global $module::$field to have mutability as ${!expected}") {
|
||||
override val asmErrString get() = "incompatible import type"
|
||||
}
|
||||
}
|
@ -263,10 +263,12 @@ data class ScriptContext(
|
||||
return Module.Compiled(mod, classLoader.fromBuiltContext(ctx), name, ctx.mem)
|
||||
}
|
||||
|
||||
fun bindImport(import: Node.Import, getter: Boolean, methodType: MethodType): MethodHandle {
|
||||
fun bindImport(import: Node.Import, getter: Boolean, methodType: MethodType) = bindImport(
|
||||
import, if (getter) "get" + import.field.javaIdent.capitalize() else import.field.javaIdent, methodType)
|
||||
|
||||
fun bindImport(import: Node.Import, javaName: String, methodType: MethodType): MethodHandle {
|
||||
// Find a method that matches our expectations
|
||||
val module = registrations[import.module] ?: throw RunErr.ImportNotFound(import.module, import.field)
|
||||
val javaName = if (getter) "get" + import.field.javaIdent.capitalize() else import.field.javaIdent
|
||||
val kind = when (import.kind) {
|
||||
is Node.Import.Kind.Func -> WasmExternalKind.FUNCTION
|
||||
is Node.Import.Kind.Table -> WasmExternalKind.TABLE
|
||||
@ -281,8 +283,18 @@ data class ScriptContext(
|
||||
bindImport(import, false,
|
||||
MethodType.methodType(funcType.ret?.jclass ?: Void.TYPE, funcType.params.map { it.jclass }))
|
||||
|
||||
fun resolveImportGlobal(import: Node.Import, globalType: Node.Type.Global) =
|
||||
bindImport(import, true, MethodType.methodType(globalType.contentType.jclass))
|
||||
fun resolveImportGlobals(import: Node.Import, globalType: Node.Type.Global): List<MethodHandle> {
|
||||
val getter = bindImport(import, true, MethodType.methodType(globalType.contentType.jclass))
|
||||
// Whether the setter is present or not defines whether it is mutable
|
||||
val setter = try {
|
||||
bindImport(import, "set" + import.field.javaIdent.capitalize(),
|
||||
MethodType.methodType(Void.TYPE, globalType.contentType.jclass))
|
||||
} catch (e: RunErr.ImportNotFound) { null }
|
||||
// Mutability must match
|
||||
if (globalType.mutable == (setter == null))
|
||||
throw RunErr.ImportGlobalInvalidMutability(import.module, import.field, globalType.mutable)
|
||||
return if (setter == null) listOf(getter) else listOf(getter, setter)
|
||||
}
|
||||
|
||||
fun resolveImportMemory(import: Node.Import, memoryType: Node.Type.Memory, mem: Mem) =
|
||||
bindImport(import, true, MethodType.methodType(Class.forName(mem.memType.asm.className))).
|
||||
|
@ -13,7 +13,7 @@ class SpecTestUnit(name: String, wast: String, expectedOutput: String?) : BaseTe
|
||||
override val shouldFail get() = name.endsWith(".fail")
|
||||
|
||||
override val defaultMaxMemPages get() = when (name) {
|
||||
"nop"-> 20
|
||||
"nop" -> 20
|
||||
"resizing" -> 830
|
||||
"imports" -> 5
|
||||
else -> 1
|
||||
|
Loading…
x
Reference in New Issue
Block a user