Building a Reusable Library Package
SuperJ compiles whole-program: a dependency's source is compiled into your program (there is no per-package binary ABI). A "library" is simply a project with no entry — it exports classes that other projects import. This tutorial walks through creating a library, consuming it from a separate project via a path dependency, and the git-dependency and workspace alternatives.
1. Scaffold the library
superj new mathlib
cd mathlib
The scaffolded Build.sj has an entry field pointing at mathlib.Main. A library has no entry point, so remove the entry line (or set it to ""):
public class Build {
static final String name = "mathlib";
static final String version = "0.1.0";
static final String spec = "1.0";
static final String[] dependencies = {};
static final String releaseSimd = "sse2";
static final boolean releaseBoundsCheck = true;
static final int releaseOpt = 3;
}
Delete the scaffolded src/mathlib/Main.sj (a library doesn't need a main class) and write the library:
rm src/mathlib/Main.sj
// src/mathlib/MathLib.sj
package mathlib;
public class MathLib {
public static int square(int x) {
return x * x;
}
public static int factorial(int n) {
int result = 1;
for (int i = 2; i <= n; i = i + 1) {
result = result * i;
}
return result;
}
public static boolean isPrime(int n) {
if (n < 2) { return false; }
if (n < 4) { return true; }
if (n % 2 == 0) { return false; }
for (int i = 3; i * i <= n; i = i + 2) {
if (n % i == 0) { return false; }
}
return true;
}
}
2. Type-check the library
A library with no entry has no runnable artifact — superj build type-checks it but doesn't link. Use superj check to verify:
superj check
Checked mathlib (1 source files)
This is the library's CI gate: it compiles the sources and reports type errors, but produces no binary. The library is ready to consume.
3. Scaffold the consumer
In a sibling directory:
cd ..
superj new myapp
cd myapp
4. Add the library as a path dependency
superj add mathlib ../mathlib
This edits Build.sj to add the dependency:
public class Build {
static final String name = "myapp";
static final String version = "0.1.0";
static final String spec = "1.0";
static final String entry = "myapp.Main";
static final String[] dependencies = { "mathlib", "../mathlib" };
static final String releaseSimd = "sse2";
static final boolean releaseBoundsCheck = true;
static final int releaseOpt = 3;
}
You can also edit Build.sj by hand — the dependencies field is a flat array of { "name", "path" } pairs.
5. Use the library
Edit src/myapp/Main.sj:
// src/myapp/Main.sj
package myapp;
import mathlib.MathLib;
public class Main {
public static void main(String[] args) {
System.out.println("square(7) = " + MathLib.square(7));
System.out.println("factorial(5) = " + MathLib.factorial(5));
System.out.println("isPrime(17) = " + MathLib.isPrime(17));
}
}
6. Build and run
superj run
Built /path/to/myapp/target/debug/myapp
square(7) = 49
factorial(5) = 120
isPrime(17) = true
The build tool compiles the library's sources together with your app's sources (whole-program compilation) and links a single binary. For a release build: superj run --release (applies the manifest's releaseSimd, releaseBoundsCheck, releaseOpt fields). For maximum performance: superj build --release --enterprise (whole-program LTO; see The Build System).
7. The lockfile
After the first build, a superj.lock file appears in the consumer project:
# superj.lock — generated by `superj build`; commit this file.
version = 1
[[package]]
name = "mathlib"
path = "../mathlib"
hash = "c989d08c7e7c3989b7687ace7847e3339b82e0b84f9b4907deb95d0d7fe929f9"
Commit superj.lock — it pins the dependency's content hash for reproducible builds. If the library source changes, the hash updates on the next build and superj.lock reflects the new state.
8. Git dependencies
For a library hosted in a git repository (e.g. on a Gitea server or a local bare repo), use --git instead of a path:
superj add mathlib --git git@example.com:me/mathlib --rev v1.0.0
This adds a gitDependencies field to Build.sj:
static final String[] gitDependencies = { "mathlib", "git@example.com:me/mathlib", "v1.0.0" };
The build tool fetches the pinned commit into a content-addressed cache ($SUPERJ_CACHE_DIR, default ~/.superj/cache) on first build; a warm cache builds offline. The lockfile records the resolved commit hash:
[[package]]
name = "mathlib"
source = "git"
url = "git@example.com:me/mathlib"
commit = "a1b2c3d4e5f6..."
hash = "..."
Local git repos for testing: you can use
file://URLs to test the git-dependency flow against a local repo. Initialize the library as a git repo (git init && git add -A && git commit -m "init"), then:superj add mathlib --git file:///path/to/mathlib --rev HEAD.
9. Workspaces (monorepo)
When the library and consumer live in the same repository, use a workspace instead of a path dependency. Create a root Build.sj with workspaceMembers:
myrepo/
Build.sj ← root manifest (workspace)
mathlib/
Build.sj ← library (no entry)
src/mathlib/...
myapp/
Build.sj ← app (entry = myapp.Main)
src/myapp/...
Root Build.sj:
public class Build {
static final String[] workspaceMembers = { "mathlib", "myapp" };
}
The consumer (myapp/Build.sj) still declares the library as a path dependency (dependencies = { "mathlib", "../mathlib" }). From the workspace root:
superj build -p myapp # build just myapp
superj check # type-check all members
superj run -p myapp # build + run myapp
Run any command from inside a member directory and it acts on just that member, exactly like a standalone project. Members depend on each other as ordinary path deps; a member that is a library (no entry) is type-checked rather than linked.
10. Naming across packages
SuperJ namespaces class symbols by fully-qualified name, so package-distinct classes with the same simple name coexist — com.a.Foo and com.b.Foo, or a com.x.Config alongside the SDK's sj.util.Config. Two things still collide, because they share a mangled symbol:
- Default-package classes. A class in the default (no-
package) package has no FQN to distinguish it, so a default-packageConfigcollides withsj.util.Config. Put your classes in a package (assuperj newscaffolds). - The exact same FQN twice — a genuine duplicate declaration.
The compiler reports both cases at superj check naming the offending fully-qualified names, rather than failing later at link.
The sj. package root is reserved for the SDK and the compiler itself: user code declaring package sj; or package sj.anything; is rejected at check/build with a reserved namespace error. This is what makes the rule above airtight — an SDK class keeps its historic symbol name, and no user class can enter that namespace and collide with it. Your packaged classes may freely reference sj.* types (by import or FQN, as always); they just can't be declared inside sj.*.
This holds across all build modes: two same-simple-name classes both compiled from source — com.a.Foo + com.b.Foo, referenced locally or fully qualified — coexist and dispatch correctly under both the community/enterprise (--sdk-path) and VIP (--sdk-source) builds.
How a bare name resolves. A bare Foo means, in order: the current package's Foo; then a single-type-imported Foo (import com.a.Foo;); then the one class named Foo in the program. If two or more packages declare Foo and neither rule above picks one, the reference is a compile error naming every candidate (reference to 'Foo' is ambiguous: com.a.Foo, com.b.Foo — qualify it or add an import) — never a silent pick. A qualified name whose package doesn't exist (new com.c.Foo() with no com.c) is likewise cannot find class 'com.c.Foo' instead of silently binding some other Foo.
A library class named Main still collides with the consumer's Main if both are in the default package — name library classes after what they do (MathLib, StringUtils, HttpClient), and give them a package.
Summary
| Step | Command |
|---|---|
| Scaffold a library | superj new mylib → remove entry from Build.sj |
| Type-check the library | superj check |
| Scaffold a consumer | superj new myapp |
| Add a path dep | superj add mylib ../mylib |
| Add a git dep | superj add mylib --git <url> --rev <rev> |
| Use the library | import mylib.MyClass; |
| Build + run | superj run |
| Release build | superj build --release |
| Max performance | superj build --release --enterprise |
| Workspace build | superj build -p myapp (from workspace root) |
See The Build System for the full manifest field reference, feature flags, and build hooks.