Language Reference (Java → SuperJ)
This is a reference for writers of SuperJ programs, not a tutorial. It assumes you already know Java and covers the deltas: what carries over unchanged, what's removed, what's changed, and what's new. For the ramp-up walkthrough see Getting Started; for the build tool see The Build System.
SuperJ reads like Java and behaves like C. It is not a subset of Java — it keeps the productive parts, drops the parts that fight predictable native performance, and adds systems-level capability Java has no answer for.
1. What's identical to Java
If you can write Java, most of what you write in SuperJ is the same:
- Lexical syntax:
//and/* */comments, identifiers, the operator and punctuator set, semicolons, braces. - Control flow:
if/else,for, enhancedfor(for-each),while,do/while,switch,break,continue,return,throw,try/catch/finally, labeled statements and labeledbreak L/continue L(Java semantics — a label prefixes any statement;break Lexits any enclosing labeled statement,continue Lcontinues an enclosing labeled loop). - Classes & interfaces:
class/interface, singleextends, multipleimplements,abstract,final, access modifiers (public/private/protected/package-private),staticmembers, constructors withthis()/super()chaining,this/super,import/import static, packages. - Generics: class-level and method-level type parameters, bounded (
<T extends Foo>), wildcard? extends/? super/?, diamond<>innew. Erased, just like Java — no reified type tokens. - Enums: with per-constant bodies,
values(),name(),ordinal(). - Exceptions:
try/catch/finally, multi-catch (catch (A | B e)), try-with-resources,throw. (But all unchecked — see §3.) - Method references
::targeting a single-abstract-method (SAM) interface. - Varargs (
T...). @Overrideannotation.- Operators: arithmetic, relational, logical, bitwise, shift, assignment, compound assignment,
?:, string+concatenation (lowered toStringBuilder). - Default values: fields default to
0/0.0/false/nulllike Java; locals have no default (see definite assignment, §3). - Casts and
instanceof, includinginstanceofpattern matching with flow scoping (see §3).
2. What's removed from Java
Each removal buys predictability or removes a class of bug. The replacement, where one exists, is listed.
| Removed | Why | Replacement |
|---|---|---|
Threads, Thread lifecycle, Runnable thread creation | Single-threaded by design — deterministic | Scale with processes + shared mmap + a total-order event stream (SEDA). Runnable/SAM interfaces remain as ordinary types, just not threaded. |
synchronized, volatile, wait/notify/notifyAll, java.util.concurrent | No memory model, no monitors | None. See Concurrency. |
| Lambdas / closures | Hidden capture machinery, unpredictable allocation | :: method references — the sole first-class function syntax. Capture-free. |
| Autoboxing / unboxing | Hides allocation; no wrapper classes | Primitives are never generic type args (List<int> is a compile error). Use primitive-specialized collections (IntArrayList, Int2IntMap, …). |
Reflection, ClassLoader | Everything known at compile time | Static resolution. instanceof/casts still work at runtime. |
| Non-static inner classes | Hidden enclosing-instance reference | Static nested classes only. Anonymous classes are allowed but do not capture locals/params/enclosing instance. |
| Checked exceptions | Noise; unenforced in practice | All exceptions unchecked. throws allowed but ignored (documentation only). |
finalize() / destroy() / Destroyable | No-op under arenas | Arena reclamation; explicit close()/free()/zeroize() for native resources. See Memory Safety. |
| JPMS modules | Packages only | package a.b.c; ⇔ directory path. |
switch pattern matching / arrow labels | Complexity | Classic colon form case V: only. |
User-defined annotations (@interface) | No consumer (no reflection, no APT) | Built-in @Override/@Test/@Ignore only. |
3. What's changed from Java
These features still exist but their semantics differ. This is where you trip if you're used to Java.
new is arena-scoped (no GC)
There is no tracing garbage collector. new allocates from an arena:
- By default, the global arena — lives for the whole program, safe to return from a method or store in a field.
- Inside an
arenablock,newallocates from that scoped arena, which is dropped (all memory freed in one O(1) operation) when the block exits.
Scoping is lexical, not dynamic: a new belongs to the arena it's written inside. A method you call from inside an arena block still uses the global arena unless it opens its own block.
Node createNode(int v) {
return new Node(v); // global arena — safe to return
}
void process() {
arena temp {
Buffer buf = new Buffer(1024); // in temp
Node n = createNode(5); // in global (createNode has no arena block)
outer = buf; // ok: escape analysis places buf where it outlives temp
} // temp dropped: buf still valid, n still valid
}
A value that outlives its arena is placed in a region that does outlive it — the stack if it stays in the frame, the long-lived arena if it leaves. Not rejected, not a use-after-free. Escape analysis runs on every build (the --no-escape-analysis flag turns off the stack-allocation optimization, not the correctness of placement). There is no destroy()/finalize() — arena drop is the reclamation mechanism. See Memory Safety.
The drop happens on every way out of the block — falling off the end, a return from inside it, a break/continue that leaves it (labeled or unlabeled). A thrown exception is the one gap: it unwinds past the frame without dropping the arena.
String is not arena memory. Only new — objects, arrays, structs — routes to the active arena. A String body (every +, substring, valueOf, StringBuilder.toString()) is allocated separately and never reclaimed, inside an arena block or outside one. An arena block does not bound the cost of code that builds strings; reuse a StringBuilder or work in byte[] on hot paths.
local — stack allocation (developer privilege)
local declares a stack-allocated object or array (lowered to alloca):
local Point p = new Point(1, 2); // on the stack, freed on function return
Constraints: not with primitives (local int x is an error), must have an initializer. The compiler does NOT escape-check local — a local ref that escapes its scope is undefined behavior, like a C stack pointer. Use arena blocks for the checked, safe form.
string / String is UTF-8, byte-indexed
One type (string is a keyword alias for String). Deliberately different from Java:
Java String | SuperJ String | |
|---|---|---|
| Encoding | UTF-16 | UTF-8 by construction |
length() | UTF-16 code units | byte count of the UTF-8 encoding |
charAt(i) | a 16-bit char | codepoint index; use charAt/codepoint access returning int |
| Backing | char[] | byte[]-backed, length-prefixed, NUL-terminated for zero-copy C handoff |
== | reference identity | reference identity (same as Java — use .equals() for content) |
An embedded NUL is an ordinary byte — "ab\0cd".length() == 5, and all String methods honor the stored length, not the first NUL. The one place NUL bites: the raw C-string view handed to printf("%s")/native code stops at the first NUL. True binary belongs in a byte[], not a String.
Arrays carry a runtime elem_size and are not Object
Arrays have no object header — a small length/elem-size prefix then elements. Consequences:
- Arrays are not
Object, not assignable toObject, not validinstanceof/cast targets. System.arraycopyis the structural copy intrinsic.- Every access is bounds-checked at runtime (
ArrayIndexOutOfBoundsException). The--no-bounds-checkbuild flag disables it for hot numeric loops.
Generics are erased, and primitives aren't allowed as type args
T erases to Object; T extends Foo erases to Foo. Bridge methods are generated where erased signatures differ. There is no autoboxing, so List<int> is a compile error — use a primitive-specialized collection (IntArrayList, Int2IntMap, …) or int[].
Exceptions are all unchecked
try/catch/finally/throw/multi-catch/try-with-resources all work as in Java. throws on a declaration is accepted but not enforced — it's documentation. Every exception is unchecked; the compiler never makes you catch or declare one.
instanceof pattern matching with flow scoping
if (obj instanceof String s) {
// s is in scope and typed String here, and only here
return s.length();
}
// s is NOT in scope here
Flow scoping: the binding is in scope in the if then-branch, the while loop body, and after a guarding if (!(obj instanceof T t)) return;. The binding is final — reassigning it is a compile error. Arrays are excluded as instanceof/cast targets (no object header).
Definite assignment is enforced
A local read before it's assigned is a compile error, same as Java — but SuperJ enforces it strictly. No accidental garbage reads.
4. What's new in SuperJ (not in Java)
arena blocks — scoped memory
arena temp {
// new allocates from temp
} // temp dropped here — O(1), no scan, no pause
Lexical, not dynamic. The compile-time escape check makes a dangling reference a compile error. See §3 above and Memory Safety.
local — stack-scoped allocation
See §3. Developer-privilege, UB-on-escape — use arena for the checked form.
Wide integers: i128, i256, i512
First-class signed primitives, usable everywhere int/long are:
i128 mask = 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFL;
i256 big = mask * mask;
i512 huge = ...;
long to = (long) mask; // narrowing cast
- All signed; no unsigned variant.
- Literals: decimal/hex/octal/binary, suffix
L/i128/i256/i512,_digit separators. i512cannot appear in anativemethod signature (no C ABI descriptor). All three wide ints (i128/i256/i512) are validstructfield types.Math.*Exactgives checked arithmetic for the narrow widths; wide-int arithmetic wraps on overflow (two's complement).
Complex numbers: cdouble, cfloat
First-class complex primitives — two floating-point values (real + imaginary) packed as one primitive type:
cdouble(128-bit): twodoubles — the default complex type.cfloat(64-bit): twofloats — use where bandwidth matters (arrays, SIMD).
cdouble z = ComplexMath.fromPair(3.0, 4.0); // 3 + 4i
cdouble j = 0.0 + 1.0i; // imaginary literal → 1i
cdouble w = z * z; // (3+4i)² = -7+24i
double r = ComplexMath.real(z); // 3.0
double im = ComplexMath.imag(z); // 4.0
double a = ComplexMath.abs(z); // 5.0
cdouble e = ComplexMath.exp(j); // e^i = cos(1)+i·sin(1)
Literal syntax: the i suffix denotes the imaginary unit — 3.0i is a cdouble with real=0, imag=3.0; 2.5fi is a cfloat with real=0, imag=2.5. Combine a real and imaginary literal to construct a complex value: ComplexMath.fromPair(3.0, 4.0) produces 3 + 4i.
Arithmetic semantics:
+,-,*,/and unary-work on complex values —*is complex multiplication (not element-wise):(a+bi)(c+di) = (ac-bd) + (ad+bc)i.==and!=compare both components;</<=/>/>=/%are compile errors (complex has no ordering).double → cdoublewidening sets imag=0;cdouble → doublenarrowing discards the imaginary part. The same applies tofloat ↔ cfloat.cfloat → cdoublewidens each component;cdouble → cfloatnarrows each component (explicit cast required).
Arrays: cdouble[] and cfloat[] are ordinary arrays — each element is a contiguous real+imag pair. Use --no-bounds-check for hot numeric loops.
ComplexMath SDK class (sj.lang.ComplexMath): static methods mirroring java.lang.Math over C99 <complex.h>:
| Method (cdouble) | cfloat variant | Description | ||
|---|---|---|---|---|
abs(cdouble) → double | absF | magnitude `\ | z\ | ` |
arg(cdouble) → double | argF | argument (phase angle) | ||
conj(cdouble) → cdouble | conjF | complex conjugate | ||
exp(cdouble) → cdouble | expF | e^z | ||
log(cdouble) → cdouble | logF | natural logarithm | ||
sqrt(cdouble) → cdouble | sqrtF | square root | ||
sin/cos/tan | sinF/cosF/tanF | trig | ||
sinh/cosh/tanh | sinhF/coshF/tanhF | hyperbolic | ||
pow(cdouble, cdouble) | powF | base^exp | ||
real(cdouble) → double | realF | real component | ||
imag(cdouble) → double | imagF | imaginary component | ||
norm(cdouble) → double | normF | ` | z | ²` (sum of squares) |
fromPair(double, double) → cdouble | fromPairF | construct from re+im |
FP determinism caveat: complex * and / involve multiple floating-point operations (fused multiply-add may apply), so results are arch-dependent — do not assert bit-exact complex FP output across architectures. This mirrors the real FP caveat: use a tolerance when comparing complex results across platforms.
struct — fixed-layout wire types
A struct is a value type with a known, packed byte layout — no object header, no identity, no inheritance, no interfaces. Designed for wire formats and zero-copy protocol code. See the Struct guide for the deep dive.
struct Header {
int magic;
long timestamp;
byte[16] uuid; // fixed-length array field
string payload; // var-length: UTF-8 text in the trailing data area
}
- Packed, little-endian, declaration order = byte order (no padding).
- Two construction modes:
new Header()(arena allocation, zero-init, with room for var-length data) and the flyweightHeader.view(buf, off)— binds to existing bytes, no copy, no allocation (the hot path).bindTo(buf, off)re-points it. - Reading a field returns its declared type — this is the part to get right: a
stringfield is assigned aStringand reads back as aString(h.payload = "hi"; String s = h.payload; dst.payload = src.payload;); abytesfield reads back as abyte[]. (new String(field)also works on astringfield.) No object is serialized — the text/bytes are stored inline by value; theString/byte[]is only the accessor's return value. To read a var-length field as raw bytes, declare itbytes. - Var-length
bytes/stringfields: an 8-byte slot in the fixed area (4-byte offset + 4-byte length) points into a trailing data area. Writes are strict declaration order, at most once each;reset()starts a fresh build. Header.SIZE(astatic final int) is the fixed-area size;totalSize()returnsSIZEplus the written var-length lengths — the number of bytes to frame/publish.- No class references or dynamic
T[]arrays as fields — a pointer in wire bytes is meaningless across processes.string/bytesare the var-length text/blob stored by value, not a pointer.
The synthesized accessors — write and read — in one round-trip through the flyweight:
struct Order {
long id; // fixed primitive
int qty;
byte side; // 0 = buy, 1 = sell
string symbol; // var-length UTF-8 text
bytes memo; // var-length raw bytes
}
// WRITE — bind a flyweight to a buffer and fill fields (no per-field allocation):
byte[] buf = new byte[256];
Order w = Order.view(buf, 0);
w.id = 42;
w.qty = 100;
w.side = 1;
w.symbol = "GOOG"; // string field <- a String
w.memo = new byte[]{7, 8, 9}; // bytes field <- a byte[]
int n = w.totalSize(); // Order.SIZE + 4 ("GOOG") + 3 (memo); the bytes to send
// READ — bind a fresh view to those bytes; each field reads back as its declared type:
Order r = Order.view(buf, 0);
long id = r.id; // primitive -> its value (42)
int qty = r.qty; // (100)
String sym = r.symbol; // string field -> a String ("GOOG")
byte[] memo = r.memo; // bytes field -> a byte[] ({7,8,9})
// String s = new String(r.symbol); // also works: a distinct copy
You assign each field its declared type and read it back the same way — a string field is a String on both sides, a bytes field a byte[]. Var-length fields (symbol, memo) are written in declaration order; reset() rewinds to refill the same buffer.
Rectangular arrays: int[,], int[,,]
Contiguous, stride-indexed, a distinct type from jagged int[][]:
int[,] m = new int[3, 4]; // single allocation, row-major
m[1, 2] = 7;
int total = m.length; // 12 (product of dimensions)
- Number of commas + 1 = rank (
int[,]rank 2,int[,,]rank 3). - Every dimension sized at creation — no jagged/empty dims.
.lengthis the total element count; per-dimension sizes are not exposed.- Not assignable to
int[][],int[], orObject.
First-class SIMD vectors
Accessed via the builtin Simd class — no new syntax, no new keywords. Small fixed-size arrays are the container; Simd methods operate on them as vector registers:
float[4] a = ..., b = ..., c = new float[4];
Simd.addFloat4(a, 0, b, 0, c, 0); // c = a + b, 4 lanes at once
Vector families (element type + lane count, encoded in the method-name suffix):
| Width | ISA | Families |
|---|---|---|
| 128-bit | SSE2 | Byte16, Short8, Int4, Long2, Float4, Double2 |
| 256-bit | AVX2 | Byte32, Short16, Int8, Long4, Float8, Double4 |
i128 and boolean are not SIMD element types. Naming: Simd.operation<Family>(src, srcOff, …, dst, dstOff). Operations: arithmetic, bitwise, compare (produce mask lanes 0/-1), blend, convert (widen/narrow), reduce (sum/min/max/any/all), math (sqrt/floor/fma), gather/scatter, dot, prefix-sum, load/store (aligned + unaligned). The full operation list is in the SIMD reference (shipped with the SDK sources). --simd=sse2|avx2|none selects the target ISA (sse2 default; avx2 enables the 256-bit families).
native methods — straight to the C ABI
public class Crypto {
public native static long hash(byte[] data, int len);
}
- Declaration ends with
;(no body); the compiler emits an externaldeclare, the linker resolves the symbol. - Symbol mangling:
sj_<package>_<Class>_<method>__<argDescriptor>(i128→Q,i256→O,cfloat→M,cdouble→N;i512not allowed). Instance natives receivethisas an implicit first C param. Stringmarshals asconst char*(NUL-terminated UTF-8) in andchar*out; other references arevoid*. Primitives map to the obvious C types.cfloat/cdoublemarshal as a two-element struct ({float,float}/{double,double}).- Natives allocate through the SuperJ allocator — using
mallocand returning that is UB. - Raise SuperJ exceptions via the runtime exception helpers (they don't return).
The systems SDK (at a glance)
Batteries included; every package is under sj.*:
| Package | What's in it |
|---|---|
sj.lang | String, StringBuilder, Math, ComplexMath, Integer, Long, Short, Byte |
sj.util | ArrayList, IntArrayList, LongArrayList, HashMap, Int2IntMap, Int2ObjMap, HashSet, Arrays, Hash, Random |
sj.io | File, FileReader, FileWriter, Closeable |
sj.http | incremental HTTP/1.1 server, ContentType, Cookie, TLS 1.3 |
sj.json | hand-written incremental JSON parser |
sj.seda | the event-driven framework (Apps → Sequencer → event queue) |
sj.crypto | SHA256 (ARM NEON), Secp256k1, EthereumAddress, EIP-712, TLS engine |
sj.net | NioAdapter event loop (TCP/UDP/Unix, epoll/kqueue), UnixGateway |
sj.test | Asserts, the unit-test harness |
Discover the API with superj doc --list and superj doc <class-fqn>.
5. Quick syntax table
Scan for the construct you need. "Java form" is what you'd write in Java; "SuperJ form" is what you write here.
| Construct | Java form | SuperJ form | Note | |
|---|---|---|---|---|
| Hello world | public class H { public static void main(String[] a){ System.out.println("hi"); } } | same | identical | |
| String type | String | string or String | string is a keyword alias; one type | |
| String length | s.length() (UTF-16 units) | s.length() (bytes) / s.charCount() (codepoints) | UTF-8 | |
| Declare a generic list | List<Integer> xs | ArrayList<Integer> — or IntArrayList | no int as type arg; no boxing | |
| Lambda | () -> doX() | Thing::doX | method refs only; target a SAM interface | |
| Functional target | Runnable r = () -> … | Runnable r = MyClass::run | SAM interface, capture-free | |
| Inner class | class Outer { class Inner {} } | class Outer { static class Inner {} } | static nested only | |
| Anonymous class w/ capture | new Foo() { void m(){ use(local); } } | new Foo() { void m(){ /* can't use local */ } } | anonymous classes don't capture | |
| Thread | new Thread(r).start() | run a process; or a SEDA app | no threads | |
| Lock | synchronized (obj) { … } | — | none; single-threaded | |
| Memory cleanup | obj = null; (hint to GC) | nothing; or arena block drop | no GC; arena frees on scope exit | |
| Scoped temp memory | (no equivalent) | arena temp { … } | new — O(1) drop at block exit | |
| Stack object | (no equivalent) | local Point p = new Point(1,2); | new — UB if it escapes | |
| 128-bit int | BigInteger | i128 x = …; | first-class primitive | |
| 256-bit int | BigInteger | i256 x = …; | first-class primitive | |
| 512-bit int | BigInteger | i512 x = …; | new; can't cross native | |
| Complex (double) | (no equivalent) | cdouble z = ComplexMath.fromPair(3.0, 4.0); | new; two doubles packed as one primitive | |
| Complex (float) | (no equivalent) | cfloat w = ComplexMath.fromPairF(2.0f, 3.0f); | new; two floats | |
| Imaginary literal | (no equivalent) | cdouble j = 1.0i; | new; i suffix = imaginary unit | |
| Wire-format record | (no equivalent) | struct Header { int magic; … } | new; Header.view(buf, off) flyweight | |
| 2-D dense array | int[][] grid | int[,] grid | new; contiguous, stride-indexed | |
| 3-D dense array | int[][][] cube | int[,,] cube | new | |
| SIMD add 4 floats | (no direct equiv) | Simd.addFloat4(a, 0, b, 0, c, 0) | new; float[4] is the container | |
| Call into C | JNI boilerplate | public native static long hash(byte[] d, int n); | new; mangled sj_… symbol | |
| Checked exception | throws IOException | throws IOException (accepted, not enforced) | all unchecked | |
| Try-with-resources | try (var r = …) { … } | same | resource needs a void close() | |
| Multi-catch | `catch (A | B e)` | same | |
| Labeled break/continue | outer: for… break outer; | same | Java semantics; unwinds try-finally | |
instanceof pattern | if (x instanceof T t) | same | flow scoping; arrays excluded | |
| Test method | JUnit @Test | @Test static void name() { … } + // UNIT marker | built-in harness | |
| Skip a test | @Ignore (JUnit) | @Test @Ignore static void name() { … } | built-in | |
| File extension | .java | .sj | ||
| Build | javac / mvn / gradle | superj build / superj run | see Build System | |
| Run | java -cp … Main | target/<name> | native binary |
6. Reserved keywords
abstract boolean break byte case catch char
class const continue cdouble cfloat default do
double else enum extends final finally float
for goto if i128 i256 i512 implements
import int interface instanceof local long native
new null package private protected public return
short static string struct super switch synchronized
this throw throws transient try void volatile
while assert strictfp
New vs Java: arena (scoped memory), local (stack allocation), i128/i256/i512 (wide ints), cdouble/cfloat (complex numbers), string (alias for String), struct (wire-format type).
Reserved-but-unused (inherited from Java's keyword set, not implemented): const, goto, assert, strictfp, transient, synchronized, volatile. They're reserved so you can't use them as identifiers; the features they name in Java are removed (§2) or not implemented.