10 Commits

Author SHA1 Message Date
b9b45cf997 Merge pull request #2 from fluencelabs/logger
Fix logger and return C example
2018-11-09 13:47:41 +04:00
2bfa39a3c6 Tweaking after merge 2018-11-09 10:30:38 +04:00
317b608048 Merge fix for late init for logger 2018-11-09 10:28:47 +04:00
21b023f1c6 Return C example and skip it by default 2018-11-09 10:18:49 +04:00
765d8b4dba Possibility to skip examples 2018-11-09 10:03:44 +04:00
fb0be9d31a Merge remote-tracking branch 'upstream/master'
# Conflicts:
#	compiler/src/test/resources/spec
2018-09-18 15:56:16 +04:00
d1f48aaaa0 Replace previous large-method-split attempt with msplit-based one for issue #19 2018-09-13 16:50:48 -05:00
46a8ce3f52 Updated to latest spec and minor fix on block insn insertion count 2018-09-13 13:10:11 -05:00
6352efaa96 Update Kotlin and ASM 2018-09-12 16:01:46 -05:00
326a0cdaba Change Go example to simple hello world 2018-09-12 15:49:49 -05:00
22 changed files with 1001 additions and 443 deletions

View File

@ -2,8 +2,8 @@ group 'asmble'
version '0.2.0' version '0.2.0'
buildscript { buildscript {
ext.kotlin_version = '1.2.51' ext.kotlin_version = '1.2.61'
ext.asm_version = '5.2' ext.asm_version = '6.2.1'
repositories { repositories {
mavenCentral() mavenCentral()
@ -21,7 +21,11 @@ buildscript {
allprojects { allprojects {
apply plugin: 'java' apply plugin: 'java'
group 'com.github.cretz.asmble' group 'com.github.cretz.asmble'
version '0.4.0-fl-fix' version '0.4.1-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 { repositories {
mavenCentral() mavenCentral()
@ -54,9 +58,9 @@ project(':compiler') {
compile "org.jetbrains.kotlin:kotlin-reflect:$kotlin_version" compile "org.jetbrains.kotlin:kotlin-reflect:$kotlin_version"
compile "org.ow2.asm:asm-tree:$asm_version" compile "org.ow2.asm:asm-tree:$asm_version"
compile "org.ow2.asm:asm-util:$asm_version" compile "org.ow2.asm:asm-util:$asm_version"
compile "org.ow2.asm:asm-commons:$asm_version"
testCompile 'junit:junit:4.12' testCompile 'junit:junit:4.12'
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"
} }
publishSettings(project, 'asmble-compiler', 'Asmble WASM Compiler') publishSettings(project, 'asmble-compiler', 'Asmble WASM Compiler')
@ -68,6 +72,38 @@ project(':examples') {
compileOnly project(':compiler') compileOnly project(':compiler')
} }
// C/C++ example helpers
task cToWasm {
doFirst {
mkdir 'build'
exec {
def cFileName = fileTree(dir: 'src', includes: ['*.c']).files.iterator().next()
commandLine 'clang', '--target=wasm32-unknown-unknown-wasm', '-O3', cFileName, '-c', '-o', 'build/lib.wasm'
}
}
}
task showCWast(type: JavaExec) {
dependsOn cToWasm
classpath configurations.compileClasspath
main = 'asmble.cli.MainKt'
doFirst {
args 'translate', 'build/lib.wasm'
}
}
task compileCWasm(type: JavaExec) {
dependsOn cToWasm
classpath configurations.compileClasspath
main = 'asmble.cli.MainKt'
doFirst {
def outFile = 'build/wasm-classes/' + wasmCompiledClassName.replace('.', '/') + '.class'
file(outFile).parentFile.mkdirs()
args 'compile', 'build/lib.wasm', wasmCompiledClassName, '-out', outFile
}
}
// Go example helpers // Go example helpers
task goToWasm { task goToWasm {
@ -142,45 +178,82 @@ 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
}
//project(':examples:go-simple') { apply plugin: 'application'
// apply plugin: 'application' ext.wasmCompiledClassName = 'asmble.generated.CSimple'
// ext.wasmCompiledClassName = 'asmble.generated.GoSimple' dependencies {
// dependencies { compile files('build/wasm-classes')
// compile files('build/wasm-classes') }
// }
// compileJava {
// dependsOn compileGoWasm
// }
// mainClassName = 'asmble.examples.gosimple.Main'
//}
// todo temporary disable Rust regex, because some strings in wasm code exceed the size in 65353 bytes. compileJava {
dependsOn compileCWasm
}
mainClassName = 'asmble.examples.csimple.Main'
}
// project(':examples:rust-regex') { project(':examples:go-simple') {
// apply plugin: 'application' if (project.name in skipExamples) {
// apply plugin: 'me.champeau.gradle.jmh' println("[Note!] Building and runnig for ${project.name} was skipped")
// ext.wasmCompiledClassName = 'asmble.generated.RustRegex' test.onlyIf { false } // explicit skipping tests
// dependencies { compileJava.onlyIf { false } // explicit skipping compile
// compile files('build/wasm-classes') return
// testCompile 'junit:junit:4.12' }
// } apply plugin: 'application'
// compileJava { ext.wasmCompiledClassName = 'asmble.generated.GoSimple'
// dependsOn compileRustWasm dependencies {
// } compile files('build/wasm-classes')
// mainClassName = 'asmble.examples.rustregex.Main' }
// test { compileJava {
// testLogging.showStandardStreams = true dependsOn compileGoWasm
// testLogging.events 'PASSED', 'SKIPPED' }
// } mainClassName = 'asmble.examples.gosimple.Main'
// jmh { }
// iterations = 5
// warmupIterations = 5 project(':examples:rust-regex') {
// fork = 3 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'
dependencies {
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') {
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' apply plugin: 'application'
ext.wasmCompiledClassName = 'asmble.generated.RustSimple' ext.wasmCompiledClassName = 'asmble.generated.RustSimple'
dependencies { dependencies {
@ -193,6 +266,12 @@ project(':examples:rust-simple') {
} }
project(':examples:rust-string') { 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' apply plugin: 'application'
ext.wasmCompiledClassName = 'asmble.generated.RustString' ext.wasmCompiledClassName = 'asmble.generated.RustString'
dependencies { dependencies {

View File

@ -0,0 +1 @@
Taken from https://github.com/cretz/msplit

View File

@ -0,0 +1,286 @@
package asmble.compile.jvm.msplit;
import org.objectweb.asm.Label;
import org.objectweb.asm.Opcodes;
import org.objectweb.asm.Type;
import org.objectweb.asm.tree.*;
import java.util.*;
import static asmble.compile.jvm.msplit.Util.*;
/** Splits a method into two */
public class SplitMethod {
protected final int api;
/** @param api Same as for {@link org.objectweb.asm.MethodVisitor#MethodVisitor(int)} or any other ASM class */
public SplitMethod(int api) { this.api = api; }
/**
* Calls {@link #split(String, MethodNode, int, int, int)} with minSize as 20% + 1 of the original, maxSize as
* 70% + 1 of the original, and firstAtLeast as maxSize. The original method is never modified and the result can
* be null if no split points are found.
*/
public Result split(String owner, MethodNode method) {
// Between 20% + 1 and 70% + 1 of size
int insnCount = method.instructions.size();
int minSize = (int) (insnCount * 0.2) + 1;
int maxSize = (int) (insnCount * 0.7) + 1;
return split(owner, method, minSize, maxSize, maxSize);
}
/**
* Splits the given method into two. This uses a {@link Splitter} to consistently create
* {@link asmble.compile.jvm.msplit.Splitter.SplitPoint}s until one reaches firstAtLeast or the largest otherwise, and then calls
* {@link #fromSplitPoint(String, MethodNode, Splitter.SplitPoint)}.
*
* @param owner The internal name of the owning class. Needed when splitting to call the split off method.
* @param method The method to split, never modified
* @param minSize The minimum number of instructions the split off method must have
* @param maxSize The maximum number of instructions the split off method can have
* @param firstAtLeast The number of instructions that, when first reached, will immediately be used without
* continuing. Since split points are streamed, this allows splitting without waiting to
* find the largest overall. If this is &lt= 0, it will not apply and all split points will be
* checked to find the largest before doing the split.
* @return The resulting split method or null if there were no split points found
*/
public Result split(String owner, MethodNode method, int minSize, int maxSize, int firstAtLeast) {
// Get the largest split point
Splitter.SplitPoint largest = null;
for (Splitter.SplitPoint point : new Splitter(api, owner, method, minSize, maxSize)) {
if (largest == null || point.length > largest.length) {
largest = point;
// Early exit?
if (firstAtLeast > 0 && largest.length >= firstAtLeast) break;
}
}
if (largest == null) return null;
return fromSplitPoint(owner, method, largest);
}
/**
* Split the given method at the given split point. Called by {@link #split(String, MethodNode, int, int, int)}. The
* original method is never modified.
*/
public Result fromSplitPoint(String owner, MethodNode orig, Splitter.SplitPoint splitPoint) {
MethodNode splitOff = createSplitOffMethod(orig, splitPoint);
MethodNode trimmed = createTrimmedMethod(owner, orig, splitOff, splitPoint);
return new Result(trimmed, splitOff);
}
protected MethodNode createSplitOffMethod(MethodNode orig, Splitter.SplitPoint splitPoint) {
// The new method is a static synthetic method named method.name + "$split" that returns an object array
// Key is previous local index, value is new local index
Map<Integer, Integer> localsMap = new HashMap<>();
// The new method's parameters are all stack items + all read locals
List<Type> args = new ArrayList<>(splitPoint.neededFromStackAtStart);
splitPoint.localsRead.forEach((index, type) -> {
args.add(type);
localsMap.put(index, args.size() - 1);
});
// Create the new method
MethodNode newMethod = new MethodNode(api,
Opcodes.ACC_STATIC + Opcodes.ACC_PRIVATE + Opcodes.ACC_SYNTHETIC, orig.name + "$split",
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();
for (Integer key : splitPoint.localsWritten.keySet()) {
if (!localsMap.containsKey(key)) {
localsMap.put(key, newLocalIndex);
newLocalIndex++;
}
}
// First set of instructions is pushing the new stack from the params
for (int i = 0; i < splitPoint.neededFromStackAtStart.size(); i++) {
Type item = splitPoint.neededFromStackAtStart.get(i);
newMethod.visitVarInsn(loadOpFromType(item), i);
}
// Next set of instructions comes verbatim from the original, but we have to change the local indexes
Set<Label> seenLabels = new HashSet<>();
for (int i = 0; i < splitPoint.length; i++) {
AbstractInsnNode insn = orig.instructions.get(i + splitPoint.start);
// Skip frames
if (insn instanceof FrameNode) continue;
// Store the label
if (insn instanceof LabelNode) seenLabels.add(((LabelNode) insn).getLabel());
// Change the local if needed
if (insn instanceof VarInsnNode) {
insn = insn.clone(Collections.emptyMap());
((VarInsnNode) insn).var = localsMap.get(((VarInsnNode) insn).var);
} else if (insn instanceof IincInsnNode) {
insn = insn.clone(Collections.emptyMap());
((VarInsnNode) insn).var = localsMap.get(((VarInsnNode) insn).var);
}
insn.accept(newMethod);
}
// Final set of instructions is an object array of stack to set and then locals written
// Create the object array
int retArrSize = splitPoint.putOnStackAtEnd.size() + splitPoint.localsWritten.size();
intConst(retArrSize).accept(newMethod);
newMethod.visitTypeInsn(Opcodes.ANEWARRAY, OBJECT_TYPE.getInternalName());
// So, we're going to store the arr in the next avail local
int retArrLocalIndex = newLocalIndex;
newMethod.visitVarInsn(Opcodes.ASTORE, retArrLocalIndex);
// Now go over each stack item and load the arr, swap w/ the stack, add the index, swap with the stack, and store
for (int i = splitPoint.putOnStackAtEnd.size() - 1; i >= 0; i--) {
Type item = splitPoint.putOnStackAtEnd.get(i);
// Box the item on the stack if necessary
boxStackIfNecessary(item, newMethod);
// Load the array
newMethod.visitVarInsn(Opcodes.ALOAD, retArrLocalIndex);
// Swap to put stack back on top
newMethod.visitInsn(Opcodes.SWAP);
// Add the index
intConst(i).accept(newMethod);
// Swap to put the stack value back on top
newMethod.visitInsn(Opcodes.SWAP);
// Now that we have arr, index, value, we can store in the array
newMethod.visitInsn(Opcodes.AASTORE);
}
// Do the same with written locals
int currIndex = splitPoint.putOnStackAtEnd.size();
for (Integer index : splitPoint.localsWritten.keySet()) {
Type item = splitPoint.localsWritten.get(index);
// Load the array
newMethod.visitVarInsn(Opcodes.ALOAD, retArrLocalIndex);
// Add the arr index
intConst(currIndex).accept(newMethod);
currIndex++;
// Load the var
newMethod.visitVarInsn(loadOpFromType(item), localsMap.get(index));
// Box it if necessary
boxStackIfNecessary(item, newMethod);
// Store in array
newMethod.visitInsn(Opcodes.AASTORE);
}
// Load the array out and return it
newMethod.visitVarInsn(Opcodes.ALOAD, retArrLocalIndex);
newMethod.visitInsn(Opcodes.ARETURN);
// Any try catch blocks that start in here
for (TryCatchBlockNode tryCatch : orig.tryCatchBlocks) {
if (seenLabels.contains(tryCatch.start.getLabel())) tryCatch.accept(newMethod);
}
// Reset the labels
newMethod.instructions.resetLabels();
return newMethod;
}
protected MethodNode createTrimmedMethod(String owner, MethodNode orig,
MethodNode splitOff, Splitter.SplitPoint splitPoint) {
// The trimmed method is the same as the original, yet the split area is replaced with a call to the split off
// portion. Before calling the split-off, we have to add locals to the stack part. Then afterwards, we have to
// replace the stack and written locals.
// Effectively clone the orig
MethodNode newMethod = new MethodNode(api, orig.access, orig.name, orig.desc,
orig.signature, orig.exceptions.toArray(new String[0]));
orig.accept(newMethod);
// Remove all insns, we'll re-add the ones outside the split range
newMethod.instructions.clear();
// Remove all try catch blocks and keep track of seen labels, we'll re-add them at the end
newMethod.tryCatchBlocks.clear();
Set<Label> seenLabels = new HashSet<>();
// Also keep track of the locals that have been stored, need to know
Set<Integer> seenStoredLocals = new HashSet<>();
// If this is an instance method, we consider "0" (i.e. "this") as seen
if ((orig.access & Opcodes.ACC_STATIC) == 0) seenStoredLocals.add(0);
// Add the insns before split
for (int i = 0; i < splitPoint.start; i++) {
AbstractInsnNode insn = orig.instructions.get(i + splitPoint.start);
// Skip frames
if (insn instanceof FrameNode) continue;
// Record label
if (insn instanceof LabelNode) seenLabels.add(((LabelNode) insn).getLabel());
// Check a local store has happened
if (insn instanceof VarInsnNode && isStoreOp(insn.getOpcode())) seenStoredLocals.add(((VarInsnNode) insn).var);
insn.accept(newMethod);
}
// Push all the read locals on the stack
splitPoint.localsRead.forEach((index, type) -> {
// We've seen a store for this, so just load it, otherwise use a zero val
// TODO: safe? if not, maybe just put at the top of the method a bunch of defaulted locals?
if (seenStoredLocals.contains(index)) newMethod.visitVarInsn(loadOpFromType(type), index);
else zeroVal(type).accept(newMethod);
});
// Invoke the split off method
newMethod.visitMethodInsn(Opcodes.INVOKESTATIC, owner, splitOff.name, splitOff.desc, false);
// Now the object array is on the stack which contains stack pieces + written locals
// Take off the locals
int localArrIndex = splitPoint.putOnStackAtEnd.size();
for (Integer index : splitPoint.localsWritten.keySet()) {
// Dupe the array
newMethod.visitInsn(Opcodes.DUP);
// Put the index on the stack
intConst(localArrIndex).accept(newMethod);
localArrIndex++;
// Load the written local
Type item = splitPoint.localsWritten.get(index);
newMethod.visitInsn(Opcodes.AALOAD);
// Cast to local type
if (!item.equals(OBJECT_TYPE)) {
newMethod.visitTypeInsn(Opcodes.CHECKCAST, boxedTypeIfNecessary(item).getInternalName());
}
// Unbox if necessary
unboxStackIfNecessary(item, newMethod);
// Store in the local
newMethod.visitVarInsn(storeOpFromType(item), index);
}
// Now just load up the stack
for (int i = 0; i < splitPoint.putOnStackAtEnd.size(); i++) {
boolean last = i == splitPoint.putOnStackAtEnd.size() - 1;
// Since the loop started with the array, we only dupe the array every time but the last
if (!last) newMethod.visitInsn(Opcodes.DUP);
// Put the index on the stack
intConst(i).accept(newMethod);
// Load the stack item
Type item = splitPoint.putOnStackAtEnd.get(i);
newMethod.visitInsn(Opcodes.AALOAD);
// Cast to local type
if (!item.equals(OBJECT_TYPE)) {
newMethod.visitTypeInsn(Opcodes.CHECKCAST, boxedTypeIfNecessary(item).getInternalName());
}
// Unbox if necessary
unboxStackIfNecessary(item, newMethod);
// For all but the last stack item, we need to swap with the arr ref above.
if (!last) {
// Note if the stack item takes two slots, we do a form of dup then pop since there's no swap1x2
if (item == Type.LONG_TYPE || item == Type.DOUBLE_TYPE) {
newMethod.visitInsn(Opcodes.DUP_X2);
newMethod.visitInsn(Opcodes.POP);
} else {
newMethod.visitInsn(Opcodes.SWAP);
}
}
}
// 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);
// Skip frames
if (insn instanceof FrameNode) continue;
// Record label
if (insn instanceof LabelNode) seenLabels.add(((LabelNode) insn).getLabel());
insn.accept(newMethod);
}
// Add any try catch blocks that started in here
for (TryCatchBlockNode tryCatch : orig.tryCatchBlocks) {
if (seenLabels.contains(tryCatch.start.getLabel())) tryCatch.accept(newMethod);
}
// Reset the labels
newMethod.instructions.resetLabels();
return newMethod;
}
/** Result of a split method */
public static class Result {
/** A copy of the original method, but changed to invoke {@link #splitOffMethod} */
public final MethodNode trimmedMethod;
/** The new method that was split off the original and is called by {@link #splitOffMethod} */
public final MethodNode splitOffMethod;
public Result(MethodNode trimmedMethod, MethodNode splitOffMethod) {
this.trimmedMethod = trimmedMethod;
this.splitOffMethod = splitOffMethod;
}
}
}

View File

@ -0,0 +1,392 @@
package asmble.compile.jvm.msplit;
import org.objectweb.asm.Label;
import org.objectweb.asm.Opcodes;
import org.objectweb.asm.Type;
import org.objectweb.asm.commons.AnalyzerAdapter;
import org.objectweb.asm.tree.*;
import java.util.*;
import static asmble.compile.jvm.msplit.Util.*;
/** For a given method, iterate over possible split points */
public class Splitter implements Iterable<Splitter.SplitPoint> {
protected final int api;
protected final String owner;
protected final MethodNode method;
protected final int minSize;
protected final int maxSize;
/**
* @param api Same as for {@link org.objectweb.asm.MethodVisitor#MethodVisitor(int)} or any other ASM class
* @param owner Internal name of the method's owner
* @param method The method to find split points for
* @param minSize The minimum number of instructions required for the split point to be valid
* @param maxSize The maximum number of instructions that split points cannot exceeed
*/
public Splitter(int api, String owner, MethodNode method, int minSize, int maxSize) {
this.api = api;
this.owner = owner;
this.method = method;
this.minSize = minSize;
this.maxSize = maxSize;
}
@Override
public Iterator<SplitPoint> iterator() { return new Iter(); }
// Types are always int, float, long, double, or ref (no other primitives)
/** A split point in a method that can be split off into another method */
public static class SplitPoint {
/**
* The locals read in this split area, keyed by index. Value type is always int, float, long, double, or object.
*/
public final SortedMap<Integer, Type> localsRead;
/**
* The locals written in this split area, keyed by index. Value type is always int, float, long, double, or object.
*/
public final SortedMap<Integer, Type> localsWritten;
/**
* The values of the stack needed at the start of this split area. Type is always int, float, long, double, or
* object.
*/
public final List<Type> neededFromStackAtStart;
/**
* The values of the stack at the end of this split area that are needed to put back on the original. Type is always
* int, float, long, double, or object.
*/
public final List<Type> putOnStackAtEnd;
/**
* The instruction index this split area begins at.
*/
public final int start;
/**
* The number of instructions this split area has.
*/
public final int length;
public SplitPoint(SortedMap<Integer, Type> localsRead, SortedMap<Integer, Type>localsWritten,
List<Type> neededFromStackAtStart, List<Type> putOnStackAtEnd, int start, int length) {
this.localsRead = localsRead;
this.localsWritten = localsWritten;
this.neededFromStackAtStart = neededFromStackAtStart;
this.putOnStackAtEnd = putOnStackAtEnd;
this.start = start;
this.length = length;
}
}
protected int compareInsnIndexes(AbstractInsnNode o1, AbstractInsnNode o2) {
return Integer.compare(method.instructions.indexOf(o1), method.instructions.indexOf(o2));
}
protected class Iter implements Iterator<SplitPoint> {
protected final AbstractInsnNode[] insns;
protected final List<TryCatchBlockNode> tryCatchBlocks;
protected int currIndex = -1;
protected boolean peeked;
protected SplitPoint peekedValue;
protected Iter() {
insns = method.instructions.toArray();
tryCatchBlocks = new ArrayList<>(method.tryCatchBlocks);
// Must be sorted by earliest starting index then earliest end index then earliest handler
tryCatchBlocks.sort((o1, o2) -> {
int cmp = compareInsnIndexes(o1.start, o2.start);
if (cmp == 0) compareInsnIndexes(o1.end, o2.end);
if (cmp == 0) compareInsnIndexes(o1.handler, o2.handler);
return cmp;
});
}
@Override
public boolean hasNext() {
if (!peeked) {
peeked = true;
peekedValue = nextOrNull();
}
return peekedValue != null;
}
@Override
public SplitPoint next() {
// If we've peeked in hasNext, use that
SplitPoint ret;
if (peeked) {
peeked = false;
ret = peekedValue;
} else {
ret = nextOrNull();
}
if (ret == null) throw new NoSuchElementException();
return ret;
}
protected SplitPoint nextOrNull() {
// Try for each index
while (++currIndex + minSize <= insns.length) {
SplitPoint longest = longestForCurrIndex();
if (longest != null) return longest;
}
return null;
}
protected SplitPoint longestForCurrIndex() {
// As a special case, if the previous insn was a line number, that was good enough
if (currIndex - 1 >- 0 && insns[currIndex - 1] instanceof LineNumberNode) return null;
// Build the info object
InsnTraverseInfo info = new InsnTraverseInfo();
info.startIndex = currIndex;
info.endIndex = Math.min(currIndex + maxSize - 1, insns.length - 1);
// 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
constrainEndByInternalJumps(info);
// 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;
// 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 constrainEndByTryCatchBlocks(InsnTraverseInfo info) {
// Go over all the try/catch blocks, sorted by earliest
for (TryCatchBlockNode block : tryCatchBlocks) {
int handleIndex = method.instructions.indexOf(block.handler);
int startIndex = method.instructions.indexOf(block.start);
int endIndex = method.instructions.indexOf(block.end) - 1;
boolean catchWithinDisallowed;
if (info.startIndex <= startIndex && info.endIndex >= endIndex) {
// The try block is entirely inside the range...
catchWithinDisallowed = false;
// Since it's entirely within, we need the catch handler within too
if (handleIndex < info.startIndex || handleIndex > info.endIndex) {
// Well, it's not within, so that means we can't include this try block at all
info.endIndex = Math.min(info.endIndex, startIndex - 1);
}
} else if (info.startIndex > startIndex && info.endIndex > endIndex) {
// The try block started before this range, but ends inside of it...
// The end has to be changed to the block's end so it doesn't go over the boundary
info.endIndex = Math.min(info.endIndex, endIndex);
// The catch can't jump in here
catchWithinDisallowed = true;
} else if (info.startIndex <= startIndex && info.endIndex < endIndex) {
// The try block started in this range, but ends outside of it...
// Can't have the block then, reduce it to before the start
info.endIndex = Math.min(info.endIndex, startIndex - 1);
// Since we don't have the block, we can't jump in here either
catchWithinDisallowed = true;
} else {
// The try block is completely outside, just restrict the catch from jumping in
catchWithinDisallowed = true;
}
// If the catch is within and not allowed to be, we have to change the end to before it
if (catchWithinDisallowed && info.startIndex <= handleIndex && info.endIndex >= handleIndex) {
info.endIndex = Math.min(info.endIndex, handleIndex - 1);
}
}
}
protected void constrainEndByInternalJumps(InsnTraverseInfo info) {
for (int i = info.startIndex; i <= info.endIndex; i++) {
AbstractInsnNode node = insns[i];
int earliestIndex;
int furthestIndex;
if (node instanceof JumpInsnNode) {
earliestIndex = method.instructions.indexOf(((JumpInsnNode) node).label);
furthestIndex = earliestIndex;
} else if (node instanceof TableSwitchInsnNode) {
earliestIndex = method.instructions.indexOf(((TableSwitchInsnNode) node).dflt);
furthestIndex = earliestIndex;
for (LabelNode label : ((TableSwitchInsnNode) node).labels) {
int index = method.instructions.indexOf(label);
earliestIndex = Math.min(earliestIndex, index);
furthestIndex = Math.max(furthestIndex, index);
}
} else if (node instanceof LookupSwitchInsnNode) {
earliestIndex = method.instructions.indexOf(((LookupSwitchInsnNode) node).dflt);
furthestIndex = earliestIndex;
for (LabelNode label : ((LookupSwitchInsnNode) node).labels) {
int index = method.instructions.indexOf(label);
earliestIndex = Math.min(earliestIndex, index);
furthestIndex = Math.max(furthestIndex, index);
}
} else continue;
// Stop here if any indexes are out of range, otherwise, change end
if (earliestIndex < info.startIndex || furthestIndex > info.endIndex) {
info.endIndex = i - 1;
return;
}
info.endIndex = Math.max(info.endIndex, furthestIndex);
}
}
protected void constrainEndByExternalJumps(InsnTraverseInfo info) {
// Basically, if any external jumps jump into our range, that can't be included in the range
for (int i = 0; i < insns.length; i++) {
if (i >= info.startIndex && i <= info.endIndex) continue;
AbstractInsnNode node = insns[i];
if (node instanceof JumpInsnNode) {
int index = method.instructions.indexOf(((JumpInsnNode) node).label);
if (index >= info.startIndex) info.endIndex = Math.min(info.endIndex, index - 1);
} else if (node instanceof TableSwitchInsnNode) {
int index = method.instructions.indexOf(((TableSwitchInsnNode) node).dflt);
if (index >= info.startIndex) info.endIndex = Math.min(info.endIndex, index - 1);
for (LabelNode label : ((TableSwitchInsnNode) node).labels) {
index = method.instructions.indexOf(label);
if (index >= info.startIndex) info.endIndex = Math.min(info.endIndex, index - 1);
}
} else if (node instanceof LookupSwitchInsnNode) {
int index = method.instructions.indexOf(((LookupSwitchInsnNode) node).dflt);
if (index >= info.startIndex) info.endIndex = Math.min(info.endIndex, index - 1);
for (LabelNode label : ((LookupSwitchInsnNode) node).labels) {
index = method.instructions.indexOf(label);
if (index >= info.startIndex) info.endIndex = Math.min(info.endIndex, index - 1);
}
}
}
}
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);
// Take the stack at the start and copy it off
List<Object> stackAtStart = new ArrayList<>(adapter.stack);
// Reset some adapter state
adapter.lowestStackSize = stackAtStart.size();
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);
// Build the split point
return new SplitPoint(
localMapFromAdapterLocalMap(adapter.localsRead, adapter.uninitializedTypes),
localMapFromAdapterLocalMap(adapter.localsWritten, adapter.uninitializedTypes),
typesFromAdapterStackRange(stackAtStart, adapter.lowestStackSize, adapter.uninitializedTypes),
typesFromAdapterStackRange(adapter.stack, adapter.lowestStackSize, adapter.uninitializedTypes),
info.startIndex,
info.getSize()
);
}
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)));
return ret;
}
protected List<Type> typesFromAdapterStackRange(
List<Object> stack, int start, Map<Object, Object> uninitializedTypes) {
List<Type> ret = new ArrayList<>();
for (int i = start; i < stack.size(); i++) {
Object item = stack.get(i);
ret.add(typeFromAdapterStackItem(item, uninitializedTypes));
// Jump an extra spot for longs and doubles
if (item == Opcodes.LONG || item == Opcodes.DOUBLE) {
if (stack.get(++i) != Opcodes.TOP) throw new IllegalStateException("Expected top after long/double");
}
}
return ret;
}
protected Type typeFromAdapterStackItem(Object item, Map<Object, Object> uninitializedTypes) {
if (item == Opcodes.INTEGER) return Type.INT_TYPE;
else if (item == Opcodes.FLOAT) return Type.FLOAT_TYPE;
else if (item == Opcodes.LONG) return Type.LONG_TYPE;
else if (item == Opcodes.DOUBLE) return Type.DOUBLE_TYPE;
else if (item == Opcodes.NULL) return OBJECT_TYPE;
else if (item == Opcodes.UNINITIALIZED_THIS) return Type.getObjectType(owner);
else if (item instanceof Label) return Type.getObjectType((String) uninitializedTypes.get(item));
else if (item instanceof String) return Type.getObjectType((String) item);
else throw new IllegalStateException("Unrecognized stack item: " + item);
}
}
protected static class StackAndLocalTrackingAdapter extends AnalyzerAdapter {
public int lowestStackSize;
public final SortedMap<Integer, Object> localsRead = new TreeMap<>();
public final SortedMap<Integer, Object> localsWritten = new TreeMap<>();
protected StackAndLocalTrackingAdapter(Splitter splitter) {
super(splitter.api, splitter.owner, splitter.method.access, splitter.method.name, splitter.method.desc, null);
stack = new SizeChangeNotifyList<Object>() {
@Override
protected void onSizeChanged() { lowestStackSize = Math.min(lowestStackSize, size()); }
};
}
@Override
public void visitVarInsn(int opcode, int var) {
switch (opcode) {
case Opcodes.ILOAD:
case Opcodes.LLOAD:
case Opcodes.FLOAD:
case Opcodes.DLOAD:
case Opcodes.ALOAD:
localsRead.put(var, locals.get(var));
break;
case Opcodes.ISTORE:
case Opcodes.FSTORE:
case Opcodes.ASTORE:
localsWritten.put(var, stack.get(stack.size() - 1));
break;
case Opcodes.LSTORE:
case Opcodes.DSTORE:
localsWritten.put(var, stack.get(stack.size() - 2));
break;
}
super.visitVarInsn(opcode, var);
}
@Override
public void visitIincInsn(int var, int increment) {
localsRead.put(var, Type.INT_TYPE);
localsWritten.put(var, Type.INT_TYPE);
super.visitIincInsn(var, increment);
}
}
protected static class SizeChangeNotifyList<T> extends AbstractList<T> {
protected final ArrayList<T> list = new ArrayList<>();
protected void onSizeChanged() { }
@Override
public T get(int index) { return list.get(index); }
@Override
public int size() { return list.size(); }
@Override
public T set(int index, T element) { return list.set(index, element); }
@Override
public void add(int index, T element) {
list.add(index, element);
onSizeChanged();
}
@Override
public T remove(int index) {
T ret = list.remove(index);
onSizeChanged();
return ret;
}
}
protected static class InsnTraverseInfo {
public int startIndex;
// Can only shrink, never increase in size
public int endIndex;
public int getSize() { return endIndex - startIndex + 1; }
}
}

View File

@ -0,0 +1,84 @@
package asmble.compile.jvm.msplit;
import org.objectweb.asm.Opcodes;
import org.objectweb.asm.Type;
import org.objectweb.asm.tree.*;
class Util {
private Util() { }
static final Type OBJECT_TYPE = Type.getType(Object.class);
static AbstractInsnNode zeroVal(Type type) {
if (type == Type.INT_TYPE) return new InsnNode(Opcodes.ICONST_0);
else if (type == Type.LONG_TYPE) return new InsnNode(Opcodes.LCONST_0);
else if (type == Type.FLOAT_TYPE) return new InsnNode(Opcodes.FCONST_0);
else if (type == Type.DOUBLE_TYPE) return new InsnNode(Opcodes.DCONST_0);
else return new InsnNode(Opcodes.ACONST_NULL);
}
static boolean isStoreOp(int opcode) {
return opcode == Opcodes.ISTORE || opcode == Opcodes.LSTORE || opcode == Opcodes.FSTORE ||
opcode == Opcodes.DSTORE || opcode == Opcodes.ASTORE;
}
static int storeOpFromType(Type type) {
if (type == Type.INT_TYPE) return Opcodes.ISTORE;
else if (type == Type.LONG_TYPE) return Opcodes.LSTORE;
else if (type == Type.FLOAT_TYPE) return Opcodes.FSTORE;
else if (type == Type.DOUBLE_TYPE) return Opcodes.DSTORE;
else return Opcodes.ASTORE;
}
static int loadOpFromType(Type type) {
if (type == Type.INT_TYPE) return Opcodes.ILOAD;
else if (type == Type.LONG_TYPE) return Opcodes.LLOAD;
else if (type == Type.FLOAT_TYPE) return Opcodes.FLOAD;
else if (type == Type.DOUBLE_TYPE) return Opcodes.DLOAD;
else return Opcodes.ALOAD;
}
static Type boxedTypeIfNecessary(Type type) {
if (type == Type.INT_TYPE) return Type.getType(Integer.class);
else if (type == Type.LONG_TYPE) return Type.getType(Long.class);
else if (type == Type.FLOAT_TYPE) return Type.getType(Float.class);
else if (type == Type.DOUBLE_TYPE) return Type.getType(Double.class);
else return type;
}
static void boxStackIfNecessary(Type type, MethodNode method) {
if (type == Type.INT_TYPE) boxCall(Integer.class, type).accept(method);
else if (type == Type.FLOAT_TYPE) boxCall(Float.class, type).accept(method);
else if (type == Type.LONG_TYPE) boxCall(Long.class, type).accept(method);
else if (type == Type.DOUBLE_TYPE) boxCall(Double.class, type).accept(method);
}
static void unboxStackIfNecessary(Type type, MethodNode method) {
if (type == Type.INT_TYPE) method.visitMethodInsn(Opcodes.INVOKEVIRTUAL,
"java/lang/Integer", "intValue", Type.getMethodDescriptor(Type.INT_TYPE), false);
else if (type == Type.FLOAT_TYPE) method.visitMethodInsn(Opcodes.INVOKEVIRTUAL,
"java/lang/Float", "floatValue", Type.getMethodDescriptor(Type.FLOAT_TYPE), false);
else if (type == Type.LONG_TYPE) method.visitMethodInsn(Opcodes.INVOKEVIRTUAL,
"java/lang/Long", "longValue", Type.getMethodDescriptor(Type.LONG_TYPE), false);
else if (type == Type.DOUBLE_TYPE) method.visitMethodInsn(Opcodes.INVOKEVIRTUAL,
"java/lang/Double", "doubleValue", Type.getMethodDescriptor(Type.DOUBLE_TYPE), false);
}
static AbstractInsnNode intConst(int v) {
switch (v) {
case -1: return new InsnNode(Opcodes.ICONST_M1);
case 0: return new InsnNode(Opcodes.ICONST_0);
case 1: return new InsnNode(Opcodes.ICONST_1);
case 2: return new InsnNode(Opcodes.ICONST_2);
case 3: return new InsnNode(Opcodes.ICONST_3);
case 4: return new InsnNode(Opcodes.ICONST_4);
case 5: return new InsnNode(Opcodes.ICONST_5);
default: return new LdcInsnNode(v);
}
}
static MethodInsnNode boxCall(Class<?> boxType, Type primType) {
return new MethodInsnNode(Opcodes.INVOKESTATIC, Type.getInternalName(boxType),
"valueOf", Type.getMethodDescriptor(Type.getType(boxType), primType), false);
}
}

View File

@ -1,183 +0,0 @@
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()
}

View File

@ -1,9 +1,9 @@
package asmble.cli package asmble.cli
import asmble.ast.Script import asmble.ast.Script
import asmble.compile.jvm.AsmToBinary
import asmble.compile.jvm.AstToAsm import asmble.compile.jvm.AstToAsm
import asmble.compile.jvm.ClsContext import asmble.compile.jvm.ClsContext
import asmble.compile.jvm.withComputedFramesAndMaxs
import java.io.FileOutputStream import java.io.FileOutputStream
@Suppress("NAME_SHADOWING") @Suppress("NAME_SHADOWING")
@ -69,7 +69,7 @@ open class Compile : Command<Compile.Args>() {
includeBinary = args.includeBinary includeBinary = args.includeBinary
) )
AstToAsm.fromModule(ctx) AstToAsm.fromModule(ctx)
outStream.write(ctx.cls.withComputedFramesAndMaxs()) outStream.write(AsmToBinary.fromClassNode(ctx.cls))
} }
} }

