Home · ← SuperJ Manual EN|中文

Coding Guidelines

Three style rules that are currently learned by getting them wrong. They are not about taste — each one is the difference between code that ships and code that quietly costs you. These guidelines are for both human and agent authors.

Contents

  1. GC-free coding
  2. Prefer switch over if/else if chains
  3. Prefer early return to reduce nesting

1. GC-free coding

SuperJ has no garbage collector. Every new is permanent — the object lives as long as the arena that owns it, and the default arena is the global one, which lives until process exit. There is no such thing as a short-lived object unless you make it one. A new in a hot path is not "a bit of pressure the collector will absorb" — it is a leak with a rate.

This is the single biggest difference from Java and the source of every memory bug downstream projects have hit. The full guide to flat memory — the allocation ladder, arenas, local stack allocation, the scratch-buffer cookbook, and the memory-event log — is in Memory Management Best Practices. This section is the rules-of-thumb summary; read that page for the why and the recipes.

The allocation ladder — try these in order

  1. Don't allocate. Most allocations are avoidable. Return a primitive instead of a wrapper; fill a caller-owned array instead of returning a new one; precompute strings once instead of concatenating per call.
  2. local — stack-allocate small, provably non-escaping temporaries. Needs an initializer; not allowed on primitives (a plain int is already a stack slot). Only works when the size is a compile-time constant — with a runtime size it is silently ignored and you get a global-arena allocation instead. The compiler does not escape-check local — a local reference that outlives its scope is undefined behaviour.
  3. Reuse a long-lived static scratch buffer. For anything sized by the data, this is the answer. Allocate once, reuse across every call, partition, query and iteration. Grow-on-demand, never shrink:

``java static double[] scratch = null; static double[] ensureScratch(int n) { if (scratch == null || scratch.length < n) scratch = new double[n]; return scratch; } ``

Single-threaded → one buffer per process is safe. No locks, no thread-locals, no defensive copies. The ordering contract replaces the lock: the caller must consume the returned data before the next call overwrites it. Write that contract in a comment at the definition.

  1. arena block for a batch of scoped intermediates you want dropped together, in O(1). The block is dropped on every exit path — fall-through, return, break/continue. One gap: an in-flight exception unwinds past the frame without dropping the arena, so throw out of an arena block still leaks. Escape is a placement decision, not a compile error — a value that would outlive its block is placed in a region that does outlive it.
  2. Global arena — the default — only for things that genuinely live for the process: precomputed path tables, the scratch buffers themselves, long-lived scanners and indexes.

Hard-won rules

The two gates

  1. Compile-time gate. Build with all four warning flags before any perf claim:

``bash superj build --warn-alloc-in-loop --warn-string-concat-in-loop \ --warn-no-reset-in-loop --warn-scratch-length ``

A clean build means the five known leak patterns are absent. It does not catch everything — the flags are pattern-specific.

  1. Runtime gate. Compare peak RSS across a low and high iteration count (--iter 3 vs --iter 200). If peak RSS grows with iterations, something in the loop still allocates. Flat is the pass condition, not "small". Use the memory-event log to attribute the growth.

Reviewing for it

Reading a diff, the questions that catch nearly everything:

2. Prefer switch over if/else if chains

SuperJ compiles switch to a jump table when the cases are dense integers, and to a binary search when they are sparse. An if/else if chain is always a linear scan.

When to use switch

Rules

Example

// Good — jump table, one line per case (enum constants are allowed)
switch (agg.op) {
    case SUM:   return c.sumDoubleRange(start, end);
    case COUNT: return (double)(end - start);
    case MIN:   return c.minDoubleRange(start, end);
    case MAX:   return c.maxDoubleRange(start, end);
    default:    return 0.0;
}
// Bad — linear scan, harder to read
if (agg.op == AggOp.SUM) {
    return c.sumDoubleRange(start, end);
} else if (agg.op == AggOp.COUNT) {
    return (double)(end - start);
} else if (agg.op == AggOp.MIN) {
    return c.minDoubleRange(start, end);
} else if (agg.op == AggOp.MAX) {
    return c.maxDoubleRange(start, end);
} else {
    return 0.0;
}

3. Prefer early return to reduce nesting

Deep nesting makes code harder to read and harder to get right. A guard clause at the top of a method is clearer and shorter than wrapping the entire body in an if.

Rules

Example

// Good — guard clauses, happy path at indent 1
static double eval(Table t, int symId, PIndex idx) {
    if (idx == null) return 0.0;
    if (symId < 0 || symId >= idx.groupCount()) return 0.0;

    long[] bounds = idx.runBoundaries();
    double sum = 0.0;
    for (int g = 0; g < idx.groupCount(); g++) {
        int start = (int) bounds[g * 2];
        int end   = (int) bounds[g * 2 + 1];
        sum += Kernels.sumDoubleRange(t.col(0).asDoubles(), start, end);
    }
    return sum;
}
// Bad — deep nesting, happy path at indent 3
static double eval(Table t, int symId, PIndex idx) {
    if (idx != null) {
        if (symId >= 0 && symId < idx.groupCount()) {
            long[] bounds = idx.runBoundaries();
            double sum = 0.0;
            for (int g = 0; g < idx.groupCount(); g++) {
                int start = (int) bounds[g * 2];
                int end   = (int) bounds[g * 2 + 1];
                sum += Kernels.sumDoubleRange(t.col(0).asDoubles(), start, end);
            }
            return sum;
        }
    }
    return 0.0;
}

The two forms produce identical code, but the first is easier to read, easier to extend, and easier to review for allocation discipline — the guard clauses separate the "can't happen" cases from the hot path, so the reviewer's attention stays on the loop.

SuperJ — manual · generated from coding-guidelines.md at pack time EN|中文