Differences from Java
Jumper reads like Java, but it allows things Java does not: variables without a fixed type, tables instead of classes, functions as values. This page lists those differences and shows what each one becomes on the JVM: the Java you would write for the same thing, and the bytecode Jumper actually generates.
INFO
The bytecode below is what Jumper 0.11 generates with -Djmp.tier1=force, shortened where marked. It is an implementation detail and changes between versions; the behavior described in the rest of the documentation does not.
At a glance
What Jumper allows that Java does not:
| Jumper | Java | On the JVM |
|---|---|---|
dyn x = 1; x = "one"; | Object x and casts | an Object local; a primitive local when only one numeric type is ever stored (below) |
{ x: 1, name: "a" } | a Map or a class written for it | a table with a shape; a literal is an instance of a generated class with real fields (below) |
dyn pair(a, b) - parameters without types | Object a, Object b | Object parameters |
pair(1) - fewer or more arguments | compile error | a missing argument is null, extra ones are dropped |
closures change outer variables (n++) | captured variables must be effectively final | captured variables live in a heap frame (below) |
if (list), name || "guest" | only boolean in conditions | only null and false are false; || returns an operand |
"a" == s compares text | == compares references | strings and arrays by content, tables and objects by reference |
s[0], s.length, date.time | s.charAt(0), s.length(), date.getTime() | resolved when the call site is linked |
xs.add(4), xs[xs.length] = 5 on [1, 2, 3] | ArrayList | a script array is a java.util.List |
| statements at the top level, functions called above their declaration | class Main, main | the top level is a function of its own |
throw { code: 404 }, catch (e) | Throwable only, typed catch | any value can be thrown |
What Java has and Jumper does not: overloading of script functions, generics, casts ((int) x - use int(x)), implicit narrowing, char, labels, switch fall-through, access modifiers, nested classes, more than one constructor, a script class extending a Java class (a Java interface is implemented by passing a function), checked exceptions, static imports.
How a script runs
A script starts in Tier 0, an interpreter over the syntax tree. A function is compiled to JVM bytecode (Tier 1) on its first call if it has a loop, otherwise on its third call. Each function becomes its own class Fn<N>_<name>, with the code in a method body whose parameter and return types are the declared ones. The JVM's JIT then compiles that bytecode to machine code like any Java method.
Generated classes live in a class loader of their own, one per Interpreter, and are unloaded together with it (except the classes of table literals, which are shared by the process).
Typed code is Java
When everything is typed, the bytecode is what javac produces:
int sum(int n) {
int s = 0;
for (int i = 0; i < n; i++) s += i;
return s;
}static int sum(int n) {
int s = 0;
for (int i = 0; i < n; i++) s += i;
return s;
}public static int body(int);
0: iconst_0 // locals are set up front
...
8: iload_2 // i
9: iload_0 // n
10: if_icmpge 23
13: iload_1
14: iload_2
15: iadd // s += i
16: istore_1
17: iinc 2, 1 // i++
20: goto 8
23: iload_1
24: ireturnA call to such a function from another compiled function is a plain invokestatic that the JVM can inline. Types are worth writing where the code is hot; nothing else changes.
dyn
A dyn variable holds any value and may change type. Jumper does not make every dyn an Object: it looks at what is stored in it.
Always one type: a primitive
If every value stored in a dyn local is an int (or every one a double...), it lives in a primitive JVM local, and the code is the same as for int:
dyn sum(int n) {
dyn s = 0;
for (dyn i = 0; i < n; i++) s += i;
return s;
}static Object sum(int n) {
int s = 0;
for (int i = 0; i < n; i++) s += i;
return s; // boxed once, on return
}public static java.lang.Object body(int);
...
20: iload_2 // i
21: iload_0 // n
22: if_icmplt 29
...
33: iload_1
34: iload_2
35: iadd // s += i, no boxing
36: istore_1
37: iinc 2, 1
40: goto 20
43: iload_1
44: invokestatic Ops.box:(I)Ljava/lang/Object;
47: areturnThe return type is dyn, so the result is boxed once at the end. Inside the loop there is no allocation.
Changing type: two halves and a flag
A dyn local that holds a number most of the time, but not always, gets two JVM locals - an Object half and a primitive half - and a flag that says which one is live. Arithmetic stays on the primitive half while it can:
dyn mixed(flag) {
dyn x = 1;
if (flag) x = "one";
return x + 1;
}static Object mixed(Object flag) {
Object xObj = null;
int xInt = 1;
boolean isInt = true;
if (Ops.truthy(flag)) {
xObj = "one";
isInt = false;
}
return Ops.add(isInt ? Integer.valueOf(xInt) : xObj, 1);
}public static java.lang.Object body(java.lang.Object);
0: aconst_null
1: astore_1 // x, Object half
...
7: istore_2 // x, int half = 1
9: istore_3 // flag: the int half is live
10: aload_0
11: invokestatic Ops.truthy:(Ljava/lang/Object;)Z
14: ifeq 41
17: ldc "one"
20: instanceof java/lang/Integer // an Integer would stay in the int half
23: ifeq 38
...
38: astore_1 // a string: the Object half
39: iconst_0
40: istore_3
41: iload_3
42: ifeq 52
45: iload_2
46: invokestatic Ops.box:(I)Ljava/lang/Object;
...
57: invokestatic Ops.add:(Ljava/lang/Object;Ljava/lang/Object;)Ljava/lang/Object;
60: areturnUnknown: the runtime decides
Where a type is not known before the program runs - a dyn parameter, a value from a table - the operation is a call to the runtime (Ops.add, Ops.lt, Ops.truthy...). It looks at the values and does what the language says: int + int is int, int + double is double, anything + a string is a string, a wrong combination is an error with the line of the script.
dyn sum(n) { // n has no type
dyn s = 0;
for (dyn i = 0; i < n; i++) s += i;
return s;
}Here s and i are still primitive (iadd, iinc), but i < n compiles to Ops.lt(box(i), n), because n may be anything. Typing the parameter (int n) turns it into if_icmplt.
Tables
A table maps keys to values like a Map, and keys can be added and removed at any time. It is not stored as a HashMap, though.
Shapes
Every table has a shape: the list of its keys in order, shared by all tables that got the same keys in the same order. Adding a key moves the table to the next shape ([x] → [x, vx] → [x, vx, name]); the moves are cached, so ten thousand tables built by one literal share one shape. Code that reads p.x remembers "for this shape, x is slot 0" and from then on reads the slot directly. The JavaScript engines in browsers do the same, under the name hidden classes.
A table that gets more than 64 keys, or loses a key (t.k = null), switches to a plain hash table and leaves this fast path.
A literal is a class
For a literal Jumper generates a class with one field per key, typed by the value that was put there first:
dyn make() {
return { x: 0.0, vx: 1.5, name: "ball" };
}final class Ball { // generated as Lay0
double x;
double vx;
Object name;
}
static Object make() {
Ball b = new Ball();
b.x = 0.0; b.vx = 1.5; b.name = "ball";
return b;
}public final class Lay0 extends JTable$Lean {
public double p0; // x
public double p1; // vx
public java.lang.Object o2; // name
public static double get0(java.lang.Object);
public static void set0(java.lang.Object, double);
...
}The literal is one allocation, the double fields are not boxed. A typed slot is a guess, not a promise: storing a string into x later turns the slot back into an Object one, and the table keeps working.
Reading and writing fields
A field read outside of loops is an invokedynamic call site that links itself to "is it this shape? read p0":
public static java.lang.Object body(java.lang.Object); // dyn name(p) { return p.name; }
0: aload_0
1: invokedynamic get:(Ljava/lang/Object;)Ljava/lang/Object;
6: areturnWhere statements only do arithmetic on fields, Jumper checks the shape of each table once for the whole group of statements and then works on the fields directly. The other branch is the general one, for a table of another shape:
dyn move(p, int steps) {
for (int i = 0; i < steps; i++) {
p.x += p.vx;
}
return p.x;
}// Ops.get / Ops.set stand for the generic path (a cached lookup of the key)
static Object move(Object p, int steps) {
for (int i = 0; i < steps; i++) {
if (p instanceof Ball b) { // the shape guard
b.x += b.vx; // plain double fields
} else {
Ops.set(p, "x", Ops.add(Ops.get(p, "x"), Ops.get(p, "vx")));
}
}
return Ops.get(p, "x");
}public static java.lang.Object body(java.lang.Object, int);
...
20: invokedynamic guard:(Ljava/lang/Object;)Z // is p a Lay0 with shape [x, vx, name]?
25: ifeq 50
28: aload_3
29: aload_3
30: invokedynamic get:(Ljava/lang/Object;)D // p.x, a double
35: aload_3
36: invokedynamic get:(Ljava/lang/Object;)D // p.vx
41: dadd
42: invokedynamic set:(Ljava/lang/Object;D)V // p.x = ...
47: goto 98
50: ... // any other table
63: invokestatic Ops.memberGet:(Ljava/lang/Object;Lme/padej/jumper/runtime/FieldCache;)Ljava/lang/Object;
...
78: invokestatic Ops.add:(Ljava/lang/Object;Ljava/lang/Object;)Ljava/lang/Object;
...
95: invokestatic Ops.memberSet:(Ljava/lang/Object;Lme/padej/jumper/runtime/FieldCache;Ljava/lang/Object;)VThe invokedynamic sites become direct field accesses once linked, and the JVM inlines them: the fast branch ends up as two loads, an add and a store.
Classes
A script class is compiled to a JVM class with real, typed fields. Methods are static methods that take the object as the first parameter:
class Point {
int x;
int y;
Point(int x, int y) { this.x = x; this.y = y; }
int sum() { return x + y; }
}class Point {
int x;
int y;
Point(int x, int y) { this.x = x; this.y = y; }
int sum() { return x + y; }
}public class Inst0_Point extends JTable {
public int f0; // x
public int f1; // y
}
// int sum()
public static int body(java.lang.Object);
4: aload_1
5: instanceof Inst0_Point
8: ifeq 21
11: aload_1
12: checkcast Inst0_Point
15: getfield Inst0_Point.f0:I // x
...
46: getfield Inst0_Point.f1:I // y
64: iadd
65: ireturnnew Point(i, 1) where the class is known creates Inst0_Point on the spot and calls the constructor's body directly. The instanceof branch is for objects stored the generic way (with -Djmp.genclass=0, or a class whose parent is declared in another file); for an Inst0_Point it is one type check.
Closures
A Java lambda can only read local variables that never change. A Jumper function can change them, so a captured variable is moved from the JVM stack to a frame on the heap:
dyn counter() {
int n = 0;
return () -> { n++; return n; };
}static Supplier<Object> counter() {
int[] n = { 0 }; // the frame
return () -> { n[0]++; return n[0]; };
}// counter
2: new Frame // a frame for the captured n
...
17: getfield Frame.p:[J // n lives in the frame's primitive slots
23: lastore
24: new FunctionNode$ScriptFunction // the lambda: code + this frame
...
40: areturn
// the lambda
3: getfield CompiledFunction.closure:Lme/padej/jumper/interp/Frame;
6: getfield Frame.p:[J
10: laload // read n
...
25: lastore // n = n + 1Only captured variables go to the frame; all others stay JVM locals. Functions as values are ScriptFunction objects; passed to a Java method that expects a functional interface (Runnable, Comparator...), they are wrapped to implement it.
Calls into Java
A call of a Java method is an invokedynamic call site. On its first call it finds the method for the receiver's class and the argument types - and checks the access policy - then links to it with a guard on the receiver's class:
dyn collect(dyn list, int n) {
for (int i = 0; i < n; i++) list.add(i);
return list.size();
}static Object collect(Object list, int n) {
for (int i = 0; i < n; i++) ((List) list).add(i);
return ((List) list).size();
}public static java.lang.Object body(java.lang.Object, int);
...
9: aload_0
10: iload_2
11: invokedynamic add:(Ljava/lang/Object;I)Ljava/lang/Object; // i passed as int
...
24: invokedynamic size:(Ljava/lang/Object;)Ljava/lang/Object;
29: areturnThe same happens when the receiver has a Java type (ArrayList list): the policy check is done once, when the site is linked, not on every call. A site that sees a new receiver class links again (and checks the policy again).
Seeing it yourself
JMP_DUMP=out java -Djmp.tier1=force -jar jmp.jar script.jmp # writes the generated classes to out/
javap -c -p out/Fn1_sum.class # the bytecode
JMP_DEBUG=1 java -jar jmp.jar script.jmp # what was compiled, linked, guarded
java -jar jmp.jar --opts # optimizations that can be switched offOn Windows set the variable first: set JMP_DUMP=out.
Every optimization can be switched off (-Djmp.unbox=0, -Djmp.layout=0...) and gives the same results, only slower.