View File

@ -1,7 +1,7 @@
package asmble.cli package asmble.cli
import asmble.compile.jvm.AsmToBinary
import asmble.compile.jvm.Linker import asmble.compile.jvm.Linker
import asmble.compile.jvm.withComputedFramesAndMaxs
import java.io.FileOutputStream import java.io.FileOutputStream
open class Link : Command<Link.Args>() { open class Link : Command<Link.Args>() {
@ -52,7 +52,7 @@ open class Link : Command<Link.Args>() {
defaultMaxMemPages = args.defaultMaxMem defaultMaxMemPages = args.defaultMaxMem
) )
Linker.link(ctx) Linker.link(ctx)
outStream.write(ctx.cls.withComputedFramesAndMaxs()) outStream.write(AsmToBinary.fromClassNode(ctx.cls))
} }
} }

View File

@ -3,7 +3,7 @@ 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, SplitFunc, Translate) val commands = listOf(Compile, Help, Invoke, Link, Run, Translate)
/** /**
* Entry point of command line interface. * Entry point of command line interface.

View File

@ -1,146 +0,0 @@
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()
}

View File

@ -2,7 +2,6 @@ package asmble.compile.jvm
import asmble.ast.Node import asmble.ast.Node
import org.objectweb.asm.ClassReader import org.objectweb.asm.ClassReader
import org.objectweb.asm.ClassWriter
import org.objectweb.asm.Opcodes import org.objectweb.asm.Opcodes
import org.objectweb.asm.Type import org.objectweb.asm.Type
import org.objectweb.asm.tree.* import org.objectweb.asm.tree.*
@ -189,16 +188,6 @@ fun MethodNode.toAsmString(): String {
val Node.Type.Func.asmDesc: String get() = val Node.Type.Func.asmDesc: String get() =
(this.ret?.typeRef ?: Void::class.ref).asMethodRetDesc(*this.params.map { it.typeRef }.toTypedArray()) (this.ret?.typeRef ?: Void::class.ref).asMethodRetDesc(*this.params.map { it.typeRef }.toTypedArray())
fun ClassNode.withComputedFramesAndMaxs(
cw: ClassWriter = ClassWriter(ClassWriter.COMPUTE_FRAMES + ClassWriter.COMPUTE_MAXS)
): ByteArray {
// Note, compute maxs adds a bunch of NOPs for unreachable code.
// See $func12 of block.wast. I don't believe the extra time over the
// instructions to remove the NOPs is worth it.
this.accept(cw)
return cw.toByteArray()
}
fun ClassNode.toAsmString(): String { fun ClassNode.toAsmString(): String {
val stringWriter = StringWriter() val stringWriter = StringWriter()
this.accept(TraceClassVisitor(PrintWriter(stringWriter))) this.accept(TraceClassVisitor(PrintWriter(stringWriter)))

View File

@ -0,0 +1,51 @@
package asmble.compile.jvm
import asmble.compile.jvm.msplit.SplitMethod
import org.objectweb.asm.ClassWriter
import org.objectweb.asm.MethodTooLargeException
import org.objectweb.asm.Opcodes
import org.objectweb.asm.tree.ClassNode
/**
* May mutate given class nodes on [fromClassNode] if [splitMethod] is present (the default). Uses the two-param
* [SplitMethod.split] call to try and split overly large methods.
*/
open class AsmToBinary(val splitMethod: SplitMethod? = SplitMethod(Opcodes.ASM6)) {
fun fromClassNode(
cn: ClassNode,
newClassWriter: () -> ClassWriter = { ClassWriter(ClassWriter.COMPUTE_FRAMES + ClassWriter.COMPUTE_MAXS) }
): ByteArray {
while (true) {
val cw = newClassWriter()
// Note, compute maxs adds a bunch of NOPs for unreachable code.
// See $func12 of block.wast. I don't believe the extra time over the
// instructions to remove the NOPs is worth it.
cn.accept(cw)
try {
return cw.toByteArray()
} catch (e: MethodTooLargeException) {
if (splitMethod == null) throw e
// Split the offending method by removing it and replacing it with the split ones
require(cn.name == e.className)
val tooLargeIndex = cn.methods.indexOfFirst { it.name == e.methodName && it.desc == e.descriptor }
require(tooLargeIndex >= 0)
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
val origName = split.splitOffMethod.name
var foundCount = 0
while (cn.methods.any { it.name == split.splitOffMethod.name }) {
split.splitOffMethod.name = origName + (++foundCount)
}
// Replace at the index
cn.methods.removeAt(tooLargeIndex)
cn.methods.add(tooLargeIndex, split.splitOffMethod)
cn.methods.add(tooLargeIndex, split.trimmedMethod)
}
}
}
companion object : AsmToBinary() {
val noSplit = AsmToBinary(null)
}
}

