The J2 Programming Language

J2 starts like a script and compiles like a native program. During a native build, the compiler looks for the loops and calls it can prove independent and spreads that work across your cores. Anything it cannot prove stays serial, exactly as you wrote it.

The language

Readable on day one

J2 keeps its surface small and its words plain:

  • Bindings are constant with = and mutable only when you mark them with :=.
  • Functions give their result.
  • Loops stop, skip, and run until a condition holds.
  • Classes and typed error handling are part of the language, not add-ons.
  • Seventeen standard library modules are available without a single import.

The documentation covers every construct in the language, and each example in it runs, verbatim, under J2 0.1.0.

# Find the longest word.
words = ["apple", "banana", "fig", "grapefruit"]

func longest(ws) = {
    best := ""
    for w in ws {
        if len(w) > len(best) { best = w }
    }
    give best
}

print(fmt("longest: {}", longest(words)))
$ j2 run longest.j2
longest: grapefruit

One command, two engines

A scripting language and a compiler in the same binary

  • Run instantly

    j2 run file.j2 goes through an interpreter that starts in milliseconds, which makes it the natural mode for scripts and for the tight loop of iterating on a program: there is no build step or cache to sit through.

  • Build natively

    j2 build file.j2 -o out compiles the same source to a standalone binary, and parallelizes whichever loops and calls it can prove safe without asking for a single annotation.

  • One behavior

    Both engines are held to identical output on every program in the test suite, so an optimization that could change your results is an optimization J2 refuses to take.

  • Nothing else to install

    The download carries the interpreter, the compiler, and every library it links against, which is why native builds keep working offline and on machines with no development tools.

Automatic parallelism

Cores without threads

J2 has no thread API, and does not need one. Native builds recognize the shapes that dominate real compute: reductions over large sequences, element-wise loops, dense kernels, and independent calls to pure functions, like the four on the right. When safety is provable, the work spreads across cores; when it is not, the code simply stays serial.

On the example programs in the repository, measured against the same binary with parallelism switched off: 2.9 times faster on numeric integration, 2.5 on a Monte Carlo estimate, 2.2 on matrix times vector. Compute-bound work scales with cores; memory-bound work meets bandwidth and plateaus. The Automatic Parallelism chapter sets out what to expect from each.

func estimate(n: int) -> float = {
    a := count_hits(1, n)
    b := count_hits(999983, n)
    c := count_hits(50000017, n)
    d := count_hits(1234567891, n)
    give ((a + b + c + d) * 4.0) / (4.0 * n)
}
$ j2 build montecarlo.j2 -o montecarlo
$ ./montecarlo        # the four calls run on four cores

Under the hood

The techniques that unlock the parallelism

Proving a loop safe to split is rarely one big theorem. It is a handful of specific techniques working together, each removing a reason the code would otherwise have to stay serial.

  • Purity analysis

    Functions cannot reach their caller's locals, and a method declares in its signature whether it mutates, so the compiler can establish that a call has no side effects, the fact every other technique leans on.

  • Idiom recognition

    Instead of attempting arbitrary code, the compiler pattern-matches the shapes that dominate real compute (reductions, element-wise loops, dense kernels such as matrix times vector) and parallelizes the instances it can verify.

  • Reduction lowering

    A loop folding a sequence into one value with an associative operation is rewritten into a call to a parallel reduction primitive in the runtime: each core folds its own chunk and the partial results combine at the end.

  • Privatization

    A scratch variable that every iteration overwrites before reading looks like a dependence between iterations but is not one; giving each core its own private copy dissolves the conflict and frees the loop to split.

  • Induction-variable rewriting

    A counter that advances by a fixed step would chain every iteration to the one before it, so the compiler replaces it with a closed form that each core computes directly for its own slice of the work.

  • A cost model

    Parallel dispatch has a fixed price, so small workloads are deliberately left serial (reductions engage only past tens of thousands of elements), and the parallel path stays a win rather than an overhead.

Safety

Deny by default

A J2 program cannot read files, spawn processes, or touch the network unless the person running it says so. Capabilities are granted per run, one flag each, so a script's powers are visible in the command that invokes it. Constants outnumber mutables, integer overflow is an error rather than a surprise, and errors carry types, not just messages.

$ j2 run report.j2
RuntimeError: read_file: capability denied

$ j2 run --allow-fs report.j2
wrote summary.txt

Tooling

The toolchain is the download

  • REPL

    j2 repl for trying expressions; definitions persist across lines.

  • Formatter and test runner

    j2 fmt keeps sources tidy; j2 test runs a directory of programs and reports the score.

  • Editor support

    A VS Code extension with highlighting, one-key run, and inline errors ships with every release.

  • Honest diagnostics

    Compile errors point at the offending line with a caret; runtime errors name their class and line. No stack-trace archaeology.