Skip to content

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:

JumperJavaOn the JVM
dyn x = 1; x = "one";Object x and castsan 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 ita table with a shape; a literal is an instance of a generated class with real fields (below)
dyn pair(a, b) - parameters without typesObject a, Object bObject parameters
pair(1) - fewer or more argumentscompile errora missing argument is null, extra ones are dropped
closures change outer variables (n++)captured variables must be effectively finalcaptured variables live in a heap frame (below)
if (list), name || "guest"only boolean in conditionsonly null and false are false; || returns an operand
"a" == s compares text== compares referencesstrings and arrays by content, tables and objects by reference
s[0], s.length, date.times.charAt(0), s.length(), date.getTime()resolved when the call site is linked
xs.add(4), xs[xs.length] = 5 on [1, 2, 3]ArrayLista script array is a java.util.List
statements at the top level, functions called above their declarationclass Main, mainthe top level is a function of its own
throw { code: 404 }, catch (e)Throwable only, typed catchany 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:

jumper
int sum(int n) {
    int s = 0;
    for (int i = 0; i < n; i++) s += i;
    return s;
}
java
static int sum(int n) {
    int s = 0;
    for (int i = 0; i < n; i++) s += i;
    return s;
}
text
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: ireturn

A 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:

jumper
dyn sum(int n) {
    dyn s = 0;
    for (dyn i = 0; i < n; i++) s += i;
    return s;
}
java
static Object sum(int n) {
    int s = 0;
    for (int i = 0; i < n; i++) s += i;
    return s;               // boxed once, on return
}
text
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: areturn

The 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:

jumper
dyn mixed(flag) {
    dyn x = 1;
    if (flag) x = "one";
    return x + 1;
}
java
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);
}
text
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: areturn

Unknown: 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.

jumper
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:

jumper
dyn make() {
    return { x: 0.0, vx: 1.5, name: "ball" };
}
java
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;
}
text
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":

text
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: areturn

Where 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:

jumper
dyn move(p, int steps) {
    for (int i = 0; i < steps; i++) {
        p.x += p.vx;
    }
    return p.x;
}
java
// 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");
}
text
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;)V

The 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:

jumper
class Point {
    int x;
    int y;
    Point(int x, int y) { this.x = x; this.y = y; }
    int sum() { return x + y; }
}
java
class Point {
    int x;
    int y;
    Point(int x, int y) { this.x = x; this.y = y; }
    int sum() { return x + y; }
}
text
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: ireturn

new 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:

jumper
dyn counter() {
    int n = 0;
    return () -> { n++; return n; };
}
java
static Supplier<Object> counter() {
    int[] n = { 0 };                  // the frame
    return () -> { n[0]++; return n[0]; };
}
text
// 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 + 1

Only 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:

jumper
dyn collect(dyn list, int n) {
    for (int i = 0; i < n; i++) list.add(i);
    return list.size();
}
java
static Object collect(Object list, int n) {
    for (int i = 0; i < n; i++) ((List) list).add(i);
    return ((List) list).size();
}
text
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: areturn

The 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 off

On 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.

Released under the MIT License.