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, andstatic { … }initializer blocks. (Instance initializer blocks — a bare{ … }in a class body — are not included; see §2.) - 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. |
Instance initializer blocks (a bare { … } in a class body) | Runs implicitly before every constructor body, so the order a field is written in is not local to the code that writes it | A constructor, or a shared private void init() the constructors call — with this() chaining (§1) covering the multi-constructor case they are usually reached for. static { … } blocks are unaffected and behave as in Java. |
| 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 ambient, not lexical: an allocation belongs to the innermost arena block open when it happens, including one made by a method called from the block. A call the compiler cannot prove keeps nothing it allocates is carved out and allocates globally instead — a native is carved out unless its declaration says otherwise (@NoAllocEscape). Under strict, a carve-out is reported (W_ARENA_CARVE_OUT).
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), and since #3714 its verdicts are graded: a promotion the analysis merely assumed necessary — nothing witnessed the value escaping, it just could not prove it stays — warns (warning[W_UNPROVEN_ESCAPE], naming the cause), while a witnessed escape is silent because its return or store is in the source (SPEC §7.4.3). --dump-escape prints every allocation's placement and grade. There is no destroy()/finalize() — arena drop is the reclamation mechanism. See Memory Safety.
Because escape is placed rather than rejected, a block can compile clean and reclaim nothing. Write strict arena to make the block a claim instead of a hint — any allocation inside it that outlives it is then error[E_ARENA_ESCAPE] rather than a silent promotion:
strict arena tmp {
Buffer b = new Buffer(1024); // dies here — fine
cache.put(k, new Entry(r)); // error[E_ARENA_ESCAPE]
}
strict is a contextual keyword — recognized only immediately before arena, so it is not reserved and remains usable as an identifier. It is the claim layer applied to placement, alongside pure and noalloc; see The Claim Layer §6.1.
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), and a thrown exception. There is no gap.
A throw gets there by a different route, which is why it is worth stating rather than assuming: an exception longjmps past the frame, so no statically placed drop sits on its path. Each block registers with the runtime, a handler records the depth when it is pushed, and the catch drops everything opened since (#3100). A block whose catch is lexically inside it is below that depth and survives, as it must.
A thrown exception's lifetime is the handler that catches it (#4888), so throw new RuntimeException(…) in a loop measures 0 bytes per throw, inside an arena block or outside one. Nothing has to be pre-allocated.
The only residual is opt-in: SUPERJ_STACKTRACE=1 adds a 48-byte frame array per throw (see below), and a handler that keeps its exception past the handler pays one copy to move it out of the region, which is what keeping it means. Reading it is not keeping it: e.getMessage(), e.getCause(), e.toString() and e.typeName() in the handler cost nothing (#5162). Keeping the message or cause they return — storing it in a field, returning it, adding it to a list — keeps the exception, and pays that one copy.
A throw can also capture a stack trace, so that the exception you catch can be asked where it came from (getStackTrace(), printStackTrace()). That is by some distance the larger of the two costs, which is why it is no longer on by default — see below. The trace is captured at the throw, and the first throw wins: rethrowing, or throwing something else while handling an exception, does not rewrite the frames an earlier exception already carries.
Capture is off by default (#4888). A default build walks no stack on a throw, builds no long[] of PCs, and getStackTrace() answers an empty array. One gate (SUPERJ_STACKTRACE) controls both capture and printing, so "off" means off — no capture, no array:
SUPERJ_STACKTRACE | capture | frames in the unhandled-exception report |
|---|---|---|
| unset (default) | no — getStackTrace() answers an empty array | no |
1 | yes | yes |
0 | no | no |
Measured on the #4888 probe, 200,000 throw/catch cycles: 20 ns and 0 B per throw by default, against ~1,350 ns and 48 B with SUPERJ_STACKTRACE=1 — the capture costs more than the throw it decorates by two orders of magnitude, and is the whole of what a throw now costs in memory. (The 48 B is the frame array alone: the exception object itself is reclaimed either way, #4888 2/2.) Set the variable when you are debugging; leave it unset in production, which is what you get by default.
Since #4939 this covers the exceptions the RUNTIME raises as well — a NullPointerException, an ArrayIndexOutOfBoundsException, an ArithmeticException, a failed Long.parseLong — and their messages with them. Those took a different path out of #4888 and still cost ~112 B of permanent arena each; 2,000,000 rejected inputs grew RSS by 252 MB, on the reject path, which is the one untrusted input reaches. Both paths are now 0 B. The one allocation a throw can still make is a message built by CONCATENATION in a throw expression (throw new NumberFormatException("bad: " + s)): the concatenated string is placed in the global arena (§7 placement of a thrown allocation), so it is the message text, not the throw, that costs. A constant message costs nothing.
The crash report is not governed by this default. A fatal SIGSEGV/SIGBUS still prints its frames (read from the signal context, no allocator, once, at death); SUPERJ_STACKTRACE=0 suppresses those too, which is the one thing that flag still does on its own.
Frames print as image-relative offsets plus the exact llvm-symbolizer command that resolves them; SuperJ emits its functions internal, so nothing in-process can name them. Build with --debug (or -gline-tables-only) to get file:line out of that command. The number of frames varies with optimization level, so do not assert on it.
A crash reports itself too
A fault that is not an exception — a wild pointer, a dangling read — prints the same kind of report before the process dies:
Fatal: SIGSEGV at address 0x5f6e696680000005
stack (6 frames, innermost first):
#0 0x100000728
…
symbolize: llvm-symbolizer --inlines --obj=<binary> <addresses above>
SIGBUS is reported the same way; the same wild dereference raises one or the other depending on what happens to be mapped. The report goes to stderr, the process still dies of its original signal (so exit status and core dumps are unchanged), and SUPERJ_STACKTRACE=0 suppresses the frames here as well. A stack overflow is not this — it remains a catchable StackOverflowError.
To make those frames walkable, generated functions keep a frame pointer (non-leaf functions only; leaf code is untouched and byte-identical without it). This is not the same thing as the DWARF unwind tables, which SuperJ has always emitted and which is why lldb could already produce a backtrace: a debugger can follow CFI, but a signal handler inside the process cannot follow it back across the signal trampoline, and needs the frame-record chain. Note that clang -fno-omit-frame-pointer cannot supply this when compiling a .ll — the choice is a per-function IR attribute the backend reads, not a frontend flag.
The cost was measured before turning it on, and is not detectable: the compiler compiling itself (82 files, 26 MB of IR, 1393 functions that gained a frame pointer) runs at 1.56–1.59 s either way across three runs each, and the frame-pointer build's binary is smaller by 66 KB.
SPEC §7.4 requires that a String built inside a block be reclaimed with it, exactly like an object or an array: "every allocation" means every new and every String, with no separate string domain and no carve-out (#3101).
The common case works (#3664, #3712). String's read-only intrinsics — length, charAt, equals, substring and friends — are known not to retain their receiver, and a concat that provably dies where it is written is placed in its block or on the stack. Measured: the same 1M-iteration "item-" + i loop inside a block reserves 0 bytes.
What remains is the general case: a string (or anything else) passed to a native outside that curated list still has no retention summary, is assumed kept, and is promoted — on an unproven verdict, which warns (W_UNPROVEN_ESCAPE, naming the call — #3714, SPEC §7.4.3). The structural fix is #3663 — a @NoRetain retention contract the declaring author writes (SPEC §7.4.4) — rather than widening the compiler's curated list, because a carve-out per method in compiler internals could never cover a user's own native.
An unsummarized native is assumed to retain what it is handed (§7.4.4, the @NoRetain contract), so an arena block does not bound the cost of strings handed to one. Reuse a StringBuilder or work in byte[] on hot paths.
The ordinary escape rule applies to strings like anything else: one that leaves the block — returned, stored in a field, thrown — is placed somewhere longer-lived, and the block does not bound its cost. Reusing a StringBuilder or working in byte[] is still the better hot-path shape, but it is not required to keep a block's memory bounded.
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. local is a claim the compiler adjudicates (#3714, SPEC §7.4.3): a witnessed escape — returned, stored past the frame, retained per a callee summary — is error[E_LOCAL_ESCAPES], and only where the analysis can neither prove nor refute is the author respected, with W_LOCAL_ESCAPES marking the residual risk. In that unproven band — only there — an escaping ref is undefined behavior, like a C stack pointer. Use arena blocks for the fully placed, always-safe form.
string / String is UTF-8 and codepoint-indexed
One type (string is a keyword alias for String). Deliberately different from Java.
Every index is a codepoint. charAt, codePointAt, substring (both overloads), indexOf, lastIndexOf and the fromIndex they take all address characters, and a codepoint above U+FFFF counts as one position rather than Java's two. The corresponding count is charCount().
The storage is UTF-8, and exactly two members expose it: length() returns the byte count and byteAt(i) takes a byte offset. They exist because a wire protocol's length prefix and a zero-copy C handoff need the encoded size — not as the type's addressing model. Those two are the exception; everything else above is codepoints (SPEC §8.2, "Index spaces").
Mixing the two is the one thing to watch, and the rest of this section is about that. In full:
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) | index is a UTF-16 code unit | index is a codepoint; returns a 16-bit char |
substring(a, b) | UTF-16 code units | codepoints; the bound is charCount(), not length() |
indexOf / lastIndexOf | returns a UTF-16 index | returns a codepoint index (and fromIndex is one) |
byteAt(i) | — | index is a byte |
| Backing | char[] | byte[]-backed, length-prefixed, NUL-terminated for zero-copy C handoff |
== | reference identity | reference identity (same as Java — use .equals() for content) |
length() and charAt(i) are in different units, and this is the one that catches people. length() counts UTF-8 bytes; charAt takes a codepoint index, whose bound is charCount(). So the reflex Java loop is wrong here:
for (int i = 0; i < s.length(); i = i + 1) { s.charAt(i); } // WRONG on non-ASCII
for (int i = 0; i < s.charCount(); i = i + 1) { s.charAt(i); } // codepoints
for (int i = 0; i < s.length(); i = i + 1) { s.byteAt(i); } // bytes
The first runs past the end and throws StringIndexOutOfBoundsException as soon as one non-ASCII character appears — on ASCII the two counts coincide, which is why it survives until real data arrives. Neither count is the wrong one to have: a wire protocol's length prefix wants bytes and getBytes().length agrees with length(); a character walk wants charCount().
The trap has a second form, and it is the more common one, because s.substring(0, s.length()) is the reflexive Java idiom:
s.substring(0, s.length()); // WRONG on non-ASCII — mixes bytes into a codepoint bound
s.substring(0, s.charCount()); // right — and so is plain `s`, or s.substring(0)
For "héllo wörld!" — 14 bytes, 12 codepoints — substring(6, 12) returns "wörld!" while substring(6, t.length()) throws StringIndexOutOfBoundsException: String index out of range: 14 (length 12). The message reports the codepoint bound, which is the clue: 12 is charCount(). indexOf("w") is 6, not the byte offset 7, so an index obtained from a search can be fed straight back into substring or charAt — mixing units only goes wrong when the number came from length().
A codepoint above U+FFFF is truncated by charAt. char is 16 bits and cannot hold one, so for "a𐍈b" (U+10348) charAt(1) gives 0x0348 — silently. Text that may contain emoji, historic scripts or the wider CJK ranges must not be examined through charAt; work in bytes (byteAt/length(), or the raw byte[] the zero-copy path already gives you). charCount(), substring, indexOf, equals and hashCode never narrow, so such a character round-trips through them intact and counts as exactly one position — unlike Java's UTF-16, where it occupies two (#4218, SPEC §8.2).
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.
Wrapping a cause. An exception carries a cause, and getCause() reads it back (#3641):
try {
connect(port);
} catch (IllegalArgumentException e) {
throw new IllegalStateException("startup failed", e); // e is retained
}
The constructors are (), (String message) and (String message, Throwable cause) — and only those. There is no (Throwable) form: the first argument is always the message, so new IllegalStateException(cause) is a compile error rather than a message that renders as an object. A second argument that isn't a Throwable, or a third argument, are errors too. The reason is that these arguments land in real object slots that getMessage()/getCause() read back at a declared type — accepting one that can't be is a type hole, not a leniency.
A user exception type forwards a cause the same way, through super:
class ConfigError extends RuntimeException {
int code;
ConfigError(string msg, Throwable cause, int code) { super(msg, cause); this.code = code; }
}
When an exception goes unhandled, the report walks the chain:
Unhandled exception: IllegalStateException: startup failed
at app.sj:42
Caused by: IllegalArgumentException: bad port 70000
Two limits worth knowing. An exception the runtime raises — ArithmeticException from x / 0, a bounds check, a null check — has no cause, so getCause() on one returns null. A NullPointerException carries a message naming the operation that dereferenced null (e.g. "Cannot invoke method on a null receiver", "Cannot access field on a null receiver"). getStackTrace() returns a long[] of frame addresses; there is no printStackTrace() — the unhandled-exception report prints the throw site and captured frames, and a caught exception exposes its trace via getStackTrace().
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
Ambient, not lexical — a method called from the block allocates in it too, unless that call is carved out (§3). 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 | |
| String slice | s.substring(0, s.length()) | s.substring(0, s.charCount()) — or just s | substring is codepoint-indexed; length() is bytes | |
| 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 — a claim: witnessed escape = compile error, UB only in the unproven band (SPEC §7.4.3) | |
| 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) and its strict form (a verified claim — contextual, so strict is not reserved), local (stack allocation), i128/i256/i512 (wide ints), cdouble/cfloat (complex numbers), string (alias for String), struct (wire-format type).
The SuperJ-specific entries, so the parse error you get from writing one as an identifier names the thing rather than a mystery word:
| Keyword | Meaning in SuperJ |
|---|---|
arena | scoped memory block (arena name { … }) |
local | stack-allocation claim on a declaration (local Point p = …) |
i128 i256 i512 | wide-integer types |
cdouble cfloat | complex-number types |
string | String alias |
struct | fixed-layout 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.