View File

@ -150,19 +150,11 @@ open class InsnReworker {
// if we are at 0, add the result of said block if necessary to the count. // if we are at 0, add the result of said block if necessary to the count.
if (insideOfBlocks > 0) { if (insideOfBlocks > 0) {
// If it's not a block, just ignore it // If it's not a block, just ignore it
val blockStackDiff = insns[insnIndex].let { (insns[insnIndex] as? Node.Instr.Args.Type)?.let {
when (it) {
is Node.Instr.Block -> if (it.type == null) 0 else 1
is Node.Instr.Loop -> 0
is Node.Instr.If -> if (it.type == null) -1 else 0
else -> null
}
}
if (blockStackDiff != null) {
insideOfBlocks-- insideOfBlocks--
ctx.trace { "Found block begin, number of blocks we're still inside: $insideOfBlocks" } ctx.trace { "Found block begin, number of blocks we're still inside: $insideOfBlocks" }
// We're back on our block, change the count // We're back on our block, change the count if it had a result
if (insideOfBlocks == 0) countSoFar += blockStackDiff if (insideOfBlocks == 0 && it.type != null) countSoFar++
} }
if (insideOfBlocks > 0) continue if (insideOfBlocks > 0) continue
} }
@ -249,10 +241,9 @@ open class InsnReworker {
*/ */
fun insnStackDiff(ctx: ClsContext, insn: Node.Instr): Int = when (insn) { 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.Else, is Node.Instr.End, is Node.Instr.Br,
is Node.Instr.End, is Node.Instr.Br, is Node.Instr.BrIf,
is Node.Instr.Return -> NOP is Node.Instr.Return -> NOP
is Node.Instr.BrTable -> POP_PARAM is Node.Instr.If, is Node.Instr.BrIf, is Node.Instr.BrTable -> POP_PARAM
is Node.Instr.Call -> ctx.funcTypeAtIndex(insn.index).let { is Node.Instr.Call -> ctx.funcTypeAtIndex(insn.index).let {
// All calls pop params and any return is a push // All calls pop params and any return is a push
(POP_PARAM * it.params.size) + (if (it.ret == null) NOP else PUSH_RESULT) (POP_PARAM * it.params.size) + (if (it.ret == null) NOP else PUSH_RESULT)

View File

@ -201,7 +201,7 @@ open class Linker {
"instance" + mod.name.javaIdent.capitalize(), mod.ref.asmDesc), "instance" + mod.name.javaIdent.capitalize(), mod.ref.asmDesc),
InsnNode(Opcodes.ARETURN) InsnNode(Opcodes.ARETURN)
) )
ctx.cls.methods.plusAssign(func) ctx.cls.methods.plusAssign(func.toMethodNode())
} }
class ModuleClass(val cls: Class<*>, overrideName: String? = null) { class ModuleClass(val cls: Class<*>, overrideName: String? = null) {

View File

@ -319,10 +319,15 @@ data class ScriptContext(
bindImport(import, true, MethodType.methodType(Array<MethodHandle>::class.java)). bindImport(import, true, MethodType.methodType(Array<MethodHandle>::class.java)).
invokeWithArguments()!! as Array<MethodHandle> invokeWithArguments()!! as Array<MethodHandle>
open class SimpleClassLoader(parent: ClassLoader, logger: Logger) : ClassLoader(parent), Logger by logger { open class SimpleClassLoader(
parent: ClassLoader,
logger: Logger,
val splitWhenTooLarge: Boolean = true
) : ClassLoader(parent), Logger by logger {
fun fromBuiltContext(ctx: ClsContext): Class<*> { fun fromBuiltContext(ctx: ClsContext): Class<*> {
trace { "Computing frames for ASM class:\n" + ctx.cls.toAsmString() } trace { "Computing frames for ASM class:\n" + ctx.cls.toAsmString() }
return ctx.cls.withComputedFramesAndMaxs().let { bytes -> val writer = if (splitWhenTooLarge) AsmToBinary else AsmToBinary.noSplit
return writer.fromClassNode(ctx.cls).let { bytes ->
debug { "ASM class:\n" + bytes.asClassNode().toAsmString() } debug { "ASM class:\n" + bytes.asClassNode().toAsmString() }
defineClass("${ctx.packageName}.${ctx.className}", bytes, 0, bytes.size) defineClass("${ctx.packageName}.${ctx.className}", bytes, 0, bytes.size)
} }

View File

@ -16,7 +16,7 @@ class SpecTestUnit(name: String, wast: String, expectedOutput: String?) : BaseTe
"nop" -> 20 "nop" -> 20
"memory_grow" -> 830 "memory_grow" -> 830
"imports" -> 5 "imports" -> 5
else -> 1 else -> 2
} }
override fun warningInsteadOfErrReason(t: Throwable) = when (name) { override fun warningInsteadOfErrReason(t: Throwable) = when (name) {

View File

@ -1,29 +1,31 @@
package asmble.ast.opt package asmble.run.jvm
import asmble.TestBase import asmble.TestBase
import asmble.ast.Node import asmble.ast.Node
import asmble.compile.jvm.AstToAsm import asmble.compile.jvm.AstToAsm
import asmble.compile.jvm.ClsContext import asmble.compile.jvm.ClsContext
import asmble.run.jvm.ScriptContext import org.junit.Assert
import org.junit.Test import org.junit.Test
import org.objectweb.asm.MethodTooLargeException
import java.nio.ByteBuffer import java.nio.ByteBuffer
import java.util.* import java.util.*
import kotlin.test.assertEquals import kotlin.test.assertEquals
class SplitLargeFuncTest : TestBase() { class LargeFuncTest : TestBase() {
@Test @Test
fun testSplitLargeFunc() { fun testLargeFunc() {
// We're going to make a large function that does some addition and then stores in mem val numInsnChunks = 6001
// Make large func that does some math
val ctx = ClsContext( val ctx = ClsContext(
packageName = "test", packageName = "test",
className = "Temp" + UUID.randomUUID().toString().replace("-", ""), className = "Temp" + UUID.randomUUID().toString().replace("-", ""),
logger = logger, logger = logger,
mod = Node.Module( mod = Node.Module(
memories = listOf(Node.Type.Memory(Node.ResizableLimits(initial = 2, maximum = 2))), memories = listOf(Node.Type.Memory(Node.ResizableLimits(initial = 4, maximum = 4))),
funcs = listOf(Node.Func( funcs = listOf(Node.Func(
type = Node.Type.Func(params = emptyList(), ret = null), type = Node.Type.Func(params = emptyList(), ret = null),
locals = emptyList(), locals = emptyList(),
instructions = (0 until 501).flatMap { instructions = (0 until numInsnChunks).flatMap {
listOf<Node.Instr>( listOf<Node.Instr>(
Node.Instr.I32Const(it * 4), Node.Instr.I32Const(it * 4),
// Let's to i * (i = 1) // Let's to i * (i = 1)
@ -47,43 +49,21 @@ class SplitLargeFuncTest : TestBase() {
) )
// Compile it // Compile it
AstToAsm.fromModule(ctx) AstToAsm.fromModule(ctx)
// Confirm the method size is too large
try {
ScriptContext.SimpleClassLoader(javaClass.classLoader, logger, splitWhenTooLarge = false).
fromBuiltContext(ctx)
Assert.fail()
} catch (e: MethodTooLargeException) { }
// Try again with split
val cls = ScriptContext.SimpleClassLoader(javaClass.classLoader, logger).fromBuiltContext(ctx) val cls = ScriptContext.SimpleClassLoader(javaClass.classLoader, logger).fromBuiltContext(ctx)
// Create it and check that it still does what we expect
val inst = cls.newInstance() val inst = cls.newInstance()
// Run someFunc // Run someFunc
cls.getMethod("someFunc").invoke(inst) cls.getMethod("someFunc").invoke(inst)
// Get the memory out // Get the memory out
val mem = cls.getMethod("getMemory").invoke(inst) as ByteBuffer val mem = cls.getMethod("getMemory").invoke(inst) as ByteBuffer
// Read out the mem values // Read out the mem values
(0 until 501).forEach { assertEquals(it * (it - 1), mem.getInt(it * 4)) } (0 until numInsnChunks).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"
}
} }
} }

View File

@ -0,0 +1,14 @@
### 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

View File

@ -0,0 +1,3 @@
int addOne(int x) {
return x + 1;
}

View File

@ -0,0 +1,13 @@
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));
}
}

View File

@ -2,9 +2,8 @@ package main
import ( import (
"fmt" "fmt"
"os"
) )
func main() { func main() {
fmt.Printf("Args: %v", os.Args) fmt.Println("Hello, World!")
} }

View File

@ -2,7 +2,7 @@ rootProject.name = 'asmble'
include 'annotations', include 'annotations',
'compiler', 'compiler',
'examples:c-simple', 'examples:c-simple',
// 'examples:go-simple', 'examples:go-simple',
// 'examples:rust-regex', // todo will be enabled when the problem with string max size will be solved 'examples:rust-regex',
'examples:rust-simple', 'examples:rust-simple',
'examples:rust-string' 'examples:rust-string'