mirror of
https://github.com/fluencelabs/asmble
synced 2025-07-03 00:11:36 +00:00
Compare commits
42 Commits
0.2.0
...
lateinit_f
Author | SHA1 | Date | |
---|---|---|---|
58cf836b76 | |||
56c2c8d672 | |||
da70c9fca4 | |||
a1a5563367 | |||
e489e7c889 | |||
c1391b2701 | |||
6b28c5a93b | |||
9dc4112512 | |||
01d89947e5 | |||
4373676448 | |||
94953a4ada | |||
472579020a | |||
75de1d76e3 | |||
67e914d683 | |||
4bc12f4b94 | |||
1990f46743 | |||
559df45f09 | |||
73862e9bc9 | |||
1127b61eb5 | |||
a66c05ad4a | |||
6786350f53 | |||
706da0d486 | |||
1d5c1e527a | |||
1430bf48a6 | |||
96febbecd5 | |||
80a8a1fbb9 | |||
dd72c7124c | |||
c04a3c4a9b | |||
51520ac07d | |||
3c25b40c40 | |||
96458bdec7 | |||
dd33676e50 | |||
e9364574a3 | |||
73e6b5769a | |||
9d87ce440f | |||
51bc8008e1 | |||
198c521dd7 | |||
97660de6ba | |||
cee7a86773 | |||
cfa4a35af1 | |||
f24342959d | |||
368ab300fa |
2
.gitignore
vendored
2
.gitignore
vendored
@ -16,6 +16,8 @@
|
|||||||
/annotations/out
|
/annotations/out
|
||||||
/examples/c-simple/bin
|
/examples/c-simple/bin
|
||||||
/examples/c-simple/build
|
/examples/c-simple/build
|
||||||
|
/examples/go-simple/bin
|
||||||
|
/examples/go-simple/build
|
||||||
/examples/rust-simple/Cargo.lock
|
/examples/rust-simple/Cargo.lock
|
||||||
/examples/rust-simple/bin
|
/examples/rust-simple/bin
|
||||||
/examples/rust-simple/build
|
/examples/rust-simple/build
|
||||||
|
2
LICENSE
2
LICENSE
@ -1,6 +1,6 @@
|
|||||||
MIT License
|
MIT License
|
||||||
|
|
||||||
Copyright (c) 2017 Chad Retz
|
Copyright (c) 2018 Chad Retz
|
||||||
|
|
||||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
of this software and associated documentation files (the "Software"), to deal
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
35
README.md
35
README.md
@ -183,9 +183,16 @@ JVM languages.
|
|||||||
|
|
||||||
### Getting
|
### Getting
|
||||||
|
|
||||||
The latest tag can be added to your build script via [JitPack](https://jitpack.io). For example,
|
The compiler and annotations are deployed to Maven Central. The compiler is written in Kotlin and can be added as a
|
||||||
[here](https://jitpack.io/#cretz/asmble/0.1.0) are instructions for using the 0.1.0 release and
|
Gradle dependency with:
|
||||||
[here](https://jitpack.io/#cretz/asmble/master-SNAPSHOT) are instructions for the latest master.
|
|
||||||
|
compile 'com.github.cretz.asmble:asmble-compiler:0.3.0'
|
||||||
|
|
||||||
|
This is only needed to compile of course, the compiled code has no runtime requirement. The compiled code does include
|
||||||
|
some annotations (but in Java its ok to have annotations that are not found). If you do want to reflect the annotations,
|
||||||
|
the annotation library can be added as a Gradle dependency with:
|
||||||
|
|
||||||
|
compile 'com.github.cretz.asmble:asmble-annotations:0.3.0'
|
||||||
|
|
||||||
### Building and Testing
|
### Building and Testing
|
||||||
|
|
||||||
@ -256,15 +263,16 @@ In the WebAssembly MVP a table is just a set of function pointers. This is store
|
|||||||
|
|
||||||
#### Globals
|
#### Globals
|
||||||
|
|
||||||
Globals are stored as fields on the class. A non-import global is simply a field, but an import global is a
|
Globals are stored as fields on the class. A non-import global is simply a field that is final if not mutable. An import
|
||||||
`MethodHandle` to the getter (and would be a `MethodHandle` to the setter if mutable globals were supported). Any values
|
global is a `MethodHandle` to the getter and a `MethodHandle` to the setter if mutable. Any values for the globals are
|
||||||
for the globals are set in the constructor.
|
set in the constructor.
|
||||||
|
|
||||||
#### Imports
|
#### Imports
|
||||||
|
|
||||||
The constructor accepts all imports as params. Memory is imported via a `ByteBuffer` param, then function
|
The constructor accepts all imports as params. Memory is imported via a `ByteBuffer` param, then function
|
||||||
imports as `MethodHandle` params, then global imports as `MethodHandle` params, then a `MethodHandle` array param for an
|
imports as `MethodHandle` params, then global imports as `MethodHandle` params (one for getter and another for setter if
|
||||||
imported table. All of these values are set as fields in the constructor.
|
mutable), then a `MethodHandle` array param for an imported table. All of these values are set as fields in the
|
||||||
|
constructor.
|
||||||
|
|
||||||
#### Exports
|
#### Exports
|
||||||
|
|
||||||
@ -363,9 +371,12 @@ stack (e.g. some places where we do a swap).
|
|||||||
Below are some performance and implementation quirks where there is a bit of an impedance mismatch between WebAssembly
|
Below are some performance and implementation quirks where there is a bit of an impedance mismatch between WebAssembly
|
||||||
and the JVM:
|
and the JVM:
|
||||||
|
|
||||||
* WebAssembly has a nice data section for byte arrays whereas the JVM does not. Right now we build a byte array from
|
* WebAssembly has a nice data section for byte arrays whereas the JVM does not. Right now we use a single-byte-char
|
||||||
a bunch of consts at runtime which is multiple operations per byte. This can bloat the class file size, but is quite
|
string constant (i.e. ISO-8859 charset). This saves class file size, but this means we call `String::getBytes` on
|
||||||
fast compared to alternatives such as string constants.
|
init to load bytes from the string constant. Due to the JVM using an unsigned 16-bit int as the string constant
|
||||||
|
length, the maximum byte length is 65536. Since the string constants are stored as UTF-8 constants, they can be up to
|
||||||
|
four bytes a character. Therefore, we populate memory in data chunks no larger than 16300 (nice round number to make
|
||||||
|
sure that even in the worse case of 4 bytes per char in UTF-8 view, we're still under the max).
|
||||||
* The JVM makes no guarantees about trailing bits being preserved on NaN floating point representations like WebAssembly
|
* The JVM makes no guarantees about trailing bits being preserved on NaN floating point representations like WebAssembly
|
||||||
does. This causes some mismatch on WebAssembly tests depending on how the JVM "feels" (I haven't dug into why some
|
does. This causes some mismatch on WebAssembly tests depending on how the JVM "feels" (I haven't dug into why some
|
||||||
bit patterns stay and some don't when NaNs are passed through methods).
|
bit patterns stay and some don't when NaNs are passed through methods).
|
||||||
@ -417,6 +428,8 @@ WASM compiled from Rust, C, Java, etc if e.g. they all have their own way of han
|
|||||||
definition of an importable set of modules that does all of these things, even if it's in WebIDL. I dunno, maybe the
|
definition of an importable set of modules that does all of these things, even if it's in WebIDL. I dunno, maybe the
|
||||||
effort is already there, I haven't really looked.
|
effort is already there, I haven't really looked.
|
||||||
|
|
||||||
|
There is https://github.com/konsoletyper/teavm
|
||||||
|
|
||||||
**So I can compile something in C via Emscripten and have it run on the JVM with this?**
|
**So I can compile something in C via Emscripten and have it run on the JVM with this?**
|
||||||
|
|
||||||
Yes, but work is required. WebAssembly is lacking any kind of standard library. So Emscripten will either embed it or
|
Yes, but work is required. WebAssembly is lacking any kind of standard library. So Emscripten will either embed it or
|
||||||
|
@ -13,4 +13,5 @@ public @interface WasmImport {
|
|||||||
WasmExternalKind kind();
|
WasmExternalKind kind();
|
||||||
int resizableLimitInitial() default -1;
|
int resizableLimitInitial() default -1;
|
||||||
int resizableLimitMaximum() default -1;
|
int resizableLimitMaximum() default -1;
|
||||||
|
boolean globalSetter() default false;
|
||||||
}
|
}
|
||||||
|
167
build.gradle
167
build.gradle
@ -2,7 +2,7 @@ group 'asmble'
|
|||||||
version '0.2.0'
|
version '0.2.0'
|
||||||
|
|
||||||
buildscript {
|
buildscript {
|
||||||
ext.kotlin_version = '1.2.30'
|
ext.kotlin_version = '1.2.51'
|
||||||
ext.asm_version = '5.2'
|
ext.asm_version = '5.2'
|
||||||
|
|
||||||
repositories {
|
repositories {
|
||||||
@ -14,17 +14,30 @@ buildscript {
|
|||||||
dependencies {
|
dependencies {
|
||||||
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
|
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
|
||||||
classpath 'me.champeau.gradle:jmh-gradle-plugin:0.4.5'
|
classpath 'me.champeau.gradle:jmh-gradle-plugin:0.4.5'
|
||||||
|
classpath 'com.jfrog.bintray.gradle:gradle-bintray-plugin:1.8.4'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
allprojects {
|
allprojects {
|
||||||
apply plugin: 'java'
|
apply plugin: 'java'
|
||||||
|
group 'com.github.cretz.asmble'
|
||||||
|
version '0.4.0-fl-fix'
|
||||||
|
|
||||||
repositories {
|
repositories {
|
||||||
mavenCentral()
|
mavenCentral()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
project(':annotations') {
|
||||||
|
javadoc {
|
||||||
|
options.links 'https://docs.oracle.com/javase/8/docs/api/'
|
||||||
|
// TODO: change when https://github.com/gradle/gradle/issues/2354 is fixed
|
||||||
|
options.addStringOption 'Xdoclint:all', '-Xdoclint:-missing'
|
||||||
|
}
|
||||||
|
|
||||||
|
publishSettings(project, 'asmble-annotations', 'Asmble WASM Annotations')
|
||||||
|
}
|
||||||
|
|
||||||
project(':compiler') {
|
project(':compiler') {
|
||||||
apply plugin: 'kotlin'
|
apply plugin: 'kotlin'
|
||||||
apply plugin: 'application'
|
apply plugin: 'application'
|
||||||
@ -45,6 +58,8 @@ project(':compiler') {
|
|||||||
testCompile "org.jetbrains.kotlin:kotlin-test-junit:$kotlin_version"
|
testCompile "org.jetbrains.kotlin:kotlin-test-junit:$kotlin_version"
|
||||||
testCompile "org.ow2.asm:asm-debug-all:$asm_version"
|
testCompile "org.ow2.asm:asm-debug-all:$asm_version"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
publishSettings(project, 'asmble-compiler', 'Asmble WASM Compiler')
|
||||||
}
|
}
|
||||||
|
|
||||||
project(':examples') {
|
project(':examples') {
|
||||||
@ -53,35 +68,28 @@ project(':examples') {
|
|||||||
compileOnly project(':compiler')
|
compileOnly project(':compiler')
|
||||||
}
|
}
|
||||||
|
|
||||||
// C/C++ example helpers
|
// Go example helpers
|
||||||
|
|
||||||
task cToWasm {
|
task goToWasm {
|
||||||
doFirst {
|
doFirst {
|
||||||
mkdir 'build'
|
mkdir 'build'
|
||||||
exec {
|
exec {
|
||||||
def cFileName = fileTree(dir: 'src', includes: ['*.c']).files.iterator().next()
|
def goFileName = fileTree(dir: '.', includes: ['*.go']).files.iterator().next()
|
||||||
commandLine 'clang', '--target=wasm32-unknown-unknown-wasm', '-O3', cFileName, '-c', '-o', 'build/lib.wasm'
|
environment 'GOOS': 'js', 'GOARCH': 'wasm'
|
||||||
|
commandLine 'go', 'build', '-o', 'build/lib.wasm', goFileName
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
task showCWast(type: JavaExec) {
|
|
||||||
dependsOn cToWasm
|
|
||||||
classpath configurations.compileClasspath
|
|
||||||
main = 'asmble.cli.MainKt'
|
|
||||||
doFirst {
|
|
||||||
args 'translate', 'build/lib.wasm'
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
task compileCWasm(type: JavaExec) {
|
task compileGoWasm(type: JavaExec) {
|
||||||
dependsOn cToWasm
|
dependsOn goToWasm
|
||||||
classpath configurations.compileClasspath
|
classpath configurations.compileClasspath
|
||||||
main = 'asmble.cli.MainKt'
|
main = 'asmble.cli.MainKt'
|
||||||
doFirst {
|
doFirst {
|
||||||
|
// args 'help', 'compile'
|
||||||
def outFile = 'build/wasm-classes/' + wasmCompiledClassName.replace('.', '/') + '.class'
|
def outFile = 'build/wasm-classes/' + wasmCompiledClassName.replace('.', '/') + '.class'
|
||||||
file(outFile).parentFile.mkdirs()
|
file(outFile).parentFile.mkdirs()
|
||||||
args 'compile', 'build/lib.wasm', wasmCompiledClassName, '-out', outFile
|
args 'compile', 'build/lib.wasm', wasmCompiledClassName, '-out', outFile, '-log', 'debug'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -134,40 +142,43 @@ project(':examples') {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
project(':examples:c-simple') {
|
|
||||||
apply plugin: 'application'
|
|
||||||
ext.wasmCompiledClassName = 'asmble.generated.CSimple'
|
|
||||||
dependencies {
|
|
||||||
compile files('build/wasm-classes')
|
|
||||||
}
|
|
||||||
compileJava {
|
|
||||||
dependsOn compileCWasm
|
|
||||||
}
|
|
||||||
mainClassName = 'asmble.examples.csimple.Main'
|
|
||||||
}
|
|
||||||
|
|
||||||
project(':examples:rust-regex') {
|
//project(':examples:go-simple') {
|
||||||
apply plugin: 'application'
|
// apply plugin: 'application'
|
||||||
apply plugin: 'me.champeau.gradle.jmh'
|
// ext.wasmCompiledClassName = 'asmble.generated.GoSimple'
|
||||||
ext.wasmCompiledClassName = 'asmble.generated.RustRegex'
|
// dependencies {
|
||||||
dependencies {
|
// compile files('build/wasm-classes')
|
||||||
compile files('build/wasm-classes')
|
// }
|
||||||
testCompile 'junit:junit:4.12'
|
// compileJava {
|
||||||
}
|
// dependsOn compileGoWasm
|
||||||
compileJava {
|
// }
|
||||||
dependsOn compileRustWasm
|
// mainClassName = 'asmble.examples.gosimple.Main'
|
||||||
}
|
//}
|
||||||
mainClassName = 'asmble.examples.rustregex.Main'
|
|
||||||
test {
|
// todo temporary disable Rust regex, because some strings in wasm code exceed the size in 65353 bytes.
|
||||||
testLogging.showStandardStreams = true
|
|
||||||
testLogging.events 'PASSED', 'SKIPPED'
|
// project(':examples:rust-regex') {
|
||||||
}
|
// apply plugin: 'application'
|
||||||
jmh {
|
// apply plugin: 'me.champeau.gradle.jmh'
|
||||||
iterations = 5
|
// ext.wasmCompiledClassName = 'asmble.generated.RustRegex'
|
||||||
warmupIterations = 5
|
// dependencies {
|
||||||
fork = 3
|
// compile files('build/wasm-classes')
|
||||||
}
|
// testCompile 'junit:junit:4.12'
|
||||||
}
|
// }
|
||||||
|
// compileJava {
|
||||||
|
// dependsOn compileRustWasm
|
||||||
|
// }
|
||||||
|
// mainClassName = 'asmble.examples.rustregex.Main'
|
||||||
|
// test {
|
||||||
|
// testLogging.showStandardStreams = true
|
||||||
|
// testLogging.events 'PASSED', 'SKIPPED'
|
||||||
|
// }
|
||||||
|
// jmh {
|
||||||
|
// iterations = 5
|
||||||
|
// warmupIterations = 5
|
||||||
|
// fork = 3
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
|
||||||
project(':examples:rust-simple') {
|
project(':examples:rust-simple') {
|
||||||
apply plugin: 'application'
|
apply plugin: 'application'
|
||||||
@ -191,4 +202,58 @@ project(':examples:rust-string') {
|
|||||||
dependsOn compileRustWasm
|
dependsOn compileRustWasm
|
||||||
}
|
}
|
||||||
mainClassName = 'asmble.examples.ruststring.Main'
|
mainClassName = 'asmble.examples.ruststring.Main'
|
||||||
}
|
}
|
||||||
|
|
||||||
|
def publishSettings(project, projectName, projectDescription) {
|
||||||
|
|
||||||
|
project.with {
|
||||||
|
apply plugin: 'com.jfrog.bintray'
|
||||||
|
apply plugin: 'maven-publish'
|
||||||
|
apply plugin: 'maven'
|
||||||
|
|
||||||
|
|
||||||
|
task sourcesJar(type: Jar) {
|
||||||
|
from sourceSets.main.allJava
|
||||||
|
classifier = 'sources'
|
||||||
|
}
|
||||||
|
|
||||||
|
publishing {
|
||||||
|
publications {
|
||||||
|
MyPublication(MavenPublication) {
|
||||||
|
from components.java
|
||||||
|
groupId group
|
||||||
|
artifactId projectName
|
||||||
|
artifact sourcesJar
|
||||||
|
version version
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
bintray {
|
||||||
|
|
||||||
|
user = bintrayUser
|
||||||
|
key = bintrayKey
|
||||||
|
publications = ['MyPublication']
|
||||||
|
//[Default: false] Whether to override version artifacts already published
|
||||||
|
override = false
|
||||||
|
//[Default: false] Whether version should be auto published after an upload
|
||||||
|
publish = true
|
||||||
|
|
||||||
|
pkg {
|
||||||
|
repo = 'releases'
|
||||||
|
name = projectName
|
||||||
|
userOrg = 'fluencelabs'
|
||||||
|
licenses = ['MIT']
|
||||||
|
vcsUrl = 'https://github.com/fluencelabs/asmble'
|
||||||
|
version {
|
||||||
|
name = project.version
|
||||||
|
desc = projectDescription
|
||||||
|
released = new Date()
|
||||||
|
vcsTag = project.version
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
@ -3,7 +3,19 @@ package asmble.ast
|
|||||||
import java.util.*
|
import java.util.*
|
||||||
import kotlin.reflect.KClass
|
import kotlin.reflect.KClass
|
||||||
|
|
||||||
|
/**
|
||||||
|
* All WebAssembly AST nodes as static inner classes.
|
||||||
|
*/
|
||||||
sealed class Node {
|
sealed class Node {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Wasm module definition.
|
||||||
|
*
|
||||||
|
* The unit of WebAssembly code is the module. A module collects definitions
|
||||||
|
* for types, functions, tables, memories, and globals. In addition, it can
|
||||||
|
* declare imports and exports and provide initialization logic in the form
|
||||||
|
* of data and element segments or a start function.
|
||||||
|
*/
|
||||||
data class Module(
|
data class Module(
|
||||||
val types: List<Type.Func> = emptyList(),
|
val types: List<Type.Func> = emptyList(),
|
||||||
val imports: List<Import> = emptyList(),
|
val imports: List<Import> = emptyList(),
|
||||||
@ -15,6 +27,7 @@ sealed class Node {
|
|||||||
val elems: List<Elem> = emptyList(),
|
val elems: List<Elem> = emptyList(),
|
||||||
val funcs: List<Func> = emptyList(),
|
val funcs: List<Func> = emptyList(),
|
||||||
val data: List<Data> = emptyList(),
|
val data: List<Data> = emptyList(),
|
||||||
|
val names: NameSection? = null,
|
||||||
val customSections: List<CustomSection> = emptyList()
|
val customSections: List<CustomSection> = emptyList()
|
||||||
) : Node()
|
) : Node()
|
||||||
|
|
||||||
@ -148,6 +161,12 @@ sealed class Node {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
data class NameSection(
|
||||||
|
val moduleName: String?,
|
||||||
|
val funcNames: Map<Int, String>,
|
||||||
|
val localNames: Map<Int, Map<Int, String>>
|
||||||
|
) : Node()
|
||||||
|
|
||||||
sealed class Instr : Node() {
|
sealed class Instr : Node() {
|
||||||
|
|
||||||
fun op() = InstrOp.classToOpMap[this::class] ?: throw Exception("No op found for ${this::class}")
|
fun op() = InstrOp.classToOpMap[this::class] ?: throw Exception("No op found for ${this::class}")
|
||||||
@ -165,12 +184,15 @@ sealed class Node {
|
|||||||
interface Const<out T : Number> : Args { val value: T }
|
interface Const<out T : Number> : Args { val value: T }
|
||||||
}
|
}
|
||||||
|
|
||||||
// Control flow
|
// Control instructions [https://www.w3.org/TR/2018/WD-wasm-core-1-20180215/#control-instructions]
|
||||||
|
|
||||||
object Unreachable : Instr(), Args.None
|
object Unreachable : Instr(), Args.None
|
||||||
object Nop : Instr(), Args.None
|
object Nop : Instr(), Args.None
|
||||||
|
|
||||||
data class Block(override val type: Type.Value?) : Instr(), Args.Type
|
data class Block(override val type: Type.Value?) : Instr(), Args.Type
|
||||||
data class Loop(override val type: Type.Value?) : Instr(), Args.Type
|
data class Loop(override val type: Type.Value?) : Instr(), Args.Type
|
||||||
data class If(override val type: Type.Value?) : Instr(), Args.Type
|
data class If(override val type: Type.Value?) : Instr(), Args.Type
|
||||||
|
|
||||||
object Else : Instr(), Args.None
|
object Else : Instr(), Args.None
|
||||||
object End : Instr(), Args.None
|
object End : Instr(), Args.None
|
||||||
data class Br(override val relativeDepth: Int) : Instr(), Args.RelativeDepth
|
data class Br(override val relativeDepth: Int) : Instr(), Args.RelativeDepth
|
||||||
@ -181,25 +203,27 @@ sealed class Node {
|
|||||||
) : Instr(), Args.Table
|
) : Instr(), Args.Table
|
||||||
object Return : Instr()
|
object Return : Instr()
|
||||||
|
|
||||||
// Call operators
|
|
||||||
data class Call(override val index: Int) : Instr(), Args.Index
|
data class Call(override val index: Int) : Instr(), Args.Index
|
||||||
data class CallIndirect(
|
data class CallIndirect(
|
||||||
override val index: Int,
|
override val index: Int,
|
||||||
override val reserved: Boolean
|
override val reserved: Boolean
|
||||||
) : Instr(), Args.ReservedIndex
|
) : Instr(), Args.ReservedIndex
|
||||||
|
|
||||||
// Parametric operators
|
// Parametric instructions [https://www.w3.org/TR/2018/WD-wasm-core-1-20180215/#parametric-instructions]
|
||||||
|
|
||||||
object Drop : Instr(), Args.None
|
object Drop : Instr(), Args.None
|
||||||
object Select : Instr(), Args.None
|
object Select : Instr(), Args.None
|
||||||
|
|
||||||
// Variable access
|
// Variable instructions [https://www.w3.org/TR/2018/WD-wasm-core-1-20180215/#variable-instructions]
|
||||||
|
|
||||||
data class GetLocal(override val index: Int) : Instr(), Args.Index
|
data class GetLocal(override val index: Int) : Instr(), Args.Index
|
||||||
data class SetLocal(override val index: Int) : Instr(), Args.Index
|
data class SetLocal(override val index: Int) : Instr(), Args.Index
|
||||||
data class TeeLocal(override val index: Int) : Instr(), Args.Index
|
data class TeeLocal(override val index: Int) : Instr(), Args.Index
|
||||||
data class GetGlobal(override val index: Int) : Instr(), Args.Index
|
data class GetGlobal(override val index: Int) : Instr(), Args.Index
|
||||||
data class SetGlobal(override val index: Int) : Instr(), Args.Index
|
data class SetGlobal(override val index: Int) : Instr(), Args.Index
|
||||||
|
|
||||||
// Memory operators
|
// Memory instructions [https://www.w3.org/TR/2018/WD-wasm-core-1-20180215/#memory-instructions]
|
||||||
|
|
||||||
data class I32Load(override val align: Int, override val offset: Long) : Instr(), Args.AlignOffset
|
data class I32Load(override val align: Int, override val offset: Long) : Instr(), Args.AlignOffset
|
||||||
data class I64Load(override val align: Int, override val offset: Long) : Instr(), Args.AlignOffset
|
data class I64Load(override val align: Int, override val offset: Long) : Instr(), Args.AlignOffset
|
||||||
data class F32Load(override val align: Int, override val offset: Long) : Instr(), Args.AlignOffset
|
data class F32Load(override val align: Int, override val offset: Long) : Instr(), Args.AlignOffset
|
||||||
@ -223,10 +247,12 @@ sealed class Node {
|
|||||||
data class I64Store8(override val align: Int, override val offset: Long) : Instr(), Args.AlignOffset
|
data class I64Store8(override val align: Int, override val offset: Long) : Instr(), Args.AlignOffset
|
||||||
data class I64Store16(override val align: Int, override val offset: Long) : Instr(), Args.AlignOffset
|
data class I64Store16(override val align: Int, override val offset: Long) : Instr(), Args.AlignOffset
|
||||||
data class I64Store32(override val align: Int, override val offset: Long) : Instr(), Args.AlignOffset
|
data class I64Store32(override val align: Int, override val offset: Long) : Instr(), Args.AlignOffset
|
||||||
data class CurrentMemory(override val reserved: Boolean) : Instr(), Args.Reserved
|
data class MemorySize(override val reserved: Boolean) : Instr(), Args.Reserved
|
||||||
data class GrowMemory(override val reserved: Boolean) : Instr(), Args.Reserved
|
data class MemoryGrow(override val reserved: Boolean) : Instr(), Args.Reserved
|
||||||
|
|
||||||
// Constants
|
// Numeric instructions [https://www.w3.org/TR/2018/WD-wasm-core-1-20180215/#numeric-instructions]
|
||||||
|
|
||||||
|
// Constants operators
|
||||||
data class I32Const(override val value: Int) : Instr(), Args.Const<Int>
|
data class I32Const(override val value: Int) : Instr(), Args.Const<Int>
|
||||||
data class I64Const(override val value: Long) : Instr(), Args.Const<Long>
|
data class I64Const(override val value: Long) : Instr(), Args.Const<Long>
|
||||||
data class F32Const(override val value: Float) : Instr(), Args.Const<Float>
|
data class F32Const(override val value: Float) : Instr(), Args.Const<Float>
|
||||||
@ -511,8 +537,8 @@ sealed class Node {
|
|||||||
opMapEntry("i64.store8", 0x3c, ::MemOpAlignOffsetArg, Instr::I64Store8, Instr.I64Store8::class)
|
opMapEntry("i64.store8", 0x3c, ::MemOpAlignOffsetArg, Instr::I64Store8, Instr.I64Store8::class)
|
||||||
opMapEntry("i64.store16", 0x3d, ::MemOpAlignOffsetArg, Instr::I64Store16, Instr.I64Store16::class)
|
opMapEntry("i64.store16", 0x3d, ::MemOpAlignOffsetArg, Instr::I64Store16, Instr.I64Store16::class)
|
||||||
opMapEntry("i64.store32", 0x3e, ::MemOpAlignOffsetArg, Instr::I64Store32, Instr.I64Store32::class)
|
opMapEntry("i64.store32", 0x3e, ::MemOpAlignOffsetArg, Instr::I64Store32, Instr.I64Store32::class)
|
||||||
opMapEntry("current_memory", 0x3f, ::MemOpReservedArg, Instr::CurrentMemory, Instr.CurrentMemory::class)
|
opMapEntry("memory.size", 0x3f, ::MemOpReservedArg, Instr::MemorySize, Instr.MemorySize::class)
|
||||||
opMapEntry("grow_memory", 0x40, ::MemOpReservedArg, Instr::GrowMemory, Instr.GrowMemory::class)
|
opMapEntry("memory.grow", 0x40, ::MemOpReservedArg, Instr::MemoryGrow, Instr.MemoryGrow::class)
|
||||||
|
|
||||||
opMapEntry("i32.const", 0x41, ::ConstOpIntArg, Instr::I32Const, Instr.I32Const::class)
|
opMapEntry("i32.const", 0x41, ::ConstOpIntArg, Instr::I32Const, Instr.I32Const::class)
|
||||||
opMapEntry("i64.const", 0x42, ::ConstOpLongArg, Instr::I64Const, Instr.I64Const::class)
|
opMapEntry("i64.const", 0x42, ::ConstOpLongArg, Instr::I64Const, Instr.I64Const::class)
|
||||||
|
@ -2,10 +2,16 @@ package asmble.ast
|
|||||||
|
|
||||||
import asmble.io.SExprToStr
|
import asmble.io.SExprToStr
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Ast representation of wasm S-expressions (wast format).
|
||||||
|
* see [[https://webassembly.github.io/spec/core/text/index.html]]
|
||||||
|
*/
|
||||||
sealed class SExpr {
|
sealed class SExpr {
|
||||||
|
|
||||||
data class Multi(val vals: List<SExpr> = emptyList()) : SExpr() {
|
data class Multi(val vals: List<SExpr> = emptyList()) : SExpr() {
|
||||||
override fun toString() = SExprToStr.Compact.fromSExpr(this)
|
override fun toString() = SExprToStr.Compact.fromSExpr(this)
|
||||||
}
|
}
|
||||||
|
|
||||||
data class Symbol(
|
data class Symbol(
|
||||||
val contents: String = "",
|
val contents: String = "",
|
||||||
val quoted: Boolean = false,
|
val quoted: Boolean = false,
|
||||||
@ -15,4 +21,5 @@ sealed class SExpr {
|
|||||||
// This is basically the same as the deprecated java.lang.String#getBytes
|
// This is basically the same as the deprecated java.lang.String#getBytes
|
||||||
fun rawContentCharsToBytes() = contents.toCharArray().map(Char::toByte)
|
fun rawContentCharsToBytes() = contents.toCharArray().map(Char::toByte)
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
@ -1,6 +1,10 @@
|
|||||||
package asmble.ast
|
package asmble.ast
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Ast representation of wasm script.
|
||||||
|
*/
|
||||||
data class Script(val commands: List<Cmd>) {
|
data class Script(val commands: List<Cmd>) {
|
||||||
|
|
||||||
sealed class Cmd {
|
sealed class Cmd {
|
||||||
data class Module(val module: Node.Module, val name: String?): Cmd()
|
data class Module(val module: Node.Module, val name: String?): Cmd()
|
||||||
data class Register(val string: String, val name: String?): Cmd()
|
data class Register(val string: String, val name: String?): Cmd()
|
||||||
|
250
compiler/src/main/kotlin/asmble/ast/Stack.kt
Normal file
250
compiler/src/main/kotlin/asmble/ast/Stack.kt
Normal file
@ -0,0 +1,250 @@
|
|||||||
|
package asmble.ast
|
||||||
|
|
||||||
|
// This is a utility for walking the stack. It can do validation or just walk naively.
|
||||||
|
data class Stack(
|
||||||
|
// If some of these values below are null, the pops/pushes may appear "unknown"
|
||||||
|
val mod: CachedModule? = null,
|
||||||
|
val func: Node.Func? = null,
|
||||||
|
// Null if not tracking the current stack and all pops succeed
|
||||||
|
val current: List<Node.Type.Value>? = null,
|
||||||
|
val insnApplies: List<InsnApply> = emptyList(),
|
||||||
|
val strict: Boolean = false,
|
||||||
|
val unreachableUntilNextEndCount: Int = 0
|
||||||
|
) {
|
||||||
|
|
||||||
|
fun next(v: Node.Instr, callFuncTypeOverride: Node.Type.Func? = null) = insnApply(v) {
|
||||||
|
// If we're unreachable, and not an end, we skip and move on
|
||||||
|
if (unreachableUntilNextEndCount > 0 && v !is Node.Instr.End) {
|
||||||
|
// If it's a block, we increase it because we'll see another end
|
||||||
|
return@insnApply if (v is Node.Instr.Args.Type) unreachable(unreachableUntilNextEndCount + 1) else nop()
|
||||||
|
}
|
||||||
|
when (v) {
|
||||||
|
is Node.Instr.Nop, is Node.Instr.Block, is Node.Instr.Loop -> nop()
|
||||||
|
is Node.Instr.If, is Node.Instr.BrIf -> popI32()
|
||||||
|
is Node.Instr.Return -> (func?.type?.ret?.let { pop(it) } ?: nop()) + unreachable(1)
|
||||||
|
is Node.Instr.Unreachable -> unreachable(1)
|
||||||
|
is Node.Instr.End, is Node.Instr.Else -> {
|
||||||
|
// Put back what was before the last block and add the block's type
|
||||||
|
// Go backwards to find the starting block
|
||||||
|
var currDepth = 0
|
||||||
|
val found = insnApplies.findLast {
|
||||||
|
when (it.insn) {
|
||||||
|
is Node.Instr.End -> { currDepth++; false }
|
||||||
|
is Node.Instr.Args.Type -> if (currDepth > 0) { currDepth--; false } else true
|
||||||
|
else -> false
|
||||||
|
}
|
||||||
|
}?.takeIf {
|
||||||
|
// When it's else, needs to be if
|
||||||
|
v !is Node.Instr.Else || it.insn is Node.Instr.If
|
||||||
|
}
|
||||||
|
val changes = when {
|
||||||
|
found != null && found.insn is Node.Instr.Args.Type &&
|
||||||
|
found.stackAtBeginning != null && this != null -> {
|
||||||
|
// Pop everything from before the block's start, then push if necessary...
|
||||||
|
// The If block includes an int at the beginning we must not include when subtracting
|
||||||
|
var preBlockStackSize = found.stackAtBeginning.size
|
||||||
|
if (found.insn is Node.Instr.If) preBlockStackSize--
|
||||||
|
val popped =
|
||||||
|
if (unreachableUntilNextEndCount > 1) nop()
|
||||||
|
else (0 until (size - preBlockStackSize)).flatMap { pop() }
|
||||||
|
// Only push if this is not an else
|
||||||
|
val pushed =
|
||||||
|
if (unreachableUntilNextEndCount > 1 || v is Node.Instr.Else) nop()
|
||||||
|
else (found.insn.type?.let { push(it) } ?: nop())
|
||||||
|
popped + pushed
|
||||||
|
}
|
||||||
|
strict -> error("Unable to find starting block for end")
|
||||||
|
else -> nop()
|
||||||
|
}
|
||||||
|
if (unreachableUntilNextEndCount > 0) changes + unreachable(unreachableUntilNextEndCount - 1)
|
||||||
|
else changes
|
||||||
|
}
|
||||||
|
is Node.Instr.Br -> unreachable(v.relativeDepth + 1)
|
||||||
|
is Node.Instr.BrTable -> popI32() + unreachable(1)
|
||||||
|
is Node.Instr.Call -> (callFuncTypeOverride ?: func(v.index)).let {
|
||||||
|
if (it == null) error("Call func type missing")
|
||||||
|
it.params.reversed().flatMap { pop(it) } + (it.ret?.let { push(it) } ?: nop())
|
||||||
|
}
|
||||||
|
is Node.Instr.CallIndirect -> (callFuncTypeOverride ?: mod?.mod?.types?.getOrNull(v.index)).let {
|
||||||
|
if (it == null) error("Call func type missing")
|
||||||
|
// We add one for the table index
|
||||||
|
popI32() + it.params.reversed().flatMap { pop(it) } + (it.ret?.let { push(it) } ?: nop())
|
||||||
|
}
|
||||||
|
is Node.Instr.Drop -> pop()
|
||||||
|
is Node.Instr.Select -> popI32() + pop().let { it + pop(it.first().type) + push(it.first().type) }
|
||||||
|
is Node.Instr.GetLocal -> push(local(v.index))
|
||||||
|
is Node.Instr.SetLocal -> pop(local(v.index))
|
||||||
|
is Node.Instr.TeeLocal -> local(v.index).let { pop(it) + push(it) }
|
||||||
|
is Node.Instr.GetGlobal -> push(global(v.index))
|
||||||
|
is Node.Instr.SetGlobal -> pop(global(v.index))
|
||||||
|
is Node.Instr.I32Load, is Node.Instr.I32Load8S, is Node.Instr.I32Load8U,
|
||||||
|
is Node.Instr.I32Load16U, is Node.Instr.I32Load16S -> popI32() + pushI32()
|
||||||
|
is Node.Instr.I64Load, is Node.Instr.I64Load8S, is Node.Instr.I64Load8U, is Node.Instr.I64Load16U,
|
||||||
|
is Node.Instr.I64Load16S, is Node.Instr.I64Load32S, is Node.Instr.I64Load32U -> popI32() + pushI64()
|
||||||
|
is Node.Instr.F32Load -> popI32() + pushF32()
|
||||||
|
is Node.Instr.F64Load -> popI32() + pushF64()
|
||||||
|
is Node.Instr.I32Store, is Node.Instr.I32Store8, is Node.Instr.I32Store16 -> popI32() + popI32()
|
||||||
|
is Node.Instr.I64Store, is Node.Instr.I64Store8,
|
||||||
|
is Node.Instr.I64Store16, is Node.Instr.I64Store32 -> popI64() + popI32()
|
||||||
|
is Node.Instr.F32Store -> popF32() + popI32()
|
||||||
|
is Node.Instr.F64Store -> popF64() + popI32()
|
||||||
|
is Node.Instr.MemorySize -> pushI32()
|
||||||
|
is Node.Instr.MemoryGrow -> popI32() + pushI32()
|
||||||
|
is Node.Instr.I32Const -> pushI32()
|
||||||
|
is Node.Instr.I64Const -> pushI64()
|
||||||
|
is Node.Instr.F32Const -> pushF32()
|
||||||
|
is Node.Instr.F64Const -> pushF64()
|
||||||
|
is Node.Instr.I32Add, is Node.Instr.I32Sub, is Node.Instr.I32Mul, is Node.Instr.I32DivS,
|
||||||
|
is Node.Instr.I32DivU, is Node.Instr.I32RemS, is Node.Instr.I32RemU, is Node.Instr.I32And,
|
||||||
|
is Node.Instr.I32Or, is Node.Instr.I32Xor, is Node.Instr.I32Shl, is Node.Instr.I32ShrS,
|
||||||
|
is Node.Instr.I32ShrU, is Node.Instr.I32Rotl, is Node.Instr.I32Rotr, is Node.Instr.I32Eq,
|
||||||
|
is Node.Instr.I32Ne, is Node.Instr.I32LtS, is Node.Instr.I32LeS, is Node.Instr.I32LtU,
|
||||||
|
is Node.Instr.I32LeU, is Node.Instr.I32GtS, is Node.Instr.I32GeS, is Node.Instr.I32GtU,
|
||||||
|
is Node.Instr.I32GeU -> popI32() + popI32() + pushI32()
|
||||||
|
is Node.Instr.I32Clz, is Node.Instr.I32Ctz, is Node.Instr.I32Popcnt,
|
||||||
|
is Node.Instr.I32Eqz -> popI32() + pushI32()
|
||||||
|
is Node.Instr.I64Add, is Node.Instr.I64Sub, is Node.Instr.I64Mul, is Node.Instr.I64DivS,
|
||||||
|
is Node.Instr.I64DivU, is Node.Instr.I64RemS, is Node.Instr.I64RemU, is Node.Instr.I64And,
|
||||||
|
is Node.Instr.I64Or, is Node.Instr.I64Xor, is Node.Instr.I64Shl, is Node.Instr.I64ShrS,
|
||||||
|
is Node.Instr.I64ShrU, is Node.Instr.I64Rotl, is Node.Instr.I64Rotr -> popI64() + popI64() + pushI64()
|
||||||
|
is Node.Instr.I64Eq, is Node.Instr.I64Ne, is Node.Instr.I64LtS, is Node.Instr.I64LeS,
|
||||||
|
is Node.Instr.I64LtU, is Node.Instr.I64LeU, is Node.Instr.I64GtS,
|
||||||
|
is Node.Instr.I64GeS, is Node.Instr.I64GtU, is Node.Instr.I64GeU -> popI64() + popI64() + pushI32()
|
||||||
|
is Node.Instr.I64Clz, is Node.Instr.I64Ctz, is Node.Instr.I64Popcnt -> popI64() + pushI64()
|
||||||
|
is Node.Instr.I64Eqz -> popI64() + pushI32()
|
||||||
|
is Node.Instr.F32Add, is Node.Instr.F32Sub, is Node.Instr.F32Mul, is Node.Instr.F32Div,
|
||||||
|
is Node.Instr.F32Min, is Node.Instr.F32Max, is Node.Instr.F32CopySign -> popF32() + popF32() + pushF32()
|
||||||
|
is Node.Instr.F32Eq, is Node.Instr.F32Ne, is Node.Instr.F32Lt, is Node.Instr.F32Le,
|
||||||
|
is Node.Instr.F32Gt, is Node.Instr.F32Ge -> popF32() + popF32() + pushI32()
|
||||||
|
is Node.Instr.F32Abs, is Node.Instr.F32Neg, is Node.Instr.F32Ceil, is Node.Instr.F32Floor,
|
||||||
|
is Node.Instr.F32Trunc, is Node.Instr.F32Nearest, is Node.Instr.F32Sqrt -> popF32() + pushF32()
|
||||||
|
is Node.Instr.F64Add, is Node.Instr.F64Sub, is Node.Instr.F64Mul, is Node.Instr.F64Div,
|
||||||
|
is Node.Instr.F64Min, is Node.Instr.F64Max, is Node.Instr.F64CopySign -> popF64() + popF64() + pushF64()
|
||||||
|
is Node.Instr.F64Eq, is Node.Instr.F64Ne, is Node.Instr.F64Lt, is Node.Instr.F64Le,
|
||||||
|
is Node.Instr.F64Gt, is Node.Instr.F64Ge -> popF64() + popF64() + pushI32()
|
||||||
|
is Node.Instr.F64Abs, is Node.Instr.F64Neg, is Node.Instr.F64Ceil, is Node.Instr.F64Floor,
|
||||||
|
is Node.Instr.F64Trunc, is Node.Instr.F64Nearest, is Node.Instr.F64Sqrt -> popF64() + pushF64()
|
||||||
|
is Node.Instr.I32WrapI64 -> popI64() + pushI32()
|
||||||
|
is Node.Instr.I32TruncSF32, is Node.Instr.I32TruncUF32,
|
||||||
|
is Node.Instr.I32ReinterpretF32 -> popF32() + pushI32()
|
||||||
|
is Node.Instr.I32TruncSF64, is Node.Instr.I32TruncUF64 -> popF64() + pushI32()
|
||||||
|
is Node.Instr.I64ExtendSI32, is Node.Instr.I64ExtendUI32 -> popI32() + pushI64()
|
||||||
|
is Node.Instr.I64TruncSF32, is Node.Instr.I64TruncUF32 -> popF32() + pushI64()
|
||||||
|
is Node.Instr.I64TruncSF64, is Node.Instr.I64TruncUF64,
|
||||||
|
is Node.Instr.I64ReinterpretF64 -> popF64() + pushI64()
|
||||||
|
is Node.Instr.F32ConvertSI32, is Node.Instr.F32ConvertUI32,
|
||||||
|
is Node.Instr.F32ReinterpretI32 -> popI32() + pushF32()
|
||||||
|
is Node.Instr.F32ConvertSI64, is Node.Instr.F32ConvertUI64 -> popI64() + pushF32()
|
||||||
|
is Node.Instr.F32DemoteF64 -> popF64() + pushF32()
|
||||||
|
is Node.Instr.F64ConvertSI32, is Node.Instr.F64ConvertUI32 -> popI32() + pushF64()
|
||||||
|
is Node.Instr.F64ConvertSI64, is Node.Instr.F64ConvertUI64,
|
||||||
|
is Node.Instr.F64ReinterpretI64 -> popI64() + pushF64()
|
||||||
|
is Node.Instr.F64PromoteF32 -> popF32() + pushF64()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected fun insnApply(v: Node.Instr, fn: MutableList<Node.Type.Value>?.() -> List<InsnApplyResponse>): Stack {
|
||||||
|
val mutStack = current?.toMutableList()
|
||||||
|
val applyResp = mutStack.fn()
|
||||||
|
val newUnreachable = (applyResp.find { it is Unreachable } as? Unreachable)?.untilEndCount
|
||||||
|
return copy(
|
||||||
|
current = mutStack,
|
||||||
|
insnApplies = insnApplies + InsnApply(
|
||||||
|
insn = v,
|
||||||
|
stackAtBeginning = current,
|
||||||
|
stackChanges = applyResp.mapNotNull { it as? StackChange },
|
||||||
|
unreachableUntilEndCount = newUnreachable ?: unreachableUntilNextEndCount
|
||||||
|
),
|
||||||
|
unreachableUntilNextEndCount = newUnreachable ?: unreachableUntilNextEndCount
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
protected fun unreachable(untilEndCount: Int) = listOf(Unreachable(untilEndCount))
|
||||||
|
protected fun local(index: Int) = func?.let {
|
||||||
|
it.type.params.getOrNull(index) ?: it.locals.getOrNull(index - it.type.params.size)
|
||||||
|
}
|
||||||
|
protected fun global(index: Int) = mod?.let {
|
||||||
|
it.importGlobals.getOrNull(index)?.type?.contentType ?:
|
||||||
|
it.mod.globals.getOrNull(index - it.importGlobals.size)?.type?.contentType
|
||||||
|
}
|
||||||
|
protected fun func(index: Int) = mod?.let {
|
||||||
|
it.importFuncs.getOrNull(index)?.typeIndex?.let { i -> it.mod.types.getOrNull(i) } ?:
|
||||||
|
it.mod.funcs.getOrNull(index - it.importFuncs.size)?.type
|
||||||
|
}
|
||||||
|
|
||||||
|
protected fun nop() = emptyList<StackChange>()
|
||||||
|
protected fun MutableList<Node.Type.Value>?.popType(expecting: Node.Type.Value? = null) =
|
||||||
|
this?.takeIf {
|
||||||
|
it.isNotEmpty().also {
|
||||||
|
require(!strict || it) { "Expected $expecting got empty" }
|
||||||
|
}
|
||||||
|
}?.let {
|
||||||
|
removeAt(size - 1).takeIf { actual -> (expecting == null || actual == expecting).also {
|
||||||
|
require(!strict || it) { "Expected $expecting got $actual" }
|
||||||
|
} }
|
||||||
|
} ?: expecting
|
||||||
|
protected fun MutableList<Node.Type.Value>?.pop(expecting: Node.Type.Value? = null) =
|
||||||
|
listOf(StackChange(popType(expecting), true))
|
||||||
|
|
||||||
|
protected fun MutableList<Node.Type.Value>?.popI32() = pop(Node.Type.Value.I32)
|
||||||
|
protected fun MutableList<Node.Type.Value>?.popI64() = pop(Node.Type.Value.I64)
|
||||||
|
protected fun MutableList<Node.Type.Value>?.popF32() = pop(Node.Type.Value.F32)
|
||||||
|
protected fun MutableList<Node.Type.Value>?.popF64() = pop(Node.Type.Value.F64)
|
||||||
|
|
||||||
|
protected fun MutableList<Node.Type.Value>?.push(type: Node.Type.Value? = null) =
|
||||||
|
listOf(StackChange(type, false)).also { if (this != null && type != null) add(type) }
|
||||||
|
protected fun MutableList<Node.Type.Value>?.pushI32() = push(Node.Type.Value.I32)
|
||||||
|
protected fun MutableList<Node.Type.Value>?.pushI64() = push(Node.Type.Value.I64)
|
||||||
|
protected fun MutableList<Node.Type.Value>?.pushF32() = push(Node.Type.Value.F32)
|
||||||
|
protected fun MutableList<Node.Type.Value>?.pushF64() = push(Node.Type.Value.F64)
|
||||||
|
|
||||||
|
data class InsnApply(
|
||||||
|
val insn: Node.Instr,
|
||||||
|
val stackAtBeginning: List<Node.Type.Value>?,
|
||||||
|
val stackChanges: List<StackChange>,
|
||||||
|
val unreachableUntilEndCount: Int
|
||||||
|
)
|
||||||
|
|
||||||
|
protected interface InsnApplyResponse
|
||||||
|
|
||||||
|
data class StackChange(
|
||||||
|
val type: Node.Type.Value?,
|
||||||
|
val pop: Boolean
|
||||||
|
) : InsnApplyResponse
|
||||||
|
|
||||||
|
data class Unreachable(
|
||||||
|
val untilEndCount: Int
|
||||||
|
) : InsnApplyResponse
|
||||||
|
|
||||||
|
class CachedModule(val mod: Node.Module) {
|
||||||
|
val importFuncs by lazy { mod.imports.mapNotNull { it.kind as? Node.Import.Kind.Func } }
|
||||||
|
val importGlobals by lazy { mod.imports.mapNotNull { it.kind as? Node.Import.Kind.Global } }
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
fun walkStrict(mod: Node.Module, func: Node.Func, afterInsn: ((Stack, Node.Instr) -> Unit)? = null) =
|
||||||
|
func.instructions.fold(Stack(
|
||||||
|
mod = CachedModule(mod),
|
||||||
|
func = func,
|
||||||
|
current = emptyList(),
|
||||||
|
strict = true
|
||||||
|
)) { stack, insn -> stack.next(insn).also { afterInsn?.invoke(it, insn) } }.also { stack ->
|
||||||
|
// We expect to be in an unreachable state at the end or have the single return value on the stack
|
||||||
|
if (stack.unreachableUntilNextEndCount == 0) {
|
||||||
|
val expectedStack = (func.type.ret?.let { listOf(it) } ?: emptyList())
|
||||||
|
require(expectedStack == stack.current) {
|
||||||
|
"Expected end to be $expectedStack, got ${stack.current}"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun stackChanges(v: Node.Instr, callFuncType: Node.Type.Func? = null) =
|
||||||
|
Stack().next(v, callFuncType).insnApplies.last().stackChanges
|
||||||
|
fun stackChanges(mod: CachedModule, func: Node.Func, v: Node.Instr) =
|
||||||
|
Stack(mod, func).next(v).insnApplies.last().stackChanges
|
||||||
|
fun stackDiff(v: Node.Instr, callFuncType: Node.Type.Func? = null) =
|
||||||
|
stackChanges(v, callFuncType).sumBy { if (it.pop) -1 else 1 }
|
||||||
|
fun stackDiff(mod: CachedModule, func: Node.Func, v: Node.Instr) =
|
||||||
|
stackChanges(mod, func, v).sumBy { if (it.pop) -1 else 1 }
|
||||||
|
}
|
||||||
|
}
|
183
compiler/src/main/kotlin/asmble/ast/opt/SplitLargeFunc.kt
Normal file
183
compiler/src/main/kotlin/asmble/ast/opt/SplitLargeFunc.kt
Normal file
@ -0,0 +1,183 @@
|
|||||||
|
package asmble.ast.opt
|
||||||
|
|
||||||
|
import asmble.ast.Node
|
||||||
|
import asmble.ast.Stack
|
||||||
|
|
||||||
|
// This is a naive implementation that just grabs adjacent sets of restricted insns and breaks the one that will save
|
||||||
|
// the most instructions off into its own function.
|
||||||
|
open class SplitLargeFunc(
|
||||||
|
val minSetLength: Int = 5,
|
||||||
|
val maxSetLength: Int = 40,
|
||||||
|
val maxParamCount: Int = 30
|
||||||
|
) {
|
||||||
|
|
||||||
|
// Null if no replacement. Second value is number of instructions saved. fnIndex must map to actual func,
|
||||||
|
// not imported one.
|
||||||
|
fun apply(mod: Node.Module, fnIndex: Int): Pair<Node.Module, Int>? {
|
||||||
|
// Get the func
|
||||||
|
val importFuncCount = mod.imports.count { it.kind is Node.Import.Kind.Func }
|
||||||
|
val actualFnIndex = fnIndex - importFuncCount
|
||||||
|
val func = mod.funcs.getOrElse(actualFnIndex) {
|
||||||
|
error("Unable to find non-import func at $fnIndex (actual $actualFnIndex)")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Just take the best pattern and apply it
|
||||||
|
val newFuncIndex = importFuncCount + mod.funcs.size
|
||||||
|
return commonPatterns(mod, func).firstOrNull()?.let { pattern ->
|
||||||
|
// Name it as <funcname>$splitN (n is num just to disambiguate) if names are part of the mod
|
||||||
|
val newName = mod.names?.funcNames?.get(fnIndex)?.let {
|
||||||
|
"$it\$split".let { it + mod.names.funcNames.count { (_, v) -> v.startsWith(it) } }
|
||||||
|
}
|
||||||
|
|
||||||
|
// Go over every replacement in reverse, changing the instructions to our new set
|
||||||
|
val newInsns = pattern.replacements.foldRight(func.instructions) { repl, insns ->
|
||||||
|
insns.take(repl.range.start) +
|
||||||
|
repl.preCallConsts +
|
||||||
|
Node.Instr.Call(newFuncIndex) +
|
||||||
|
insns.drop(repl.range.endInclusive + 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Return the module w/ the new function, it's new name, and the insns saved
|
||||||
|
mod.copy(
|
||||||
|
funcs = mod.funcs.toMutableList().also {
|
||||||
|
it[actualFnIndex] = func.copy(instructions = newInsns)
|
||||||
|
} + pattern.newFunc,
|
||||||
|
names = mod.names?.copy(funcNames = mod.names.funcNames.toMutableMap().also {
|
||||||
|
it[newFuncIndex] = newName!!
|
||||||
|
})
|
||||||
|
) to pattern.insnsSaved
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Results are by most insns saved. There can be overlap across patterns but never within a single pattern.
|
||||||
|
fun commonPatterns(mod: Node.Module, fn: Node.Func): List<CommonPattern> {
|
||||||
|
// Walk the stack for validation needs
|
||||||
|
val stack = Stack.walkStrict(mod, fn)
|
||||||
|
|
||||||
|
// Let's grab sets of insns that qualify. In this naive impl, in order to qualify the insn set needs to
|
||||||
|
// only have a certain set of insns that can be broken off. It can also only change the stack by 0 or 1
|
||||||
|
// value while never dipping below the starting stack. We also store the index they started at.
|
||||||
|
var insnSets = emptyList<InsnSet>()
|
||||||
|
// Pair in fold keyed by insn index
|
||||||
|
fn.instructions.foldIndexed(null as List<Pair<Int, Node.Instr>>?) { index, lastInsns, insn ->
|
||||||
|
if (!insn.canBeMoved) null else (lastInsns ?: emptyList()).plus(index to insn).also { fullNewInsnSet ->
|
||||||
|
// Get all final instructions between min and max size and with allowed param count (i.e. const count)
|
||||||
|
val trailingInsnSet = fullNewInsnSet.takeLast(maxSetLength)
|
||||||
|
|
||||||
|
// Get all instructions between the min and max
|
||||||
|
insnSets += (minSetLength..maxSetLength).
|
||||||
|
asSequence().
|
||||||
|
flatMap { trailingInsnSet.asSequence().windowed(it) }.
|
||||||
|
filter { it.count { it.second is Node.Instr.Args.Const<*> } <= maxParamCount }.
|
||||||
|
mapNotNull { newIndexedInsnSet ->
|
||||||
|
// Before adding, make sure it qualifies with the stack
|
||||||
|
InsnSet(
|
||||||
|
startIndex = newIndexedInsnSet.first().first,
|
||||||
|
insns = newIndexedInsnSet.map { it.second },
|
||||||
|
valueAddedToStack = null
|
||||||
|
).withStackValueIfValid(stack)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Sort the insn sets by the ones with the most insns
|
||||||
|
insnSets = insnSets.sortedByDescending { it.insns.size }
|
||||||
|
|
||||||
|
// Now let's create replacements for each, keyed by the extracted func
|
||||||
|
val patterns = insnSets.fold(emptyMap<Node.Func, List<Replacement>>()) { map, insnSet ->
|
||||||
|
insnSet.extractCommonFunc().let { (func, replacement) ->
|
||||||
|
val existingReplacements = map.getOrDefault(func, emptyList())
|
||||||
|
// Ignore if there is any overlap
|
||||||
|
if (existingReplacements.any(replacement::overlaps)) map
|
||||||
|
else map + (func to existingReplacements.plus(replacement))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Now sort the patterns by most insns saved and return
|
||||||
|
return patterns.map { (k, v) ->
|
||||||
|
CommonPattern(k, v.sortedBy { it.range.first })
|
||||||
|
}.sortedByDescending { it.insnsSaved }
|
||||||
|
}
|
||||||
|
|
||||||
|
val Node.Instr.canBeMoved get() =
|
||||||
|
// No blocks
|
||||||
|
this !is Node.Instr.Block && this !is Node.Instr.Loop && this !is Node.Instr.If &&
|
||||||
|
this !is Node.Instr.Else && this !is Node.Instr.End &&
|
||||||
|
// No breaks
|
||||||
|
this !is Node.Instr.Br && this !is Node.Instr.BrIf && this !is Node.Instr.BrTable &&
|
||||||
|
// No return
|
||||||
|
this !is Node.Instr.Return &&
|
||||||
|
// No local access
|
||||||
|
this !is Node.Instr.GetLocal && this !is Node.Instr.SetLocal && this !is Node.Instr.TeeLocal
|
||||||
|
|
||||||
|
fun InsnSet.withStackValueIfValid(stack: Stack): InsnSet? {
|
||||||
|
// This makes sure that the stack only changes by at most one item and never dips below its starting val.
|
||||||
|
// If it is invalid, null is returned. If it qualifies and does change 1 value, it is set.
|
||||||
|
|
||||||
|
// First, make sure the stack after the last insn is the same as the first or the same + 1 val
|
||||||
|
val startingStack = stack.insnApplies[startIndex].stackAtBeginning!!
|
||||||
|
val endingStack = stack.insnApplies.getOrNull(startIndex + insns.size)?.stackAtBeginning ?: stack.current!!
|
||||||
|
if (endingStack.size != startingStack.size && endingStack.size != startingStack.size + 1) return null
|
||||||
|
if (endingStack.take(startingStack.size) != startingStack) return null
|
||||||
|
|
||||||
|
// Now, walk the insns and make sure they never pop below the start
|
||||||
|
var stackCounter = 0
|
||||||
|
stack.insnApplies.subList(startIndex, startIndex + insns.size).forEach {
|
||||||
|
it.stackChanges.forEach {
|
||||||
|
stackCounter += if (it.pop) -1 else 1
|
||||||
|
if (stackCounter < 0) return null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// We're good, now only if the ending stack is one over the start do we have a ret val
|
||||||
|
return copy(
|
||||||
|
valueAddedToStack = endingStack.lastOrNull()?.takeIf { endingStack.size == startingStack.size + 1 }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun InsnSet.extractCommonFunc() =
|
||||||
|
// This extracts a function with constants changed to parameters
|
||||||
|
insns.fold(Pair(
|
||||||
|
Node.Func(Node.Type.Func(params = emptyList(), ret = valueAddedToStack), emptyList(), emptyList()),
|
||||||
|
Replacement(range = startIndex until startIndex + insns.size, preCallConsts = emptyList()))
|
||||||
|
) { (func, repl), insn ->
|
||||||
|
if (insn !is Node.Instr.Args.Const<*>) func.copy(instructions = func.instructions + insn) to repl
|
||||||
|
else func.copy(
|
||||||
|
type = func.type.copy(params = func.type.params + insn.constType),
|
||||||
|
instructions = func.instructions + Node.Instr.GetLocal(func.type.params.size)
|
||||||
|
) to repl.copy(preCallConsts = repl.preCallConsts + insn)
|
||||||
|
}
|
||||||
|
|
||||||
|
protected val Node.Instr.Args.Const<*>.constType get() = when (this) {
|
||||||
|
is Node.Instr.I32Const -> Node.Type.Value.I32
|
||||||
|
is Node.Instr.I64Const -> Node.Type.Value.I64
|
||||||
|
is Node.Instr.F32Const -> Node.Type.Value.F32
|
||||||
|
is Node.Instr.F64Const -> Node.Type.Value.F64
|
||||||
|
else -> error("unreachable")
|
||||||
|
}
|
||||||
|
|
||||||
|
data class InsnSet(
|
||||||
|
val startIndex: Int,
|
||||||
|
val insns: List<Node.Instr>,
|
||||||
|
val valueAddedToStack: Node.Type.Value?
|
||||||
|
)
|
||||||
|
|
||||||
|
data class Replacement(
|
||||||
|
val range: IntRange,
|
||||||
|
val preCallConsts: List<Node.Instr>
|
||||||
|
) {
|
||||||
|
// Subtract one because there is a call after this
|
||||||
|
val insnsSaved get() = (range.last + 1) - range.first - 1 - preCallConsts.size
|
||||||
|
fun overlaps(o: Replacement) = range.contains(o.range.first) || range.contains(o.range.last) ||
|
||||||
|
o.range.contains(range.first) || o.range.contains(range.last)
|
||||||
|
}
|
||||||
|
|
||||||
|
data class CommonPattern(
|
||||||
|
val newFunc: Node.Func,
|
||||||
|
// In order by earliest replacement first
|
||||||
|
val replacements: List<Replacement>
|
||||||
|
) {
|
||||||
|
// Replacement pieces saved (with one added for the invocation) less new func instructions
|
||||||
|
val insnsSaved get() = replacements.sumBy { it.insnsSaved } - newFunc.instructions.size
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object : SplitLargeFunc()
|
||||||
|
}
|
@ -51,7 +51,7 @@ open class Compile : Command<Compile.Args>() {
|
|||||||
val inFormat =
|
val inFormat =
|
||||||
if (args.inFormat != "<use file extension>") args.inFormat
|
if (args.inFormat != "<use file extension>") args.inFormat
|
||||||
else args.inFile.substringAfterLast('.', "<unknown>")
|
else args.inFile.substringAfterLast('.', "<unknown>")
|
||||||
val script = Translate.inToAst(args.inFile, inFormat)
|
val script = Translate().also { it.logger = logger }.inToAst(args.inFile, inFormat)
|
||||||
val mod = (script.commands.firstOrNull() as? Script.Cmd.Module) ?:
|
val mod = (script.commands.firstOrNull() as? Script.Cmd.Module) ?:
|
||||||
error("Only a single sexpr for (module) allowed")
|
error("Only a single sexpr for (module) allowed")
|
||||||
val outStream = when (args.outFile) {
|
val outStream = when (args.outFile) {
|
||||||
|
@ -3,6 +3,9 @@ package asmble.cli
|
|||||||
import asmble.compile.jvm.javaIdent
|
import asmble.compile.jvm.javaIdent
|
||||||
import asmble.run.jvm.Module
|
import asmble.run.jvm.Module
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This class provide ''invoke'' WASM code functionality.
|
||||||
|
*/
|
||||||
open class Invoke : ScriptCommand<Invoke.Args>() {
|
open class Invoke : ScriptCommand<Invoke.Args>() {
|
||||||
|
|
||||||
override val name = "invoke"
|
override val name = "invoke"
|
||||||
@ -34,6 +37,7 @@ open class Invoke : ScriptCommand<Invoke.Args>() {
|
|||||||
).also { bld.done() }
|
).also { bld.done() }
|
||||||
|
|
||||||
override fun run(args: Args) {
|
override fun run(args: Args) {
|
||||||
|
// Compiles wasm to bytecode, do registrations and so on.
|
||||||
val ctx = prepareContext(args.scriptArgs)
|
val ctx = prepareContext(args.scriptArgs)
|
||||||
// Instantiate the module
|
// Instantiate the module
|
||||||
val module =
|
val module =
|
||||||
@ -41,11 +45,11 @@ open class Invoke : ScriptCommand<Invoke.Args>() {
|
|||||||
else ctx.registrations[args.module] as? Module.Instance ?:
|
else ctx.registrations[args.module] as? Module.Instance ?:
|
||||||
error("Unable to find module registered as ${args.module}")
|
error("Unable to find module registered as ${args.module}")
|
||||||
// Just make sure the module is instantiated here...
|
// Just make sure the module is instantiated here...
|
||||||
module.instance(ctx)
|
val instance = module.instance(ctx)
|
||||||
// If an export is provided, call it
|
// If an export is provided, call it
|
||||||
if (args.export != "<start-func>") args.export.javaIdent.let { javaName ->
|
if (args.export != "<start-func>") args.export.javaIdent.let { javaName ->
|
||||||
val method = module.cls.declaredMethods.find { it.name == javaName } ?:
|
// Finds java method(wasm fn) in class(wasm module) by name(declared in <start-func>)
|
||||||
error("Unable to find export '${args.export}'")
|
val method = module.cls.declaredMethods.find { it.name == javaName } ?: error("Unable to find export '${args.export}'")
|
||||||
// Map args to params
|
// Map args to params
|
||||||
require(method.parameterTypes.size == args.args.size) {
|
require(method.parameterTypes.size == args.args.size) {
|
||||||
"Given arg count of ${args.args.size} is invalid for $method"
|
"Given arg count of ${args.args.size} is invalid for $method"
|
||||||
@ -59,11 +63,20 @@ open class Invoke : ScriptCommand<Invoke.Args>() {
|
|||||||
else -> error("Unrecognized type for param ${index + 1}: $paramType")
|
else -> error("Unrecognized type for param ${index + 1}: $paramType")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
val result = method.invoke(module.instance(ctx), *params.toTypedArray())
|
val result = method.invoke(instance, *params.toTypedArray())
|
||||||
if (args.resultToStdout && method.returnType != Void.TYPE) println(result)
|
if (args.resultToStdout && method.returnType != Void.TYPE) println(result)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Arguments for 'invoke' command.
|
||||||
|
*
|
||||||
|
* @param scriptArgs Common arguments for 'invoke' and 'run' ScriptCommands.
|
||||||
|
* @param module The module name to run. If it's a JVM class, it must have a no-arg constructor
|
||||||
|
* @param export The specific export function to invoke
|
||||||
|
* @param args Parameter for the export if export is present
|
||||||
|
* @param resultToStdout If true result will print to stout
|
||||||
|
*/
|
||||||
data class Args(
|
data class Args(
|
||||||
val scriptArgs: ScriptCommand.ScriptArgs,
|
val scriptArgs: ScriptCommand.ScriptArgs,
|
||||||
val module: String,
|
val module: String,
|
||||||
|
@ -3,8 +3,11 @@ package asmble.cli
|
|||||||
import asmble.util.Logger
|
import asmble.util.Logger
|
||||||
import kotlin.system.exitProcess
|
import kotlin.system.exitProcess
|
||||||
|
|
||||||
val commands = listOf(Compile, Help, Invoke, Link, Run, Translate)
|
val commands = listOf(Compile, Help, Invoke, Link, Run, SplitFunc, Translate)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Entry point of command line interface.
|
||||||
|
*/
|
||||||
fun main(args: Array<String>) {
|
fun main(args: Array<String>) {
|
||||||
if (args.isEmpty()) return println(
|
if (args.isEmpty()) return println(
|
||||||
"""
|
"""
|
||||||
@ -28,6 +31,7 @@ fun main(args: Array<String>) {
|
|||||||
val globals = Main.globalArgs(argBuild)
|
val globals = Main.globalArgs(argBuild)
|
||||||
logger = Logger.Print(globals.logLevel)
|
logger = Logger.Print(globals.logLevel)
|
||||||
command.logger = logger
|
command.logger = logger
|
||||||
|
logger.info { "Running the command=${command.name} with args=${argBuild.args}" }
|
||||||
command.runWithArgs(argBuild)
|
command.runWithArgs(argBuild)
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
logger.error { "Error ${command?.let { "in command '${it.name}'" } ?: ""}: ${e.message}" }
|
logger.error { "Error ${command?.let { "in command '${it.name}'" } ?: ""}: ${e.message}" }
|
||||||
|
@ -45,39 +45,56 @@ abstract class ScriptCommand<T> : Command<T>() {
|
|||||||
)
|
)
|
||||||
|
|
||||||
fun prepareContext(args: ScriptArgs): ScriptContext {
|
fun prepareContext(args: ScriptArgs): ScriptContext {
|
||||||
var ctx = ScriptContext(
|
var context = ScriptContext(
|
||||||
packageName = "asmble.temp" + UUID.randomUUID().toString().replace("-", ""),
|
packageName = "asmble.temp" + UUID.randomUUID().toString().replace("-", ""),
|
||||||
defaultMaxMemPages = args.defaultMaxMemPages
|
defaultMaxMemPages = args.defaultMaxMemPages
|
||||||
)
|
)
|
||||||
// Compile everything
|
// Compile everything
|
||||||
ctx = args.inFiles.foldIndexed(ctx) { index, ctx, inFile ->
|
context = args.inFiles.foldIndexed(context) { index, ctx, inFile ->
|
||||||
try {
|
try {
|
||||||
when (inFile.substringAfterLast('.')) {
|
when (inFile.substringAfterLast('.')) {
|
||||||
|
// if input file is class file
|
||||||
"class" -> ctx.classLoader.addClass(File(inFile).readBytes()).let { ctx }
|
"class" -> ctx.classLoader.addClass(File(inFile).readBytes()).let { ctx }
|
||||||
else -> Translate.inToAst(inFile, inFile.substringAfterLast('.')).let { inAst ->
|
// if input file is wasm file
|
||||||
val (mod, name) = (inAst.commands.singleOrNull() as? Script.Cmd.Module) ?:
|
else -> {
|
||||||
|
val translateCmd = Translate
|
||||||
|
translateCmd.logger = this.logger
|
||||||
|
translateCmd.inToAst(inFile, inFile.substringAfterLast('.')).let { inAst ->
|
||||||
|
val (mod, name) = (inAst.commands.singleOrNull() as? Script.Cmd.Module) ?:
|
||||||
error("Input file must only contain a single module")
|
error("Input file must only contain a single module")
|
||||||
val className = name?.javaIdent?.capitalize() ?:
|
val className = name?.javaIdent?.capitalize() ?:
|
||||||
"Temp" + UUID.randomUUID().toString().replace("-", "")
|
"Temp" + UUID.randomUUID().toString().replace("-", "")
|
||||||
ctx.withCompiledModule(mod, className, name).let { ctx ->
|
ctx.withCompiledModule(mod, className, name).let { ctx ->
|
||||||
if (name == null && index != args.inFiles.size - 1)
|
if (name == null && index != args.inFiles.size - 1)
|
||||||
logger.warn { "File '$inFile' not last and has no name so will be unused" }
|
logger.warn { "File '$inFile' not last and has no name so will be unused" }
|
||||||
if (name == null || args.disableAutoRegister) ctx
|
if (name == null || args.disableAutoRegister) ctx
|
||||||
else ctx.runCommand(Script.Cmd.Register(name, null))
|
else ctx.runCommand(Script.Cmd.Register(name, null))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (e: Exception) { throw Exception("Failed loading $inFile - ${e.message}", e) }
|
} catch (e: Exception) {
|
||||||
|
throw Exception("Failed loading $inFile - ${e.message}", e)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
// Do registrations
|
// Do registrations
|
||||||
ctx = args.registrations.fold(ctx) { ctx, (moduleName, className) ->
|
context = args.registrations.fold(context) { ctx, (moduleName, className) ->
|
||||||
ctx.withModuleRegistered(moduleName,
|
ctx.withModuleRegistered(moduleName,
|
||||||
Module.Native(Class.forName(className, true, ctx.classLoader).newInstance()))
|
Module.Native(Class.forName(className, true, ctx.classLoader).newInstance()))
|
||||||
}
|
}
|
||||||
if (args.specTestRegister) ctx = ctx.withHarnessRegistered()
|
if (args.specTestRegister) context = context.withHarnessRegistered() // проверить что не так с "Cannot find compatible import for spectest::print"
|
||||||
return ctx
|
return context
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Common arguments for 'invoke' and 'run' ScriptCommands.
|
||||||
|
*
|
||||||
|
* @param inFiles Files to add to classpath. Can be wasm, wast, or class file
|
||||||
|
* @param registrations Register class name to a module name
|
||||||
|
* @param disableAutoRegister If set, this will not auto-register modules with names
|
||||||
|
* @param specTestRegister If true, registers the spec test harness as 'spectest'
|
||||||
|
* @param defaultMaxMemPages The maximum number of memory pages when a module doesn't say
|
||||||
|
*/
|
||||||
data class ScriptArgs(
|
data class ScriptArgs(
|
||||||
val inFiles: List<String>,
|
val inFiles: List<String>,
|
||||||
val registrations: List<Pair<String, String>>,
|
val registrations: List<Pair<String, String>>,
|
||||||
|
146
compiler/src/main/kotlin/asmble/cli/SplitFunc.kt
Normal file
146
compiler/src/main/kotlin/asmble/cli/SplitFunc.kt
Normal file
@ -0,0 +1,146 @@
|
|||||||
|
package asmble.cli
|
||||||
|
|
||||||
|
import asmble.ast.Node
|
||||||
|
import asmble.ast.Script
|
||||||
|
import asmble.ast.opt.SplitLargeFunc
|
||||||
|
|
||||||
|
open class SplitFunc : Command<SplitFunc.Args>() {
|
||||||
|
override val name = "split-func"
|
||||||
|
override val desc = "Split a WebAssembly function into two"
|
||||||
|
|
||||||
|
override fun args(bld: Command.ArgsBuilder) = Args(
|
||||||
|
inFile = bld.arg(
|
||||||
|
name = "inFile",
|
||||||
|
desc = "The wast or wasm WebAssembly file name. Can be '--' to read from stdin."
|
||||||
|
),
|
||||||
|
funcName = bld.arg(
|
||||||
|
name = "funcName",
|
||||||
|
desc = "The name (or '#' + function space index) of the function to split"
|
||||||
|
),
|
||||||
|
inFormat = bld.arg(
|
||||||
|
name = "inFormat",
|
||||||
|
opt = "in",
|
||||||
|
desc = "Either 'wast' or 'wasm' to describe format.",
|
||||||
|
default = "<use file extension>",
|
||||||
|
lowPriority = true
|
||||||
|
),
|
||||||
|
outFile = bld.arg(
|
||||||
|
name = "outFile",
|
||||||
|
opt = "outFile",
|
||||||
|
desc = "The wast or wasm WebAssembly file name. Can be '--' to write to stdout.",
|
||||||
|
default = "<inFileSansExt.split.wasm or stdout>",
|
||||||
|
lowPriority = true
|
||||||
|
),
|
||||||
|
outFormat = bld.arg(
|
||||||
|
name = "outFormat",
|
||||||
|
opt = "out",
|
||||||
|
desc = "Either 'wast' or 'wasm' to describe format.",
|
||||||
|
default = "<use file extension or wast for stdout>",
|
||||||
|
lowPriority = true
|
||||||
|
),
|
||||||
|
compact = bld.flag(
|
||||||
|
opt = "compact",
|
||||||
|
desc = "If set for wast out format, will be compacted.",
|
||||||
|
lowPriority = true
|
||||||
|
),
|
||||||
|
minInsnSetLength = bld.arg(
|
||||||
|
name = "minInsnSetLength",
|
||||||
|
opt = "minLen",
|
||||||
|
desc = "The minimum number of instructions allowed for the split off function.",
|
||||||
|
default = "5",
|
||||||
|
lowPriority = true
|
||||||
|
).toInt(),
|
||||||
|
maxInsnSetLength = bld.arg(
|
||||||
|
name = "maxInsnSetLength",
|
||||||
|
opt = "maxLen",
|
||||||
|
desc = "The maximum number of instructions allowed for the split off function.",
|
||||||
|
default = "40",
|
||||||
|
lowPriority = true
|
||||||
|
).toInt(),
|
||||||
|
maxNewFuncParamCount = bld.arg(
|
||||||
|
name = "maxNewFuncParamCount",
|
||||||
|
opt = "maxParams",
|
||||||
|
desc = "The maximum number of params allowed for the split off function.",
|
||||||
|
default = "30",
|
||||||
|
lowPriority = true
|
||||||
|
).toInt(),
|
||||||
|
attempts = bld.arg(
|
||||||
|
name = "attempts",
|
||||||
|
opt = "attempts",
|
||||||
|
desc = "The number of attempts to perform.",
|
||||||
|
default = "1",
|
||||||
|
lowPriority = true
|
||||||
|
).toInt()
|
||||||
|
).also { bld.done() }
|
||||||
|
|
||||||
|
override fun run(args: Args) {
|
||||||
|
// Load the mod
|
||||||
|
val translate = Translate().also { it.logger = logger }
|
||||||
|
val inFormat =
|
||||||
|
if (args.inFormat != "<use file extension>") args.inFormat
|
||||||
|
else args.inFile.substringAfterLast('.', "<unknown>")
|
||||||
|
val script = translate.inToAst(args.inFile, inFormat)
|
||||||
|
var mod = (script.commands.firstOrNull() as? Script.Cmd.Module)?.module ?: error("Only a single module allowed")
|
||||||
|
|
||||||
|
// Do attempts
|
||||||
|
val splitter = SplitLargeFunc(
|
||||||
|
minSetLength = args.minInsnSetLength,
|
||||||
|
maxSetLength = args.maxInsnSetLength,
|
||||||
|
maxParamCount = args.maxNewFuncParamCount
|
||||||
|
)
|
||||||
|
for (attempt in 0 until args.attempts) {
|
||||||
|
// Find the function
|
||||||
|
var index = mod.names?.funcNames?.toList()?.find { it.second == args.funcName }?.first
|
||||||
|
if (index == null && args.funcName.startsWith('#')) index = args.funcName.drop(1).toInt()
|
||||||
|
val origFunc = index?.let {
|
||||||
|
mod.funcs.getOrNull(it - mod.imports.count { it.kind is Node.Import.Kind.Func })
|
||||||
|
} ?: error("Unable to find func")
|
||||||
|
|
||||||
|
// Split it
|
||||||
|
val results = splitter.apply(mod, index)
|
||||||
|
if (results == null) {
|
||||||
|
logger.warn { "No instructions after attempt $attempt" }
|
||||||
|
break
|
||||||
|
}
|
||||||
|
val (splitMod, insnsSaved) = results
|
||||||
|
val newFunc = splitMod.funcs[index - mod.imports.count { it.kind is Node.Import.Kind.Func }]
|
||||||
|
val splitFunc = splitMod.funcs.last()
|
||||||
|
logger.warn {
|
||||||
|
"Split complete, from func with ${origFunc.instructions.size} insns to a func " +
|
||||||
|
"with ${newFunc.instructions.size} insns + delegated func " +
|
||||||
|
"with ${splitFunc.instructions.size} insns and ${splitFunc.type.params.size} params, " +
|
||||||
|
"saved $insnsSaved insns"
|
||||||
|
}
|
||||||
|
mod = splitMod
|
||||||
|
}
|
||||||
|
|
||||||
|
// Write it
|
||||||
|
val outFile = when {
|
||||||
|
args.outFile != "<inFileSansExt.split.wasm or stdout>" -> args.outFile
|
||||||
|
args.inFile == "--" -> "--"
|
||||||
|
else -> args.inFile.replaceAfterLast('.', "split." + args.inFile.substringAfterLast('.'))
|
||||||
|
}
|
||||||
|
val outFormat = when {
|
||||||
|
args.outFormat != "<use file extension or wast for stdout>" -> args.outFormat
|
||||||
|
outFile == "--" -> "wast"
|
||||||
|
else -> outFile.substringAfterLast('.', "<unknown>")
|
||||||
|
}
|
||||||
|
translate.astToOut(outFile, outFormat, args.compact,
|
||||||
|
Script(listOf(Script.Cmd.Module(mod, mod.names?.moduleName))))
|
||||||
|
}
|
||||||
|
|
||||||
|
data class Args(
|
||||||
|
val inFile: String,
|
||||||
|
val inFormat: String,
|
||||||
|
val funcName: String,
|
||||||
|
val outFile: String,
|
||||||
|
val outFormat: String,
|
||||||
|
val compact: Boolean,
|
||||||
|
val minInsnSetLength: Int,
|
||||||
|
val maxInsnSetLength: Int,
|
||||||
|
val maxNewFuncParamCount: Int,
|
||||||
|
val attempts: Int
|
||||||
|
)
|
||||||
|
|
||||||
|
companion object : SplitFunc()
|
||||||
|
}
|
@ -52,30 +52,16 @@ open class Translate : Command<Translate.Args>() {
|
|||||||
if (args.outFormat != "<use file extension or wast for stdout>") args.outFormat
|
if (args.outFormat != "<use file extension or wast for stdout>") args.outFormat
|
||||||
else if (args.outFile == "--") "wast"
|
else if (args.outFile == "--") "wast"
|
||||||
else args.outFile.substringAfterLast('.', "<unknown>")
|
else args.outFile.substringAfterLast('.', "<unknown>")
|
||||||
val outStream =
|
astToOut(args.outFile, outFormat, args.compact, script)
|
||||||
if (args.outFile == "--") System.out
|
|
||||||
else FileOutputStream(args.outFile)
|
|
||||||
outStream.use { outStream ->
|
|
||||||
when (outFormat) {
|
|
||||||
"wast" -> {
|
|
||||||
val sexprToStr = if (args.compact) SExprToStr.Compact else SExprToStr
|
|
||||||
val sexprs = AstToSExpr.fromScript(script)
|
|
||||||
outStream.write(sexprToStr.fromSExpr(*sexprs.toTypedArray()).toByteArray())
|
|
||||||
}
|
|
||||||
"wasm" -> {
|
|
||||||
val mod = (script.commands.firstOrNull() as? Script.Cmd.Module)?.module ?:
|
|
||||||
error("Output to WASM requires input be just a single module")
|
|
||||||
AstToBinary.fromModule(ByteWriter.OutputStream(outStream), mod)
|
|
||||||
}
|
|
||||||
else -> error("Unknown out format '$outFormat'")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun inToAst(inFile: String, inFormat: String): Script {
|
fun inToAst(inFile: String, inFormat: String): Script {
|
||||||
val inBytes =
|
val inBytes =
|
||||||
if (inFile == "--") System.`in`.use { it.readBytes() }
|
if (inFile == "--") System.`in`.use { it.readBytes() }
|
||||||
else File(inFile).let { f -> FileInputStream(f).use { it.readBytes(f.length().toIntExact()) } }
|
else File(inFile).let { f ->
|
||||||
|
// Input file might not fit into the memory
|
||||||
|
FileInputStream(f).use { it.readBytes(f.length().toIntExact()) }
|
||||||
|
}
|
||||||
return when (inFormat) {
|
return when (inFormat) {
|
||||||
"wast" -> StrToSExpr.parse(inBytes.toString(Charsets.UTF_8)).let { res ->
|
"wast" -> StrToSExpr.parse(inBytes.toString(Charsets.UTF_8)).let { res ->
|
||||||
when (res) {
|
when (res) {
|
||||||
@ -84,12 +70,33 @@ open class Translate : Command<Translate.Args>() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
"wasm" ->
|
"wasm" ->
|
||||||
Script(listOf(Script.Cmd.Module(BinaryToAst.toModule(
|
Script(listOf(Script.Cmd.Module(BinaryToAst(logger = this.logger).toModule(
|
||||||
ByteReader.InputStream(inBytes.inputStream())), null)))
|
ByteReader.InputStream(inBytes.inputStream())), null)))
|
||||||
else -> error("Unknown in format '$inFormat'")
|
else -> error("Unknown in format '$inFormat'")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun astToOut(outFile: String, outFormat: String, compact: Boolean, script: Script) {
|
||||||
|
val outStream =
|
||||||
|
if (outFile == "--") System.out
|
||||||
|
else FileOutputStream(outFile)
|
||||||
|
outStream.use { outStream ->
|
||||||
|
when (outFormat) {
|
||||||
|
"wast" -> {
|
||||||
|
val sexprToStr = if (compact) SExprToStr.Compact else SExprToStr
|
||||||
|
val sexprs = AstToSExpr.fromScript(script)
|
||||||
|
outStream.write(sexprToStr.fromSExpr(*sexprs.toTypedArray()).toByteArray())
|
||||||
|
}
|
||||||
|
"wasm" -> {
|
||||||
|
val mod = (script.commands.firstOrNull() as? Script.Cmd.Module)?.module ?:
|
||||||
|
error("Output to WASM requires input be just a single module")
|
||||||
|
AstToBinary.fromModule(ByteWriter.OutputStream(outStream), mod)
|
||||||
|
}
|
||||||
|
else -> error("Unknown out format '$outFormat'")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
data class Args(
|
data class Args(
|
||||||
val inFile: String,
|
val inFile: String,
|
||||||
val inFormat: String,
|
val inFormat: String,
|
||||||
|
@ -210,3 +210,7 @@ fun ByteArray.asClassNode(): ClassNode {
|
|||||||
ClassReader(this).accept(newNode, 0)
|
ClassReader(this).accept(newNode, 0)
|
||||||
return newNode
|
return newNode
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun ByteArray.chunked(v: Int) = (0 until size step v).asSequence().map {
|
||||||
|
copyOfRange(it, (it + v).takeIf { it < size } ?: size)
|
||||||
|
}
|
@ -30,10 +30,11 @@ open class AstToAsm {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun addFields(ctx: ClsContext) {
|
fun addFields(ctx: ClsContext) {
|
||||||
// Mem field if present
|
// Mem field if present, adds `private final field memory` to
|
||||||
if (ctx.hasMemory)
|
if (ctx.hasMemory)
|
||||||
ctx.cls.fields.add(FieldNode(Opcodes.ACC_PRIVATE + Opcodes.ACC_FINAL, "memory",
|
ctx.cls.fields.add(
|
||||||
ctx.mem.memType.asmDesc, null, null))
|
FieldNode((Opcodes.ACC_PRIVATE + Opcodes.ACC_FINAL), "memory", ctx.mem.memType.asmDesc, null, null)
|
||||||
|
)
|
||||||
// Table field if present...
|
// Table field if present...
|
||||||
// Private final for now, but likely won't be final in future versions supporting
|
// Private final for now, but likely won't be final in future versions supporting
|
||||||
// mutable tables, may be not even a table but a list (and final)
|
// mutable tables, may be not even a table but a list (and final)
|
||||||
@ -47,10 +48,13 @@ open class AstToAsm {
|
|||||||
})
|
})
|
||||||
// Now all import globals as getter (and maybe setter) method handles
|
// Now all import globals as getter (and maybe setter) method handles
|
||||||
ctx.cls.fields.addAll(ctx.importGlobals.mapIndexed { index, import ->
|
ctx.cls.fields.addAll(ctx.importGlobals.mapIndexed { index, import ->
|
||||||
if ((import.kind as Node.Import.Kind.Global).type.mutable) throw CompileErr.MutableGlobalImport(index)
|
val getter = FieldNode(Opcodes.ACC_PRIVATE + Opcodes.ACC_FINAL, ctx.importGlobalGetterFieldName(index),
|
||||||
FieldNode(Opcodes.ACC_PRIVATE + Opcodes.ACC_FINAL, ctx.importGlobalGetterFieldName(index),
|
|
||||||
MethodHandle::class.ref.asmDesc, null, null)
|
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
|
// Now all non-import globals
|
||||||
ctx.cls.fields.addAll(ctx.mod.globals.mapIndexed { index, global ->
|
ctx.cls.fields.addAll(ctx.mod.globals.mapIndexed { index, global ->
|
||||||
val access = Opcodes.ACC_PRIVATE + if (!global.type.mutable) Opcodes.ACC_FINAL else 0
|
val access = Opcodes.ACC_PRIVATE + if (!global.type.mutable) Opcodes.ACC_FINAL else 0
|
||||||
@ -180,9 +184,11 @@ open class AstToAsm {
|
|||||||
|
|
||||||
fun constructorImportTypes(ctx: ClsContext) =
|
fun constructorImportTypes(ctx: ClsContext) =
|
||||||
ctx.importFuncs.map { MethodHandle::class.ref } +
|
ctx.importFuncs.map { MethodHandle::class.ref } +
|
||||||
// We know it's only getters
|
ctx.importGlobals.flatMap {
|
||||||
ctx.importGlobals.map { MethodHandle::class.ref } +
|
// If it's mutable, it also comes with a setter
|
||||||
ctx.mod.imports.filter { it.kind is Node.Import.Kind.Table }.map { Array<MethodHandle>::class.ref }
|
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 ->
|
fun toConstructorNode(ctx: ClsContext, func: Func) = mutableListOf<List<AnnotationNode>>().let { paramAnns ->
|
||||||
// If the first param is a mem class and imported, add annotation
|
// If the first param is a mem class and imported, add annotation
|
||||||
@ -199,7 +205,15 @@ open class AstToAsm {
|
|||||||
}
|
}
|
||||||
// All non-mem imports one after another
|
// All non-mem imports one after another
|
||||||
ctx.importFuncs.forEach { paramAnns.add(listOf(importAnnotation(ctx, it))) }
|
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 {
|
ctx.mod.imports.forEach {
|
||||||
if (it.kind is Node.Import.Kind.Table) paramAnns.add(listOf(importAnnotation(ctx, it)))
|
if (it.kind is Node.Import.Kind.Table) paramAnns.add(listOf(importAnnotation(ctx, it)))
|
||||||
}
|
}
|
||||||
@ -240,14 +254,25 @@ open class AstToAsm {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun setConstructorGlobalImports(ctx: ClsContext, func: Func, paramsBeforeImports: Int) =
|
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(
|
func.addInsns(
|
||||||
VarInsnNode(Opcodes.ALOAD, 0),
|
VarInsnNode(Opcodes.ALOAD, 0),
|
||||||
VarInsnNode(Opcodes.ALOAD, ctx.importFuncs.size + importIndex + paramsBeforeImports + 1),
|
VarInsnNode(Opcodes.ALOAD, importParamOffset + 1),
|
||||||
FieldInsnNode(Opcodes.PUTFIELD, ctx.thisRef.asmName,
|
FieldInsnNode(Opcodes.PUTFIELD, ctx.thisRef.asmName,
|
||||||
ctx.importGlobalGetterFieldName(importIndex), MethodHandle::class.ref.asmDesc)
|
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) =
|
fun setConstructorFunctionImports(ctx: ClsContext, func: Func, paramsBeforeImports: Int) =
|
||||||
ctx.importFuncs.indices.fold(func) { func, importIndex ->
|
ctx.importFuncs.indices.fold(func) { func, importIndex ->
|
||||||
@ -261,7 +286,10 @@ open class AstToAsm {
|
|||||||
|
|
||||||
fun setConstructorTableImports(ctx: ClsContext, func: Func, paramsBeforeImports: Int) =
|
fun setConstructorTableImports(ctx: ClsContext, func: Func, paramsBeforeImports: Int) =
|
||||||
if (ctx.mod.imports.none { it.kind is Node.Import.Kind.Table }) func else {
|
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(
|
func.addInsns(
|
||||||
VarInsnNode(Opcodes.ALOAD, 0),
|
VarInsnNode(Opcodes.ALOAD, 0),
|
||||||
VarInsnNode(Opcodes.ALOAD, importIndex),
|
VarInsnNode(Opcodes.ALOAD, importIndex),
|
||||||
@ -299,11 +327,14 @@ open class AstToAsm {
|
|||||||
global.type.contentType.typeRef,
|
global.type.contentType.typeRef,
|
||||||
refGlobalKind.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(
|
listOf(
|
||||||
VarInsnNode(
|
VarInsnNode(Opcodes.ALOAD, paramOffset),
|
||||||
Opcodes.ALOAD,
|
|
||||||
ctx.importFuncs.size + it.index + paramsBeforeImports + 1
|
|
||||||
),
|
|
||||||
MethodInsnNode(
|
MethodInsnNode(
|
||||||
Opcodes.INVOKEVIRTUAL,
|
Opcodes.INVOKEVIRTUAL,
|
||||||
MethodHandle::class.ref.asmName,
|
MethodHandle::class.ref.asmName,
|
||||||
@ -356,7 +387,10 @@ open class AstToAsm {
|
|||||||
// Otherwise, it was imported and we can set the elems on the imported one
|
// Otherwise, it was imported and we can set the elems on the imported one
|
||||||
// from the parameter
|
// from the parameter
|
||||||
// TODO: I think this is a security concern and bad practice, may revisit
|
// 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)).
|
return func.addInsns(VarInsnNode(Opcodes.ALOAD, importIndex)).
|
||||||
let { func -> addElemsToTable(ctx, func, paramsBeforeImports) }.
|
let { func -> addElemsToTable(ctx, func, paramsBeforeImports) }.
|
||||||
// Remove the array that's still there
|
// Remove the array that's still there
|
||||||
@ -532,28 +566,58 @@ open class AstToAsm {
|
|||||||
is Either.Left -> (global.v.kind as Node.Import.Kind.Global).type
|
is Either.Left -> (global.v.kind as Node.Import.Kind.Global).type
|
||||||
is Either.Right -> global.v.type
|
is Either.Right -> global.v.type
|
||||||
}
|
}
|
||||||
if (type.mutable) throw CompileErr.MutableGlobalExport(export.index)
|
|
||||||
// Create a simple getter
|
// 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)
|
"()" + type.contentType.typeRef.asmDesc, null, null)
|
||||||
method.addInsns(VarInsnNode(Opcodes.ALOAD, 0))
|
getter.addInsns(VarInsnNode(Opcodes.ALOAD, 0))
|
||||||
if (global is Either.Left) method.addInsns(
|
if (global is Either.Left) getter.addInsns(
|
||||||
FieldInsnNode(Opcodes.GETFIELD, ctx.thisRef.asmName,
|
FieldInsnNode(Opcodes.GETFIELD, ctx.thisRef.asmName,
|
||||||
ctx.importGlobalGetterFieldName(export.index), MethodHandle::class.ref.asmDesc),
|
ctx.importGlobalGetterFieldName(export.index), MethodHandle::class.ref.asmDesc),
|
||||||
MethodInsnNode(Opcodes.INVOKEVIRTUAL, MethodHandle::class.ref.asmName, "invokeExact",
|
MethodInsnNode(Opcodes.INVOKEVIRTUAL, MethodHandle::class.ref.asmName, "invokeExact",
|
||||||
"()" + type.contentType.typeRef.asmDesc, false)
|
"()" + type.contentType.typeRef.asmDesc, false)
|
||||||
) else method.addInsns(
|
) else getter.addInsns(
|
||||||
FieldInsnNode(Opcodes.GETFIELD, ctx.thisRef.asmName, ctx.globalName(export.index),
|
FieldInsnNode(Opcodes.GETFIELD, ctx.thisRef.asmName, ctx.globalName(export.index),
|
||||||
type.contentType.typeRef.asmDesc)
|
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.I32 -> Opcodes.IRETURN
|
||||||
Node.Type.Value.I64 -> Opcodes.LRETURN
|
Node.Type.Value.I64 -> Opcodes.LRETURN
|
||||||
Node.Type.Value.F32 -> Opcodes.FRETURN
|
Node.Type.Value.F32 -> Opcodes.FRETURN
|
||||||
Node.Type.Value.F64 -> Opcodes.DRETURN
|
Node.Type.Value.F64 -> Opcodes.DRETURN
|
||||||
}))
|
}))
|
||||||
method.visibleAnnotations = listOf(exportAnnotation(export))
|
getter.visibleAnnotations = listOf(exportAnnotation(export))
|
||||||
ctx.cls.methods.plusAssign(method)
|
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) {
|
fun addExportMemory(ctx: ClsContext, export: Node.Export) {
|
||||||
|
@ -46,16 +46,25 @@ open class ByteBufferMem(val direct: Boolean = true) : Mem {
|
|||||||
let(buildOffset).popExpecting(Int::class.ref).
|
let(buildOffset).popExpecting(Int::class.ref).
|
||||||
addInsns(
|
addInsns(
|
||||||
forceFnType<ByteBuffer.(Int) -> Buffer>(ByteBuffer::position).invokeVirtual(),
|
forceFnType<ByteBuffer.(Int) -> Buffer>(ByteBuffer::position).invokeVirtual(),
|
||||||
TypeInsnNode(Opcodes.CHECKCAST, memType.asmName),
|
TypeInsnNode(Opcodes.CHECKCAST, memType.asmName)
|
||||||
// We're going to do this as an LDC string in ISO-8859 and read it back at runtime
|
).addInsns(
|
||||||
LdcInsnNode(bytes.toString(Charsets.ISO_8859_1)),
|
// We're going to do this as an LDC string in ISO-8859 and read it back at runtime. However,
|
||||||
LdcInsnNode("ISO-8859-1"),
|
// due to JVM limits, we can't have a string > 65536 chars. We chunk into 16300 because when
|
||||||
// Ug, can't do func refs on native types here...
|
// converting to UTF8 const it can be up to 4 bytes per char, so this makes sure it doesn't
|
||||||
MethodInsnNode(Opcodes.INVOKEVIRTUAL, String::class.ref.asmName,
|
// overflow.
|
||||||
"getBytes", "(Ljava/lang/String;)[B", false),
|
bytes.chunked(16300).flatMap { bytes ->
|
||||||
0.const,
|
sequenceOf(
|
||||||
bytes.size.const,
|
LdcInsnNode(bytes.toString(Charsets.ISO_8859_1)),
|
||||||
forceFnType<ByteBuffer.(ByteArray, Int, Int) -> ByteBuffer>(ByteBuffer::put).invokeVirtual(),
|
LdcInsnNode("ISO-8859-1"),
|
||||||
|
// Ug, can't do func refs on native types here...
|
||||||
|
MethodInsnNode(Opcodes.INVOKEVIRTUAL, String::class.ref.asmName,
|
||||||
|
"getBytes", "(Ljava/lang/String;)[B", false),
|
||||||
|
0.const,
|
||||||
|
bytes.size.const,
|
||||||
|
forceFnType<ByteBuffer.(ByteArray, Int, Int) -> ByteBuffer>(ByteBuffer::put).invokeVirtual()
|
||||||
|
)
|
||||||
|
}.toList()
|
||||||
|
).addInsns(
|
||||||
InsnNode(Opcodes.POP)
|
InsnNode(Opcodes.POP)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -39,6 +39,24 @@ data class ClsContext(
|
|||||||
val hasTable: Boolean by lazy {
|
val hasTable: Boolean by lazy {
|
||||||
mod.tables.isNotEmpty() || mod.imports.any { it.kind is Node.Import.Kind.Table }
|
mod.tables.isNotEmpty() || mod.imports.any { it.kind is Node.Import.Kind.Table }
|
||||||
}
|
}
|
||||||
|
val dedupedFuncNames: Map<Int, String>? by lazy {
|
||||||
|
// Consider all exports as seen
|
||||||
|
val seen = mod.exports.flatMap { export ->
|
||||||
|
when {
|
||||||
|
export.kind == Node.ExternalKind.FUNCTION -> listOf(export.field.javaIdent)
|
||||||
|
// Just to make it easy, consider all globals as having setters
|
||||||
|
export.kind == Node.ExternalKind.GLOBAL ->
|
||||||
|
export.field.javaIdent.capitalize().let { listOf("get$it", "set$it") }
|
||||||
|
else -> listOf("get" + export.field.javaIdent.capitalize())
|
||||||
|
}
|
||||||
|
}.toMutableSet()
|
||||||
|
mod.names?.funcNames?.toList()?.sortedBy { it.first }?.map { (index, origName) ->
|
||||||
|
var name = origName.javaIdent
|
||||||
|
var nameIndex = 0
|
||||||
|
while (!seen.add(name)) name = origName.javaIdent + (nameIndex++)
|
||||||
|
index to name
|
||||||
|
}?.toMap()
|
||||||
|
}
|
||||||
|
|
||||||
fun assertHasMemory() { if (!hasMemory) throw CompileErr.UnknownMemory(0) }
|
fun assertHasMemory() { if (!hasMemory) throw CompileErr.UnknownMemory(0) }
|
||||||
|
|
||||||
@ -71,7 +89,7 @@ data class ClsContext(
|
|||||||
fun importGlobalGetterFieldName(index: Int) = "import\$get" + globalName(index)
|
fun importGlobalGetterFieldName(index: Int) = "import\$get" + globalName(index)
|
||||||
fun importGlobalSetterFieldName(index: Int) = "import\$set" + globalName(index)
|
fun importGlobalSetterFieldName(index: Int) = "import\$set" + globalName(index)
|
||||||
fun globalName(index: Int) = "\$global$index"
|
fun globalName(index: Int) = "\$global$index"
|
||||||
fun funcName(index: Int) = "\$func$index"
|
fun funcName(index: Int) = dedupedFuncNames?.get(index) ?: "\$func$index"
|
||||||
|
|
||||||
private fun syntheticFunc(
|
private fun syntheticFunc(
|
||||||
nameSuffix: String,
|
nameSuffix: String,
|
||||||
|
@ -102,18 +102,6 @@ sealed class CompileErr(message: String, cause: Throwable? = null) : RuntimeExce
|
|||||||
override val asmErrString get() = "global 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 GlobalInitNotConstant(
|
class GlobalInitNotConstant(
|
||||||
val index: Int
|
val index: Int
|
||||||
) : CompileErr("Expected init for global $index to be single constant value") {
|
) : CompileErr("Expected init for global $index to be single constant value") {
|
||||||
|
@ -4,6 +4,24 @@ import asmble.ast.Node
|
|||||||
import org.objectweb.asm.Opcodes
|
import org.objectweb.asm.Opcodes
|
||||||
import org.objectweb.asm.tree.*
|
import org.objectweb.asm.tree.*
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Jvm representation of a function.
|
||||||
|
*
|
||||||
|
* @param name Name of the fn.
|
||||||
|
* @param params List of parameters of the fn.
|
||||||
|
* @param ret Type of the fn returner value.
|
||||||
|
* @param access The value of the access_flags item is a mask of flags used to
|
||||||
|
* denote access permissions to and properties of this class or
|
||||||
|
* interface [https://docs.oracle.com/javase/specs/jvms/se8/html/jvms-4.html#jvms-4.1-200-E.1].
|
||||||
|
* @param insns List of nodes that represents a bytecode instruction.
|
||||||
|
* @param stack A stack of operand types. Mirror of the operand stack(jvm stack)
|
||||||
|
* where types of operands instead operands.
|
||||||
|
* @param blockStack List of blocks of code
|
||||||
|
* @param ifStack Contains index of [org.objectweb.asm.tree.JumpInsnNode] that
|
||||||
|
* has a null label initially
|
||||||
|
* @param lastStackIsMemLeftover If there is the memory on the stack and we need it
|
||||||
|
* in the future, we mark it as leftover and reuse
|
||||||
|
*/
|
||||||
data class Func(
|
data class Func(
|
||||||
val name: String,
|
val name: String,
|
||||||
val params: List<TypeRef> = emptyList(),
|
val params: List<TypeRef> = emptyList(),
|
||||||
@ -12,7 +30,6 @@ 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 initially
|
|
||||||
val ifStack: List<Int> = emptyList(),
|
val ifStack: List<Int> = emptyList(),
|
||||||
val lastStackIsMemLeftover: Boolean = false
|
val lastStackIsMemLeftover: Boolean = false
|
||||||
) {
|
) {
|
||||||
@ -110,10 +127,11 @@ data class Func(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun pushBlock(insn: Node.Instr, labelType: Node.Type.Value?, endType: Node.Type.Value?) =
|
/** Creates new block with specified instruction and pushes it into the blockStack.*/
|
||||||
|
fun pushBlock(insn: Node.Instr, labelType: Node.Type.Value?, endType: Node.Type.Value?): Func =
|
||||||
pushBlock(insn, listOfNotNull(labelType?.typeRef), listOfNotNull(endType?.typeRef))
|
pushBlock(insn, listOfNotNull(labelType?.typeRef), listOfNotNull(endType?.typeRef))
|
||||||
|
|
||||||
fun pushBlock(insn: Node.Instr, labelTypes: List<TypeRef>, endTypes: List<TypeRef>) =
|
fun pushBlock(insn: Node.Instr, labelTypes: List<TypeRef>, endTypes: List<TypeRef>): Func =
|
||||||
copy(blockStack = blockStack + Block(insn, insns.size, stack, labelTypes, endTypes))
|
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()
|
||||||
@ -127,6 +145,22 @@ data class Func(
|
|||||||
|
|
||||||
fun popIf() = copy(ifStack = ifStack.dropLast(1)) to peekIf()
|
fun popIf() = copy(ifStack = ifStack.dropLast(1)) to peekIf()
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Representation of code block.
|
||||||
|
*
|
||||||
|
* Blocks are composed of matched pairs of `block ... end` instructions, loops
|
||||||
|
* with matched pairs of `loop ... end` instructions, and ifs with either
|
||||||
|
* `if ... end` or if ... else ... end sequences. For each of these constructs
|
||||||
|
* the instructions in the ellipsis are said to be enclosed in the construct.
|
||||||
|
*
|
||||||
|
* @param isns Start instruction of this block, might be a 'Block', 'Loop'
|
||||||
|
* or 'If'
|
||||||
|
* @param startIndex Index of start instruction of this block in list of all
|
||||||
|
* instructions
|
||||||
|
* @param origStack Current block stack of operand types.
|
||||||
|
* @param labelTypes A type of label for this block
|
||||||
|
* @param endTypes A type of block return value
|
||||||
|
*/
|
||||||
class Block(
|
class Block(
|
||||||
val insn: Node.Instr,
|
val insn: Node.Instr,
|
||||||
val startIndex: Int,
|
val startIndex: Int,
|
||||||
|
@ -14,23 +14,31 @@ import java.lang.invoke.MethodHandle
|
|||||||
// TODO: modularize
|
// TODO: modularize
|
||||||
|
|
||||||
open class FuncBuilder {
|
open class FuncBuilder {
|
||||||
fun fromFunc(ctx: ClsContext, f: Node.Func, index: Int): Func {
|
|
||||||
|
/**
|
||||||
|
* Converts wasm AST [asmble.ast.Node.Func] to Jvm bytecode representation [asmble.compile.jvm.Func].
|
||||||
|
*
|
||||||
|
* @param ctx A Global context for converting.
|
||||||
|
* @param fn AST of wasm fn.
|
||||||
|
* @param index Fn index, used for generating fn name
|
||||||
|
*/
|
||||||
|
fun fromFunc(ctx: ClsContext, fn: Node.Func, index: Int): Func {
|
||||||
ctx.debug { "Building function ${ctx.funcName(index)}" }
|
ctx.debug { "Building function ${ctx.funcName(index)}" }
|
||||||
ctx.trace { "Function ast:\n${SExprToStr.fromSExpr(AstToSExpr.fromFunc(f))}" }
|
ctx.trace { "Function ast:\n${SExprToStr.fromSExpr(AstToSExpr.fromFunc(fn))}" }
|
||||||
var func = Func(
|
var func = Func(
|
||||||
access = Opcodes.ACC_PRIVATE,
|
access = Opcodes.ACC_PRIVATE,
|
||||||
name = ctx.funcName(index),
|
name = ctx.funcName(index),
|
||||||
params = f.type.params.map(Node.Type.Value::typeRef),
|
params = fn.type.params.map(Node.Type.Value::typeRef),
|
||||||
ret = f.type.ret?.let(Node.Type.Value::typeRef) ?: Void::class.ref
|
ret = fn.type.ret?.let(Node.Type.Value::typeRef) ?: Void::class.ref
|
||||||
)
|
)
|
||||||
// Rework the instructions
|
// Rework the instructions
|
||||||
val reworkedInsns = ctx.reworker.rework(ctx, f)
|
val reworkedInsns = ctx.reworker.rework(ctx, fn)
|
||||||
// Start the implicit block
|
// Start the implicit block
|
||||||
func = func.pushBlock(Node.Instr.Block(f.type.ret), f.type.ret, f.type.ret)
|
func = func.pushBlock(Node.Instr.Block(fn.type.ret), fn.type.ret, fn.type.ret)
|
||||||
// Create the context
|
// Create the context
|
||||||
val funcCtx = FuncContext(
|
val funcCtx = FuncContext(
|
||||||
cls = ctx,
|
cls = ctx,
|
||||||
node = f,
|
node = fn,
|
||||||
insns = reworkedInsns,
|
insns = reworkedInsns,
|
||||||
memIsLocalVar =
|
memIsLocalVar =
|
||||||
ctx.reworker.nonAdjacentMemAccesses(reworkedInsns) >= ctx.nonAdjacentMemAccessesRequiringLocalVar
|
ctx.reworker.nonAdjacentMemAccesses(reworkedInsns) >= ctx.nonAdjacentMemAccessesRequiringLocalVar
|
||||||
@ -46,9 +54,9 @@ open class FuncBuilder {
|
|||||||
// Add all instructions
|
// Add all instructions
|
||||||
ctx.debug { "Applying insns for function ${ctx.funcName(index)}" }
|
ctx.debug { "Applying insns for function ${ctx.funcName(index)}" }
|
||||||
// All functions have an implicit block
|
// All functions have an implicit block
|
||||||
func = funcCtx.insns.foldIndexed(func) { index, func, insn ->
|
func = funcCtx.insns.foldIndexed(func) { idx, f, insn ->
|
||||||
ctx.debug { "Applying insn $insn" }
|
ctx.debug { "Applying insn $insn" }
|
||||||
val ret = applyInsn(funcCtx, func, insn, index)
|
val ret = applyInsn(funcCtx, f, insn, idx)
|
||||||
ctx.trace { "Resulting stack: ${ret.stack}"}
|
ctx.trace { "Resulting stack: ${ret.stack}"}
|
||||||
ret
|
ret
|
||||||
}
|
}
|
||||||
@ -56,11 +64,11 @@ open class FuncBuilder {
|
|||||||
// End the implicit block
|
// End the implicit block
|
||||||
val implicitBlock = func.currentBlock
|
val implicitBlock = func.currentBlock
|
||||||
func = applyEnd(funcCtx, func)
|
func = applyEnd(funcCtx, func)
|
||||||
f.type.ret?.typeRef?.also { func = func.popExpecting(it, implicitBlock) }
|
fn.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) {
|
||||||
func = func.addInsns(InsnNode(when (f.type.ret) {
|
func = func.addInsns(InsnNode(when (fn.type.ret) {
|
||||||
null -> Opcodes.RETURN
|
null -> Opcodes.RETURN
|
||||||
Node.Type.Value.I32 -> Opcodes.IRETURN
|
Node.Type.Value.I32 -> Opcodes.IRETURN
|
||||||
Node.Type.Value.I64 -> Opcodes.LRETURN
|
Node.Type.Value.I64 -> Opcodes.LRETURN
|
||||||
@ -72,8 +80,10 @@ open class FuncBuilder {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun applyInsn(ctx: FuncContext, fn: Func, i: Insn, index: Int) = when (i) {
|
fun applyInsn(ctx: FuncContext, fn: Func, i: Insn, index: Int) = when (i) {
|
||||||
|
|
||||||
is Insn.Node ->
|
is Insn.Node ->
|
||||||
applyNodeInsn(ctx, fn, i.insn, index)
|
applyNodeInsn(ctx, fn, i.insn, index)
|
||||||
|
|
||||||
is Insn.ImportFuncRefNeededOnStack ->
|
is Insn.ImportFuncRefNeededOnStack ->
|
||||||
// Func refs are method handle fields
|
// Func refs are method handle fields
|
||||||
fn.addInsns(
|
fn.addInsns(
|
||||||
@ -81,6 +91,7 @@ open class FuncBuilder {
|
|||||||
FieldInsnNode(Opcodes.GETFIELD, ctx.cls.thisRef.asmName,
|
FieldInsnNode(Opcodes.GETFIELD, ctx.cls.thisRef.asmName,
|
||||||
ctx.cls.funcName(i.index), MethodHandle::class.ref.asmDesc)
|
ctx.cls.funcName(i.index), MethodHandle::class.ref.asmDesc)
|
||||||
).push(MethodHandle::class.ref)
|
).push(MethodHandle::class.ref)
|
||||||
|
|
||||||
is Insn.ImportGlobalSetRefNeededOnStack ->
|
is Insn.ImportGlobalSetRefNeededOnStack ->
|
||||||
// Import setters are method handle fields
|
// Import setters are method handle fields
|
||||||
fn.addInsns(
|
fn.addInsns(
|
||||||
@ -88,13 +99,17 @@ open class FuncBuilder {
|
|||||||
FieldInsnNode(Opcodes.GETFIELD, ctx.cls.thisRef.asmName,
|
FieldInsnNode(Opcodes.GETFIELD, ctx.cls.thisRef.asmName,
|
||||||
ctx.cls.importGlobalSetterFieldName(i.index), MethodHandle::class.ref.asmDesc)
|
ctx.cls.importGlobalSetterFieldName(i.index), MethodHandle::class.ref.asmDesc)
|
||||||
).push(MethodHandle::class.ref)
|
).push(MethodHandle::class.ref)
|
||||||
|
|
||||||
is Insn.ThisNeededOnStack ->
|
is Insn.ThisNeededOnStack ->
|
||||||
|
// load a reference onto the stack from a local variable
|
||||||
fn.addInsns(VarInsnNode(Opcodes.ALOAD, 0)).push(ctx.cls.thisRef)
|
fn.addInsns(VarInsnNode(Opcodes.ALOAD, 0)).push(ctx.cls.thisRef)
|
||||||
|
|
||||||
is Insn.MemNeededOnStack ->
|
is Insn.MemNeededOnStack ->
|
||||||
putMemoryOnStack(ctx, fn)
|
putMemoryOnStack(ctx, fn)
|
||||||
}
|
}
|
||||||
|
|
||||||
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")).markUnreachable()
|
fn.addInsns(UnsupportedOperationException::class.athrow("Unreachable")).markUnreachable()
|
||||||
is Node.Instr.Nop ->
|
is Node.Instr.Nop ->
|
||||||
@ -127,18 +142,16 @@ open class FuncBuilder {
|
|||||||
fn.pop().let { (fn, popped) ->
|
fn.pop().let { (fn, popped) ->
|
||||||
fn.addInsns(InsnNode(if (popped.stackSize == 2) Opcodes.POP2 else Opcodes.POP))
|
fn.addInsns(InsnNode(if (popped.stackSize == 2) Opcodes.POP2 else Opcodes.POP))
|
||||||
}
|
}
|
||||||
is Node.Instr.Select ->
|
is Node.Instr.Select -> applySelectInsn(ctx, fn)
|
||||||
applySelectInsn(ctx, fn)
|
|
||||||
is Node.Instr.GetLocal ->
|
// Variable access
|
||||||
applyGetLocal(ctx, fn, i.index)
|
is Node.Instr.GetLocal -> applyGetLocal(ctx, fn, i.index)
|
||||||
is Node.Instr.SetLocal ->
|
is Node.Instr.SetLocal -> applySetLocal(ctx, fn, i.index)
|
||||||
applySetLocal(ctx, fn, i.index)
|
is Node.Instr.TeeLocal -> applyTeeLocal(ctx, fn, i.index)
|
||||||
is Node.Instr.TeeLocal ->
|
is Node.Instr.GetGlobal -> applyGetGlobal(ctx, fn, i.index)
|
||||||
applyTeeLocal(ctx, fn, i.index)
|
is Node.Instr.SetGlobal -> applySetGlobal(ctx, fn, i.index)
|
||||||
is Node.Instr.GetGlobal ->
|
|
||||||
applyGetGlobal(ctx, fn, i.index)
|
// Memory operators
|
||||||
is Node.Instr.SetGlobal ->
|
|
||||||
applySetGlobal(ctx, fn, i.index)
|
|
||||||
is Node.Instr.I32Load, is Node.Instr.I64Load, is Node.Instr.F32Load, is Node.Instr.F64Load,
|
is Node.Instr.I32Load, is Node.Instr.I64Load, is Node.Instr.F32Load, is Node.Instr.F64Load,
|
||||||
is Node.Instr.I32Load8S, is Node.Instr.I32Load8U, is Node.Instr.I32Load16U, is Node.Instr.I32Load16S,
|
is Node.Instr.I32Load8S, is Node.Instr.I32Load8U, is Node.Instr.I32Load16U, is Node.Instr.I32Load16S,
|
||||||
is Node.Instr.I64Load8S, is Node.Instr.I64Load8U, is Node.Instr.I64Load16U, is Node.Instr.I64Load16S,
|
is Node.Instr.I64Load8S, is Node.Instr.I64Load8U, is Node.Instr.I64Load16U, is Node.Instr.I64Load16S,
|
||||||
@ -148,10 +161,10 @@ open class FuncBuilder {
|
|||||||
is Node.Instr.I32Store8, is Node.Instr.I32Store16, is Node.Instr.I64Store8, is Node.Instr.I64Store16,
|
is Node.Instr.I32Store8, is Node.Instr.I32Store16, is Node.Instr.I64Store8, is Node.Instr.I64Store16,
|
||||||
is Node.Instr.I64Store32 ->
|
is Node.Instr.I64Store32 ->
|
||||||
applyStoreOp(ctx, fn, i as Node.Instr.Args.AlignOffset, index)
|
applyStoreOp(ctx, fn, i as Node.Instr.Args.AlignOffset, index)
|
||||||
is Node.Instr.CurrentMemory ->
|
is Node.Instr.MemorySize ->
|
||||||
applyCurrentMemory(ctx, fn)
|
applyMemorySize(ctx, fn)
|
||||||
is Node.Instr.GrowMemory ->
|
is Node.Instr.MemoryGrow ->
|
||||||
applyGrowMemory(ctx, fn)
|
applyMemoryGrow(ctx, fn)
|
||||||
is Node.Instr.I32Const ->
|
is Node.Instr.I32Const ->
|
||||||
fn.addInsns(i.value.const).push(Int::class.ref)
|
fn.addInsns(i.value.const).push(Int::class.ref)
|
||||||
is Node.Instr.I64Const ->
|
is Node.Instr.I64Const ->
|
||||||
@ -461,18 +474,18 @@ open class FuncBuilder {
|
|||||||
|
|
||||||
fun popUntilStackSize(
|
fun popUntilStackSize(
|
||||||
ctx: FuncContext,
|
ctx: FuncContext,
|
||||||
fn: Func,
|
func: Func,
|
||||||
block: Func.Block,
|
block: Func.Block,
|
||||||
untilStackSize: Int,
|
untilStackSize: Int,
|
||||||
keepLast: Boolean
|
keepLast: Boolean
|
||||||
): Func {
|
): Func {
|
||||||
ctx.debug { "For block ${block.insn}, popping until stack size $untilStackSize, keeping last? $keepLast" }
|
ctx.debug { "For block ${block.insn}, popping until stack size $untilStackSize, keeping last? $keepLast" }
|
||||||
// Just get the latest, don't actually pop...
|
// Just get the latest, don't actually pop...
|
||||||
val type = if (keepLast) fn.pop().second else null
|
val type = if (keepLast) func.pop().second else null
|
||||||
return (0 until Math.max(0, fn.stack.size - untilStackSize)).fold(fn) { fn, _ ->
|
return (0 until Math.max(0, func.stack.size - untilStackSize)).fold(func) { fn, _ ->
|
||||||
// Essentially swap and pop if they want to keep the latest
|
// Essentially swap and pop if they want to keep the latest
|
||||||
(if (type != null && fn.stack.size > 1) fn.stackSwap(block) else fn).let { fn ->
|
(if (type != null && fn.stack.size > 1) fn.stackSwap(block) else fn).let { f ->
|
||||||
fn.pop(block).let { (fn, poppedType) ->
|
f.pop(block).let { (fn, poppedType) ->
|
||||||
fn.addInsns(InsnNode(if (poppedType.stackSize == 2) Opcodes.POP2 else Opcodes.POP))
|
fn.addInsns(InsnNode(if (poppedType.stackSize == 2) Opcodes.POP2 else Opcodes.POP))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -1062,24 +1075,26 @@ open class FuncBuilder {
|
|||||||
).push(Int::class.ref)
|
).push(Int::class.ref)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun applyGrowMemory(ctx: FuncContext, fn: Func) =
|
fun applyMemoryGrow(ctx: FuncContext, fn: Func) =
|
||||||
// Grow mem is a special case where the memory ref is already pre-injected on
|
// Grow mem is a special case where the memory ref is already pre-injected on
|
||||||
// the stack before this call. Result is an int.
|
// the stack before this call. Result is an int.
|
||||||
ctx.cls.assertHasMemory().let {
|
ctx.cls.assertHasMemory().let {
|
||||||
ctx.cls.mem.growMemory(ctx, fn)
|
ctx.cls.mem.growMemory(ctx, fn)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun applyCurrentMemory(ctx: FuncContext, fn: Func) =
|
fun applyMemorySize(ctx: FuncContext, fn: Func) =
|
||||||
// Curr mem is not specially injected, so we have to put the memory on the
|
// Curr mem is not specially injected, so we have to put the memory on the
|
||||||
// stack since we need it
|
// stack since we need it
|
||||||
ctx.cls.assertHasMemory().let {
|
ctx.cls.assertHasMemory().let {
|
||||||
putMemoryOnStack(ctx, fn).let { fn -> ctx.cls.mem.currentMemory(ctx, fn) }
|
putMemoryOnStack(ctx, fn).let { fn -> ctx.cls.mem.currentMemory(ctx, fn) }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Store is a special case where the memory ref is already pre-injected on
|
||||||
|
* the stack before this call. But it can have a memory leftover on the stack
|
||||||
|
* so we pop it if we need to
|
||||||
|
*/
|
||||||
fun applyStoreOp(ctx: FuncContext, fn: Func, insn: Node.Instr.Args.AlignOffset, insnIndex: Int) =
|
fun applyStoreOp(ctx: FuncContext, fn: Func, insn: Node.Instr.Args.AlignOffset, insnIndex: Int) =
|
||||||
// Store is a special case where the memory ref is already pre-injected on
|
|
||||||
// the stack before this call. But it can have a memory leftover on the stack
|
|
||||||
// so we pop it if we need to
|
|
||||||
ctx.cls.assertHasMemory().let {
|
ctx.cls.assertHasMemory().let {
|
||||||
ctx.cls.mem.storeOp(ctx, fn, insn).let { fn ->
|
ctx.cls.mem.storeOp(ctx, fn, insn).let { fn ->
|
||||||
// As a special case, if this leaves the mem on the stack
|
// As a special case, if this leaves the mem on the stack
|
||||||
@ -1268,11 +1283,11 @@ open class FuncBuilder {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun applyReturnInsn(ctx: FuncContext, fn: Func): Func {
|
fun applyReturnInsn(ctx: FuncContext, func: Func): Func {
|
||||||
// If the current stakc is unreachable, we consider that our block since it
|
// If the current stack is unreachable, we consider that our block since it
|
||||||
// will pop properly.
|
// will pop properly.
|
||||||
val block = if (fn.currentBlock.unreachable) fn.currentBlock else fn.blockStack.first()
|
val block = if (func.currentBlock.unreachable) func.currentBlock else func.blockStack.first()
|
||||||
popForBlockEscape(ctx, fn, block).let { fn ->
|
popForBlockEscape(ctx, func, block).let { fn ->
|
||||||
return when (ctx.node.type.ret) {
|
return when (ctx.node.type.ret) {
|
||||||
null ->
|
null ->
|
||||||
fn.addInsns(InsnNode(Opcodes.RETURN))
|
fn.addInsns(InsnNode(Opcodes.RETURN))
|
||||||
@ -1284,9 +1299,9 @@ open class FuncBuilder {
|
|||||||
fn.popExpecting(Float::class.ref, block).addInsns(InsnNode(Opcodes.FRETURN))
|
fn.popExpecting(Float::class.ref, block).addInsns(InsnNode(Opcodes.FRETURN))
|
||||||
Node.Type.Value.F64 ->
|
Node.Type.Value.F64 ->
|
||||||
fn.popExpecting(Double::class.ref, block).addInsns(InsnNode(Opcodes.DRETURN))
|
fn.popExpecting(Double::class.ref, block).addInsns(InsnNode(Opcodes.DRETURN))
|
||||||
}.let { fn ->
|
}.let { it ->
|
||||||
if (fn.stack.isNotEmpty()) throw CompileErr.UnusedStackOnReturn(fn.stack)
|
if (it.stack.isNotEmpty()) throw CompileErr.UnusedStackOnReturn(it.stack)
|
||||||
fn.markUnreachable()
|
it.markUnreachable()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -3,6 +3,15 @@ package asmble.compile.jvm
|
|||||||
import asmble.ast.Node
|
import asmble.ast.Node
|
||||||
import asmble.util.Logger
|
import asmble.util.Logger
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Jvm context of execution a function.
|
||||||
|
*
|
||||||
|
* @param cls Class execution context
|
||||||
|
* @param node Ast of this function
|
||||||
|
* @param insns A list of instructions
|
||||||
|
* @param memIsLocalVar If true then function use only local variables and don't load
|
||||||
|
* and store from memory.
|
||||||
|
*/
|
||||||
data class FuncContext(
|
data class FuncContext(
|
||||||
val cls: ClsContext,
|
val cls: ClsContext,
|
||||||
val node: Node.Func,
|
val node: Node.Func,
|
||||||
|
@ -2,6 +2,9 @@ package asmble.compile.jvm
|
|||||||
|
|
||||||
import asmble.ast.Node
|
import asmble.ast.Node
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Does some special manipulations with instruction.
|
||||||
|
*/
|
||||||
open class InsnReworker {
|
open class InsnReworker {
|
||||||
|
|
||||||
fun rework(ctx: ClsContext, func: Node.Func): List<Insn> {
|
fun rework(ctx: ClsContext, func: Node.Func): List<Insn> {
|
||||||
@ -29,6 +32,7 @@ open class InsnReworker {
|
|||||||
// Note, while walking backwards up the insns to find set/tee, we do skip entire
|
// Note, while walking backwards up the insns to find set/tee, we do skip entire
|
||||||
// blocks/loops/if+else combined with "end"
|
// blocks/loops/if+else combined with "end"
|
||||||
var neededEagerLocalIndices = emptySet<Int>()
|
var neededEagerLocalIndices = emptySet<Int>()
|
||||||
|
|
||||||
fun addEagerSetIfNeeded(getInsnIndex: Int, localIndex: Int) {
|
fun addEagerSetIfNeeded(getInsnIndex: Int, localIndex: Int) {
|
||||||
// Within the param range? nothing needed
|
// Within the param range? nothing needed
|
||||||
if (localIndex < func.type.params.size) return
|
if (localIndex < func.type.params.size) return
|
||||||
@ -71,6 +75,7 @@ open class InsnReworker {
|
|||||||
(insn is Insn.Node && insn.insn !is Node.Instr.SetLocal && insn.insn !is Node.Instr.TeeLocal)
|
(insn is Insn.Node && insn.insn !is Node.Instr.SetLocal && insn.insn !is Node.Instr.TeeLocal)
|
||||||
if (needsEagerInit) neededEagerLocalIndices += localIndex
|
if (needsEagerInit) neededEagerLocalIndices += localIndex
|
||||||
}
|
}
|
||||||
|
|
||||||
insns.forEachIndexed { index, insn ->
|
insns.forEachIndexed { index, insn ->
|
||||||
if (insn is Insn.Node && insn.insn is Node.Instr.GetLocal) addEagerSetIfNeeded(index, insn.insn.index)
|
if (insn is Insn.Node && insn.insn is Node.Instr.GetLocal) addEagerSetIfNeeded(index, insn.insn.index)
|
||||||
}
|
}
|
||||||
@ -86,11 +91,18 @@ open class InsnReworker {
|
|||||||
} + insns
|
} + insns
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Puts into instruction list needed instructions for pushing local variables
|
||||||
|
* into the stack and returns list of resulting instructions.
|
||||||
|
*
|
||||||
|
* @param ctx The Execution context
|
||||||
|
* @param insns The original instructions
|
||||||
|
*/
|
||||||
fun injectNeededStackVars(ctx: ClsContext, insns: List<Node.Instr>): List<Insn> {
|
fun injectNeededStackVars(ctx: ClsContext, insns: List<Node.Instr>): List<Insn> {
|
||||||
ctx.trace { "Calculating places to inject needed stack variables" }
|
ctx.trace { "Calculating places to inject needed stack variables" }
|
||||||
// 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
|
||||||
// manips. If there is an insn that needs something so far back,
|
// manips. If there is an ins'n that needs something so far back,
|
||||||
// we calc where it needs to be added and keep a running list of
|
// we calc where it needs to be added and keep a running list of
|
||||||
// insn inserts. Then at the end we settle up.
|
// insn inserts. Then at the end we settle up.
|
||||||
//
|
//
|
||||||
@ -109,6 +121,14 @@ open class InsnReworker {
|
|||||||
// guarantee the value will be in the right order if there are
|
// guarantee the value will be in the right order if there are
|
||||||
// multiple for the same index
|
// multiple for the same index
|
||||||
var insnsToInject = emptyMap<Int, List<Insn>>()
|
var insnsToInject = emptyMap<Int, List<Insn>>()
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This function inject current instruction in stack.
|
||||||
|
*
|
||||||
|
* @param insn The instruction to inject
|
||||||
|
* @param count Number of step back on the stack that should we do for
|
||||||
|
* finding injection index.
|
||||||
|
*/
|
||||||
fun injectBeforeLastStackCount(insn: Insn, count: Int) {
|
fun injectBeforeLastStackCount(insn: Insn, count: Int) {
|
||||||
ctx.trace { "Injecting $insn back $count stack values" }
|
ctx.trace { "Injecting $insn back $count stack values" }
|
||||||
fun inject(index: Int) {
|
fun inject(index: Int) {
|
||||||
@ -148,9 +168,11 @@ open class InsnReworker {
|
|||||||
}
|
}
|
||||||
|
|
||||||
countSoFar += amountChanged
|
countSoFar += amountChanged
|
||||||
if (!foundUnconditionalJump) foundUnconditionalJump = insns[insnIndex].let { insn ->
|
if (!foundUnconditionalJump) {
|
||||||
insn is Node.Instr.Br || insn is Node.Instr.BrTable ||
|
foundUnconditionalJump = insns[insnIndex].let { insn ->
|
||||||
insn is Node.Instr.Unreachable || insn is Node.Instr.Return
|
insn is Node.Instr.Br || insn is Node.Instr.BrTable ||
|
||||||
|
insn is Node.Instr.Unreachable || insn is Node.Instr.Return
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if (countSoFar == count) {
|
if (countSoFar == count) {
|
||||||
ctx.trace { "Found injection point as before insn #$insnIndex" }
|
ctx.trace { "Found injection point as before insn #$insnIndex" }
|
||||||
@ -161,6 +183,7 @@ open class InsnReworker {
|
|||||||
if (!foundUnconditionalJump) throw CompileErr.StackInjectionMismatch(count, insn)
|
if (!foundUnconditionalJump) throw CompileErr.StackInjectionMismatch(count, insn)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var traceStackSize = 0 // Used only for trace
|
||||||
// Go over each insn, determining where to inject
|
// Go over each insn, determining where to inject
|
||||||
insns.forEachIndexed { index, insn ->
|
insns.forEachIndexed { index, insn ->
|
||||||
// Handle special injection cases
|
// Handle special injection cases
|
||||||
@ -194,24 +217,37 @@ open class InsnReworker {
|
|||||||
is Node.Instr.I64Store32 ->
|
is Node.Instr.I64Store32 ->
|
||||||
injectBeforeLastStackCount(Insn.MemNeededOnStack, 2)
|
injectBeforeLastStackCount(Insn.MemNeededOnStack, 2)
|
||||||
// Grow memory requires "mem" before the single param
|
// Grow memory requires "mem" before the single param
|
||||||
is Node.Instr.GrowMemory ->
|
is Node.Instr.MemoryGrow ->
|
||||||
injectBeforeLastStackCount(Insn.MemNeededOnStack, 1)
|
injectBeforeLastStackCount(Insn.MemNeededOnStack, 1)
|
||||||
else -> { }
|
else -> { }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Log some trace output
|
||||||
|
ctx.trace {
|
||||||
|
insnStackDiff(ctx, insn).let {
|
||||||
|
traceStackSize += it
|
||||||
|
"Stack diff is $it for insn #$index $insn, stack size now: $traceStackSize"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Add the current diff
|
// Add the current diff
|
||||||
ctx.trace { "Stack diff is ${insnStackDiff(ctx, insn)} for insn #$index $insn" }
|
|
||||||
stackManips += insnStackDiff(ctx, insn) to index
|
stackManips += insnStackDiff(ctx, insn) to index
|
||||||
}
|
}
|
||||||
|
|
||||||
// Build resulting list
|
// Build resulting list
|
||||||
return insns.foldIndexed(emptyList<Insn>()) { index, ret, insn ->
|
return insns.foldIndexed(emptyList()) { index, ret, insn ->
|
||||||
val injections = insnsToInject[index] ?: emptyList()
|
val injections = insnsToInject[index] ?: emptyList()
|
||||||
ret + injections + Insn.Node(insn)
|
ret + injections + Insn.Node(insn)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun insnStackDiff(ctx: ClsContext, insn: Node.Instr) = when (insn) {
|
/**
|
||||||
|
* Calculate stack difference after calling instruction current instruction.
|
||||||
|
* Returns the difference from stack cursor position before instruction and after.
|
||||||
|
* `N = PUSH_OPS - POP_OPS.` '-n' mean that POP operation will be more than PUSH.
|
||||||
|
* If '0' then stack won't changed.
|
||||||
|
*/
|
||||||
|
fun insnStackDiff(ctx: ClsContext, insn: Node.Instr): Int = when (insn) {
|
||||||
is Node.Instr.Unreachable, is Node.Instr.Nop, is Node.Instr.Block,
|
is Node.Instr.Unreachable, is Node.Instr.Nop, is Node.Instr.Block,
|
||||||
is Node.Instr.Loop, is Node.Instr.If, is Node.Instr.Else,
|
is Node.Instr.Loop, is Node.Instr.If, is Node.Instr.Else,
|
||||||
is Node.Instr.End, is Node.Instr.Br, is Node.Instr.BrIf,
|
is Node.Instr.End, is Node.Instr.Br, is Node.Instr.BrIf,
|
||||||
@ -238,9 +274,9 @@ open class InsnReworker {
|
|||||||
is Node.Instr.I64Load32S, is Node.Instr.I64Load32U -> POP_PARAM + PUSH_RESULT
|
is Node.Instr.I64Load32S, is Node.Instr.I64Load32U -> POP_PARAM + PUSH_RESULT
|
||||||
is Node.Instr.I32Store, is Node.Instr.I64Store, is Node.Instr.F32Store, is Node.Instr.F64Store,
|
is Node.Instr.I32Store, is Node.Instr.I64Store, is Node.Instr.F32Store, is Node.Instr.F64Store,
|
||||||
is Node.Instr.I32Store8, is Node.Instr.I32Store16, is Node.Instr.I64Store8, is Node.Instr.I64Store16,
|
is Node.Instr.I32Store8, is Node.Instr.I32Store16, is Node.Instr.I64Store8, is Node.Instr.I64Store16,
|
||||||
is Node.Instr.I64Store32 -> POP_PARAM
|
is Node.Instr.I64Store32 -> POP_PARAM + POP_PARAM
|
||||||
is Node.Instr.CurrentMemory -> PUSH_RESULT
|
is Node.Instr.MemorySize -> PUSH_RESULT
|
||||||
is Node.Instr.GrowMemory -> POP_PARAM + PUSH_RESULT
|
is Node.Instr.MemoryGrow -> POP_PARAM + PUSH_RESULT
|
||||||
is Node.Instr.I32Const, is Node.Instr.I64Const,
|
is Node.Instr.I32Const, is Node.Instr.I64Const,
|
||||||
is Node.Instr.F32Const, is Node.Instr.F64Const -> PUSH_RESULT
|
is Node.Instr.F32Const, is Node.Instr.F64Const -> PUSH_RESULT
|
||||||
is Node.Instr.I32Add, is Node.Instr.I32Sub, is Node.Instr.I32Mul, is Node.Instr.I32DivS,
|
is Node.Instr.I32Add, is Node.Instr.I32Sub, is Node.Instr.I32Mul, is Node.Instr.I32DivS,
|
||||||
@ -284,16 +320,17 @@ open class InsnReworker {
|
|||||||
is Node.Instr.F64ReinterpretI64 -> POP_PARAM + PUSH_RESULT
|
is Node.Instr.F64ReinterpretI64 -> POP_PARAM + PUSH_RESULT
|
||||||
}
|
}
|
||||||
|
|
||||||
fun nonAdjacentMemAccesses(insns: List<Insn>) = insns.fold(0 to false) { (count, lastCouldHaveMem), insn ->
|
/** Returns number of memory accesses. */
|
||||||
|
fun nonAdjacentMemAccesses(insns: List<Insn>): Int = insns.fold(0 to false) { (count, lastCouldHaveMem), insn ->
|
||||||
val inc =
|
val inc =
|
||||||
if (lastCouldHaveMem) 0
|
if (lastCouldHaveMem) 0
|
||||||
else if (insn == Insn.MemNeededOnStack) 1
|
else if (insn == Insn.MemNeededOnStack) 1
|
||||||
else if (insn is Insn.Node && insn.insn is Node.Instr.CurrentMemory) 1
|
else if (insn is Insn.Node && insn.insn is Node.Instr.MemorySize) 1
|
||||||
else 0
|
else 0
|
||||||
val couldSetMemNext = if (insn !is Insn.Node) false else when (insn.insn) {
|
val couldSetMemNext = if (insn !is Insn.Node) false else when (insn.insn) {
|
||||||
is Node.Instr.I32Store, is Node.Instr.I64Store, is Node.Instr.F32Store, is Node.Instr.F64Store,
|
is Node.Instr.I32Store, is Node.Instr.I64Store, is Node.Instr.F32Store, is Node.Instr.F64Store,
|
||||||
is Node.Instr.I32Store8, is Node.Instr.I32Store16, is Node.Instr.I64Store8, is Node.Instr.I64Store16,
|
is Node.Instr.I32Store8, is Node.Instr.I32Store16, is Node.Instr.I64Store8, is Node.Instr.I64Store16,
|
||||||
is Node.Instr.I64Store32, is Node.Instr.GrowMemory -> true
|
is Node.Instr.I64Store32, is Node.Instr.MemoryGrow -> true
|
||||||
else -> false
|
else -> false
|
||||||
}
|
}
|
||||||
(count + inc) to couldSetMemNext
|
(count + inc) to couldSetMemNext
|
||||||
|
@ -2,14 +2,25 @@ package asmble.compile.jvm
|
|||||||
|
|
||||||
import org.objectweb.asm.Type
|
import org.objectweb.asm.Type
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A Java field or method type. This class can be used to make it easier to
|
||||||
|
* manipulate type and method descriptors.
|
||||||
|
*
|
||||||
|
* @param asm Wrapped [org.objectweb.asm.Type] from asm library
|
||||||
|
*/
|
||||||
data class TypeRef(val asm: Type) {
|
data class TypeRef(val asm: Type) {
|
||||||
|
|
||||||
|
/** The internal name of the class corresponding to this object or array type. */
|
||||||
val asmName: String get() = asm.internalName
|
val asmName: String get() = asm.internalName
|
||||||
|
|
||||||
|
/** The descriptor corresponding to this Java type. */
|
||||||
val asmDesc: String get() = asm.descriptor
|
val asmDesc: String get() = asm.descriptor
|
||||||
|
|
||||||
fun asMethodRetDesc(vararg args: TypeRef) = Type.getMethodDescriptor(asm, *args.map { it.asm }.toTypedArray())
|
/** Size of this type in stack, either 1 or 2 only allowed, where 1 = 2^32` bits */
|
||||||
|
|
||||||
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
|
||||||
|
|
||||||
|
fun asMethodRetDesc(vararg args: TypeRef) = Type.getMethodDescriptor(asm, *args.map { it.asm }.toTypedArray())
|
||||||
|
|
||||||
fun equivalentTo(other: TypeRef) = this == other || this == Unknown || other == Unknown
|
fun equivalentTo(other: TypeRef) = this == other || this == Unknown || other == Unknown
|
||||||
|
|
||||||
object UnknownType
|
object UnknownType
|
||||||
|
@ -5,6 +5,7 @@ import asmble.util.toRawIntBits
|
|||||||
import asmble.util.toRawLongBits
|
import asmble.util.toRawLongBits
|
||||||
import asmble.util.toUnsignedBigInt
|
import asmble.util.toUnsignedBigInt
|
||||||
import asmble.util.toUnsignedLong
|
import asmble.util.toUnsignedLong
|
||||||
|
import java.io.ByteArrayOutputStream
|
||||||
|
|
||||||
open class AstToBinary(val version: Long = 1L) {
|
open class AstToBinary(val version: Long = 1L) {
|
||||||
|
|
||||||
@ -140,6 +141,9 @@ open class AstToBinary(val version: Long = 1L) {
|
|||||||
fromResizableLimits(b, n.limits)
|
fromResizableLimits(b, n.limits)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun fromModule(n: Node.Module) =
|
||||||
|
ByteArrayOutputStream().also { fromModule(ByteWriter.OutputStream(it), n) }.toByteArray()
|
||||||
|
|
||||||
fun fromModule(b: ByteWriter, n: Node.Module) {
|
fun fromModule(b: ByteWriter, n: Node.Module) {
|
||||||
b.writeUInt32(0x6d736100)
|
b.writeUInt32(0x6d736100)
|
||||||
b.writeUInt32(version)
|
b.writeUInt32(version)
|
||||||
@ -160,10 +164,33 @@ open class AstToBinary(val version: Long = 1L) {
|
|||||||
wrapListSection(b, n, 9, n.elems, this::fromElem)
|
wrapListSection(b, n, 9, n.elems, this::fromElem)
|
||||||
wrapListSection(b, n, 10, n.funcs, this::fromFuncBody)
|
wrapListSection(b, n, 10, n.funcs, this::fromFuncBody)
|
||||||
wrapListSection(b, n, 11, n.data, this::fromData)
|
wrapListSection(b, n, 11, n.data, this::fromData)
|
||||||
|
n.names?.also { fromNames(b, it) }
|
||||||
// All other custom sections after the previous
|
// All other custom sections after the previous
|
||||||
n.customSections.filter { it.afterSectionId > 11 }.forEach { fromCustomSection(b, it) }
|
n.customSections.filter { it.afterSectionId > 11 }.forEach { fromCustomSection(b, it) }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun fromNames(b: ByteWriter, n: Node.NameSection) {
|
||||||
|
fun <T> indexMap(b: ByteWriter, map: Map<Int, T>, fn: (T) -> Unit) {
|
||||||
|
b.writeVarUInt32(map.size)
|
||||||
|
map.forEach { index, v -> b.writeVarUInt32(index).also { fn(v) } }
|
||||||
|
}
|
||||||
|
fun nameMap(b: ByteWriter, map: Map<Int, String>) = indexMap(b, map) { b.writeString(it) }
|
||||||
|
b.writeVarUInt7(0)
|
||||||
|
b.withVarUInt32PayloadSizePrepended { b ->
|
||||||
|
b.writeString("name")
|
||||||
|
n.moduleName?.also { moduleName ->
|
||||||
|
b.writeVarUInt7(0)
|
||||||
|
b.withVarUInt32PayloadSizePrepended { b -> b.writeString(moduleName) }
|
||||||
|
}
|
||||||
|
if (n.funcNames.isNotEmpty()) b.writeVarUInt7(1).also {
|
||||||
|
b.withVarUInt32PayloadSizePrepended { b -> nameMap(b, n.funcNames) }
|
||||||
|
}
|
||||||
|
if (n.localNames.isNotEmpty()) b.writeVarUInt7(2).also {
|
||||||
|
b.withVarUInt32PayloadSizePrepended { b -> indexMap(b, n.localNames) { nameMap(b, it) } }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fun fromResizableLimits(b: ByteWriter, n: Node.ResizableLimits) {
|
fun fromResizableLimits(b: ByteWriter, n: Node.ResizableLimits) {
|
||||||
b.writeVarUInt1(n.maximum != null)
|
b.writeVarUInt1(n.maximum != null)
|
||||||
b.writeVarUInt32(n.initial)
|
b.writeVarUInt32(n.initial)
|
||||||
|
@ -62,13 +62,21 @@ open class AstToSExpr(val parensInstrs: Boolean = true) {
|
|||||||
Node.ExternalKind.GLOBAL -> newMulti("global") + v.index
|
Node.ExternalKind.GLOBAL -> newMulti("global") + v.index
|
||||||
}
|
}
|
||||||
|
|
||||||
fun fromFunc(v: Node.Func, name: String? = null, impExp: ImportOrExport? = null) =
|
fun fromFunc(
|
||||||
newMulti("func", name) + impExp?.let(this::fromImportOrExport) + fromFuncSig(v.type) +
|
v: Node.Func,
|
||||||
fromLocals(v.locals) + fromInstrs(v.instructions).unwrapInstrs()
|
name: String? = null,
|
||||||
|
impExp: ImportOrExport? = null,
|
||||||
|
localNames: Map<Int, String> = emptyMap()
|
||||||
|
) =
|
||||||
|
newMulti("func", name) + impExp?.let(this::fromImportOrExport) + fromFuncSig(v.type, localNames) +
|
||||||
|
fromLocals(v.locals, v.type.params.size, localNames) + fromInstrs(v.instructions).unwrapInstrs()
|
||||||
|
|
||||||
fun fromFuncSig(v: Node.Type.Func): List<SExpr> {
|
fun fromFuncSig(v: Node.Type.Func, localNames: Map<Int, String> = emptyMap()): List<SExpr> {
|
||||||
var ret = emptyList<SExpr>()
|
var ret = emptyList<SExpr>()
|
||||||
if (v.params.isNotEmpty()) ret += newMulti("param") + v.params.map(this::fromType)
|
if (v.params.isNotEmpty()) {
|
||||||
|
if (localNames.isEmpty()) ret += newMulti("param") + v.params.map(this::fromType)
|
||||||
|
else ret += v.params.mapIndexed { index, param -> newMulti("param", localNames[index]) + fromType(param) }
|
||||||
|
}
|
||||||
v.ret?.also { ret += newMulti("result") + fromType(it) }
|
v.ret?.also { ret += newMulti("result") + fromType(it) }
|
||||||
return ret
|
return ret
|
||||||
}
|
}
|
||||||
@ -80,8 +88,8 @@ open class AstToSExpr(val parensInstrs: Boolean = true) {
|
|||||||
fun fromGlobalSig(v: Node.Type.Global) =
|
fun fromGlobalSig(v: Node.Type.Global) =
|
||||||
if (v.mutable) newMulti("mut") + fromType(v.contentType) else fromType(v.contentType)
|
if (v.mutable) newMulti("mut") + fromType(v.contentType) else fromType(v.contentType)
|
||||||
|
|
||||||
fun fromImport(v: Node.Import, types: List<Node.Type.Func>) =
|
fun fromImport(v: Node.Import, types: List<Node.Type.Func>, name: String? = null) =
|
||||||
(newMulti("import") + v.module.quoted) + v.field.quoted + fromImportKind(v.kind, types)
|
(newMulti("import") + v.module.quoted) + v.field.quoted + fromImportKind(v.kind, types, name)
|
||||||
|
|
||||||
fun fromImportFunc(v: Node.Import.Kind.Func, types: List<Node.Type.Func>, name: String? = null) =
|
fun fromImportFunc(v: Node.Import.Kind.Func, types: List<Node.Type.Func>, name: String? = null) =
|
||||||
fromImportFunc(types.getOrElse(v.typeIndex) { throw Exception("No type at ${v.typeIndex}") }, name)
|
fromImportFunc(types.getOrElse(v.typeIndex) { throw Exception("No type at ${v.typeIndex}") }, name)
|
||||||
@ -91,11 +99,11 @@ open class AstToSExpr(val parensInstrs: Boolean = true) {
|
|||||||
fun fromImportGlobal(v: Node.Import.Kind.Global, name: String? = null) =
|
fun fromImportGlobal(v: Node.Import.Kind.Global, name: String? = null) =
|
||||||
newMulti("global", name) + fromGlobalSig(v.type)
|
newMulti("global", name) + fromGlobalSig(v.type)
|
||||||
|
|
||||||
fun fromImportKind(v: Node.Import.Kind, types: List<Node.Type.Func>) = when(v) {
|
fun fromImportKind(v: Node.Import.Kind, types: List<Node.Type.Func>, name: String? = null) = when(v) {
|
||||||
is Node.Import.Kind.Func -> fromImportFunc(v, types)
|
is Node.Import.Kind.Func -> fromImportFunc(v, types, name)
|
||||||
is Node.Import.Kind.Table -> fromImportTable(v)
|
is Node.Import.Kind.Table -> fromImportTable(v, name)
|
||||||
is Node.Import.Kind.Memory -> fromImportMemory(v)
|
is Node.Import.Kind.Memory -> fromImportMemory(v, name)
|
||||||
is Node.Import.Kind.Global -> fromImportGlobal(v)
|
is Node.Import.Kind.Global -> fromImportGlobal(v, name)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun fromImportMemory(v: Node.Import.Kind.Memory, name: String? = null) =
|
fun fromImportMemory(v: Node.Import.Kind.Memory, name: String? = null) =
|
||||||
@ -161,8 +169,10 @@ open class AstToSExpr(val parensInstrs: Boolean = true) {
|
|||||||
return listOf(SExpr.Multi(untilNext().first))
|
return listOf(SExpr.Multi(untilNext().first))
|
||||||
}
|
}
|
||||||
|
|
||||||
fun fromLocals(v: List<Node.Type.Value>) =
|
fun fromLocals(v: List<Node.Type.Value>, paramOffset: Int, localNames: Map<Int, String> = emptyMap()) =
|
||||||
if (v.isEmpty()) null else newMulti("local") + v.map(this::fromType)
|
if (v.isEmpty()) emptyList()
|
||||||
|
else if (localNames.isEmpty()) listOf(newMulti("local") + v.map(this::fromType))
|
||||||
|
else v.mapIndexed { index, v -> newMulti("local", localNames[paramOffset + index]) + fromType(v) }
|
||||||
|
|
||||||
fun fromMemory(v: Node.Type.Memory, name: String? = null, impExp: ImportOrExport? = null) =
|
fun fromMemory(v: Node.Type.Memory, name: String? = null, impExp: ImportOrExport? = null) =
|
||||||
newMulti("memory", name) + impExp?.let(this::fromImportOrExport) + fromMemorySig(v)
|
newMulti("memory", name) + impExp?.let(this::fromImportOrExport) + fromMemorySig(v)
|
||||||
@ -175,7 +185,7 @@ open class AstToSExpr(val parensInstrs: Boolean = true) {
|
|||||||
is Script.Cmd.Meta.Output -> newMulti("output", v.name) + v.str
|
is Script.Cmd.Meta.Output -> newMulti("output", v.name) + v.str
|
||||||
}
|
}
|
||||||
|
|
||||||
fun fromModule(v: Node.Module, name: String? = null): SExpr.Multi {
|
fun fromModule(v: Node.Module, name: String? = v.names?.moduleName): SExpr.Multi {
|
||||||
var ret = newMulti("module", name)
|
var ret = newMulti("module", name)
|
||||||
|
|
||||||
// If there is a call_indirect, then we need to output all types in exact order.
|
// If there is a call_indirect, then we need to output all types in exact order.
|
||||||
@ -187,8 +197,14 @@ open class AstToSExpr(val parensInstrs: Boolean = true) {
|
|||||||
v.types.filterIndexed { i, _ -> importIndices.contains(i) } - v.funcs.map { it.type }
|
v.types.filterIndexed { i, _ -> importIndices.contains(i) } - v.funcs.map { it.type }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Keep track of the current function index for names
|
||||||
|
var funcIndex = -1
|
||||||
|
|
||||||
ret += types.map { fromTypeDef(it) }
|
ret += types.map { fromTypeDef(it) }
|
||||||
ret += v.imports.map { fromImport(it, v.types) }
|
ret += v.imports.map {
|
||||||
|
if (it.kind is Node.Import.Kind.Func) funcIndex++
|
||||||
|
fromImport(it, v.types, v.names?.funcNames?.get(funcIndex))
|
||||||
|
}
|
||||||
ret += v.exports.map(this::fromExport)
|
ret += v.exports.map(this::fromExport)
|
||||||
ret += v.tables.map { fromTable(it) }
|
ret += v.tables.map { fromTable(it) }
|
||||||
ret += v.memories.map { fromMemory(it) }
|
ret += v.memories.map { fromMemory(it) }
|
||||||
@ -196,7 +212,14 @@ open class AstToSExpr(val parensInstrs: Boolean = true) {
|
|||||||
ret += v.elems.map(this::fromElem)
|
ret += v.elems.map(this::fromElem)
|
||||||
ret += v.data.map(this::fromData)
|
ret += v.data.map(this::fromData)
|
||||||
ret += v.startFuncIndex?.let(this::fromStart)
|
ret += v.startFuncIndex?.let(this::fromStart)
|
||||||
ret += v.funcs.map { fromFunc(it) }
|
ret += v.funcs.map {
|
||||||
|
funcIndex++
|
||||||
|
fromFunc(
|
||||||
|
v = it,
|
||||||
|
name = v.names?.funcNames?.get(funcIndex),
|
||||||
|
localNames = v.names?.localNames?.get(funcIndex) ?: emptyMap()
|
||||||
|
)
|
||||||
|
}
|
||||||
return ret
|
return ret
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -235,10 +258,8 @@ open class AstToSExpr(val parensInstrs: Boolean = true) {
|
|||||||
if (exp == null) this else this.copy(vals = this.vals + exp)
|
if (exp == null) this else this.copy(vals = this.vals + exp)
|
||||||
private operator fun SExpr.Multi.plus(exps: List<SExpr>?) =
|
private operator fun SExpr.Multi.plus(exps: List<SExpr>?) =
|
||||||
if (exps == null || exps.isEmpty()) this else this.copy(vals = this.vals + exps)
|
if (exps == null || exps.isEmpty()) this else this.copy(vals = this.vals + exps)
|
||||||
private fun newMulti(initSymb: String? = null, initName: String? = null): SExpr.Multi {
|
private fun newMulti(initSymb: String? = null, initName: String? = null) =
|
||||||
initName?.also { require(it.startsWith("$")) }
|
SExpr.Multi() + initSymb + initName?.let { "$$it" }
|
||||||
return SExpr.Multi() + initSymb + initName
|
|
||||||
}
|
|
||||||
private fun List<SExpr.Multi>.unwrapInstrs() =
|
private fun List<SExpr.Multi>.unwrapInstrs() =
|
||||||
if (parensInstrs) this else this.single().vals
|
if (parensInstrs) this else this.single().vals
|
||||||
private val String.quoted get() = fromString(this, true)
|
private val String.quoted get() = fromString(this, true)
|
||||||
|
@ -6,7 +6,8 @@ import java.nio.ByteBuffer
|
|||||||
|
|
||||||
open class BinaryToAst(
|
open class BinaryToAst(
|
||||||
val version: Long = 1L,
|
val version: Long = 1L,
|
||||||
val logger: Logger = Logger.Print(Logger.Level.OFF)
|
val logger: Logger = Logger.Print(Logger.Level.WARN),
|
||||||
|
val includeNameSection: Boolean = true
|
||||||
) : Logger by logger {
|
) : Logger by logger {
|
||||||
|
|
||||||
fun toBlockType(b: ByteReader) = b.readVarInt7().toInt().let {
|
fun toBlockType(b: ByteReader) = b.readVarInt7().toInt().let {
|
||||||
@ -19,6 +20,23 @@ open class BinaryToAst(
|
|||||||
payload = b.readBytes()
|
payload = b.readBytes()
|
||||||
)
|
)
|
||||||
|
|
||||||
|
fun toNameSection(b: ByteReader) = generateSequence {
|
||||||
|
if (b.isEof) null
|
||||||
|
else b.readVarUInt7().toInt() to b.read(b.readVarUInt32AsInt())
|
||||||
|
}.fold(Node.NameSection(null, emptyMap(), emptyMap())) { sect, (type, b) ->
|
||||||
|
fun <T> indexMap(b: ByteReader, fn: (ByteReader) -> T) =
|
||||||
|
b.readList { it.readVarUInt32AsInt() to fn(it) }.let { pairs ->
|
||||||
|
pairs.toMap().also { require(it.size == pairs.size) { "Malformed names: duplicate indices" } }
|
||||||
|
}
|
||||||
|
fun nameMap(b: ByteReader) = indexMap(b) { it.readString() }
|
||||||
|
when (type) {
|
||||||
|
0 -> sect.copy(moduleName = b.readString())
|
||||||
|
1 -> sect.copy(funcNames = nameMap(b))
|
||||||
|
2 -> sect.copy(localNames = indexMap(b, ::nameMap))
|
||||||
|
else -> error("Malformed names: unrecognized type: $type")
|
||||||
|
}.also { require(b.isEof) }
|
||||||
|
}
|
||||||
|
|
||||||
fun toData(b: ByteReader) = Node.Data(
|
fun toData(b: ByteReader) = Node.Data(
|
||||||
index = b.readVarUInt32AsInt(),
|
index = b.readVarUInt32AsInt(),
|
||||||
offset = toInitExpr(b),
|
offset = toInitExpr(b),
|
||||||
@ -144,12 +162,15 @@ open class BinaryToAst(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun toLocals(b: ByteReader) = b.readVarUInt32AsInt().let { size ->
|
fun toLocals(b: ByteReader): List<Node.Type.Value> {
|
||||||
toValueType(b).let { type -> List(size) { type } }
|
val size = try { b.readVarUInt32AsInt() } catch (e: NumberFormatException) { throw IoErr.InvalidLocalSize(e) }
|
||||||
|
return toValueType(b).let { type -> List(size) { type } }
|
||||||
}
|
}
|
||||||
|
|
||||||
fun toMemoryType(b: ByteReader) = Node.Type.Memory(toResizableLimits(b))
|
fun toMemoryType(b: ByteReader) = Node.Type.Memory(toResizableLimits(b))
|
||||||
|
|
||||||
|
fun toModule(b: ByteArray) = toModule(ByteReader.InputStream(b.inputStream()))
|
||||||
|
|
||||||
fun toModule(b: ByteReader): Node.Module {
|
fun toModule(b: ByteReader): Node.Module {
|
||||||
if (b.readUInt32() != 0x6d736100L) throw IoErr.InvalidMagicNumber()
|
if (b.readUInt32() != 0x6d736100L) throw IoErr.InvalidMagicNumber()
|
||||||
b.readUInt32().let { if (it != version) throw IoErr.InvalidVersion(it, listOf(version)) }
|
b.readUInt32().let { if (it != version) throw IoErr.InvalidVersion(it, listOf(version)) }
|
||||||
@ -164,14 +185,17 @@ open class BinaryToAst(
|
|||||||
require(sectionId > maxSectionId) { "Section ID $sectionId came after $maxSectionId" }.
|
require(sectionId > maxSectionId) { "Section ID $sectionId came after $maxSectionId" }.
|
||||||
also { maxSectionId = sectionId }
|
also { maxSectionId = sectionId }
|
||||||
val sectionLen = b.readVarUInt32AsInt()
|
val sectionLen = b.readVarUInt32AsInt()
|
||||||
|
// each 'read' invocation creates new InputStream and don't closes it
|
||||||
sections += sectionId to b.read(sectionLen)
|
sections += sectionId to b.read(sectionLen)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Now build the module
|
// Now build the module
|
||||||
fun <T> readSectionList(sectionId: Int, fn: (ByteReader) -> T) =
|
fun <T> readSectionList(sectionId: Int, fn: (ByteReader) -> T) =
|
||||||
sections.find { it.first == sectionId }?.second?.readList(fn) ?: emptyList()
|
sections.find { it.first == sectionId }?.second?.readList(fn) ?: emptyList()
|
||||||
|
|
||||||
val types = readSectionList(1, this::toFuncType)
|
val types = readSectionList(1, this::toFuncType)
|
||||||
val funcIndices = readSectionList(3) { it.readVarUInt32AsInt() }
|
val funcIndices = readSectionList(3) { it.readVarUInt32AsInt() }
|
||||||
|
var nameSection: Node.NameSection? = null
|
||||||
return Node.Module(
|
return Node.Module(
|
||||||
types = types,
|
types = types,
|
||||||
imports = readSectionList(2, this::toImport),
|
imports = readSectionList(2, this::toImport),
|
||||||
@ -192,10 +216,18 @@ open class BinaryToAst(
|
|||||||
val afterSectionId = if (index == 0) 0 else sections[index - 1].let { (prevSectionId, _) ->
|
val afterSectionId = if (index == 0) 0 else sections[index - 1].let { (prevSectionId, _) ->
|
||||||
if (prevSectionId == 0) customSections.last().afterSectionId else prevSectionId
|
if (prevSectionId == 0) customSections.last().afterSectionId else prevSectionId
|
||||||
}
|
}
|
||||||
customSections + toCustomSection(b, afterSectionId)
|
// Try to parse the name section
|
||||||
|
val section = toCustomSection(b, afterSectionId).takeIf { section ->
|
||||||
|
val shouldParseNames = includeNameSection && nameSection == null && section.name == "name"
|
||||||
|
!shouldParseNames || try {
|
||||||
|
nameSection = toNameSection(ByteReader.InputStream(section.payload.inputStream()))
|
||||||
|
false
|
||||||
|
} catch (e: Exception) { warn { "Failed parsing name section: $e" }; true }
|
||||||
|
}
|
||||||
|
if (section == null) customSections else customSections + section
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
)
|
).copy(names = nameSection)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun toResizableLimits(b: ByteReader) = b.readVarUInt1().let {
|
fun toResizableLimits(b: ByteReader) = b.readVarUInt1().let {
|
||||||
|
@ -1,12 +1,12 @@
|
|||||||
package asmble.io
|
package asmble.io
|
||||||
|
|
||||||
import asmble.util.toIntExact
|
|
||||||
import asmble.util.toUnsignedBigInt
|
import asmble.util.toUnsignedBigInt
|
||||||
import asmble.util.toUnsignedLong
|
import asmble.util.toUnsignedLong
|
||||||
import java.io.ByteArrayInputStream
|
import java.io.ByteArrayInputStream
|
||||||
import java.io.ByteArrayOutputStream
|
import java.io.ByteArrayOutputStream
|
||||||
import java.math.BigInteger
|
import java.math.BigInteger
|
||||||
|
|
||||||
|
|
||||||
abstract class ByteReader {
|
abstract class ByteReader {
|
||||||
abstract val isEof: Boolean
|
abstract val isEof: Boolean
|
||||||
|
|
||||||
@ -34,27 +34,30 @@ abstract class ByteReader {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun readVarInt7() = readSignedLeb128().let {
|
fun readVarInt7() = readSignedLeb128().let {
|
||||||
require(it >= Byte.MIN_VALUE.toLong() && it <= Byte.MAX_VALUE.toLong())
|
if (it < Byte.MIN_VALUE.toLong() || it > Byte.MAX_VALUE.toLong()) throw IoErr.InvalidLeb128Number()
|
||||||
it.toByte()
|
it.toByte()
|
||||||
}
|
}
|
||||||
|
|
||||||
fun readVarInt32() = readSignedLeb128().toIntExact()
|
fun readVarInt32() = readSignedLeb128().let {
|
||||||
|
if (it < Int.MIN_VALUE.toLong() || it > Int.MAX_VALUE.toLong()) throw IoErr.InvalidLeb128Number()
|
||||||
|
it.toInt()
|
||||||
|
}
|
||||||
|
|
||||||
fun readVarInt64() = readSignedLeb128()
|
fun readVarInt64() = readSignedLeb128(9)
|
||||||
|
|
||||||
fun readVarUInt1() = readUnsignedLeb128().let {
|
fun readVarUInt1() = readUnsignedLeb128().let {
|
||||||
require(it == 1 || it == 0)
|
if (it != 1 && it != 0) throw IoErr.InvalidLeb128Number()
|
||||||
it == 1
|
it == 1
|
||||||
}
|
}
|
||||||
|
|
||||||
fun readVarUInt7() = readUnsignedLeb128().let {
|
fun readVarUInt7() = readUnsignedLeb128().let {
|
||||||
require(it <= 255)
|
if (it > 255) throw IoErr.InvalidLeb128Number()
|
||||||
it.toShort()
|
it.toShort()
|
||||||
}
|
}
|
||||||
|
|
||||||
fun readVarUInt32() = readUnsignedLeb128().toUnsignedLong()
|
fun readVarUInt32() = readUnsignedLeb128().toUnsignedLong()
|
||||||
|
|
||||||
protected fun readUnsignedLeb128(): Int {
|
protected fun readUnsignedLeb128(maxCount: Int = 4): Int {
|
||||||
// Taken from Android source, Apache licensed
|
// Taken from Android source, Apache licensed
|
||||||
var result = 0
|
var result = 0
|
||||||
var cur: Int
|
var cur: Int
|
||||||
@ -63,12 +66,12 @@ abstract class ByteReader {
|
|||||||
cur = readByte().toInt() and 0xff
|
cur = readByte().toInt() and 0xff
|
||||||
result = result or ((cur and 0x7f) shl (count * 7))
|
result = result or ((cur and 0x7f) shl (count * 7))
|
||||||
count++
|
count++
|
||||||
} while (cur and 0x80 == 0x80 && count < 5)
|
} while (cur and 0x80 == 0x80 && count <= maxCount)
|
||||||
if (cur and 0x80 == 0x80) throw NumberFormatException()
|
if (cur and 0x80 == 0x80) throw IoErr.InvalidLeb128Number()
|
||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun readSignedLeb128(): Long {
|
private fun readSignedLeb128(maxCount: Int = 4): Long {
|
||||||
// Taken from Android source, Apache licensed
|
// Taken from Android source, Apache licensed
|
||||||
var result = 0L
|
var result = 0L
|
||||||
var cur: Int
|
var cur: Int
|
||||||
@ -79,12 +82,25 @@ abstract class ByteReader {
|
|||||||
result = result or ((cur and 0x7f).toLong() shl (count * 7))
|
result = result or ((cur and 0x7f).toLong() shl (count * 7))
|
||||||
signBits = signBits shl 7
|
signBits = signBits shl 7
|
||||||
count++
|
count++
|
||||||
} while (cur and 0x80 == 0x80 && count < 10)
|
} while (cur and 0x80 == 0x80 && count <= maxCount)
|
||||||
if (cur and 0x80 == 0x80) throw NumberFormatException()
|
if (cur and 0x80 == 0x80) throw IoErr.InvalidLeb128Number()
|
||||||
|
|
||||||
|
// Check for 64 bit invalid, taken from Apache/MIT licensed:
|
||||||
|
// https://github.com/paritytech/parity-wasm/blob/2650fc14c458c6a252c9dc43dd8e0b14b6d264ff/src/elements/primitives.rs#L351
|
||||||
|
// TODO: probably need 32 bit checks too, but meh, not in the suite
|
||||||
|
if (count > maxCount && maxCount == 9) {
|
||||||
|
if (cur and 0b0100_0000 == 0b0100_0000) {
|
||||||
|
if ((cur or 0b1000_0000).toByte() != (-1).toByte()) throw IoErr.InvalidLeb128Number()
|
||||||
|
} else if (cur != 0) {
|
||||||
|
throw IoErr.InvalidLeb128Number()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if ((signBits shr 1) and result != 0L) result = result or signBits
|
if ((signBits shr 1) and result != 0L) result = result or signBits
|
||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// todo looks like this InputStream isn't ever closed
|
||||||
class InputStream(val ins: java.io.InputStream) : ByteReader() {
|
class InputStream(val ins: java.io.InputStream) : ByteReader() {
|
||||||
private var nextByte: Byte? = null
|
private var nextByte: Byte? = null
|
||||||
private var sawEof = false
|
private var sawEof = false
|
||||||
|
@ -1,7 +1,6 @@
|
|||||||
package asmble.io
|
package asmble.io
|
||||||
|
|
||||||
import asmble.AsmErr
|
import asmble.AsmErr
|
||||||
import java.math.BigInteger
|
|
||||||
|
|
||||||
sealed class IoErr(message: String, cause: Throwable? = null) : RuntimeException(message, cause), AsmErr {
|
sealed class IoErr(message: String, cause: Throwable? = null) : RuntimeException(message, cause), AsmErr {
|
||||||
class UnexpectedEnd : IoErr("Unexpected EOF") {
|
class UnexpectedEnd : IoErr("Unexpected EOF") {
|
||||||
@ -119,4 +118,13 @@ sealed class IoErr(message: String, cause: Throwable? = null) : RuntimeException
|
|||||||
class InvalidUtf8Encoding : IoErr("Some byte sequence was not UTF-8 compatible") {
|
class InvalidUtf8Encoding : IoErr("Some byte sequence was not UTF-8 compatible") {
|
||||||
override val asmErrString get() = "invalid UTF-8 encoding"
|
override val asmErrString get() = "invalid UTF-8 encoding"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class InvalidLeb128Number : IoErr("Invalid LEB128 number") {
|
||||||
|
override val asmErrString get() = "integer representation too long"
|
||||||
|
override val asmErrStrings get() = listOf(asmErrString, "integer too large")
|
||||||
|
}
|
||||||
|
|
||||||
|
class InvalidLocalSize(cause: NumberFormatException) : IoErr("Invalid local size", cause) {
|
||||||
|
override val asmErrString get() = "too many locals"
|
||||||
|
}
|
||||||
}
|
}
|
@ -9,22 +9,27 @@ import asmble.util.*
|
|||||||
import java.io.ByteArrayInputStream
|
import java.io.ByteArrayInputStream
|
||||||
import java.math.BigInteger
|
import java.math.BigInteger
|
||||||
|
|
||||||
typealias NameMap = Map<String, Int>
|
open class SExprToAst(
|
||||||
|
val includeNames: Boolean = true
|
||||||
open class SExprToAst {
|
) {
|
||||||
data class ExprContext(
|
data class ExprContext(
|
||||||
val nameMap: NameMap,
|
val nameMap: NameMap,
|
||||||
val blockDepth: Int = 0,
|
val blockDepth: Int = 0,
|
||||||
val types: List<Node.Type.Func> = emptyList(),
|
val types: List<Node.Type.Func> = emptyList(),
|
||||||
val callIndirectNeverBeforeSeenFuncTypes: MutableList<Node.Type.Func> = mutableListOf()
|
val callIndirectNeverBeforeSeenFuncTypes: MutableList<Node.Type.Func> = mutableListOf()
|
||||||
)
|
) {
|
||||||
|
companion object {
|
||||||
|
val empty = ExprContext(NameMap(emptyMap(), null, null))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
data class FuncResult(
|
data class FuncResult(
|
||||||
val name: String?,
|
val name: String?,
|
||||||
val func: Node.Func,
|
val func: Node.Func,
|
||||||
val importOrExport: ImportOrExport?,
|
val importOrExport: ImportOrExport?,
|
||||||
// These come from call_indirect insns
|
// These come from call_indirect insns
|
||||||
val additionalFuncTypesToAdd: List<Node.Type.Func>
|
val additionalFuncTypesToAdd: List<Node.Type.Func>,
|
||||||
|
val nameMap: NameMap
|
||||||
)
|
)
|
||||||
|
|
||||||
fun toAction(exp: SExpr.Multi): Script.Cmd.Action {
|
fun toAction(exp: SExpr.Multi): Script.Cmd.Action {
|
||||||
@ -36,7 +41,7 @@ open class SExprToAst {
|
|||||||
return when(exp.vals.first().symbolStr()) {
|
return when(exp.vals.first().symbolStr()) {
|
||||||
"invoke" ->
|
"invoke" ->
|
||||||
Script.Cmd.Action.Invoke(name, str, exp.vals.drop(index).map {
|
Script.Cmd.Action.Invoke(name, str, exp.vals.drop(index).map {
|
||||||
toExprMaybe(it as SExpr.Multi, ExprContext(emptyMap()))
|
toExprMaybe(it as SExpr.Multi, ExprContext.empty)
|
||||||
})
|
})
|
||||||
"get" ->
|
"get" ->
|
||||||
Script.Cmd.Action.Get(name, str)
|
Script.Cmd.Action.Get(name, str)
|
||||||
@ -49,7 +54,7 @@ open class SExprToAst {
|
|||||||
return when(exp.vals.first().symbolStr()) {
|
return when(exp.vals.first().symbolStr()) {
|
||||||
"assert_return" ->
|
"assert_return" ->
|
||||||
Script.Cmd.Assertion.Return(toAction(mult),
|
Script.Cmd.Assertion.Return(toAction(mult),
|
||||||
exp.vals.drop(2).map { toExprMaybe(it as SExpr.Multi, ExprContext(emptyMap())) })
|
exp.vals.drop(2).map { toExprMaybe(it as SExpr.Multi, ExprContext.empty) })
|
||||||
"assert_return_canonical_nan" ->
|
"assert_return_canonical_nan" ->
|
||||||
Script.Cmd.Assertion.ReturnNan(toAction(mult), canonical = true)
|
Script.Cmd.Assertion.ReturnNan(toAction(mult), canonical = true)
|
||||||
"assert_return_arithmetic_nan" ->
|
"assert_return_arithmetic_nan" ->
|
||||||
@ -176,7 +181,7 @@ open class SExprToAst {
|
|||||||
var innerCtx = ctx.copy(blockDepth = ctx.blockDepth + 1)
|
var innerCtx = ctx.copy(blockDepth = ctx.blockDepth + 1)
|
||||||
exp.maybeName(opOffset)?.also {
|
exp.maybeName(opOffset)?.also {
|
||||||
opOffset++
|
opOffset++
|
||||||
innerCtx = innerCtx.copy(nameMap = innerCtx.nameMap + ("block:$it" to innerCtx.blockDepth))
|
innerCtx = innerCtx.copy(nameMap = innerCtx.nameMap.add("block", it, innerCtx.blockDepth))
|
||||||
}
|
}
|
||||||
|
|
||||||
val sigs = toBlockSigMaybe(exp, opOffset)
|
val sigs = toBlockSigMaybe(exp, opOffset)
|
||||||
@ -233,7 +238,7 @@ open class SExprToAst {
|
|||||||
var (nameMap, exprsUsed, sig) = toFuncSig(exp, currentIndex, origNameMap, types)
|
var (nameMap, exprsUsed, sig) = toFuncSig(exp, currentIndex, origNameMap, types)
|
||||||
currentIndex += exprsUsed
|
currentIndex += exprsUsed
|
||||||
val locals = exp.repeated("local", currentIndex, { toLocals(it) }).mapIndexed { index, (nameMaybe, vals) ->
|
val locals = exp.repeated("local", currentIndex, { toLocals(it) }).mapIndexed { index, (nameMaybe, vals) ->
|
||||||
nameMaybe?.also { require(vals.size == 1); nameMap += "local:$it" to (index + sig.params.size) }
|
nameMaybe?.also { require(vals.size == 1); nameMap = nameMap.add("local", it, index + sig.params.size) }
|
||||||
vals
|
vals
|
||||||
}
|
}
|
||||||
currentIndex += locals.size
|
currentIndex += locals.size
|
||||||
@ -250,7 +255,8 @@ open class SExprToAst {
|
|||||||
name = name,
|
name = name,
|
||||||
func = Node.Func(sig, locals.flatten(), instrs),
|
func = Node.Func(sig, locals.flatten(), instrs),
|
||||||
importOrExport = maybeImpExp,
|
importOrExport = maybeImpExp,
|
||||||
additionalFuncTypesToAdd = ctx.callIndirectNeverBeforeSeenFuncTypes
|
additionalFuncTypesToAdd = ctx.callIndirectNeverBeforeSeenFuncTypes,
|
||||||
|
nameMap = nameMap
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -268,7 +274,7 @@ open class SExprToAst {
|
|||||||
} else null to offset
|
} else null to offset
|
||||||
var nameMap = origNameMap
|
var nameMap = origNameMap
|
||||||
val params = exp.repeated("param", offset, { toParams(it) }).mapIndexed { index, (nameMaybe, vals) ->
|
val params = exp.repeated("param", offset, { toParams(it) }).mapIndexed { index, (nameMaybe, vals) ->
|
||||||
nameMaybe?.also { require(vals.size == 1); nameMap += "local:$it" to index }
|
nameMaybe?.also { require(vals.size == 1); nameMap = nameMap.add("local", it, index) }
|
||||||
vals
|
vals
|
||||||
}
|
}
|
||||||
val resultExps = exp.repeated("result", offset + params.size, this::toResult)
|
val resultExps = exp.repeated("result", offset + params.size, this::toResult)
|
||||||
@ -395,7 +401,7 @@ open class SExprToAst {
|
|||||||
val maybeName = exp.maybeName(offset + opOffset)
|
val maybeName = exp.maybeName(offset + opOffset)
|
||||||
if (maybeName != null) {
|
if (maybeName != null) {
|
||||||
opOffset++
|
opOffset++
|
||||||
innerCtx = innerCtx.copy(nameMap = innerCtx.nameMap + ("block:$maybeName" to innerCtx.blockDepth))
|
innerCtx = innerCtx.copy(nameMap = innerCtx.nameMap.add("block", maybeName, innerCtx.blockDepth))
|
||||||
}
|
}
|
||||||
val sigs = toBlockSigMaybe(exp, offset + opOffset)
|
val sigs = toBlockSigMaybe(exp, offset + opOffset)
|
||||||
opOffset += sigs.size
|
opOffset += sigs.size
|
||||||
@ -428,7 +434,7 @@ open class SExprToAst {
|
|||||||
opOffset++
|
opOffset++
|
||||||
exp.maybeName(offset + opOffset)?.also {
|
exp.maybeName(offset + opOffset)?.also {
|
||||||
opOffset++
|
opOffset++
|
||||||
innerCtx = innerCtx.copy(nameMap = innerCtx.nameMap + ("block:$it" to ctx.blockDepth))
|
innerCtx = innerCtx.copy(nameMap = innerCtx.nameMap.add("block", it, ctx.blockDepth))
|
||||||
}
|
}
|
||||||
toInstrs(exp, offset + opOffset, innerCtx, false).also {
|
toInstrs(exp, offset + opOffset, innerCtx, false).also {
|
||||||
ret += it.first
|
ret += it.first
|
||||||
@ -522,7 +528,7 @@ open class SExprToAst {
|
|||||||
val exps = exp.vals.mapNotNull { it as? SExpr.Multi }
|
val exps = exp.vals.mapNotNull { it as? SExpr.Multi }
|
||||||
|
|
||||||
// Eagerly build the names (for forward decls)
|
// Eagerly build the names (for forward decls)
|
||||||
val (nameMap, eagerTypes) = toModuleForwardNameMapAndTypes(exps)
|
var (nameMap, eagerTypes) = toModuleForwardNameMapAndTypes(exps)
|
||||||
mod = mod.copy(types = eagerTypes)
|
mod = mod.copy(types = eagerTypes)
|
||||||
|
|
||||||
fun Node.Module.addTypeIfNotPresent(type: Node.Type.Func): Pair<Node.Module, Int> {
|
fun Node.Module.addTypeIfNotPresent(type: Node.Type.Func): Pair<Node.Module, Int> {
|
||||||
@ -555,6 +561,7 @@ open class SExprToAst {
|
|||||||
Node.Import.Kind.Memory(kind) to (memoryCount++ to Node.ExternalKind.MEMORY)
|
Node.Import.Kind.Memory(kind) to (memoryCount++ to Node.ExternalKind.MEMORY)
|
||||||
else -> throw Exception("Unrecognized import kind: $kind")
|
else -> throw Exception("Unrecognized import kind: $kind")
|
||||||
}
|
}
|
||||||
|
|
||||||
mod = mod.copy(
|
mod = mod.copy(
|
||||||
imports = mod.imports + Node.Import(module, field, importKind),
|
imports = mod.imports + Node.Import(module, field, importKind),
|
||||||
exports = mod.exports + exportFields.map {
|
exports = mod.exports + exportFields.map {
|
||||||
@ -579,11 +586,14 @@ open class SExprToAst {
|
|||||||
"elem" -> mod = mod.copy(elems = mod.elems + toElem(exp, nameMap))
|
"elem" -> mod = mod.copy(elems = mod.elems + toElem(exp, nameMap))
|
||||||
"data" -> mod = mod.copy(data = mod.data + toData(exp, nameMap))
|
"data" -> mod = mod.copy(data = mod.data + toData(exp, nameMap))
|
||||||
"start" -> mod = mod.copy(startFuncIndex = toStart(exp, nameMap))
|
"start" -> mod = mod.copy(startFuncIndex = toStart(exp, nameMap))
|
||||||
"func" -> toFunc(exp, nameMap, mod.types).also { (_, fn, impExp, additionalFuncTypes) ->
|
"func" -> toFunc(exp, nameMap, mod.types).also { (_, fn, impExp, additionalFuncTypes, localNameMap) ->
|
||||||
if (impExp is ImportOrExport.Import) {
|
if (impExp is ImportOrExport.Import) {
|
||||||
handleImport(impExp.module, impExp.name, fn.type, impExp.exportFields)
|
handleImport(impExp.module, impExp.name, fn.type, impExp.exportFields)
|
||||||
} else {
|
} else {
|
||||||
if (impExp is ImportOrExport.Export) addExport(impExp, Node.ExternalKind.FUNCTION, funcCount)
|
if (impExp is ImportOrExport.Export) addExport(impExp, Node.ExternalKind.FUNCTION, funcCount)
|
||||||
|
if (includeNames) nameMap = nameMap.copy(
|
||||||
|
localNames = nameMap.localNames!! + (funcCount to localNameMap.getAllNamesByIndex("local"))
|
||||||
|
)
|
||||||
funcCount++
|
funcCount++
|
||||||
mod = mod.copy(funcs = mod.funcs + fn).addTypeIfNotPresent(fn.type).first
|
mod = mod.copy(funcs = mod.funcs + fn).addTypeIfNotPresent(fn.type).first
|
||||||
mod = additionalFuncTypes.fold(mod) { mod, typ -> mod.addTypeIfNotPresent(typ).first }
|
mod = additionalFuncTypes.fold(mod) { mod, typ -> mod.addTypeIfNotPresent(typ).first }
|
||||||
@ -644,6 +654,15 @@ open class SExprToAst {
|
|||||||
if (mod.tables.size + mod.imports.count { it.kind is Node.Import.Kind.Table } > 1)
|
if (mod.tables.size + mod.imports.count { it.kind is Node.Import.Kind.Table } > 1)
|
||||||
throw IoErr.MultipleTables()
|
throw IoErr.MultipleTables()
|
||||||
|
|
||||||
|
// Set the name map pieces if we're including them
|
||||||
|
if (includeNames) mod = mod.copy(
|
||||||
|
names = Node.NameSection(
|
||||||
|
moduleName = name,
|
||||||
|
funcNames = nameMap.funcNames!!,
|
||||||
|
localNames = nameMap.localNames!!
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
return name to mod
|
return name to mod
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -680,10 +699,14 @@ open class SExprToAst {
|
|||||||
var globalCount = 0
|
var globalCount = 0
|
||||||
var tableCount = 0
|
var tableCount = 0
|
||||||
var memoryCount = 0
|
var memoryCount = 0
|
||||||
var namesToIndices = emptyMap<String, Int>()
|
var nameMap = NameMap(
|
||||||
|
names = emptyMap(),
|
||||||
|
funcNames = if (includeNames) emptyMap() else null,
|
||||||
|
localNames = if (includeNames) emptyMap() else null
|
||||||
|
)
|
||||||
var types = emptyList<Node.Type.Func>()
|
var types = emptyList<Node.Type.Func>()
|
||||||
fun maybeAddName(name: String?, index: Int, type: String) {
|
fun maybeAddName(name: String?, index: Int, type: String) {
|
||||||
name?.let { namesToIndices += "$type:$it" to index }
|
name?.also { nameMap = nameMap.add(type, it, index) }
|
||||||
}
|
}
|
||||||
|
|
||||||
// All imports first
|
// All imports first
|
||||||
@ -711,12 +734,12 @@ open class SExprToAst {
|
|||||||
"memory" -> maybeAddName(kindName, memoryCount++, "memory")
|
"memory" -> maybeAddName(kindName, memoryCount++, "memory")
|
||||||
// We go ahead and do the full type def build here eagerly
|
// We go ahead and do the full type def build here eagerly
|
||||||
"type" -> maybeAddName(kindName, types.size, "type").also { _ ->
|
"type" -> maybeAddName(kindName, types.size, "type").also { _ ->
|
||||||
toTypeDef(it, namesToIndices).also { (_, type) -> types += type }
|
toTypeDef(it, nameMap).also { (_, type) -> types += type }
|
||||||
}
|
}
|
||||||
else -> {}
|
else -> {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return namesToIndices to types
|
return nameMap to types
|
||||||
}
|
}
|
||||||
|
|
||||||
fun toOpMaybe(exp: SExpr.Multi, offset: Int, ctx: ExprContext): Pair<Node.Instr, Int>? {
|
fun toOpMaybe(exp: SExpr.Multi, offset: Int, ctx: ExprContext): Pair<Node.Instr, Int>? {
|
||||||
@ -753,7 +776,8 @@ open class SExprToAst {
|
|||||||
// First lookup the func sig
|
// First lookup the func sig
|
||||||
val (updatedNameMap, expsUsed, funcType) = toFuncSig(exp, offset + 1, ctx.nameMap, ctx.types)
|
val (updatedNameMap, expsUsed, funcType) = toFuncSig(exp, offset + 1, ctx.nameMap, ctx.types)
|
||||||
// Make sure there are no changes to the name map
|
// Make sure there are no changes to the name map
|
||||||
if (ctx.nameMap.size != updatedNameMap.size) throw IoErr.IndirectCallSetParamNames()
|
if (ctx.nameMap.size != updatedNameMap.size)
|
||||||
|
throw IoErr.IndirectCallSetParamNames()
|
||||||
// Obtain the func index from the types table, the indirects table, or just add it
|
// Obtain the func index from the types table, the indirects table, or just add it
|
||||||
var funcTypeIndex = ctx.types.indexOf(funcType)
|
var funcTypeIndex = ctx.types.indexOf(funcType)
|
||||||
// If it's not in the type list, check the call indirect list
|
// If it's not in the type list, check the call indirect list
|
||||||
@ -910,7 +934,7 @@ open class SExprToAst {
|
|||||||
fun toVarMaybe(exp: SExpr, nameMap: NameMap, nameType: String): Int? {
|
fun toVarMaybe(exp: SExpr, nameMap: NameMap, nameType: String): Int? {
|
||||||
return exp.symbolStr()?.let { it ->
|
return exp.symbolStr()?.let { it ->
|
||||||
if (it.startsWith("$"))
|
if (it.startsWith("$"))
|
||||||
nameMap["$nameType:$it"] ?:
|
nameMap.get(nameType, it.drop(1)) ?:
|
||||||
throw Exception("Unable to find index for name $it of type $nameType in $nameMap")
|
throw Exception("Unable to find index for name $it of type $nameType in $nameMap")
|
||||||
else if (it.startsWith("0x")) it.substring(2).toIntOrNull(16)
|
else if (it.startsWith("0x")) it.substring(2).toIntOrNull(16)
|
||||||
else it.toIntOrNull()
|
else it.toIntOrNull()
|
||||||
@ -1005,7 +1029,7 @@ open class SExprToAst {
|
|||||||
private fun SExpr.Multi.maybeName(index: Int): String? {
|
private fun SExpr.Multi.maybeName(index: Int): String? {
|
||||||
if (this.vals.size > index && this.vals[index] is SExpr.Symbol) {
|
if (this.vals.size > index && this.vals[index] is SExpr.Symbol) {
|
||||||
val sym = this.vals[index] as SExpr.Symbol
|
val sym = this.vals[index] as SExpr.Symbol
|
||||||
if (!sym.quoted && sym.contents[0] == '$') return sym.contents
|
if (!sym.quoted && sym.contents[0] == '$') return sym.contents.drop(1)
|
||||||
}
|
}
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
@ -1028,5 +1052,26 @@ open class SExprToAst {
|
|||||||
return this.vals.first().requireSymbol(contents, quotedCheck)
|
return this.vals.first().requireSymbol(contents, quotedCheck)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
data class NameMap(
|
||||||
|
// Key prefixed with type then colon before actual name
|
||||||
|
val names: Map<String, Int>,
|
||||||
|
// Null if not including names
|
||||||
|
val funcNames: Map<Int, String>?,
|
||||||
|
val localNames: Map<Int, Map<Int, String>>?
|
||||||
|
) {
|
||||||
|
val size get() = names.size
|
||||||
|
|
||||||
|
fun add(type: String, name: String, index: Int) = copy(
|
||||||
|
names = names + ("$type:$name" to index),
|
||||||
|
funcNames = funcNames?.let { if (type == "func") it + (index to name) else it }
|
||||||
|
)
|
||||||
|
|
||||||
|
fun get(type: String, name: String) = names["$type:$name"]
|
||||||
|
|
||||||
|
fun getAllNamesByIndex(type: String) = names.mapNotNull { (k, v) ->
|
||||||
|
k.takeIf { k.startsWith("$type:") }?.let { v to k.substring(type.length + 1) }
|
||||||
|
}.toMap()
|
||||||
|
}
|
||||||
|
|
||||||
companion object : SExprToAst()
|
companion object : SExprToAst()
|
||||||
}
|
}
|
||||||
|
@ -18,6 +18,13 @@ open class StrToSExpr {
|
|||||||
data class Error(val pos: Pos, val msg: String) : ParseResult()
|
data class Error(val pos: Pos, val msg: String) : ParseResult()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun parseSingleMulti(str: CharSequence) = parse(str).let {
|
||||||
|
when (it) {
|
||||||
|
is ParseResult.Success -> (it.vals.singleOrNull() as? SExpr.Multi) ?: error("Not a single multi-expr")
|
||||||
|
is ParseResult.Error -> error("Failed parsing at ${it.pos.line}:${it.pos.char} - ${it.msg}")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fun parse(str: CharSequence): ParseResult {
|
fun parse(str: CharSequence): ParseResult {
|
||||||
val state = ParseState(str)
|
val state = ParseState(str)
|
||||||
val ret = mutableListOf<SExpr>()
|
val ret = mutableListOf<SExpr>()
|
||||||
|
@ -116,9 +116,9 @@ interface Module {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Global imports
|
// Global imports
|
||||||
val globalImports = mod.imports.mapNotNull {
|
val globalImports = mod.imports.flatMap {
|
||||||
if (it.kind is Node.Import.Kind.Global) ctx.resolveImportGlobal(it, it.kind.type)
|
if (it.kind is Node.Import.Kind.Global) ctx.resolveImportGlobals(it, it.kind.type)
|
||||||
else null
|
else emptyList()
|
||||||
}
|
}
|
||||||
constructorParams += globalImports
|
constructorParams += globalImports
|
||||||
|
|
||||||
|
@ -55,4 +55,12 @@ sealed class RunErr(message: String, cause: Throwable? = null) : RuntimeExceptio
|
|||||||
override val asmErrString get() = "unknown import"
|
override val asmErrString get() = "unknown import"
|
||||||
override val asmErrStrings get() = listOf(asmErrString, "incompatible import type")
|
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"
|
||||||
|
}
|
||||||
}
|
}
|
@ -19,6 +19,20 @@ import java.lang.invoke.MethodType
|
|||||||
import java.lang.reflect.InvocationTargetException
|
import java.lang.reflect.InvocationTargetException
|
||||||
import java.util.*
|
import java.util.*
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Script context. Contains all the information needed to execute this script.
|
||||||
|
*
|
||||||
|
* @param packageName Package name for this script
|
||||||
|
* @param modules List of all modules to be load for this script
|
||||||
|
* @param registrations Registered modules, key - module name, value - module instance
|
||||||
|
* @param logger Logger for this script
|
||||||
|
* @param adjustContext Fn for tuning context (looks redundant)
|
||||||
|
* @param classLoader ClassLoader for loading all classes for this script
|
||||||
|
* @param exceptionTranslator Converts exceptions to error messages
|
||||||
|
* @param defaultMaxMemPages The maximum number of memory pages when a module doesn't say
|
||||||
|
* @param includeBinaryInCompiledClass Store binary wasm code to compiled class
|
||||||
|
* file as annotation [asmble.annotation.WasmModule]
|
||||||
|
*/
|
||||||
data class ScriptContext(
|
data class ScriptContext(
|
||||||
val packageName: String,
|
val packageName: String,
|
||||||
val modules: List<Module.Compiled> = emptyList(),
|
val modules: List<Module.Compiled> = emptyList(),
|
||||||
@ -263,10 +277,12 @@ data class ScriptContext(
|
|||||||
return Module.Compiled(mod, classLoader.fromBuiltContext(ctx), name, ctx.mem)
|
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
|
// Find a method that matches our expectations
|
||||||
val module = registrations[import.module] ?: throw RunErr.ImportNotFound(import.module, import.field)
|
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) {
|
val kind = when (import.kind) {
|
||||||
is Node.Import.Kind.Func -> WasmExternalKind.FUNCTION
|
is Node.Import.Kind.Func -> WasmExternalKind.FUNCTION
|
||||||
is Node.Import.Kind.Table -> WasmExternalKind.TABLE
|
is Node.Import.Kind.Table -> WasmExternalKind.TABLE
|
||||||
@ -281,8 +297,18 @@ data class ScriptContext(
|
|||||||
bindImport(import, false,
|
bindImport(import, false,
|
||||||
MethodType.methodType(funcType.ret?.jclass ?: Void.TYPE, funcType.params.map { it.jclass }))
|
MethodType.methodType(funcType.ret?.jclass ?: Void.TYPE, funcType.params.map { it.jclass }))
|
||||||
|
|
||||||
fun resolveImportGlobal(import: Node.Import, globalType: Node.Type.Global) =
|
fun resolveImportGlobals(import: Node.Import, globalType: Node.Type.Global): List<MethodHandle> {
|
||||||
bindImport(import, true, MethodType.methodType(globalType.contentType.jclass))
|
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) =
|
fun resolveImportMemory(import: Node.Import, memoryType: Node.Type.Memory, mem: Mem) =
|
||||||
bindImport(import, true, MethodType.methodType(Class.forName(mem.memType.asm.className))).
|
bindImport(import, true, MethodType.methodType(Class.forName(mem.memType.asm.className))).
|
||||||
|
@ -13,8 +13,8 @@ class SpecTestUnit(name: String, wast: String, expectedOutput: String?) : BaseTe
|
|||||||
override val shouldFail get() = name.endsWith(".fail")
|
override val shouldFail get() = name.endsWith(".fail")
|
||||||
|
|
||||||
override val defaultMaxMemPages get() = when (name) {
|
override val defaultMaxMemPages get() = when (name) {
|
||||||
"nop"-> 20
|
"nop" -> 20
|
||||||
"resizing" -> 830
|
"memory_grow" -> 830
|
||||||
"imports" -> 5
|
"imports" -> 5
|
||||||
else -> 1
|
else -> 1
|
||||||
}
|
}
|
||||||
|
33
compiler/src/test/kotlin/asmble/ast/StackTest.kt
Normal file
33
compiler/src/test/kotlin/asmble/ast/StackTest.kt
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
package asmble.ast
|
||||||
|
|
||||||
|
import asmble.SpecTestUnit
|
||||||
|
import asmble.TestBase
|
||||||
|
import org.junit.Test
|
||||||
|
import org.junit.runner.RunWith
|
||||||
|
import org.junit.runners.Parameterized
|
||||||
|
|
||||||
|
@RunWith(Parameterized::class)
|
||||||
|
class StackTest(val unit: SpecTestUnit) : TestBase() {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun testStack() {
|
||||||
|
// If it's not a module expecting an error, we'll try to walk the stack on each function
|
||||||
|
unit.script.commands.mapNotNull { it as? Script.Cmd.Module }.forEach { mod ->
|
||||||
|
mod.module.funcs.filter { it.instructions.isNotEmpty() }.forEach { func ->
|
||||||
|
debug { "Func: ${func.type}" }
|
||||||
|
var indexCount = 0
|
||||||
|
Stack.walkStrict(mod.module, func) { stack, insn ->
|
||||||
|
debug { " After $insn (next: ${func.instructions.getOrNull(++indexCount)}, " +
|
||||||
|
"unreach depth: ${stack.unreachableUntilNextEndCount})" }
|
||||||
|
debug { " " + stack.current }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
// Only tests that shouldn't fail
|
||||||
|
@JvmStatic @Parameterized.Parameters(name = "{0}")
|
||||||
|
fun data() = SpecTestUnit.allUnits.filterNot { it.shouldFail }//.filter { it.name == "loop" }
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,89 @@
|
|||||||
|
package asmble.ast.opt
|
||||||
|
|
||||||
|
import asmble.TestBase
|
||||||
|
import asmble.ast.Node
|
||||||
|
import asmble.compile.jvm.AstToAsm
|
||||||
|
import asmble.compile.jvm.ClsContext
|
||||||
|
import asmble.run.jvm.ScriptContext
|
||||||
|
import org.junit.Test
|
||||||
|
import java.nio.ByteBuffer
|
||||||
|
import java.util.*
|
||||||
|
import kotlin.test.assertEquals
|
||||||
|
|
||||||
|
class SplitLargeFuncTest : TestBase() {
|
||||||
|
@Test
|
||||||
|
fun testSplitLargeFunc() {
|
||||||
|
// We're going to make a large function that does some addition and then stores in mem
|
||||||
|
val ctx = ClsContext(
|
||||||
|
packageName = "test",
|
||||||
|
className = "Temp" + UUID.randomUUID().toString().replace("-", ""),
|
||||||
|
logger = logger,
|
||||||
|
mod = Node.Module(
|
||||||
|
memories = listOf(Node.Type.Memory(Node.ResizableLimits(initial = 2, maximum = 2))),
|
||||||
|
funcs = listOf(Node.Func(
|
||||||
|
type = Node.Type.Func(params = emptyList(), ret = null),
|
||||||
|
locals = emptyList(),
|
||||||
|
instructions = (0 until 501).flatMap {
|
||||||
|
listOf<Node.Instr>(
|
||||||
|
Node.Instr.I32Const(it * 4),
|
||||||
|
// Let's to i * (i = 1)
|
||||||
|
Node.Instr.I32Const(it),
|
||||||
|
Node.Instr.I32Const(it - 1),
|
||||||
|
Node.Instr.I32Mul,
|
||||||
|
Node.Instr.I32Store(0, 0)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
)),
|
||||||
|
names = Node.NameSection(
|
||||||
|
moduleName = null,
|
||||||
|
funcNames = mapOf(0 to "someFunc"),
|
||||||
|
localNames = emptyMap()
|
||||||
|
),
|
||||||
|
exports = listOf(
|
||||||
|
Node.Export("memory", Node.ExternalKind.MEMORY, 0),
|
||||||
|
Node.Export("someFunc", Node.ExternalKind.FUNCTION, 0)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
// Compile it
|
||||||
|
AstToAsm.fromModule(ctx)
|
||||||
|
val cls = ScriptContext.SimpleClassLoader(javaClass.classLoader, logger).fromBuiltContext(ctx)
|
||||||
|
val inst = cls.newInstance()
|
||||||
|
// Run someFunc
|
||||||
|
cls.getMethod("someFunc").invoke(inst)
|
||||||
|
// Get the memory out
|
||||||
|
val mem = cls.getMethod("getMemory").invoke(inst) as ByteBuffer
|
||||||
|
// Read out the mem values
|
||||||
|
(0 until 501).forEach { assertEquals(it * (it - 1), mem.getInt(it * 4)) }
|
||||||
|
|
||||||
|
// Now split it
|
||||||
|
val (splitMod, insnsSaved) = SplitLargeFunc.apply(ctx.mod, 0) ?: error("Nothing could be split")
|
||||||
|
// Count insns and confirm it is as expected
|
||||||
|
val origInsnCount = ctx.mod.funcs.sumBy { it.instructions.size }
|
||||||
|
val newInsnCount = splitMod.funcs.sumBy { it.instructions.size }
|
||||||
|
assertEquals(origInsnCount - newInsnCount, insnsSaved)
|
||||||
|
// Compile it
|
||||||
|
val splitCtx = ClsContext(
|
||||||
|
packageName = "test",
|
||||||
|
className = "Temp" + UUID.randomUUID().toString().replace("-", ""),
|
||||||
|
logger = logger,
|
||||||
|
mod = splitMod
|
||||||
|
)
|
||||||
|
AstToAsm.fromModule(splitCtx)
|
||||||
|
val splitCls = ScriptContext.SimpleClassLoader(javaClass.classLoader, logger).fromBuiltContext(splitCtx)
|
||||||
|
val splitInst = splitCls.newInstance()
|
||||||
|
// Run someFunc
|
||||||
|
splitCls.getMethod("someFunc").invoke(splitInst)
|
||||||
|
// Get the memory out and compare it
|
||||||
|
val splitMem = splitCls.getMethod("getMemory").invoke(splitInst) as ByteBuffer
|
||||||
|
assertEquals(mem, splitMem)
|
||||||
|
// Dump some info
|
||||||
|
logger.debug {
|
||||||
|
val orig = ctx.mod.funcs.first()
|
||||||
|
val (new, split) = splitMod.funcs.let { it.first() to it.last() }
|
||||||
|
"Split complete, from single func with ${orig.instructions.size} insns to func " +
|
||||||
|
"with ${new.instructions.size} insns + delegated func " +
|
||||||
|
"with ${split.instructions.size} insns and ${split.type.params.size} params"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
45
compiler/src/test/kotlin/asmble/compile/jvm/LargeDataTest.kt
Normal file
45
compiler/src/test/kotlin/asmble/compile/jvm/LargeDataTest.kt
Normal file
@ -0,0 +1,45 @@
|
|||||||
|
package asmble.compile.jvm
|
||||||
|
|
||||||
|
import asmble.TestBase
|
||||||
|
import asmble.ast.Node
|
||||||
|
import asmble.run.jvm.ScriptContext
|
||||||
|
import asmble.util.get
|
||||||
|
import org.junit.Test
|
||||||
|
import java.nio.ByteBuffer
|
||||||
|
import java.util.*
|
||||||
|
import kotlin.test.assertEquals
|
||||||
|
|
||||||
|
class LargeDataTest : TestBase() {
|
||||||
|
@Test
|
||||||
|
fun testLargeData() {
|
||||||
|
// This previously failed because string constants can't be longer than 65536 chars.
|
||||||
|
// We create a byte array across the whole gambit of bytes to test UTF8 encoding.
|
||||||
|
val bytesExpected = ByteArray(70000) { ((it % 255) - Byte.MIN_VALUE).toByte() }
|
||||||
|
val mod = Node.Module(
|
||||||
|
memories = listOf(Node.Type.Memory(
|
||||||
|
limits = Node.ResizableLimits(initial = 2, maximum = 2)
|
||||||
|
)),
|
||||||
|
data = listOf(Node.Data(
|
||||||
|
index = 0,
|
||||||
|
offset = listOf(Node.Instr.I32Const(0)),
|
||||||
|
data = bytesExpected
|
||||||
|
))
|
||||||
|
)
|
||||||
|
val ctx = ClsContext(
|
||||||
|
packageName = "test",
|
||||||
|
className = "Temp" + UUID.randomUUID().toString().replace("-", ""),
|
||||||
|
mod = mod,
|
||||||
|
logger = logger
|
||||||
|
)
|
||||||
|
AstToAsm.fromModule(ctx)
|
||||||
|
val cls = ScriptContext.SimpleClassLoader(javaClass.classLoader, logger).fromBuiltContext(ctx)
|
||||||
|
// Instantiate it, get the memory out, and check it
|
||||||
|
val field = cls.getDeclaredField("memory").apply { isAccessible = true }
|
||||||
|
val buf = field[cls.newInstance()] as ByteBuffer
|
||||||
|
// Grab all + 1 and check values
|
||||||
|
val bytesActual = ByteArray(70001).also { buf.get(0, it) }
|
||||||
|
bytesActual.forEachIndexed { index, byte ->
|
||||||
|
assertEquals(if (index == 70000) 0.toByte() else bytesExpected[index], byte)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
38
compiler/src/test/kotlin/asmble/compile/jvm/NamesTest.kt
Normal file
38
compiler/src/test/kotlin/asmble/compile/jvm/NamesTest.kt
Normal file
@ -0,0 +1,38 @@
|
|||||||
|
package asmble.compile.jvm
|
||||||
|
|
||||||
|
import asmble.TestBase
|
||||||
|
import asmble.io.SExprToAst
|
||||||
|
import asmble.io.StrToSExpr
|
||||||
|
import asmble.run.jvm.ScriptContext
|
||||||
|
import org.junit.Test
|
||||||
|
import java.util.*
|
||||||
|
|
||||||
|
class NamesTest : TestBase() {
|
||||||
|
@Test
|
||||||
|
fun testNames() {
|
||||||
|
// Compile and make sure the names are set right
|
||||||
|
val (_, mod) = SExprToAst.toModule(StrToSExpr.parseSingleMulti("""
|
||||||
|
(module ${'$'}mod_name
|
||||||
|
(import "foo" "bar" (func ${'$'}import_func (param i32)))
|
||||||
|
(type ${'$'}some_sig (func (param ${'$'}type_param i32)))
|
||||||
|
(func ${'$'}some_func
|
||||||
|
(type ${'$'}some_sig)
|
||||||
|
(param ${'$'}func_param i32)
|
||||||
|
(local ${'$'}func_local0 i32)
|
||||||
|
(local ${'$'}func_local1 f64)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
""".trimIndent()))
|
||||||
|
val ctx = ClsContext(
|
||||||
|
packageName = "test",
|
||||||
|
className = "Temp" + UUID.randomUUID().toString().replace("-", ""),
|
||||||
|
mod = mod,
|
||||||
|
logger = logger
|
||||||
|
)
|
||||||
|
AstToAsm.fromModule(ctx)
|
||||||
|
val cls = ScriptContext.SimpleClassLoader(javaClass.classLoader, logger).fromBuiltContext(ctx)
|
||||||
|
// Make sure the import field and the func are present named
|
||||||
|
cls.getDeclaredField("import_func")
|
||||||
|
cls.getDeclaredMethod("some_func", Integer.TYPE)
|
||||||
|
}
|
||||||
|
}
|
@ -1,6 +1,7 @@
|
|||||||
package asmble.io
|
package asmble.io
|
||||||
|
|
||||||
import asmble.SpecTestUnit
|
import asmble.SpecTestUnit
|
||||||
|
import asmble.TestBase
|
||||||
import asmble.ast.Node
|
import asmble.ast.Node
|
||||||
import asmble.ast.Script
|
import asmble.ast.Script
|
||||||
import asmble.util.Logger
|
import asmble.util.Logger
|
||||||
@ -13,12 +14,10 @@ import java.io.ByteArrayOutputStream
|
|||||||
import kotlin.test.assertEquals
|
import kotlin.test.assertEquals
|
||||||
|
|
||||||
@RunWith(Parameterized::class)
|
@RunWith(Parameterized::class)
|
||||||
class IoTest(val unit: SpecTestUnit) : Logger by Logger.Print(Logger.Level.INFO) {
|
class IoTest(val unit: SpecTestUnit) : TestBase() {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun testIo() {
|
fun testIo() {
|
||||||
// Ignore things that are supposed to fail
|
|
||||||
if (unit.shouldFail) return
|
|
||||||
// Go from the AST to binary then back to AST then back to binary and confirm values are as expected
|
// Go from the AST to binary then back to AST then back to binary and confirm values are as expected
|
||||||
val ast1 = unit.script.commands.mapNotNull { (it as? Script.Cmd.Module)?.module?.also {
|
val ast1 = unit.script.commands.mapNotNull { (it as? Script.Cmd.Module)?.module?.also {
|
||||||
trace { "AST from script:\n" + SExprToStr.fromSExpr(AstToSExpr.fromModule(it)) }
|
trace { "AST from script:\n" + SExprToStr.fromSExpr(AstToSExpr.fromModule(it)) }
|
||||||
@ -46,7 +45,8 @@ class IoTest(val unit: SpecTestUnit) : Logger by Logger.Print(Logger.Level.INFO)
|
|||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
|
// Only tests that shouldn't fail
|
||||||
@JvmStatic @Parameterized.Parameters(name = "{0}")
|
@JvmStatic @Parameterized.Parameters(name = "{0}")
|
||||||
fun data() = SpecTestUnit.allUnits
|
fun data() = SpecTestUnit.allUnits.filterNot { it.shouldFail }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
47
compiler/src/test/kotlin/asmble/io/NamesTest.kt
Normal file
47
compiler/src/test/kotlin/asmble/io/NamesTest.kt
Normal file
@ -0,0 +1,47 @@
|
|||||||
|
package asmble.io
|
||||||
|
|
||||||
|
import asmble.ast.Node
|
||||||
|
import org.junit.Test
|
||||||
|
import kotlin.test.assertEquals
|
||||||
|
|
||||||
|
class NamesTest {
|
||||||
|
@Test
|
||||||
|
fun testNames() {
|
||||||
|
// First, make sure it can parse from sexpr
|
||||||
|
val (_, mod1) = SExprToAst.toModule(StrToSExpr.parseSingleMulti("""
|
||||||
|
(module ${'$'}mod_name
|
||||||
|
(import "foo" "bar" (func ${'$'}import_func (param i32)))
|
||||||
|
(type ${'$'}some_sig (func (param ${'$'}type_param i32)))
|
||||||
|
(func ${'$'}some_func
|
||||||
|
(type ${'$'}some_sig)
|
||||||
|
(param ${'$'}func_param i32)
|
||||||
|
(local ${'$'}func_local0 i32)
|
||||||
|
(local ${'$'}func_local1 f64)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
""".trimIndent()))
|
||||||
|
val expected = Node.NameSection(
|
||||||
|
moduleName = "mod_name",
|
||||||
|
funcNames = mapOf(
|
||||||
|
0 to "import_func",
|
||||||
|
1 to "some_func"
|
||||||
|
),
|
||||||
|
localNames = mapOf(
|
||||||
|
1 to mapOf(
|
||||||
|
0 to "func_param",
|
||||||
|
1 to "func_local0",
|
||||||
|
2 to "func_local1"
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
assertEquals(expected, mod1.names)
|
||||||
|
// Now back to binary and then back and make sure it's still there
|
||||||
|
val bytes = AstToBinary.fromModule(mod1)
|
||||||
|
val mod2 = BinaryToAst.toModule(bytes)
|
||||||
|
assertEquals(expected, mod2.names)
|
||||||
|
// Now back to sexpr and then back to make sure the sexpr writer works
|
||||||
|
val sexpr = AstToSExpr.fromModule(mod2)
|
||||||
|
val (_, mod3) = SExprToAst.toModule(sexpr)
|
||||||
|
assertEquals(expected, mod3.names)
|
||||||
|
}
|
||||||
|
}
|
Submodule compiler/src/test/resources/spec updated: 98b90e2ab2...1f00d57d00
@ -9,9 +9,3 @@ Compile Rust to WASM and then to the JVM. In order of complexity:
|
|||||||
* [rust-simple](rust-simple)
|
* [rust-simple](rust-simple)
|
||||||
* [rust-string](rust-string)
|
* [rust-string](rust-string)
|
||||||
* [rust-regex](rust-regex)
|
* [rust-regex](rust-regex)
|
||||||
|
|
||||||
### C/C++
|
|
||||||
|
|
||||||
Compile C to WASM and then to the JVM. In order of complexity:
|
|
||||||
|
|
||||||
* [c-simple](c-simple)
|
|
@ -1,14 +0,0 @@
|
|||||||
### Example: C Simple
|
|
||||||
|
|
||||||
This shows a simple example of compiling C to WASM and then to the JVM. This is the C version of
|
|
||||||
[rust-simple](../rust-simple).
|
|
||||||
|
|
||||||
In order to run the C or C++ examples, the latest LLVM binaries must be on the `PATH`, built with the experimental
|
|
||||||
WebAssembly target. This can be built by passing `-DLLVM_EXPERIMENTAL_TARGETS_TO_BUILD=WebAssembly` to `cmake` when
|
|
||||||
building WebAssembly. Or it can be downloaded from a nightly build site
|
|
||||||
([this one](http://gsdview.appspot.com/wasm-llvm/builds/) was used for these examples at the time of writing).
|
|
||||||
|
|
||||||
Everything else is basically the same as [rust-simple](../rust-simple) except with C code and using `clang`. To run
|
|
||||||
execute the following from the root `asmble` dir:
|
|
||||||
|
|
||||||
gradlew --no-daemon :examples:c-simple:run
|
|
@ -1,3 +0,0 @@
|
|||||||
int addOne(int x) {
|
|
||||||
return x + 1;
|
|
||||||
}
|
|
@ -1,13 +0,0 @@
|
|||||||
package asmble.examples.csimple;
|
|
||||||
|
|
||||||
import java.lang.invoke.MethodHandle;
|
|
||||||
|
|
||||||
import asmble.generated.CSimple;
|
|
||||||
|
|
||||||
class Main {
|
|
||||||
public static void main(String[] args) {
|
|
||||||
// Doesn't need memory or method table
|
|
||||||
CSimple simple = new CSimple(0, new MethodHandle[0]);
|
|
||||||
System.out.println("25 + 1 = " + simple.addOne(25));
|
|
||||||
}
|
|
||||||
}
|
|
10
examples/go-simple/simple.go
Normal file
10
examples/go-simple/simple.go
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
)
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
fmt.Printf("Args: %v", os.Args)
|
||||||
|
}
|
@ -2,8 +2,9 @@
|
|||||||
|
|
||||||
extern crate regex;
|
extern crate regex;
|
||||||
|
|
||||||
|
use std::ptr::NonNull;
|
||||||
use regex::Regex;
|
use regex::Regex;
|
||||||
use std::heap::{Alloc, Heap, Layout};
|
use std::alloc::{Alloc, Global, Layout};
|
||||||
use std::mem;
|
use std::mem;
|
||||||
use std::str;
|
use std::str;
|
||||||
|
|
||||||
@ -37,17 +38,17 @@ pub extern "C" fn match_count(r: *mut Regex, str_ptr: *mut u8, len: usize) -> us
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[no_mangle]
|
#[no_mangle]
|
||||||
pub extern "C" fn alloc(size: usize) -> *mut u8 {
|
pub extern "C" fn alloc(size: usize) -> NonNull<u8> {
|
||||||
unsafe {
|
unsafe {
|
||||||
let layout = Layout::from_size_align(size, mem::align_of::<u8>()).unwrap();
|
let layout = Layout::from_size_align(size, mem::align_of::<u8>()).unwrap();
|
||||||
Heap.alloc(layout).unwrap()
|
Global.alloc(layout).unwrap()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[no_mangle]
|
#[no_mangle]
|
||||||
pub extern "C" fn dealloc(ptr: *mut u8, size: usize) {
|
pub extern "C" fn dealloc(ptr: NonNull<u8>, size: usize) {
|
||||||
unsafe {
|
unsafe {
|
||||||
let layout = Layout::from_size_align(size, mem::align_of::<u8>()).unwrap();
|
let layout = Layout::from_size_align(size, mem::align_of::<u8>()).unwrap();
|
||||||
Heap.dealloc(ptr, layout);
|
Global.dealloc(ptr, layout);
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -3,6 +3,9 @@ package asmble.examples.rustregex;
|
|||||||
import java.util.regex.Matcher;
|
import java.util.regex.Matcher;
|
||||||
import java.util.regex.Pattern;
|
import java.util.regex.Pattern;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Implementation of {@link RegexLib} based on `java.util.regex`.
|
||||||
|
*/
|
||||||
public class JavaLib implements RegexLib<String> {
|
public class JavaLib implements RegexLib<String> {
|
||||||
@Override
|
@Override
|
||||||
public JavaPattern compile(String str) {
|
public JavaPattern compile(String str) {
|
||||||
|
@ -5,6 +5,10 @@ import asmble.generated.RustRegex;
|
|||||||
import java.nio.ByteBuffer;
|
import java.nio.ByteBuffer;
|
||||||
import java.nio.charset.StandardCharsets;
|
import java.nio.charset.StandardCharsets;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Implementation of {@link RegexLib} based on `asmble.generated.RustRegex` that
|
||||||
|
* was composed from Rust code (see lib.rs).
|
||||||
|
*/
|
||||||
public class RustLib implements RegexLib<RustLib.Ptr> {
|
public class RustLib implements RegexLib<RustLib.Ptr> {
|
||||||
|
|
||||||
// 600 pages is enough for our use
|
// 600 pages is enough for our use
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
#![feature(allocator_api)]
|
#![feature(allocator_api)]
|
||||||
|
|
||||||
use std::heap::{Alloc, Heap, Layout};
|
use std::ptr::NonNull;
|
||||||
|
use std::alloc::{Alloc, Global, Layout};
|
||||||
use std::ffi::{CString};
|
use std::ffi::{CString};
|
||||||
use std::mem;
|
use std::mem;
|
||||||
use std::os::raw::c_char;
|
use std::os::raw::c_char;
|
||||||
@ -30,17 +31,17 @@ pub extern "C" fn prepend_from_rust(ptr: *mut u8, len: usize) -> *const c_char {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[no_mangle]
|
#[no_mangle]
|
||||||
pub extern "C" fn alloc(size: usize) -> *mut u8 {
|
pub extern "C" fn alloc(size: usize) -> NonNull<u8> {
|
||||||
unsafe {
|
unsafe {
|
||||||
let layout = Layout::from_size_align(size, mem::align_of::<u8>()).unwrap();
|
let layout = Layout::from_size_align(size, mem::align_of::<u8>()).unwrap();
|
||||||
Heap.alloc(layout).unwrap()
|
Global.alloc(layout).unwrap()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[no_mangle]
|
#[no_mangle]
|
||||||
pub extern "C" fn dealloc(ptr: *mut u8, size: usize) {
|
pub extern "C" fn dealloc(ptr: NonNull<u8>, size: usize) {
|
||||||
unsafe {
|
unsafe {
|
||||||
let layout = Layout::from_size_align(size, mem::align_of::<u8>()).unwrap();
|
let layout = Layout::from_size_align(size, mem::align_of::<u8>()).unwrap();
|
||||||
Heap.dealloc(ptr, layout);
|
Global.dealloc(ptr, layout);
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -2,6 +2,7 @@ rootProject.name = 'asmble'
|
|||||||
include 'annotations',
|
include 'annotations',
|
||||||
'compiler',
|
'compiler',
|
||||||
'examples:c-simple',
|
'examples:c-simple',
|
||||||
'examples:rust-regex',
|
// 'examples:go-simple',
|
||||||
|
// 'examples:rust-regex', // todo will be enabled when the problem with string max size will be solved
|
||||||
'examples:rust-simple',
|
'examples:rust-simple',
|
||||||
'examples:rust-string'
|
'examples:rust-string'
|
Reference in New Issue
Block a user