Compare commits

...

21 Commits

Author SHA1 Message Date
vms
4c65740d03 initial commit (#13) 2019-08-10 14:52:25 +03:00
vms
728b78d713
add EIC metering (#12) 2019-08-06 17:25:48 +03:00
vms
cb907ae2da
Env module for gas metering (#11) 2019-08-06 10:45:16 +03:00
Dima
1d6002624f
Fix memory builder (#10)
* fix memory instance creation

* change version
2019-06-03 14:02:15 +03:00
Dima
ad2b7c071f
Bytebuffer abstraction (#9) 2019-05-13 16:40:07 +03:00
vms
119ce58c9e
add using optional module name to registred names (#8) 2019-03-29 19:01:49 +03:00
vms
1323e02c95
fix bintray user and key properties absence (#4) 2018-11-19 15:25:31 +03:00
Constantine Solovev
9172fba948
Add posibillity to add a Logger Wasm module (#3)
* Add logger module

* Add LoggerModuleTest
2018-11-15 12:38:50 +04:00
Constantine Solovev
b9b45cf997
Merge pull request #2 from fluencelabs/logger
Fix logger and return C example
2018-11-09 13:47:41 +04:00
C.Solovev
2bfa39a3c6 Tweaking after merge 2018-11-09 10:30:38 +04:00
C.Solovev
317b608048 Merge fix for late init for logger 2018-11-09 10:28:47 +04:00
C.Solovev
21b023f1c6 Return C example and skip it by default 2018-11-09 10:18:49 +04:00
C.Solovev
765d8b4dba Possibility to skip examples 2018-11-09 10:03:44 +04:00
C.Solovev
58cf836b76 Disable Go and Rust examples and up project version 2018-10-01 13:40:38 +04:00
C.Solovev
56c2c8d672 Fix lateinit error with logger for wasm files 2018-10-01 13:39:08 +04:00
C.Solovev
fb0be9d31a Merge remote-tracking branch 'upstream/master'
# Conflicts:
#	compiler/src/test/resources/spec
2018-09-18 15:56:16 +04:00
Chad Retz
d1f48aaaa0 Replace previous large-method-split attempt with msplit-based one for issue #19 2018-09-13 16:50:48 -05:00
Chad Retz
46a8ce3f52 Updated to latest spec and minor fix on block insn insertion count 2018-09-13 13:10:11 -05:00
Chad Retz
6352efaa96 Update Kotlin and ASM 2018-09-12 16:01:46 -05:00
Chad Retz
326a0cdaba Change Go example to simple hello world 2018-09-12 15:49:49 -05:00
C.Solovev
da70c9fca4 Fix for "lateinit property logger has not been initialized" 2018-08-28 16:50:37 +04:00
36 changed files with 1488 additions and 550 deletions

View File

@ -2,8 +2,8 @@ group 'asmble'
version '0.2.0'
buildscript {
ext.kotlin_version = '1.2.51'
ext.asm_version = '5.2'
ext.kotlin_version = '1.2.61'
ext.asm_version = '6.2.1'
repositories {
mavenCentral()
@ -21,7 +21,11 @@ buildscript {
allprojects {
apply plugin: 'java'
group 'com.github.cretz.asmble'
version '0.4.0-fl'
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()
@ -54,9 +58,9 @@ project(':compiler') {
compile "org.jetbrains.kotlin:kotlin-reflect:$kotlin_version"
compile "org.ow2.asm:asm-tree:$asm_version"
compile "org.ow2.asm:asm-util:$asm_version"
compile "org.ow2.asm:asm-commons:$asm_version"
testCompile 'junit:junit:4.12'
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')
@ -67,6 +71,38 @@ project(':examples') {
dependencies {
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
@ -142,8 +178,33 @@ 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
}
mainClassName = 'asmble.examples.csimple.Main'
}
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 {
@ -155,32 +216,44 @@ project(':examples:go-simple') {
mainClassName = 'asmble.examples.gosimple.Main'
}
// todo temporary disable Rust regex, because some strings in wasm code exceed the size in 65353 bytes.
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'
dependencies {
compile files('build/wasm-classes')
testCompile 'junit:junit:4.12'
}
compileJava {
dependsOn compileRustWasm
}
mainClassName = 'asmble.examples.rustregex.Main'
// project(':examples:rust-regex') {
// 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
// }
// }
test {
testLogging.showStandardStreams = true
testLogging.events 'PASSED', 'SKIPPED'
}
jmh {
iterations = 5
warmupIterations = 5
fork = 3
}
}
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 {
@ -193,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 {
@ -230,6 +309,9 @@ def publishSettings(project, projectName, projectDescription) {
}
bintray {
if(!hasProperty("bintrayUser") || !hasProperty("bintrayKey")) {
return
}
user = bintrayUser
key = bintrayKey

View 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);
}

View File

@ -0,0 +1,8 @@
package asmble.compile.jvm;
/**
* Interface to initialize MemoryBuffer
*/
public interface MemoryBufferBuilder {
MemoryBuffer build(int capacity);
}

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

View File

@ -1,7 +1,7 @@
package asmble.cli
import asmble.compile.jvm.AsmToBinary
import asmble.compile.jvm.Linker
import asmble.compile.jvm.withComputedFramesAndMaxs
import java.io.FileOutputStream
open class Link : Command<Link.Args>() {
@ -52,7 +52,7 @@ open class Link : Command<Link.Args>() {
defaultMaxMemPages = args.defaultMaxMem
)
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 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.

View File

@ -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,13 +41,21 @@ 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 context = ScriptContext(
packageName = "asmble.temp" + UUID.randomUUID().toString().replace("-", ""),
defaultMaxMemPages = args.defaultMaxMemPages
defaultMaxMemPages = args.defaultMaxMemPages,
memoryBuilder = args.memoryBuilder
)
// Compile everything
context = args.inFiles.foldIndexed(context) { index, ctx, inFile ->
@ -56,27 +64,53 @@ abstract class ScriptCommand<T> : Command<T>() {
// if input file is class file
"class" -> ctx.classLoader.addClass(File(inFile).readBytes()).let { ctx }
// if input file is wasm file
else -> Translate.inToAst(inFile, inFile.substringAfterLast('.')).let { inAst ->
val (mod, name) = (inAst.commands.singleOrNull() as? Script.Cmd.Module) ?:
else -> {
val translateCmd = Translate
translateCmd.logger = this.logger
translateCmd.inToAst(inFile, inFile.substringAfterLast('.')).let { inAst ->
val (mod, name) = (inAst.commands.singleOrNull() as? Script.Cmd.Module) ?:
error("Input file must only contain a single module")
val className = name?.javaIdent?.capitalize() ?:
val className = name?.javaIdent?.capitalize() ?:
"Temp" + UUID.randomUUID().toString().replace("-", "")
ctx.withCompiledModule(mod, className, name).let { ctx ->
if (name == null && index != args.inFiles.size - 1)
logger.warn { "File '$inFile' not last and has no name so will be unused" }
if (name == null || args.disableAutoRegister) ctx
else ctx.runCommand(Script.Cmd.Register(name, null))
ctx.withCompiledModule(mod, className, name).let { ctx ->
if (name == null && index != args.inFiles.size - 1)
logger.warn { "File '$inFile' not last and has no name so will be unused" }
if (name == null || args.disableAutoRegister) ctx
else ctx.runCommand(Script.Cmd.Register(name, null))
}
}
}
}
} catch (e: Exception) { throw Exception("Failed loading $inFile - ${e.message}", e) }
} catch (e: Exception) {
throw Exception("Failed loading $inFile - ${e.message}", e)
}
}
// Do registrations
context = args.registrations.fold(context) { ctx, (moduleName, className) ->
ctx.withModuleRegistered(moduleName,
Module.Native(Class.forName(className, true, ctx.classLoader).newInstance()))
Module.Native(Class.forName(className, true, ctx.classLoader).newInstance()))
}
if (args.specTestRegister) context = context.withHarnessRegistered() // проверить что не так с "Cannot find compatible import for spectest::print"
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
}
@ -88,12 +122,16 @@ abstract class ScriptCommand<T> : Command<T>() {
* @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
)
}
}

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

@ -70,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'")
}
}

View File

@ -2,7 +2,6 @@ package asmble.compile.jvm
import asmble.ast.Node
import org.objectweb.asm.ClassReader
import org.objectweb.asm.ClassWriter
import org.objectweb.asm.Opcodes
import org.objectweb.asm.Type
import org.objectweb.asm.tree.*
@ -189,16 +188,6 @@ fun MethodNode.toAsmString(): String {
val Node.Type.Func.asmDesc: String get() =
(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 {
val stringWriter = 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

@ -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) {

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 (insideOfBlocks > 0) {
// If it's not a block, just ignore it
val blockStackDiff = insns[insnIndex].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) {
(insns[insnIndex] as? Node.Instr.Args.Type)?.let {
insideOfBlocks--
ctx.trace { "Found block begin, number of blocks we're still inside: $insideOfBlocks" }
// We're back on our block, change the count
if (insideOfBlocks == 0) countSoFar += blockStackDiff
// We're back on our block, change the count if it had a result
if (insideOfBlocks == 0 && it.type != null) countSoFar++
}
if (insideOfBlocks > 0) continue
}
@ -249,10 +241,9 @@ open class InsnReworker {
*/
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.If, is Node.Instr.Else,
is Node.Instr.End, is Node.Instr.Br, is Node.Instr.BrIf,
is Node.Instr.Loop, is Node.Instr.Else, is Node.Instr.End, is Node.Instr.Br,
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 {
// All calls pop params and any return is a push
(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),
InsnNode(Opcodes.ARETURN)
)
ctx.cls.methods.plusAssign(func)
ctx.cls.methods.plusAssign(func.toMethodNode())
}
class ModuleClass(val cls: Class<*>, overrideName: String? = null) {

View 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()
}
}

View File

@ -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 {

View 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;
}
}

View 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()
}
}

View File

@ -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 {

View File

@ -43,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)))
@ -319,10 +321,15 @@ data class ScriptContext(
bindImport(import, true, MethodType.methodType(Array<MethodHandle>::class.java)).
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<*> {
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() }
defineClass("${ctx.packageName}.${ctx.className}", bytes, 0, bytes.size)
}
@ -339,4 +346,4 @@ data class ScriptContext(
defineClass(className, bytes, 0, bytes.size)
}
}
}
}

View File

@ -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

View File

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

View File

@ -1,89 +0,0 @@
package asmble.ast.opt
import asmble.TestBase
import asmble.ast.Node
import asmble.compile.jvm.AstToAsm
import asmble.compile.jvm.ClsContext
import asmble.run.jvm.ScriptContext
import org.junit.Test
import java.nio.ByteBuffer
import java.util.*
import kotlin.test.assertEquals
class SplitLargeFuncTest : TestBase() {
@Test
fun testSplitLargeFunc() {
// We're going to make a large function that does some addition and then stores in mem
val ctx = ClsContext(
packageName = "test",
className = "Temp" + UUID.randomUUID().toString().replace("-", ""),
logger = logger,
mod = Node.Module(
memories = listOf(Node.Type.Memory(Node.ResizableLimits(initial = 2, maximum = 2))),
funcs = listOf(Node.Func(
type = Node.Type.Func(params = emptyList(), ret = null),
locals = emptyList(),
instructions = (0 until 501).flatMap {
listOf<Node.Instr>(
Node.Instr.I32Const(it * 4),
// Let's to i * (i = 1)
Node.Instr.I32Const(it),
Node.Instr.I32Const(it - 1),
Node.Instr.I32Mul,
Node.Instr.I32Store(0, 0)
)
}
)),
names = Node.NameSection(
moduleName = null,
funcNames = mapOf(0 to "someFunc"),
localNames = emptyMap()
),
exports = listOf(
Node.Export("memory", Node.ExternalKind.MEMORY, 0),
Node.Export("someFunc", Node.ExternalKind.FUNCTION, 0)
)
)
)
// Compile it
AstToAsm.fromModule(ctx)
val cls = ScriptContext.SimpleClassLoader(javaClass.classLoader, logger).fromBuiltContext(ctx)
val inst = cls.newInstance()
// Run someFunc
cls.getMethod("someFunc").invoke(inst)
// Get the memory out
val mem = cls.getMethod("getMemory").invoke(inst) as ByteBuffer
// Read out the mem values
(0 until 501).forEach { assertEquals(it * (it - 1), mem.getInt(it * 4)) }
// Now split it
val (splitMod, insnsSaved) = SplitLargeFunc.apply(ctx.mod, 0) ?: error("Nothing could be split")
// Count insns and confirm it is as expected
val origInsnCount = ctx.mod.funcs.sumBy { it.instructions.size }
val newInsnCount = splitMod.funcs.sumBy { it.instructions.size }
assertEquals(origInsnCount - newInsnCount, insnsSaved)
// Compile it
val splitCtx = ClsContext(
packageName = "test",
className = "Temp" + UUID.randomUUID().toString().replace("-", ""),
logger = logger,
mod = splitMod
)
AstToAsm.fromModule(splitCtx)
val splitCls = ScriptContext.SimpleClassLoader(javaClass.classLoader, logger).fromBuiltContext(splitCtx)
val splitInst = splitCls.newInstance()
// Run someFunc
splitCls.getMethod("someFunc").invoke(splitInst)
// Get the memory out and compare it
val splitMem = splitCls.getMethod("getMemory").invoke(splitInst) as ByteBuffer
assertEquals(mem, splitMem)
// Dump some info
logger.debug {
val orig = ctx.mod.funcs.first()
val (new, split) = splitMod.funcs.let { it.first() to it.last() }
"Split complete, from single func with ${orig.instructions.size} insns to func " +
"with ${new.instructions.size} insns + delegated func " +
"with ${split.instructions.size} insns and ${split.type.params.size} params"
}
}
}

View File

@ -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)
}

View File

@ -0,0 +1,69 @@
package asmble.run.jvm
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.util.*
import kotlin.test.assertEquals
class LargeFuncTest : TestBase() {
@Test
fun testLargeFunc() {
val numInsnChunks = 6001
// Make large func that does some math
val ctx = ClsContext(
packageName = "test",
className = "Temp" + UUID.randomUUID().toString().replace("-", ""),
logger = logger,
mod = Node.Module(
memories = listOf(Node.Type.Memory(Node.ResizableLimits(initial = 4, maximum = 4))),
funcs = listOf(Node.Func(
type = Node.Type.Func(params = emptyList(), ret = null),
locals = emptyList(),
instructions = (0 until numInsnChunks).flatMap {
listOf<Node.Instr>(
Node.Instr.I32Const(it * 4),
// Let's to i * (i = 1)
Node.Instr.I32Const(it),
Node.Instr.I32Const(it - 1),
Node.Instr.I32Mul,
Node.Instr.I32Store(0, 0)
)
}
)),
names = Node.NameSection(
moduleName = null,
funcNames = mapOf(0 to "someFunc"),
localNames = emptyMap()
),
exports = listOf(
Node.Export("memory", Node.ExternalKind.MEMORY, 0),
Node.Export("someFunc", Node.ExternalKind.FUNCTION, 0)
)
)
)
// Compile it
AstToAsm.fromModule(ctx)
// 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)
// Create it and check that it still does what we expect
val inst = cls.newInstance()
// Run someFunc
cls.getMethod("someFunc").invoke(inst)
// Get the memory out
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)) }
}
}

View 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()
}
}

View File

@ -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

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 (
"fmt"
"os"
)
func main() {
fmt.Printf("Args: %v", os.Args)
fmt.Println("Hello, World!")
}