mirror of
https://github.com/fluencelabs/asmble
synced 2025-07-04 00:41:34 +00:00
Compare commits
31 Commits
original-m
...
split_fix
Author | SHA1 | Date | |
---|---|---|---|
23665fa16b | |||
4c65740d03 | |||
728b78d713 | |||
cb907ae2da | |||
1d6002624f | |||
ad2b7c071f | |||
119ce58c9e | |||
1323e02c95 | |||
9172fba948 | |||
b9b45cf997 | |||
2bfa39a3c6 | |||
317b608048 | |||
21b023f1c6 | |||
765d8b4dba | |||
58cf836b76 | |||
56c2c8d672 | |||
fb0be9d31a | |||
da70c9fca4 | |||
a1a5563367 | |||
e489e7c889 | |||
c1391b2701 | |||
6b28c5a93b | |||
1990f46743 | |||
559df45f09 | |||
80a8a1fbb9 | |||
dd72c7124c | |||
c04a3c4a9b | |||
51520ac07d | |||
97660de6ba | |||
cee7a86773 | |||
cfa4a35af1 |
137
build.gradle
137
build.gradle
@ -14,13 +14,18 @@ buildscript {
|
||||
dependencies {
|
||||
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
|
||||
classpath 'me.champeau.gradle:jmh-gradle-plugin:0.4.5'
|
||||
classpath 'com.jfrog.bintray.gradle:gradle-bintray-plugin:1.8.4'
|
||||
}
|
||||
}
|
||||
|
||||
allprojects {
|
||||
apply plugin: 'java'
|
||||
group 'com.github.cretz.asmble'
|
||||
version '0.4.0-SNAPSHOT'
|
||||
version '0.4.11-fl'
|
||||
|
||||
// skips building and running for the specified examples
|
||||
ext.skipExamples = ['c-simple', 'go-simple', 'rust-regex']
|
||||
// todo disabling Rust regex is temporary because some strings in wasm code exceed the size in 65353 bytes.
|
||||
|
||||
repositories {
|
||||
mavenCentral()
|
||||
@ -34,7 +39,7 @@ project(':annotations') {
|
||||
options.addStringOption 'Xdoclint:all', '-Xdoclint:-missing'
|
||||
}
|
||||
|
||||
publishSettings(project, 'asmble-annotations', 'Asmble WASM Annotations', true)
|
||||
publishSettings(project, 'asmble-annotations', 'Asmble WASM Annotations')
|
||||
}
|
||||
|
||||
project(':compiler') {
|
||||
@ -58,7 +63,7 @@ project(':compiler') {
|
||||
testCompile "org.jetbrains.kotlin:kotlin-test-junit:$kotlin_version"
|
||||
}
|
||||
|
||||
publishSettings(project, 'asmble-compiler', 'Asmble WASM Compiler', false)
|
||||
publishSettings(project, 'asmble-compiler', 'Asmble WASM Compiler')
|
||||
}
|
||||
|
||||
project(':examples') {
|
||||
@ -174,11 +179,19 @@ project(':examples') {
|
||||
}
|
||||
|
||||
project(':examples:c-simple') {
|
||||
if (project.name in skipExamples) {
|
||||
println("[Note!] Building and runnig for ${project.name} was skipped")
|
||||
test.onlyIf { false } // explicit skipping tests
|
||||
compileJava.onlyIf { false } // explicit skipping compile
|
||||
return
|
||||
}
|
||||
|
||||
apply plugin: 'application'
|
||||
ext.wasmCompiledClassName = 'asmble.generated.CSimple'
|
||||
dependencies {
|
||||
compile files('build/wasm-classes')
|
||||
}
|
||||
|
||||
compileJava {
|
||||
dependsOn compileCWasm
|
||||
}
|
||||
@ -186,6 +199,12 @@ project(':examples:c-simple') {
|
||||
}
|
||||
|
||||
project(':examples:go-simple') {
|
||||
if (project.name in skipExamples) {
|
||||
println("[Note!] Building and runnig for ${project.name} was skipped")
|
||||
test.onlyIf { false } // explicit skipping tests
|
||||
compileJava.onlyIf { false } // explicit skipping compile
|
||||
return
|
||||
}
|
||||
apply plugin: 'application'
|
||||
ext.wasmCompiledClassName = 'asmble.generated.GoSimple'
|
||||
dependencies {
|
||||
@ -198,6 +217,13 @@ project(':examples:go-simple') {
|
||||
}
|
||||
|
||||
project(':examples:rust-regex') {
|
||||
if (project.name in skipExamples) {
|
||||
println("[Note!] Building and runnig for ${project.name} was skipped")
|
||||
test.onlyIf { false } // explicit skipping tests
|
||||
compileJava.onlyIf { false } // explicit skipping compile
|
||||
compileTestJava.onlyIf { false } // explicit skipping compile
|
||||
return
|
||||
}
|
||||
apply plugin: 'application'
|
||||
apply plugin: 'me.champeau.gradle.jmh'
|
||||
ext.wasmCompiledClassName = 'asmble.generated.RustRegex'
|
||||
@ -209,6 +235,7 @@ project(':examples:rust-regex') {
|
||||
dependsOn compileRustWasm
|
||||
}
|
||||
mainClassName = 'asmble.examples.rustregex.Main'
|
||||
|
||||
test {
|
||||
testLogging.showStandardStreams = true
|
||||
testLogging.events 'PASSED', 'SKIPPED'
|
||||
@ -221,6 +248,12 @@ project(':examples:rust-regex') {
|
||||
}
|
||||
|
||||
project(':examples:rust-simple') {
|
||||
if (project.name in skipExamples) {
|
||||
println("[Note!] Building and runnig for ${project.name} was skipped")
|
||||
test.onlyIf { false } // explicit skipping tests
|
||||
compileJava.onlyIf { false } // explicit skipping compile
|
||||
return
|
||||
}
|
||||
apply plugin: 'application'
|
||||
ext.wasmCompiledClassName = 'asmble.generated.RustSimple'
|
||||
dependencies {
|
||||
@ -233,6 +266,12 @@ project(':examples:rust-simple') {
|
||||
}
|
||||
|
||||
project(':examples:rust-string') {
|
||||
if (project.name in skipExamples) {
|
||||
println("[Note!] Building and runnig for ${project.name} was skipped")
|
||||
test.onlyIf { false } // explicit skipping tests
|
||||
compileJava.onlyIf { false } // explicit skipping compile
|
||||
return
|
||||
}
|
||||
apply plugin: 'application'
|
||||
ext.wasmCompiledClassName = 'asmble.generated.RustString'
|
||||
dependencies {
|
||||
@ -244,75 +283,59 @@ project(':examples:rust-string') {
|
||||
mainClassName = 'asmble.examples.ruststring.Main'
|
||||
}
|
||||
|
||||
def publishSettings(project, projectName, projectDescription, includeJavadoc) {
|
||||
def publishSettings(project, projectName, projectDescription) {
|
||||
|
||||
project.with {
|
||||
if (!project.hasProperty('ossrhUsername')) return
|
||||
apply plugin: 'com.jfrog.bintray'
|
||||
apply plugin: 'maven-publish'
|
||||
apply plugin: 'maven'
|
||||
apply plugin: 'signing'
|
||||
|
||||
archivesBaseName = projectName
|
||||
|
||||
task packageSources(type: Jar) {
|
||||
task sourcesJar(type: Jar) {
|
||||
from sourceSets.main.allJava
|
||||
classifier = 'sources'
|
||||
from sourceSets.main.allSource
|
||||
}
|
||||
|
||||
if (includeJavadoc) {
|
||||
task packageJavadoc(type: Jar, dependsOn: 'javadoc') {
|
||||
from javadoc.destinationDir
|
||||
classifier = 'javadoc'
|
||||
publishing {
|
||||
publications {
|
||||
MyPublication(MavenPublication) {
|
||||
from components.java
|
||||
groupId group
|
||||
artifactId projectName
|
||||
artifact sourcesJar
|
||||
version version
|
||||
}
|
||||
} else {
|
||||
task packageJavadoc(type: Jar) {
|
||||
// Empty to satisfy Sonatype's javadoc.jar requirement
|
||||
classifier 'javadoc'
|
||||
}
|
||||
}
|
||||
|
||||
artifacts {
|
||||
archives packageSources, packageJavadoc
|
||||
bintray {
|
||||
if(!hasProperty("bintrayUser") || !hasProperty("bintrayKey")) {
|
||||
return
|
||||
}
|
||||
|
||||
signing {
|
||||
sign configurations.archives
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
uploadArchives {
|
||||
repositories {
|
||||
mavenDeployer {
|
||||
beforeDeployment { MavenDeployment deployment -> signing.signPom(deployment) }
|
||||
repository(url: 'https://oss.sonatype.org/service/local/staging/deploy/maven2/') {
|
||||
authentication(userName: ossrhUsername, password: ossrhPassword)
|
||||
}
|
||||
snapshotRepository(url: 'https://oss.sonatype.org/content/repositories/snapshots/') {
|
||||
authentication(userName: ossrhUsername, password: ossrhPassword)
|
||||
}
|
||||
pom.project {
|
||||
name projectName
|
||||
packaging 'jar'
|
||||
description projectDescription
|
||||
url 'https://github.com/cretz/asmble'
|
||||
scm {
|
||||
connection 'scm:git:git@github.com:cretz/asmble.git'
|
||||
developerConnection 'scm:git:git@github.com:cretz/asmble.git'
|
||||
url 'git@github.com:cretz/asmble.git'
|
||||
}
|
||||
licenses {
|
||||
license {
|
||||
name 'MIT License'
|
||||
url 'https://opensource.org/licenses/MIT'
|
||||
}
|
||||
}
|
||||
developers {
|
||||
developer {
|
||||
id 'cretz'
|
||||
name 'Chad Retz'
|
||||
url 'https://github.com/cretz'
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
40
compiler/src/main/java/asmble/compile/jvm/MemoryBuffer.java
Normal file
40
compiler/src/main/java/asmble/compile/jvm/MemoryBuffer.java
Normal file
@ -0,0 +1,40 @@
|
||||
package asmble.compile.jvm;
|
||||
|
||||
import java.nio.ByteBuffer;
|
||||
import java.nio.ByteOrder;
|
||||
|
||||
/**
|
||||
* The abstraction that describes work with the memory of the virtual machine.
|
||||
*/
|
||||
public abstract class MemoryBuffer {
|
||||
|
||||
/**
|
||||
* The default implementation of MemoryBuffer that based on java.nio.DirectByteBuffer
|
||||
*/
|
||||
public static MemoryBuffer init(int capacity) {
|
||||
return new MemoryByteBuffer(ByteBuffer.allocateDirect(capacity));
|
||||
}
|
||||
|
||||
public abstract int capacity();
|
||||
public abstract int limit();
|
||||
public abstract MemoryBuffer clear();
|
||||
public abstract MemoryBuffer limit(int newLimit);
|
||||
public abstract MemoryBuffer position(int newPosition);
|
||||
public abstract MemoryBuffer order(ByteOrder order);
|
||||
public abstract MemoryBuffer duplicate();
|
||||
public abstract MemoryBuffer put(byte[] arr, int offset, int length);
|
||||
public abstract MemoryBuffer put(byte[] arr);
|
||||
public abstract MemoryBuffer put(int index, byte b);
|
||||
public abstract MemoryBuffer putInt(int index, int n);
|
||||
public abstract MemoryBuffer putLong(int index, long n);
|
||||
public abstract MemoryBuffer putDouble(int index, double n);
|
||||
public abstract MemoryBuffer putShort(int index, short n);
|
||||
public abstract MemoryBuffer putFloat(int index, float n);
|
||||
public abstract byte get(int index);
|
||||
public abstract int getInt(int index);
|
||||
public abstract long getLong(int index);
|
||||
public abstract short getShort(int index);
|
||||
public abstract float getFloat(int index);
|
||||
public abstract double getDouble(int index);
|
||||
public abstract MemoryBuffer get(byte[] arr);
|
||||
}
|
@ -0,0 +1,8 @@
|
||||
package asmble.compile.jvm;
|
||||
|
||||
/**
|
||||
* Interface to initialize MemoryBuffer
|
||||
*/
|
||||
public interface MemoryBufferBuilder {
|
||||
MemoryBuffer build(int capacity);
|
||||
}
|
@ -81,8 +81,9 @@ public class SplitMethod {
|
||||
localsMap.put(index, args.size() - 1);
|
||||
});
|
||||
// Create the new method
|
||||
String name = orig.name.replace("<", "__").replace(">", "__") + "$split";
|
||||
MethodNode newMethod = new MethodNode(api,
|
||||
Opcodes.ACC_STATIC + Opcodes.ACC_PRIVATE + Opcodes.ACC_SYNTHETIC, orig.name + "$split",
|
||||
Opcodes.ACC_STATIC + Opcodes.ACC_PRIVATE + Opcodes.ACC_SYNTHETIC, name,
|
||||
Type.getMethodDescriptor(Type.getType(Object[].class), args.toArray(new Type[0])), null, null);
|
||||
// Add the written locals to the map that are not already there
|
||||
int newLocalIndex = args.size();
|
||||
@ -102,9 +103,13 @@ public class SplitMethod {
|
||||
for (int i = 0; i < splitPoint.length; i++) {
|
||||
AbstractInsnNode insn = orig.instructions.get(i + splitPoint.start);
|
||||
// Skip frames
|
||||
if (insn instanceof FrameNode) continue;
|
||||
if (insn instanceof FrameNode) {
|
||||
insn.accept(newMethod);
|
||||
continue;
|
||||
}
|
||||
// Store the label
|
||||
if (insn instanceof LabelNode) seenLabels.add(((LabelNode) insn).getLabel());
|
||||
if (insn instanceof LabelNode)
|
||||
seenLabels.add(((LabelNode) insn).getLabel());
|
||||
// Change the local if needed
|
||||
if (insn instanceof VarInsnNode) {
|
||||
insn = insn.clone(Collections.emptyMap());
|
||||
@ -183,11 +188,18 @@ public class SplitMethod {
|
||||
Set<Label> seenLabels = new HashSet<>();
|
||||
// Also keep track of the locals that have been stored, need to know
|
||||
Set<Integer> seenStoredLocals = new HashSet<>();
|
||||
int paramOffset = 0;
|
||||
// If this is an instance method, we consider "0" (i.e. "this") as seen
|
||||
if ((orig.access & Opcodes.ACC_STATIC) == 0) seenStoredLocals.add(0);
|
||||
if ((orig.access & Opcodes.ACC_STATIC) == 0) {
|
||||
seenStoredLocals.add(0);
|
||||
paramOffset = 1;
|
||||
}
|
||||
// We also consider parameters as seen
|
||||
int paramCount = Type.getArgumentTypes(orig.desc).length;
|
||||
for (int i = 0; i < paramCount; i++) seenStoredLocals.add(i + paramOffset);
|
||||
// Add the insns before split
|
||||
for (int i = 0; i < splitPoint.start; i++) {
|
||||
AbstractInsnNode insn = orig.instructions.get(i + splitPoint.start);
|
||||
AbstractInsnNode insn = orig.instructions.get(i);
|
||||
// Skip frames
|
||||
if (insn instanceof FrameNode) continue;
|
||||
// Record label
|
||||
@ -255,7 +267,7 @@ public class SplitMethod {
|
||||
}
|
||||
// Now we have restored all locals and all stack...add the rest of the insns after the split
|
||||
for (int i = splitPoint.start + splitPoint.length; i < orig.instructions.size(); i++) {
|
||||
AbstractInsnNode insn = orig.instructions.get(i + splitPoint.start);
|
||||
AbstractInsnNode insn = orig.instructions.get(i);
|
||||
// Skip frames
|
||||
if (insn instanceof FrameNode) continue;
|
||||
// Record label
|
||||
|
@ -123,11 +123,22 @@ public class Splitter implements Iterable<Splitter.SplitPoint> {
|
||||
return ret;
|
||||
}
|
||||
|
||||
private int shiftIndexValue = 1;
|
||||
|
||||
protected SplitPoint nextOrNull() {
|
||||
// Try for each index
|
||||
while (++currIndex + minSize <= insns.length) {
|
||||
if(shiftIndexValue > 0) {
|
||||
currIndex += shiftIndexValue;
|
||||
} else {
|
||||
++currIndex;
|
||||
}
|
||||
|
||||
SplitPoint longest = longestForCurrIndex();
|
||||
if (longest != null) return longest;
|
||||
if (longest != null) {
|
||||
shiftIndexValue += longest.length;
|
||||
return longest;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
@ -139,6 +150,8 @@ public class Splitter implements Iterable<Splitter.SplitPoint> {
|
||||
InsnTraverseInfo info = new InsnTraverseInfo();
|
||||
info.startIndex = currIndex;
|
||||
info.endIndex = Math.min(currIndex + maxSize - 1, insns.length - 1);
|
||||
// Reduce the end by special calls
|
||||
constrainEndByInvokeSpecial(info);
|
||||
// Reduce the end based on try/catch blocks the start is in or that jump to
|
||||
constrainEndByTryCatchBlocks(info);
|
||||
// Reduce the end based on any jumps within
|
||||
@ -146,12 +159,23 @@ public class Splitter implements Iterable<Splitter.SplitPoint> {
|
||||
// Reduce the end based on any jumps into
|
||||
constrainEndByExternalJumps(info);
|
||||
// Make sure we didn't reduce the end too far
|
||||
if (info.getSize() < minSize) return null;
|
||||
//if (info.getSize() < minSize) return null;
|
||||
// Now that we have our largest range from the start index, we can go over each updating the local refs and stack
|
||||
// For the stack, we are going to use the
|
||||
return splitPointFromInfo(info);
|
||||
}
|
||||
|
||||
protected void constrainEndByInvokeSpecial(InsnTraverseInfo info) {
|
||||
// Can't have an invoke special of <init>
|
||||
for (int i = info.startIndex; i <= info.endIndex; i++) {
|
||||
AbstractInsnNode node = insns[i];
|
||||
if (node.getOpcode() == Opcodes.INVOKESPECIAL && ((MethodInsnNode) node).name.equals("<init>")) {
|
||||
info.endIndex = Math.max(info.startIndex, i - 1);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected void constrainEndByTryCatchBlocks(InsnTraverseInfo info) {
|
||||
// Go over all the try/catch blocks, sorted by earliest
|
||||
for (TryCatchBlockNode block : tryCatchBlocks) {
|
||||
@ -251,12 +275,53 @@ public class Splitter implements Iterable<Splitter.SplitPoint> {
|
||||
}
|
||||
}
|
||||
|
||||
// visits instructions with given adaptor
|
||||
private void visitInstructions(int begin, int end, AnalyzerAdapter adapter) {
|
||||
if(end - begin <= 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
List<Object> stack = new ArrayList<>();
|
||||
List<Object> locals = new ArrayList<>();
|
||||
boolean gotoSeen = false;
|
||||
|
||||
for (int i = begin; i < end; ++i) {
|
||||
if(insns[i].getOpcode() == Opcodes.GOTO || insns[i].getOpcode() == Opcodes.ATHROW ||
|
||||
(insns[i].getOpcode() >= Opcodes.TABLESWITCH && insns[i].getOpcode() <= Opcodes.RETURN) ) {
|
||||
stack = adapter.stack;
|
||||
locals = adapter.locals;
|
||||
gotoSeen = true;
|
||||
} else if(gotoSeen) {
|
||||
gotoSeen = false;
|
||||
if(insns[i].getOpcode() >= Opcodes.IRETURN && insns[i].getOpcode() <= Opcodes.RETURN) {
|
||||
stack.clear();
|
||||
locals.clear();
|
||||
}
|
||||
adapter.visitFrame(Opcodes.F_NEW, locals.size(), locals.toArray(), stack.size(), stack.toArray());
|
||||
}
|
||||
|
||||
insns[i].accept(adapter);
|
||||
}
|
||||
|
||||
AbstractInsnNode lastInsn = insns[end - 1];
|
||||
|
||||
if(lastInsn.getOpcode() == Opcodes.GOTO || lastInsn.getOpcode() == Opcodes.ATHROW ||
|
||||
(lastInsn.getOpcode() >= Opcodes.TABLESWITCH && lastInsn.getOpcode() <= Opcodes.RETURN) ) {
|
||||
|
||||
if(lastInsn.getOpcode() >= Opcodes.IRETURN && lastInsn.getOpcode() <= Opcodes.RETURN) {
|
||||
stack.clear();
|
||||
locals.clear();
|
||||
}
|
||||
adapter.visitFrame(Opcodes.F_NEW, locals.size(), locals.toArray(), stack.size(), stack.toArray());
|
||||
}
|
||||
}
|
||||
|
||||
protected SplitPoint splitPointFromInfo(InsnTraverseInfo info) {
|
||||
// We're going to use the analyzer adapter and run it for the up until the end, a step at a time
|
||||
StackAndLocalTrackingAdapter adapter = new StackAndLocalTrackingAdapter(Splitter.this);
|
||||
// Visit all of the insns up our start.
|
||||
// XXX: I checked the source of AnalyzerAdapter to confirm I don't need any of the surrounding stuff
|
||||
for (int i = 0; i < info.startIndex; i++) insns[i].accept(adapter);
|
||||
visitInstructions(0, info.startIndex, adapter);
|
||||
// Take the stack at the start and copy it off
|
||||
List<Object> stackAtStart = new ArrayList<>(adapter.stack);
|
||||
// Reset some adapter state
|
||||
@ -264,7 +329,8 @@ public class Splitter implements Iterable<Splitter.SplitPoint> {
|
||||
adapter.localsRead.clear();
|
||||
adapter.localsWritten.clear();
|
||||
// Now go over the remaining range
|
||||
for (int i = info.startIndex; i <= info.endIndex; i++) insns[i].accept(adapter);
|
||||
visitInstructions(info.startIndex, info.endIndex + 1, adapter);
|
||||
|
||||
// Build the split point
|
||||
return new SplitPoint(
|
||||
localMapFromAdapterLocalMap(adapter.localsRead, adapter.uninitializedTypes),
|
||||
@ -279,7 +345,13 @@ public class Splitter implements Iterable<Splitter.SplitPoint> {
|
||||
protected SortedMap<Integer, Type> localMapFromAdapterLocalMap(
|
||||
SortedMap<Integer, Object> map, Map<Object, Object> uninitializedTypes) {
|
||||
SortedMap<Integer, Type> ret = new TreeMap<>();
|
||||
map.forEach((k, v) -> ret.put(k, typeFromAdapterStackItem(v, uninitializedTypes)));
|
||||
map.forEach((k, v) ->
|
||||
{
|
||||
if(v != Opcodes.TOP ) {
|
||||
ret.put(k, typeFromAdapterStackItem(v, uninitializedTypes));
|
||||
}
|
||||
}
|
||||
);
|
||||
return ret;
|
||||
}
|
||||
|
||||
|
@ -3,7 +3,19 @@ package asmble.ast
|
||||
import java.util.*
|
||||
import kotlin.reflect.KClass
|
||||
|
||||
/**
|
||||
* All WebAssembly AST nodes as static inner classes.
|
||||
*/
|
||||
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(
|
||||
val types: List<Type.Func> = emptyList(),
|
||||
val imports: List<Import> = emptyList(),
|
||||
@ -172,12 +184,15 @@ sealed class Node {
|
||||
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 Nop : Instr(), Args.None
|
||||
|
||||
data class Block(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
|
||||
|
||||
object Else : Instr(), Args.None
|
||||
object End : Instr(), Args.None
|
||||
data class Br(override val relativeDepth: Int) : Instr(), Args.RelativeDepth
|
||||
@ -188,25 +203,27 @@ sealed class Node {
|
||||
) : Instr(), Args.Table
|
||||
object Return : Instr()
|
||||
|
||||
// Call operators
|
||||
data class Call(override val index: Int) : Instr(), Args.Index
|
||||
data class CallIndirect(
|
||||
override val index: Int,
|
||||
override val reserved: Boolean
|
||||
) : 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 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 SetLocal(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 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 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
|
||||
@ -233,7 +250,9 @@ sealed class Node {
|
||||
data class MemorySize(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 I64Const(override val value: Long) : Instr(), Args.Const<Long>
|
||||
data class F32Const(override val value: Float) : Instr(), Args.Const<Float>
|
||||
|
@ -2,10 +2,16 @@ package asmble.ast
|
||||
|
||||
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 {
|
||||
|
||||
data class Multi(val vals: List<SExpr> = emptyList()) : SExpr() {
|
||||
override fun toString() = SExprToStr.Compact.fromSExpr(this)
|
||||
}
|
||||
|
||||
data class Symbol(
|
||||
val contents: String = "",
|
||||
val quoted: Boolean = false,
|
||||
@ -15,4 +21,5 @@ sealed class SExpr {
|
||||
// This is basically the same as the deprecated java.lang.String#getBytes
|
||||
fun rawContentCharsToBytes() = contents.toCharArray().map(Char::toByte)
|
||||
}
|
||||
|
||||
}
|
@ -1,6 +1,10 @@
|
||||
package asmble.ast
|
||||
|
||||
/**
|
||||
* Ast representation of wasm script.
|
||||
*/
|
||||
data class Script(val commands: List<Cmd>) {
|
||||
|
||||
sealed class Cmd {
|
||||
data class Module(val module: Node.Module, val name: String?): Cmd()
|
||||
data class Register(val string: String, val name: String?): Cmd()
|
||||
|
@ -3,6 +3,9 @@ package asmble.cli
|
||||
import asmble.compile.jvm.javaIdent
|
||||
import asmble.run.jvm.Module
|
||||
|
||||
/**
|
||||
* This class provide ''invoke'' WASM code functionality.
|
||||
*/
|
||||
open class Invoke : ScriptCommand<Invoke.Args>() {
|
||||
|
||||
override val name = "invoke"
|
||||
@ -34,6 +37,7 @@ open class Invoke : ScriptCommand<Invoke.Args>() {
|
||||
).also { bld.done() }
|
||||
|
||||
override fun run(args: Args) {
|
||||
// Compiles wasm to bytecode, do registrations and so on.
|
||||
val ctx = prepareContext(args.scriptArgs)
|
||||
// Instantiate the module
|
||||
val module =
|
||||
@ -41,11 +45,11 @@ open class Invoke : ScriptCommand<Invoke.Args>() {
|
||||
else ctx.registrations[args.module] as? Module.Instance ?:
|
||||
error("Unable to find module registered as ${args.module}")
|
||||
// Just make sure the module is instantiated here...
|
||||
module.instance(ctx)
|
||||
val instance = module.instance(ctx)
|
||||
// If an export is provided, call it
|
||||
if (args.export != "<start-func>") args.export.javaIdent.let { javaName ->
|
||||
val method = module.cls.declaredMethods.find { it.name == javaName } ?:
|
||||
error("Unable to find export '${args.export}'")
|
||||
// Finds java method(wasm fn) in class(wasm module) by name(declared in <start-func>)
|
||||
val method = module.cls.declaredMethods.find { it.name == javaName } ?: error("Unable to find export '${args.export}'")
|
||||
// Map args to params
|
||||
require(method.parameterTypes.size == args.args.size) {
|
||||
"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")
|
||||
}
|
||||
}
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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(
|
||||
val scriptArgs: ScriptCommand.ScriptArgs,
|
||||
val module: String,
|
||||
|
@ -5,6 +5,9 @@ import kotlin.system.exitProcess
|
||||
|
||||
val commands = listOf(Compile, Help, Invoke, Link, Run, Translate)
|
||||
|
||||
/**
|
||||
* Entry point of command line interface.
|
||||
*/
|
||||
fun main(args: Array<String>) {
|
||||
if (args.isEmpty()) return println(
|
||||
"""
|
||||
@ -28,6 +31,7 @@ fun main(args: Array<String>) {
|
||||
val globals = Main.globalArgs(argBuild)
|
||||
logger = Logger.Print(globals.logLevel)
|
||||
command.logger = logger
|
||||
logger.info { "Running the command=${command.name} with args=${argBuild.args}" }
|
||||
command.runWithArgs(argBuild)
|
||||
} catch (e: Exception) {
|
||||
logger.error { "Error ${command?.let { "in command '${it.name}'" } ?: ""}: ${e.message}" }
|
||||
|
@ -1,10 +1,10 @@
|
||||
package asmble.cli
|
||||
|
||||
import asmble.ast.Script
|
||||
import asmble.compile.jvm.javaIdent
|
||||
import asmble.run.jvm.Module
|
||||
import asmble.run.jvm.ScriptContext
|
||||
import asmble.compile.jvm.*
|
||||
import asmble.run.jvm.*
|
||||
import java.io.File
|
||||
import java.io.PrintWriter
|
||||
import java.util.*
|
||||
|
||||
abstract class ScriptCommand<T> : Command<T>() {
|
||||
@ -41,20 +41,33 @@ abstract class ScriptCommand<T> : Command<T>() {
|
||||
desc = "The maximum number of memory pages when a module doesn't say.",
|
||||
default = "5",
|
||||
lowPriority = true
|
||||
).toInt()
|
||||
).toInt(),
|
||||
enableLogger = bld.arg(
|
||||
name = "enableLogger",
|
||||
opt = "enableLogger",
|
||||
desc = "Enables the special module the could be used for logging",
|
||||
default = "false",
|
||||
lowPriority = true
|
||||
).toBoolean()
|
||||
)
|
||||
|
||||
fun prepareContext(args: ScriptArgs): ScriptContext {
|
||||
var ctx = ScriptContext(
|
||||
var context = ScriptContext(
|
||||
packageName = "asmble.temp" + UUID.randomUUID().toString().replace("-", ""),
|
||||
defaultMaxMemPages = args.defaultMaxMemPages
|
||||
defaultMaxMemPages = args.defaultMaxMemPages,
|
||||
memoryBuilder = args.memoryBuilder
|
||||
)
|
||||
// Compile everything
|
||||
ctx = args.inFiles.foldIndexed(ctx) { index, ctx, inFile ->
|
||||
context = args.inFiles.foldIndexed(context) { index, ctx, inFile ->
|
||||
try {
|
||||
when (inFile.substringAfterLast('.')) {
|
||||
// if input file is class file
|
||||
"class" -> ctx.classLoader.addClass(File(inFile).readBytes()).let { ctx }
|
||||
else -> Translate.inToAst(inFile, inFile.substringAfterLast('.')).let { inAst ->
|
||||
// if input file is wasm file
|
||||
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")
|
||||
val className = name?.javaIdent?.capitalize() ?:
|
||||
@ -67,22 +80,58 @@ abstract class ScriptCommand<T> : Command<T>() {
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (e: Exception) { throw Exception("Failed loading $inFile - ${e.message}", e) }
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
throw Exception("Failed loading $inFile - ${e.message}", e)
|
||||
}
|
||||
}
|
||||
|
||||
// Do registrations
|
||||
ctx = args.registrations.fold(ctx) { ctx, (moduleName, className) ->
|
||||
context = args.registrations.fold(context) { ctx, (moduleName, className) ->
|
||||
ctx.withModuleRegistered(moduleName,
|
||||
Module.Native(Class.forName(className, true, ctx.classLoader).newInstance()))
|
||||
}
|
||||
if (args.specTestRegister) ctx = ctx.withHarnessRegistered()
|
||||
return ctx
|
||||
|
||||
if (args.specTestRegister) context = context.withHarnessRegistered()
|
||||
|
||||
if (args.enableLogger) {
|
||||
// add logger Wasm module for logging
|
||||
context =
|
||||
context.withModuleRegistered(
|
||||
"logger",
|
||||
Module.Native(LoggerModule(PrintWriter(System.out)))
|
||||
)
|
||||
}
|
||||
|
||||
// add env Wasm module for gas metering
|
||||
context =
|
||||
context.withModuleRegistered(
|
||||
"env",
|
||||
// TODO: currently we are using almost infinite gas limit
|
||||
Module.Native(EnvModule(Long.MAX_VALUE))
|
||||
)
|
||||
|
||||
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
|
||||
* @param enableLogger If set, the special logger module will be registred.
|
||||
* @param memoryBuilder The builder to initialize new memory class.
|
||||
*/
|
||||
data class ScriptArgs(
|
||||
val inFiles: List<String>,
|
||||
val registrations: List<Pair<String, String>>,
|
||||
val disableAutoRegister: Boolean,
|
||||
val specTestRegister: Boolean,
|
||||
val defaultMaxMemPages: Int
|
||||
val defaultMaxMemPages: Int,
|
||||
val enableLogger: Boolean,
|
||||
val memoryBuilder: MemoryBufferBuilder? = null
|
||||
)
|
||||
}
|
@ -58,7 +58,10 @@ open class Translate : Command<Translate.Args>() {
|
||||
fun inToAst(inFile: String, inFormat: String): Script {
|
||||
val inBytes =
|
||||
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) {
|
||||
"wast" -> StrToSExpr.parse(inBytes.toString(Charsets.UTF_8)).let { res ->
|
||||
when (res) {
|
||||
@ -67,8 +70,10 @@ open class Translate : Command<Translate.Args>() {
|
||||
}
|
||||
}
|
||||
"wasm" ->
|
||||
Script(listOf(Script.Cmd.Module(BinaryToAst(logger = logger).toModule(
|
||||
ByteReader.InputStream(inBytes.inputStream())), null)))
|
||||
BinaryToAst(logger = this.logger).toModule(
|
||||
ByteReader.InputStream(inBytes.inputStream())).let { module ->
|
||||
Script(listOf(Script.Cmd.Module(module, module.names?.moduleName)))
|
||||
}
|
||||
else -> error("Unknown in format '$inFormat'")
|
||||
}
|
||||
}
|
||||
|
@ -13,7 +13,7 @@ import org.objectweb.asm.tree.ClassNode
|
||||
open class AsmToBinary(val splitMethod: SplitMethod? = SplitMethod(Opcodes.ASM6)) {
|
||||
fun fromClassNode(
|
||||
cn: ClassNode,
|
||||
newClassWriter: () -> ClassWriter = { ClassWriter(ClassWriter.COMPUTE_FRAMES + ClassWriter.COMPUTE_MAXS) }
|
||||
newClassWriter: () -> ClassWriter = { ClassWriter(ClassWriter.COMPUTE_FRAMES) }
|
||||
): ByteArray {
|
||||
while (true) {
|
||||
val cw = newClassWriter()
|
||||
@ -29,6 +29,7 @@ open class AsmToBinary(val splitMethod: SplitMethod? = SplitMethod(Opcodes.ASM6)
|
||||
require(cn.name == e.className)
|
||||
val tooLargeIndex = cn.methods.indexOfFirst { it.name == e.methodName && it.desc == e.descriptor }
|
||||
require(tooLargeIndex >= 0)
|
||||
val tt = cn.methods[tooLargeIndex]
|
||||
val split = splitMethod.split(cn.name, cn.methods[tooLargeIndex])
|
||||
split ?: throw IllegalStateException("Failed to split", e)
|
||||
// Change the split off method's name if there's already one
|
||||
|
@ -30,10 +30,11 @@ open class AstToAsm {
|
||||
}
|
||||
|
||||
fun addFields(ctx: ClsContext) {
|
||||
// Mem field if present
|
||||
// Mem field if present, adds `private final field memory` to
|
||||
if (ctx.hasMemory)
|
||||
ctx.cls.fields.add(FieldNode(Opcodes.ACC_PRIVATE + Opcodes.ACC_FINAL, "memory",
|
||||
ctx.mem.memType.asmDesc, null, null))
|
||||
ctx.cls.fields.add(
|
||||
FieldNode((Opcodes.ACC_PRIVATE + Opcodes.ACC_FINAL), "memory", ctx.mem.memType.asmDesc, null, null)
|
||||
)
|
||||
// Table field if present...
|
||||
// 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)
|
||||
|
@ -4,32 +4,30 @@ import asmble.ast.Node
|
||||
import org.objectweb.asm.Opcodes
|
||||
import org.objectweb.asm.Type
|
||||
import org.objectweb.asm.tree.*
|
||||
import java.nio.Buffer
|
||||
import java.nio.ByteBuffer
|
||||
import java.nio.ByteOrder
|
||||
import kotlin.reflect.KClass
|
||||
import kotlin.reflect.KFunction
|
||||
|
||||
open class ByteBufferMem(val direct: Boolean = true) : Mem {
|
||||
override val memType = ByteBuffer::class.ref
|
||||
open class ByteBufferMem : Mem {
|
||||
override val memType: TypeRef = MemoryBuffer::class.ref
|
||||
|
||||
override fun limitAndCapacity(instance: Any) =
|
||||
if (instance !is ByteBuffer) error("Unrecognized memory instance: $instance")
|
||||
override fun limitAndCapacity(instance: Any): Pair<Int, Int> =
|
||||
if (instance !is MemoryBuffer) error("Unrecognized memory instance: $instance")
|
||||
else instance.limit() to instance.capacity()
|
||||
|
||||
override fun create(func: Func) = func.popExpecting(Int::class.ref).addInsns(
|
||||
(if (direct) ByteBuffer::allocateDirect else ByteBuffer::allocate).invokeStatic()
|
||||
(MemoryBuffer::init).invokeStatic()
|
||||
).push(memType)
|
||||
|
||||
override fun init(func: Func, initial: Int) = func.popExpecting(memType).addInsns(
|
||||
// Set the limit to initial
|
||||
(initial * Mem.PAGE_SIZE).const,
|
||||
forceFnType<ByteBuffer.(Int) -> Buffer>(ByteBuffer::limit).invokeVirtual(),
|
||||
TypeInsnNode(Opcodes.CHECKCAST, ByteBuffer::class.ref.asmName),
|
||||
forceFnType<MemoryBuffer.(Int) -> MemoryBuffer>(MemoryBuffer::limit).invokeVirtual(),
|
||||
TypeInsnNode(Opcodes.CHECKCAST, memType.asmName),
|
||||
// Set it to use little endian
|
||||
ByteOrder::LITTLE_ENDIAN.getStatic(),
|
||||
forceFnType<ByteBuffer.(ByteOrder) -> ByteBuffer>(ByteBuffer::order).invokeVirtual()
|
||||
).push(ByteBuffer::class.ref)
|
||||
forceFnType<MemoryBuffer.(ByteOrder) -> MemoryBuffer>(MemoryBuffer::order).invokeVirtual()
|
||||
).push(memType)
|
||||
|
||||
override fun data(func: Func, bytes: ByteArray, buildOffset: (Func) -> Func) =
|
||||
// Sadly there is no absolute bulk put, so we need to fake one. Ref:
|
||||
@ -42,10 +40,10 @@ open class ByteBufferMem(val direct: Boolean = true) : Mem {
|
||||
// where we could call put directly, but it too is negligible for now.
|
||||
// Note, with this approach, the mem not be left on the stack for future data() calls which is fine.
|
||||
func.popExpecting(memType).
|
||||
addInsns(ByteBuffer::duplicate.invokeVirtual()).
|
||||
addInsns(MemoryBuffer::duplicate.invokeVirtual()).
|
||||
let(buildOffset).popExpecting(Int::class.ref).
|
||||
addInsns(
|
||||
forceFnType<ByteBuffer.(Int) -> Buffer>(ByteBuffer::position).invokeVirtual(),
|
||||
forceFnType<MemoryBuffer.(Int) -> MemoryBuffer>(MemoryBuffer::position).invokeVirtual(),
|
||||
TypeInsnNode(Opcodes.CHECKCAST, memType.asmName)
|
||||
).addInsns(
|
||||
// We're going to do this as an LDC string in ISO-8859 and read it back at runtime. However,
|
||||
@ -61,7 +59,7 @@ open class ByteBufferMem(val direct: Boolean = true) : Mem {
|
||||
"getBytes", "(Ljava/lang/String;)[B", false),
|
||||
0.const,
|
||||
bytes.size.const,
|
||||
forceFnType<ByteBuffer.(ByteArray, Int, Int) -> ByteBuffer>(ByteBuffer::put).invokeVirtual()
|
||||
forceFnType<MemoryBuffer.(ByteArray, Int, Int) -> MemoryBuffer>(MemoryBuffer::put).invokeVirtual()
|
||||
)
|
||||
}.toList()
|
||||
).addInsns(
|
||||
@ -69,7 +67,7 @@ open class ByteBufferMem(val direct: Boolean = true) : Mem {
|
||||
)
|
||||
|
||||
override fun currentMemory(ctx: FuncContext, func: Func) = func.popExpecting(memType).addInsns(
|
||||
forceFnType<ByteBuffer.() -> Int>(ByteBuffer::limit).invokeVirtual(),
|
||||
forceFnType<MemoryBuffer.() -> Int>(MemoryBuffer::limit).invokeVirtual(),
|
||||
Mem.PAGE_SIZE.const,
|
||||
InsnNode(Opcodes.IDIV)
|
||||
).push(Int::class.ref)
|
||||
@ -86,10 +84,10 @@ open class ByteBufferMem(val direct: Boolean = true) : Mem {
|
||||
val okLim = LabelNode()
|
||||
val node = MethodNode(
|
||||
Opcodes.ACC_PRIVATE + Opcodes.ACC_STATIC + Opcodes.ACC_SYNTHETIC,
|
||||
"\$\$growMemory", "(Ljava/nio/ByteBuffer;I)I", null, null
|
||||
"\$\$growMemory", "(Lasmble/compile/jvm/MemoryBuffer;I)I", null, null
|
||||
).addInsns(
|
||||
VarInsnNode(Opcodes.ALOAD, 0), // [mem]
|
||||
forceFnType<ByteBuffer.() -> Int>(ByteBuffer::limit).invokeVirtual(), // [lim]
|
||||
forceFnType<MemoryBuffer.() -> Int>(MemoryBuffer::limit).invokeVirtual(), // [lim]
|
||||
InsnNode(Opcodes.DUP), // [lim, lim]
|
||||
VarInsnNode(Opcodes.ALOAD, 0), // [lim, lim, mem]
|
||||
InsnNode(Opcodes.SWAP), // [lim, mem, lim]
|
||||
@ -102,7 +100,7 @@ open class ByteBufferMem(val direct: Boolean = true) : Mem {
|
||||
InsnNode(Opcodes.LADD), // [lim, mem, newlimL]
|
||||
InsnNode(Opcodes.DUP2), // [lim, mem, newlimL, newlimL]
|
||||
VarInsnNode(Opcodes.ALOAD, 0), // [lim, mem, newlimL, newlimL, mem]
|
||||
ByteBuffer::capacity.invokeVirtual(), // [lim, mem, newlimL, newlimL, cap]
|
||||
MemoryBuffer::capacity.invokeVirtual(), // [lim, mem, newlimL, newlimL, cap]
|
||||
InsnNode(Opcodes.I2L), // [lim, mem, newlimL, newlimL, capL]
|
||||
InsnNode(Opcodes.LCMP), // [lim, mem, newlimL, cmpres]
|
||||
JumpInsnNode(Opcodes.IFLE, okLim), // [lim, mem, newlimL]
|
||||
@ -111,7 +109,7 @@ open class ByteBufferMem(val direct: Boolean = true) : Mem {
|
||||
InsnNode(Opcodes.IRETURN),
|
||||
okLim, // [lim, mem, newlimL]
|
||||
InsnNode(Opcodes.L2I), // [lim, mem, newlim]
|
||||
forceFnType<ByteBuffer.(Int) -> Buffer>(ByteBuffer::limit).invokeVirtual(), // [lim, mem]
|
||||
forceFnType<MemoryBuffer.(Int) -> MemoryBuffer>(MemoryBuffer::limit).invokeVirtual(), // [lim, mem]
|
||||
InsnNode(Opcodes.POP), // [lim]
|
||||
Mem.PAGE_SIZE.const, // [lim, pagesize]
|
||||
InsnNode(Opcodes.IDIV), // [limpages]
|
||||
@ -125,7 +123,7 @@ open class ByteBufferMem(val direct: Boolean = true) : Mem {
|
||||
// Ug, some tests expect this to be a runtime failure so we feature flagged it
|
||||
if (ctx.cls.eagerFailLargeMemOffset)
|
||||
require(insn.offset <= Int.MAX_VALUE, { "Offsets > ${Int.MAX_VALUE} unsupported" }).let { this }
|
||||
fun Func.load(fn: ByteBuffer.(Int) -> Any, retClass: KClass<*>) =
|
||||
fun Func.load(fn: MemoryBuffer.(Int) -> Any, retClass: KClass<*>) =
|
||||
this.popExpecting(Int::class.ref).let { func ->
|
||||
// No offset means we'll access it directly
|
||||
(if (insn.offset == 0L) func else {
|
||||
@ -141,9 +139,9 @@ open class ByteBufferMem(val direct: Boolean = true) : Mem {
|
||||
}
|
||||
}).popExpecting(memType).addInsns((fn as KFunction<*>).invokeVirtual())
|
||||
}.push(retClass.ref)
|
||||
fun Func.loadI32(fn: ByteBuffer.(Int) -> Any) =
|
||||
fun Func.loadI32(fn: MemoryBuffer.(Int) -> Any) =
|
||||
this.load(fn, Int::class)
|
||||
fun Func.loadI64(fn: ByteBuffer.(Int) -> Any) =
|
||||
fun Func.loadI64(fn: MemoryBuffer.(Int) -> Any) =
|
||||
this.load(fn, Long::class)
|
||||
/* Ug: https://youtrack.jetbrains.com/issue/KT-17064
|
||||
fun Func.toUnsigned(fn: KFunction<*>) =
|
||||
@ -163,33 +161,33 @@ open class ByteBufferMem(val direct: Boolean = true) : Mem {
|
||||
// Had to move this in here instead of as first expr because of https://youtrack.jetbrains.com/issue/KT-8689
|
||||
return when (insn) {
|
||||
is Node.Instr.I32Load ->
|
||||
func.loadI32(ByteBuffer::getInt)
|
||||
func.loadI32(MemoryBuffer::getInt)
|
||||
is Node.Instr.I64Load ->
|
||||
func.loadI64(ByteBuffer::getLong)
|
||||
func.loadI64(MemoryBuffer::getLong)
|
||||
is Node.Instr.F32Load ->
|
||||
func.load(ByteBuffer::getFloat, Float::class)
|
||||
func.load(MemoryBuffer::getFloat, Float::class)
|
||||
is Node.Instr.F64Load ->
|
||||
func.load(ByteBuffer::getDouble, Double::class)
|
||||
func.load(MemoryBuffer::getDouble, Double::class)
|
||||
is Node.Instr.I32Load8S ->
|
||||
func.loadI32(ByteBuffer::get)
|
||||
func.loadI32(MemoryBuffer::get)
|
||||
is Node.Instr.I32Load8U ->
|
||||
func.loadI32(ByteBuffer::get).toUnsigned32(java.lang.Byte::class, "toUnsignedInt", Byte::class)
|
||||
func.loadI32(MemoryBuffer::get).toUnsigned32(java.lang.Byte::class, "toUnsignedInt", Byte::class)
|
||||
is Node.Instr.I32Load16S ->
|
||||
func.loadI32(ByteBuffer::getShort)
|
||||
func.loadI32(MemoryBuffer::getShort)
|
||||
is Node.Instr.I32Load16U ->
|
||||
func.loadI32(ByteBuffer::getShort).toUnsigned32(java.lang.Short::class, "toUnsignedInt", Short::class)
|
||||
func.loadI32(MemoryBuffer::getShort).toUnsigned32(java.lang.Short::class, "toUnsignedInt", Short::class)
|
||||
is Node.Instr.I64Load8S ->
|
||||
func.loadI32(ByteBuffer::get).i32ToI64()
|
||||
func.loadI32(MemoryBuffer::get).i32ToI64()
|
||||
is Node.Instr.I64Load8U ->
|
||||
func.loadI32(ByteBuffer::get).toUnsigned64(java.lang.Byte::class, "toUnsignedLong", Byte::class)
|
||||
func.loadI32(MemoryBuffer::get).toUnsigned64(java.lang.Byte::class, "toUnsignedLong", Byte::class)
|
||||
is Node.Instr.I64Load16S ->
|
||||
func.loadI32(ByteBuffer::getShort).i32ToI64()
|
||||
func.loadI32(MemoryBuffer::getShort).i32ToI64()
|
||||
is Node.Instr.I64Load16U ->
|
||||
func.loadI32(ByteBuffer::getShort).toUnsigned64(java.lang.Short::class, "toUnsignedLong", Short::class)
|
||||
func.loadI32(MemoryBuffer::getShort).toUnsigned64(java.lang.Short::class, "toUnsignedLong", Short::class)
|
||||
is Node.Instr.I64Load32S ->
|
||||
func.loadI32(ByteBuffer::getInt).i32ToI64()
|
||||
func.loadI32(MemoryBuffer::getInt).i32ToI64()
|
||||
is Node.Instr.I64Load32U ->
|
||||
func.loadI32(ByteBuffer::getInt).toUnsigned64(java.lang.Integer::class, "toUnsignedLong", Int::class)
|
||||
func.loadI32(MemoryBuffer::getInt).toUnsigned64(java.lang.Integer::class, "toUnsignedLong", Int::class)
|
||||
else -> throw IllegalArgumentException("Unknown load op $insn")
|
||||
}
|
||||
}
|
||||
@ -224,12 +222,12 @@ open class ByteBufferMem(val direct: Boolean = true) : Mem {
|
||||
popExpecting(Int::class.ref).
|
||||
popExpecting(memType).
|
||||
addInsns(fn).
|
||||
push(ByteBuffer::class.ref)
|
||||
push(memType)
|
||||
}
|
||||
// Ug, I hate these as strings but can't introspect Kotlin overloads
|
||||
fun bufStoreFunc(name: String, valType: KClass<*>) =
|
||||
MethodInsnNode(Opcodes.INVOKEVIRTUAL, ByteBuffer::class.ref.asmName, name,
|
||||
ByteBuffer::class.ref.asMethodRetDesc(Int::class.ref, valType.ref), false)
|
||||
MethodInsnNode(Opcodes.INVOKEVIRTUAL, memType.asmName, name,
|
||||
memType.asMethodRetDesc(Int::class.ref, valType.ref), false)
|
||||
fun Func.changeI64ToI32() =
|
||||
this.popExpecting(Long::class.ref).push(Int::class.ref)
|
||||
when (insn) {
|
||||
|
@ -4,6 +4,24 @@ import asmble.ast.Node
|
||||
import org.objectweb.asm.Opcodes
|
||||
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(
|
||||
val name: String,
|
||||
val params: List<TypeRef> = emptyList(),
|
||||
@ -12,7 +30,6 @@ data class Func(
|
||||
val insns: List<AbstractInsnNode> = emptyList(),
|
||||
val stack: List<TypeRef> = emptyList(),
|
||||
val blockStack: List<Block> = emptyList(),
|
||||
// Contains index of JumpInsnNode that has a null label initially
|
||||
val ifStack: List<Int> = emptyList(),
|
||||
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))
|
||||
|
||||
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))
|
||||
|
||||
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()
|
||||
|
||||
/**
|
||||
* 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(
|
||||
val insn: Node.Instr,
|
||||
val startIndex: Int,
|
||||
|
@ -14,23 +14,31 @@ import java.lang.invoke.MethodHandle
|
||||
// TODO: modularize
|
||||
|
||||
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.trace { "Function ast:\n${SExprToStr.fromSExpr(AstToSExpr.fromFunc(f))}" }
|
||||
ctx.trace { "Function ast:\n${SExprToStr.fromSExpr(AstToSExpr.fromFunc(fn))}" }
|
||||
var func = Func(
|
||||
access = Opcodes.ACC_PRIVATE,
|
||||
name = ctx.funcName(index),
|
||||
params = f.type.params.map(Node.Type.Value::typeRef),
|
||||
ret = f.type.ret?.let(Node.Type.Value::typeRef) ?: Void::class.ref
|
||||
params = fn.type.params.map(Node.Type.Value::typeRef),
|
||||
ret = fn.type.ret?.let(Node.Type.Value::typeRef) ?: Void::class.ref
|
||||
)
|
||||
// Rework the instructions
|
||||
val reworkedInsns = ctx.reworker.rework(ctx, f)
|
||||
val reworkedInsns = ctx.reworker.rework(ctx, fn)
|
||||
// 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
|
||||
val funcCtx = FuncContext(
|
||||
cls = ctx,
|
||||
node = f,
|
||||
node = fn,
|
||||
insns = reworkedInsns,
|
||||
memIsLocalVar =
|
||||
ctx.reworker.nonAdjacentMemAccesses(reworkedInsns) >= ctx.nonAdjacentMemAccessesRequiringLocalVar
|
||||
@ -46,9 +54,9 @@ open class FuncBuilder {
|
||||
// Add all instructions
|
||||
ctx.debug { "Applying insns for function ${ctx.funcName(index)}" }
|
||||
// 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" }
|
||||
val ret = applyInsn(funcCtx, func, insn, index)
|
||||
val ret = applyInsn(funcCtx, f, insn, idx)
|
||||
ctx.trace { "Resulting stack: ${ret.stack}"}
|
||||
ret
|
||||
}
|
||||
@ -56,11 +64,11 @@ open class FuncBuilder {
|
||||
// End the implicit block
|
||||
val implicitBlock = func.currentBlock
|
||||
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 (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
|
||||
Node.Type.Value.I32 -> Opcodes.IRETURN
|
||||
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) {
|
||||
|
||||
is Insn.Node ->
|
||||
applyNodeInsn(ctx, fn, i.insn, index)
|
||||
|
||||
is Insn.ImportFuncRefNeededOnStack ->
|
||||
// Func refs are method handle fields
|
||||
fn.addInsns(
|
||||
@ -81,6 +91,7 @@ open class FuncBuilder {
|
||||
FieldInsnNode(Opcodes.GETFIELD, ctx.cls.thisRef.asmName,
|
||||
ctx.cls.funcName(i.index), MethodHandle::class.ref.asmDesc)
|
||||
).push(MethodHandle::class.ref)
|
||||
|
||||
is Insn.ImportGlobalSetRefNeededOnStack ->
|
||||
// Import setters are method handle fields
|
||||
fn.addInsns(
|
||||
@ -88,13 +99,17 @@ open class FuncBuilder {
|
||||
FieldInsnNode(Opcodes.GETFIELD, ctx.cls.thisRef.asmName,
|
||||
ctx.cls.importGlobalSetterFieldName(i.index), MethodHandle::class.ref.asmDesc)
|
||||
).push(MethodHandle::class.ref)
|
||||
|
||||
is Insn.ThisNeededOnStack ->
|
||||
// load a reference onto the stack from a local variable
|
||||
fn.addInsns(VarInsnNode(Opcodes.ALOAD, 0)).push(ctx.cls.thisRef)
|
||||
|
||||
is Insn.MemNeededOnStack ->
|
||||
putMemoryOnStack(ctx, fn)
|
||||
}
|
||||
|
||||
fun applyNodeInsn(ctx: FuncContext, fn: Func, i: Node.Instr, index: Int) = when (i) {
|
||||
|
||||
is Node.Instr.Unreachable ->
|
||||
fn.addInsns(UnsupportedOperationException::class.athrow("Unreachable")).markUnreachable()
|
||||
is Node.Instr.Nop ->
|
||||
@ -127,18 +142,16 @@ open class FuncBuilder {
|
||||
fn.pop().let { (fn, popped) ->
|
||||
fn.addInsns(InsnNode(if (popped.stackSize == 2) Opcodes.POP2 else Opcodes.POP))
|
||||
}
|
||||
is Node.Instr.Select ->
|
||||
applySelectInsn(ctx, fn)
|
||||
is Node.Instr.GetLocal ->
|
||||
applyGetLocal(ctx, fn, i.index)
|
||||
is Node.Instr.SetLocal ->
|
||||
applySetLocal(ctx, fn, i.index)
|
||||
is Node.Instr.TeeLocal ->
|
||||
applyTeeLocal(ctx, fn, i.index)
|
||||
is Node.Instr.GetGlobal ->
|
||||
applyGetGlobal(ctx, fn, i.index)
|
||||
is Node.Instr.SetGlobal ->
|
||||
applySetGlobal(ctx, fn, i.index)
|
||||
is Node.Instr.Select -> applySelectInsn(ctx, fn)
|
||||
|
||||
// Variable access
|
||||
is Node.Instr.GetLocal -> applyGetLocal(ctx, fn, i.index)
|
||||
is Node.Instr.SetLocal -> applySetLocal(ctx, fn, i.index)
|
||||
is Node.Instr.TeeLocal -> applyTeeLocal(ctx, fn, i.index)
|
||||
is Node.Instr.GetGlobal -> applyGetGlobal(ctx, fn, i.index)
|
||||
is Node.Instr.SetGlobal -> applySetGlobal(ctx, fn, i.index)
|
||||
|
||||
// Memory operators
|
||||
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.I64Load8S, is Node.Instr.I64Load8U, is Node.Instr.I64Load16U, is Node.Instr.I64Load16S,
|
||||
@ -461,18 +474,18 @@ open class FuncBuilder {
|
||||
|
||||
fun popUntilStackSize(
|
||||
ctx: FuncContext,
|
||||
fn: Func,
|
||||
func: Func,
|
||||
block: Func.Block,
|
||||
untilStackSize: Int,
|
||||
keepLast: Boolean
|
||||
): Func {
|
||||
ctx.debug { "For block ${block.insn}, popping until stack size $untilStackSize, keeping last? $keepLast" }
|
||||
// Just get the latest, don't actually pop...
|
||||
val type = if (keepLast) fn.pop().second else null
|
||||
return (0 until Math.max(0, fn.stack.size - untilStackSize)).fold(fn) { fn, _ ->
|
||||
val type = if (keepLast) func.pop().second else null
|
||||
return (0 until Math.max(0, func.stack.size - untilStackSize)).fold(func) { fn, _ ->
|
||||
// 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 ->
|
||||
fn.pop(block).let { (fn, poppedType) ->
|
||||
(if (type != null && fn.stack.size > 1) fn.stackSwap(block) else fn).let { f ->
|
||||
f.pop(block).let { (fn, poppedType) ->
|
||||
fn.addInsns(InsnNode(if (poppedType.stackSize == 2) Opcodes.POP2 else Opcodes.POP))
|
||||
}
|
||||
}
|
||||
@ -1076,10 +1089,12 @@ open class FuncBuilder {
|
||||
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) =
|
||||
// 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.mem.storeOp(ctx, fn, insn).let { fn ->
|
||||
// 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 {
|
||||
// If the current stakc is unreachable, we consider that our block since it
|
||||
fun applyReturnInsn(ctx: FuncContext, func: Func): Func {
|
||||
// If the current stack is unreachable, we consider that our block since it
|
||||
// will pop properly.
|
||||
val block = if (fn.currentBlock.unreachable) fn.currentBlock else fn.blockStack.first()
|
||||
popForBlockEscape(ctx, fn, block).let { fn ->
|
||||
val block = if (func.currentBlock.unreachable) func.currentBlock else func.blockStack.first()
|
||||
popForBlockEscape(ctx, func, block).let { fn ->
|
||||
return when (ctx.node.type.ret) {
|
||||
null ->
|
||||
fn.addInsns(InsnNode(Opcodes.RETURN))
|
||||
@ -1284,9 +1299,9 @@ open class FuncBuilder {
|
||||
fn.popExpecting(Float::class.ref, block).addInsns(InsnNode(Opcodes.FRETURN))
|
||||
Node.Type.Value.F64 ->
|
||||
fn.popExpecting(Double::class.ref, block).addInsns(InsnNode(Opcodes.DRETURN))
|
||||
}.let { fn ->
|
||||
if (fn.stack.isNotEmpty()) throw CompileErr.UnusedStackOnReturn(fn.stack)
|
||||
fn.markUnreachable()
|
||||
}.let { it ->
|
||||
if (it.stack.isNotEmpty()) throw CompileErr.UnusedStackOnReturn(it.stack)
|
||||
it.markUnreachable()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -3,6 +3,15 @@ package asmble.compile.jvm
|
||||
import asmble.ast.Node
|
||||
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(
|
||||
val cls: ClsContext,
|
||||
val node: Node.Func,
|
||||
|
@ -2,6 +2,9 @@ package asmble.compile.jvm
|
||||
|
||||
import asmble.ast.Node
|
||||
|
||||
/**
|
||||
* Does some special manipulations with instruction.
|
||||
*/
|
||||
open class InsnReworker {
|
||||
|
||||
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
|
||||
// blocks/loops/if+else combined with "end"
|
||||
var neededEagerLocalIndices = emptySet<Int>()
|
||||
|
||||
fun addEagerSetIfNeeded(getInsnIndex: Int, localIndex: Int) {
|
||||
// Within the param range? nothing needed
|
||||
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)
|
||||
if (needsEagerInit) neededEagerLocalIndices += localIndex
|
||||
}
|
||||
|
||||
insns.forEachIndexed { index, insn ->
|
||||
if (insn is Insn.Node && insn.insn is Node.Instr.GetLocal) addEagerSetIfNeeded(index, insn.insn.index)
|
||||
}
|
||||
@ -86,11 +91,18 @@ open class InsnReworker {
|
||||
} + 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> {
|
||||
ctx.trace { "Calculating places to inject needed stack variables" }
|
||||
// How we do this:
|
||||
// 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
|
||||
// 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
|
||||
// multiple for the same index
|
||||
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) {
|
||||
ctx.trace { "Injecting $insn back $count stack values" }
|
||||
fun inject(index: Int) {
|
||||
@ -140,10 +160,12 @@ open class InsnReworker {
|
||||
}
|
||||
|
||||
countSoFar += amountChanged
|
||||
if (!foundUnconditionalJump) foundUnconditionalJump = insns[insnIndex].let { insn ->
|
||||
if (!foundUnconditionalJump) {
|
||||
foundUnconditionalJump = insns[insnIndex].let { insn ->
|
||||
insn is Node.Instr.Br || insn is Node.Instr.BrTable ||
|
||||
insn is Node.Instr.Unreachable || insn is Node.Instr.Return
|
||||
}
|
||||
}
|
||||
if (countSoFar == count) {
|
||||
ctx.trace { "Found injection point as before insn #$insnIndex" }
|
||||
return inject(insnIndex)
|
||||
@ -205,13 +227,19 @@ open class InsnReworker {
|
||||
}
|
||||
|
||||
// Build resulting list
|
||||
return insns.foldIndexed(emptyList<Insn>()) { index, ret, insn ->
|
||||
return insns.foldIndexed(emptyList()) { index, ret, insn ->
|
||||
val injections = insnsToInject[index] ?: emptyList()
|
||||
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.Loop, is Node.Instr.Else, is Node.Instr.End, is Node.Instr.Br,
|
||||
is Node.Instr.Return -> NOP
|
||||
@ -283,7 +311,8 @@ open class InsnReworker {
|
||||
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 =
|
||||
if (lastCouldHaveMem) 0
|
||||
else if (insn == Insn.MemNeededOnStack) 1
|
||||
|
122
compiler/src/main/kotlin/asmble/compile/jvm/MemoryByteBuffer.kt
Normal file
122
compiler/src/main/kotlin/asmble/compile/jvm/MemoryByteBuffer.kt
Normal file
@ -0,0 +1,122 @@
|
||||
package asmble.compile.jvm
|
||||
|
||||
import java.nio.ByteBuffer
|
||||
import java.nio.ByteOrder
|
||||
|
||||
/**
|
||||
* The default implementation of MemoryBuffer that based on java.nio.ByteBuffer
|
||||
*/
|
||||
open class MemoryByteBuffer(val bb: ByteBuffer) : MemoryBuffer() {
|
||||
override fun put(arr: ByteArray): MemoryBuffer {
|
||||
bb.put(arr)
|
||||
return this
|
||||
}
|
||||
|
||||
override fun clear(): MemoryBuffer {
|
||||
bb.clear()
|
||||
return this
|
||||
}
|
||||
|
||||
override fun get(arr: ByteArray): MemoryBuffer {
|
||||
bb.get(arr)
|
||||
return this
|
||||
}
|
||||
|
||||
override fun putLong(index: Int, n: Long): MemoryBuffer {
|
||||
bb.putLong(index, n)
|
||||
return this
|
||||
}
|
||||
|
||||
override fun putDouble(index: Int, n: Double): MemoryBuffer {
|
||||
bb.putDouble(index, n)
|
||||
return this
|
||||
}
|
||||
|
||||
override fun putShort(index: Int, n: Short): MemoryBuffer {
|
||||
bb.putShort(index, n)
|
||||
return this
|
||||
}
|
||||
|
||||
override fun putFloat(index: Int, n: Float): MemoryBuffer {
|
||||
bb.putFloat(index, n)
|
||||
return this
|
||||
}
|
||||
|
||||
override fun put(index: Int, b: Byte): MemoryBuffer {
|
||||
bb.put(index, b)
|
||||
return this
|
||||
}
|
||||
|
||||
override fun putInt(index: Int, n: Int): MemoryBuffer {
|
||||
bb.putInt(index, n)
|
||||
return this
|
||||
}
|
||||
|
||||
override fun capacity(): Int {
|
||||
return bb.capacity()
|
||||
}
|
||||
|
||||
override fun limit(): Int {
|
||||
return bb.limit()
|
||||
}
|
||||
|
||||
override fun limit(newLimit: Int): MemoryBuffer {
|
||||
bb.limit(newLimit)
|
||||
return this
|
||||
}
|
||||
|
||||
override fun position(newPosition: Int): MemoryBuffer {
|
||||
bb.position(newPosition)
|
||||
return this
|
||||
}
|
||||
|
||||
override fun order(order: ByteOrder): MemoryBuffer {
|
||||
bb.order(order)
|
||||
return this
|
||||
}
|
||||
|
||||
override fun duplicate(): MemoryBuffer {
|
||||
return MemoryByteBuffer(bb.duplicate())
|
||||
}
|
||||
|
||||
override fun put(arr: ByteArray, offset: Int, length: Int): MemoryBuffer {
|
||||
bb.put(arr, offset, length)
|
||||
return this
|
||||
}
|
||||
|
||||
override fun getInt(index: Int): Int {
|
||||
return bb.getInt(index)
|
||||
}
|
||||
|
||||
override fun get(index: Int): Byte {
|
||||
return bb.get(index)
|
||||
}
|
||||
|
||||
override fun getLong(index: Int): Long {
|
||||
return bb.getLong(index)
|
||||
}
|
||||
|
||||
override fun getShort(index: Int): Short {
|
||||
return bb.getShort(index)
|
||||
}
|
||||
|
||||
override fun getFloat(index: Int): Float {
|
||||
return bb.getFloat(index)
|
||||
}
|
||||
|
||||
override fun getDouble(index: Int): Double {
|
||||
return bb.getDouble(index)
|
||||
}
|
||||
|
||||
override fun equals(other: Any?): Boolean {
|
||||
if (this === other)
|
||||
return true
|
||||
if (other !is MemoryByteBuffer)
|
||||
return false
|
||||
return bb == other.bb
|
||||
}
|
||||
|
||||
override fun hashCode(): Int {
|
||||
return bb.hashCode()
|
||||
}
|
||||
}
|
@ -2,14 +2,25 @@ package asmble.compile.jvm
|
||||
|
||||
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) {
|
||||
|
||||
/** The internal name of the class corresponding to this object or array type. */
|
||||
val asmName: String get() = asm.internalName
|
||||
|
||||
/** The descriptor corresponding to this Java type. */
|
||||
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
|
||||
|
||||
fun asMethodRetDesc(vararg args: TypeRef) = Type.getMethodDescriptor(asm, *args.map { it.asm }.toTypedArray())
|
||||
|
||||
fun equivalentTo(other: TypeRef) = this == other || this == Unknown || other == Unknown
|
||||
|
||||
object UnknownType
|
||||
|
@ -2,12 +2,11 @@ package asmble.io
|
||||
|
||||
import asmble.ast.Node
|
||||
import asmble.util.*
|
||||
import java.io.ByteArrayInputStream
|
||||
import java.nio.ByteBuffer
|
||||
|
||||
open class BinaryToAst(
|
||||
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 {
|
||||
|
||||
@ -186,12 +185,14 @@ open class BinaryToAst(
|
||||
require(sectionId > maxSectionId) { "Section ID $sectionId came after $maxSectionId" }.
|
||||
also { maxSectionId = sectionId }
|
||||
val sectionLen = b.readVarUInt32AsInt()
|
||||
// each 'read' invocation creates new InputStream and don't closes it
|
||||
sections += sectionId to b.read(sectionLen)
|
||||
}
|
||||
|
||||
// Now build the module
|
||||
fun <T> readSectionList(sectionId: Int, fn: (ByteReader) -> T) =
|
||||
sections.find { it.first == sectionId }?.second?.readList(fn) ?: emptyList()
|
||||
|
||||
val types = readSectionList(1, this::toFuncType)
|
||||
val funcIndices = readSectionList(3) { it.readVarUInt32AsInt() }
|
||||
var nameSection: Node.NameSection? = null
|
||||
|
@ -100,6 +100,7 @@ abstract class ByteReader {
|
||||
return result
|
||||
}
|
||||
|
||||
// todo looks like this InputStream isn't ever closed
|
||||
class InputStream(val ins: java.io.InputStream) : ByteReader() {
|
||||
private var nextByte: Byte? = null
|
||||
private var sawEof = false
|
||||
|
56
compiler/src/main/kotlin/asmble/run/jvm/EnvModule.kt
Normal file
56
compiler/src/main/kotlin/asmble/run/jvm/EnvModule.kt
Normal file
@ -0,0 +1,56 @@
|
||||
package asmble.run.jvm
|
||||
|
||||
/**
|
||||
* Used to tack the state of the environment module.
|
||||
*/
|
||||
data class EnvState(
|
||||
var spentGas: Long = 0,
|
||||
// executed instruction counter
|
||||
var EIC: Long = 0
|
||||
)
|
||||
|
||||
/**
|
||||
* Module used for gas and EIC metering.
|
||||
*/
|
||||
open class EnvModule(private val gasLimit: Long) {
|
||||
|
||||
private var state = EnvState();
|
||||
|
||||
/**
|
||||
* [Wasm function]
|
||||
* Adds spent gas to overall spent gas and checks limit exceeding.
|
||||
*/
|
||||
fun gas(spentGas: Int) {
|
||||
if(state.spentGas + spentGas > gasLimit) {
|
||||
// TODO : check for overflow, throw an exception
|
||||
}
|
||||
|
||||
state.spentGas += spentGas;
|
||||
}
|
||||
|
||||
/**
|
||||
* [Wasm function]
|
||||
* Adds EIC to overall executed instruction counter.
|
||||
*/
|
||||
fun eic(EIC: Int) {
|
||||
state.EIC += EIC;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets spent gas and EIC value to 0. Used from WasmVm to clear gas value before metering.
|
||||
* It should be impossible to call this function from a Wasm module.
|
||||
*/
|
||||
fun clearState() {
|
||||
state.spentGas = 0;
|
||||
state.EIC = 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns environment module state.
|
||||
* Used from WasmVm to determine spent gas and executed instruction counter after each invocation.
|
||||
*/
|
||||
fun getState(): EnvState {
|
||||
return state;
|
||||
}
|
||||
|
||||
}
|
45
compiler/src/main/kotlin/asmble/run/jvm/LoggerModule.kt
Normal file
45
compiler/src/main/kotlin/asmble/run/jvm/LoggerModule.kt
Normal file
@ -0,0 +1,45 @@
|
||||
package asmble.run.jvm
|
||||
|
||||
import asmble.compile.jvm.Mem
|
||||
import java.io.PrintWriter
|
||||
import java.nio.ByteBuffer
|
||||
|
||||
/**
|
||||
* Module used for logging UTF-8 strings from a Wasm module to a given writer.
|
||||
*/
|
||||
open class LoggerModule(val writer: PrintWriter) {
|
||||
|
||||
// one memory page is quite enough for save temporary buffer
|
||||
private val memoryPages = 1
|
||||
|
||||
private val memory =
|
||||
ByteBuffer.allocate(memoryPages * Mem.PAGE_SIZE) as ByteBuffer
|
||||
|
||||
/**
|
||||
* [Wasm function]
|
||||
* Writes one byte to the logger memory buffer. If there is no place flushes
|
||||
* all data from the buffer to [PrintWriter] and try to put the byte again.
|
||||
*/
|
||||
fun write(byte: Int) {
|
||||
val isFull = memory.position() >= memory.limit()
|
||||
if (isFull) {
|
||||
flush()
|
||||
}
|
||||
memory.put(byte.toByte())
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* [Wasm function]
|
||||
* Reads all bytes from the logger memory buffer, convert its to UTF-8
|
||||
* string and writes to stdout.
|
||||
* Cleans the logger memory buffer.
|
||||
*/
|
||||
fun flush() {
|
||||
val message = String(memory.array(), 0, memory.position())
|
||||
writer.print(message)
|
||||
writer.flush()
|
||||
memory.clear()
|
||||
}
|
||||
|
||||
}
|
@ -76,6 +76,8 @@ interface Module {
|
||||
|
||||
// If there is a memory import, we have to get the one with the mem class as the first
|
||||
val memImport = mod.imports.find { it.kind is Node.Import.Kind.Memory }
|
||||
val builder = ctx.memoryBuilder
|
||||
|
||||
val memLimit = if (memImport != null) {
|
||||
constructor = cls.declaredConstructors.find { it.parameterTypes.firstOrNull()?.ref == mem.memType }
|
||||
val memImportKind = memImport.kind as Node.Import.Kind.Memory
|
||||
@ -89,6 +91,13 @@ interface Module {
|
||||
throw RunErr.ImportMemoryCapacityTooLarge(it * Mem.PAGE_SIZE, memCap)
|
||||
}
|
||||
memLimit
|
||||
} else if (builder != null) {
|
||||
constructor = cls.declaredConstructors.find { it.parameterTypes.firstOrNull()?.ref == mem.memType }
|
||||
|
||||
val memLimit = ctx.defaultMaxMemPages * Mem.PAGE_SIZE
|
||||
val memInst = builder.build(memLimit)
|
||||
constructorParams += memInst
|
||||
memLimit
|
||||
} else {
|
||||
// Find the constructor with no max mem amount (i.e. not int and not memory)
|
||||
constructor = cls.declaredConstructors.find {
|
||||
|
@ -19,6 +19,20 @@ import java.lang.invoke.MethodType
|
||||
import java.lang.reflect.InvocationTargetException
|
||||
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(
|
||||
val packageName: String,
|
||||
val modules: List<Module.Compiled> = emptyList(),
|
||||
@ -29,8 +43,10 @@ data class ScriptContext(
|
||||
ScriptContext.SimpleClassLoader(ScriptContext::class.java.classLoader, logger),
|
||||
val exceptionTranslator: ExceptionTranslator = ExceptionTranslator,
|
||||
val defaultMaxMemPages: Int = 1,
|
||||
val includeBinaryInCompiledClass: Boolean = false
|
||||
val includeBinaryInCompiledClass: Boolean = false,
|
||||
val memoryBuilder: MemoryBufferBuilder? = null
|
||||
) : Logger by logger {
|
||||
|
||||
fun withHarnessRegistered(out: PrintWriter = PrintWriter(System.out, true)) =
|
||||
withModuleRegistered("spectest", Module.Native(TestHarness(out)))
|
||||
|
||||
|
@ -3,6 +3,8 @@ package asmble.run.jvm
|
||||
import asmble.annotation.WasmExport
|
||||
import asmble.annotation.WasmExternalKind
|
||||
import asmble.compile.jvm.Mem
|
||||
import asmble.compile.jvm.MemoryBuffer
|
||||
import asmble.compile.jvm.MemoryByteBuffer
|
||||
import java.io.PrintWriter
|
||||
import java.lang.invoke.MethodHandle
|
||||
import java.nio.ByteBuffer
|
||||
@ -17,10 +19,10 @@ open class TestHarness(val out: PrintWriter) {
|
||||
val global_f32 = 666.6f
|
||||
val global_f64 = 666.6
|
||||
val table = arrayOfNulls<MethodHandle>(10)
|
||||
val memory = ByteBuffer.
|
||||
val memory = MemoryByteBuffer(ByteBuffer.
|
||||
allocateDirect(2 * Mem.PAGE_SIZE).
|
||||
order(ByteOrder.LITTLE_ENDIAN).
|
||||
limit(Mem.PAGE_SIZE) as ByteBuffer
|
||||
limit(Mem.PAGE_SIZE) as ByteBuffer) as MemoryBuffer
|
||||
|
||||
// Note, we have all of these overloads because my import method
|
||||
// resolver is simple right now and only finds exact methods via
|
||||
|
@ -5,7 +5,6 @@ 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
|
||||
|
||||
@ -35,9 +34,9 @@ class LargeDataTest : TestBase() {
|
||||
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
|
||||
val buf = field[cls.newInstance()] as MemoryByteBuffer
|
||||
// Grab all + 1 and check values
|
||||
val bytesActual = ByteArray(70001).also { buf.get(0, it) }
|
||||
val bytesActual = ByteArray(70001).also { buf.bb.get(0, it) }
|
||||
bytesActual.forEachIndexed { index, byte ->
|
||||
assertEquals(if (index == 70000) 0.toByte() else bytesExpected[index], byte)
|
||||
}
|
||||
|
@ -4,10 +4,10 @@ import asmble.TestBase
|
||||
import asmble.ast.Node
|
||||
import asmble.compile.jvm.AstToAsm
|
||||
import asmble.compile.jvm.ClsContext
|
||||
import asmble.compile.jvm.MemoryByteBuffer
|
||||
import org.junit.Assert
|
||||
import org.junit.Test
|
||||
import org.objectweb.asm.MethodTooLargeException
|
||||
import java.nio.ByteBuffer
|
||||
import java.util.*
|
||||
import kotlin.test.assertEquals
|
||||
|
||||
@ -62,7 +62,7 @@ class LargeFuncTest : TestBase() {
|
||||
// Run someFunc
|
||||
cls.getMethod("someFunc").invoke(inst)
|
||||
// Get the memory out
|
||||
val mem = cls.getMethod("getMemory").invoke(inst) as ByteBuffer
|
||||
val mem = cls.getMethod("getMemory").invoke(inst) as MemoryByteBuffer
|
||||
// Read out the mem values
|
||||
(0 until numInsnChunks).forEach { assertEquals(it * (it - 1), mem.getInt(it * 4)) }
|
||||
}
|
||||
|
51
compiler/src/test/kotlin/asmble/run/jvm/LoggerModuleTest.kt
Normal file
51
compiler/src/test/kotlin/asmble/run/jvm/LoggerModuleTest.kt
Normal file
@ -0,0 +1,51 @@
|
||||
package asmble.run.jvm
|
||||
|
||||
import asmble.TestBase
|
||||
import org.junit.Test
|
||||
import java.io.PrintWriter
|
||||
import java.io.StringWriter
|
||||
import kotlin.test.assertEquals
|
||||
|
||||
class LoggerModuleTest : TestBase() {
|
||||
|
||||
@Test
|
||||
fun writeAndFlushTest() {
|
||||
val stream = StringWriter()
|
||||
val logger = LoggerModule(PrintWriter(stream))
|
||||
|
||||
logger.flush() // checks that no raise error
|
||||
|
||||
val testString = "test String for log to stdout"
|
||||
for (byte: Byte in testString.toByteArray()) {
|
||||
logger.write(byte.toInt())
|
||||
}
|
||||
|
||||
logger.flush()
|
||||
val loggedString = stream.toString()
|
||||
assertEquals(testString, loggedString)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun writeAndFlushMoreThanLoggerBufferTest() {
|
||||
val stream = StringWriter()
|
||||
// logger buffer has 65Kb size
|
||||
val logger = LoggerModule(PrintWriter(stream))
|
||||
|
||||
val testString = longString(65_000 * 2) // twice as much as logger buffer
|
||||
for (byte: Byte in testString.toByteArray()) {
|
||||
logger.write(byte.toInt())
|
||||
}
|
||||
|
||||
logger.flush()
|
||||
val loggedString = stream.toString()
|
||||
assertEquals(testString, loggedString)
|
||||
}
|
||||
|
||||
private fun longString(size: Int): String {
|
||||
val stringBuffer = StringBuffer()
|
||||
for (idx: Int in (1 until size)) {
|
||||
stringBuffer.append((idx % Byte.MAX_VALUE).toChar())
|
||||
}
|
||||
return stringBuffer.toString()
|
||||
}
|
||||
}
|
@ -3,6 +3,8 @@ package asmble.run.jvm
|
||||
import asmble.BaseTestUnit
|
||||
import asmble.TestBase
|
||||
import asmble.annotation.WasmModule
|
||||
import asmble.compile.jvm.MemoryBufferBuilder
|
||||
import asmble.compile.jvm.MemoryByteBuffer
|
||||
import asmble.io.AstToBinary
|
||||
import asmble.io.AstToSExpr
|
||||
import asmble.io.ByteWriter
|
||||
@ -12,6 +14,7 @@ import org.junit.Test
|
||||
import java.io.ByteArrayOutputStream
|
||||
import java.io.OutputStreamWriter
|
||||
import java.io.PrintWriter
|
||||
import java.nio.ByteBuffer
|
||||
import kotlin.test.assertEquals
|
||||
import kotlin.test.assertNotNull
|
||||
|
||||
@ -40,7 +43,10 @@ abstract class TestRunner<out T : BaseTestUnit>(val unit: T) : TestBase() {
|
||||
adjustContext = { it.copy(eagerFailLargeMemOffset = false) },
|
||||
defaultMaxMemPages = unit.defaultMaxMemPages,
|
||||
// Include the binary data so we can check it later
|
||||
includeBinaryInCompiledClass = true
|
||||
includeBinaryInCompiledClass = true,
|
||||
memoryBuilder = MemoryBufferBuilder { it ->
|
||||
MemoryByteBuffer(ByteBuffer.allocateDirect(it))
|
||||
}
|
||||
).withHarnessRegistered(PrintWriter(OutputStreamWriter(out, Charsets.UTF_8), true))
|
||||
|
||||
// This will fail assertions as necessary
|
||||
|
Submodule compiler/src/test/resources/spec updated: fda397a56f...1f00d57d00
@ -9,9 +9,3 @@ Compile Rust to WASM and then to the JVM. In order of complexity:
|
||||
* [rust-simple](rust-simple)
|
||||
* [rust-string](rust-string)
|
||||
* [rust-regex](rust-regex)
|
||||
|
||||
### C/C++
|
||||
|
||||
Compile C to WASM and then to the JVM. In order of complexity:
|
||||
|
||||
* [c-simple](c-simple)
|
@ -2,8 +2,9 @@
|
||||
|
||||
extern crate regex;
|
||||
|
||||
use std::ptr::NonNull;
|
||||
use regex::Regex;
|
||||
use std::heap::{Alloc, Heap, Layout};
|
||||
use std::alloc::{Alloc, Global, Layout};
|
||||
use std::mem;
|
||||
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]
|
||||
pub extern "C" fn alloc(size: usize) -> *mut u8 {
|
||||
pub extern "C" fn alloc(size: usize) -> NonNull<u8> {
|
||||
unsafe {
|
||||
let layout = Layout::from_size_align(size, mem::align_of::<u8>()).unwrap();
|
||||
Heap.alloc(layout).unwrap()
|
||||
Global.alloc(layout).unwrap()
|
||||
}
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub extern "C" fn dealloc(ptr: *mut u8, size: usize) {
|
||||
pub extern "C" fn dealloc(ptr: NonNull<u8>, size: usize) {
|
||||
unsafe {
|
||||
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.Pattern;
|
||||
|
||||
/**
|
||||
* Implementation of {@link RegexLib} based on `java.util.regex`.
|
||||
*/
|
||||
public class JavaLib implements RegexLib<String> {
|
||||
@Override
|
||||
public JavaPattern compile(String str) {
|
||||
|
@ -5,6 +5,10 @@ import asmble.generated.RustRegex;
|
||||
import java.nio.ByteBuffer;
|
||||
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> {
|
||||
|
||||
// 600 pages is enough for our use
|
||||
|
@ -1,6 +1,7 @@
|
||||
#![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::mem;
|
||||
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]
|
||||
pub extern "C" fn alloc(size: usize) -> *mut u8 {
|
||||
pub extern "C" fn alloc(size: usize) -> NonNull<u8> {
|
||||
unsafe {
|
||||
let layout = Layout::from_size_align(size, mem::align_of::<u8>()).unwrap();
|
||||
Heap.alloc(layout).unwrap()
|
||||
Global.alloc(layout).unwrap()
|
||||
}
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub extern "C" fn dealloc(ptr: *mut u8, size: usize) {
|
||||
pub extern "C" fn dealloc(ptr: NonNull<u8>, size: usize) {
|
||||
unsafe {
|
||||
let layout = Layout::from_size_align(size, mem::align_of::<u8>()).unwrap();
|
||||
Heap.dealloc(ptr, layout);
|
||||
Global.dealloc(ptr, layout);
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user