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
givetheir result. - Loops
stop,skip, and rununtila 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.j2goes 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 outcompiles 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 replfor trying expressions; definitions persist across lines. -
Formatter and test runner
j2 fmtkeeps sources tidy;j2 testruns 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.