Automatic Parallelism

J2 has no threads, no locks, and no task API. You write the obvious loop or the obvious pair of calls; when the compiler can prove a computation safe to split, the native build runs it across your cores.

The idea

Most parallel speedups in real programs come from a small family of shapes: reductions over big data, element-wise transformations, dense numeric kernels, and groups of independent calls. Those shapes are recognizable in code, and their safety is provable when the pieces are pure. J2 leans on both facts. The interpreter runs everything serially; j2 build (and J2_FORCE_NATIVE=1) lowers your program to native code, looks for the shapes, and rewrites the ones it can prove safe.

Nothing about your source changes. There is no annotation to add and no API to call, and a loop the compiler cannot prove safe simply stays serial.

What parallelizes

Reductions over large sequences

Folding a sequence into one value with an associative operation: sum, min, max, or the loop that spells the same thing. Small inputs are left alone; the reduction path engages at 32,768 elements, below which a serial loop is faster than any coordination.

data = collect(1..2000000)
print(sum(data))    # splits across cores in a native build

Element-wise loops

Loops where iteration i writes dst[i] from the inputs at the same index: scaling, combining two sequences, applying a function across an array. These are the workhorse patterns of numeric code, and the typed forms lower to the same machine code a systems language would produce:

func axpy(y: seq<float>, x: seq<float>, a: float) -> nil = {
    for i in 0..(len(y) - 1) {
        y[i] = y[i] + a * x[i]
    }
}

y := make_seq(100000, 1.0)
x := make_seq(100000, 2.0)
axpy(y, x, 0.5)
print(y[0])    # 2

Nested numeric kernels

The classic dense shapes with flat row-major storage: matrix times vector and one-dimensional convolution. The outer loop splits across cores, each core computing a band of the output:

# y = A x, with A stored row-major in a flat seq
for i in 0..(m - 1) {
    acc := 0.0
    for k in 0..(n - 1) { acc += a[i * n + k] * x[k] }
    y[i] = acc
}

Independent calls

When a function body makes several independent calls to pure functions and then combines the results, the calls run concurrently. This is task parallelism without tasks, and it is where compute-bound programs gain the most:

func estimate(n: int) -> float = {
    a := count_hits(1, n)              # four independent, pure calls:
    b := count_hits(999983, n)         # the native build runs them
    c := count_hits(50000017, n)       # on four cores
    d := count_hits(1234567891, n)
    give ((a + b + c + d) * 4.0) / (4.0 * n)
}

The calls must be independent (no result feeding another) and provably pure, and the fan-out fires for calls inside one function body, which is one more reason to factor work into functions.

What purity buys

Every rewrite above rests on knowing that reordering cannot change behavior. J2 is built to make that provable often: functions cannot reach their caller's locals, methods declare mutation in their signature (= pure, := mutating; see Classes), and the compiler checks purity structurally rather than guessing. When it cannot prove a piece pure, that piece runs serially. False negatives cost a missed speedup; there is no false positive to cost you a wrong answer.

Correctness is not negotiable

A parallel build must produce the same output as the serial build and the interpreter, byte for byte. The test suite runs every program through both engines and compares; floating-point reductions whose grouping could change results are left serial rather than approximated. If parallelism ever changes an answer, that is a bug in J2, not a caveat in your program.

What to expect

The programs that gain the most share a profile: they are compute bound, they spend their time in a few hot loops or calls rather than spread thinly across thousands of lines, and they work on data large enough to be worth splitting. A native build is likely to show a real speedup when:

  • The hot loop is a reduction over a large sequence: summing, counting, or folding with an associative operation, over tens of thousands of elements or more, enough to pay for the parallel dispatch.
  • The work is element-wise over big arrays, each iteration writing its own index from inputs at the same index: scaling a sequence, combining two of them, applying a function across an array.
  • The kernel is dense and numeric, such as matrix times vector or one-dimensional convolution, with typed signatures that let the compiler specialize it before splitting the outer loop.
  • A function fans out into several independent calls to pure, compute-heavy functions and combines their results, the shape of many simulation and integration workloads.

The same profile says what will not accelerate. Scripts dominated by I/O or by startup give extra cores nothing to do, and small inputs are deliberately left serial because the dispatch would cost more than it saves. An untyped hot loop is limited by dynamic dispatch rather than core count, so annotate it first. Loops whose iterations feed each other cannot be split at all, and memory-bound kernels stop scaling once they meet the machine's bandwidth, however many cores join. Interpreted runs with j2 run are always serial; the parallelism belongs to native builds.