CLI (swoftc)
swoftc is the OCaml compiler binary. It does all parsing and checking; the Java runtime never sees a .sw file it hasn't blessed.
Usage: swoftc compile <file.sw...> [--addon-path <dir>] [-o <out.json>] | swoftc check <file.sw...> [--addon-path <dir>] | swoftc --property-table | swoftc --check-props | swoftc --versionCommands
swoftc compile <file.sw...> [--addon-path <dir>] [-o <out.json>]
Parses, typechecks, and emits the JSON AST — pretty-printed to stdout, or to a file with -o. Nothing is emitted if any check fails. Multiple entry files compile as one unit; imports are resolved transitively and the whole module graph lands in a single bundle document.
swoftc compile scripts/showcase.sw # JSON to stdout
swoftc compile scripts/showcase.sw -o scripts/showcase.sw.json
swoftc compile scripts/lobby.sw --addon-path addons -o scripts/lobby.sw.jsonswoftc check <file.sw...> [--addon-path <dir>]
Front end only: parse + typecheck (including every imported module), no output on success. This is the fast feedback loop for editors and CI.
swoftc check scripts/showcase.sw && echo ok
swoftc check scripts/lobby.sw --addon-path addons--addon-path <dir>
Where import "name" looks for modules, after the importing script's own directory. Defaults to ./addons next to the scripts. Relative imports (import "./util.sw") ignore it. See the module system for resolution details.
swoftc --version
swoftc 2.0.0swoftc --property-table
Dumps the compiler's property registry as JSON — every owner / name / type / writable row the typechecker uses for dotted paths:
[
{ "owner": "Player", "name": "name", "type": "String", "writable": false },
{ "owner": "Player", "name": "health", "type": "Double", "writable": true },
...
]This table must match the Java runtime's PropertyRegistry; the Java test harness consumes this exact output to keep the two sides honest. It's also handy for building editor tooling.
swoftc --check-props [--json]
Validates the compiler's property/accessor ownership tables against its type catalogs — the internal consistency check behind property reconciliation. It confirms that every dotted property (player.health, item.name, …) the typechecker knows about is owned by exactly the right type with a matching type and writable flag, catching a registry that has drifted from the catalogs before it can mistype a script.
$ swoftc --check-props
check-props: 0 issues — property ownership tables agree with the catalogsEach issue is printed as one kind owner.prop detail line; --json emits a machine report ({ "clean": bool, "issue_count": N, "issues": [...] }) for tooling. Exit code is 0 when clean, 1 when any issue is found — so it drops straight into CI as a compiler self-check alongside swoftc check.
Exit codes
| Code | Meaning |
|---|---|
0 | success (check passed / compile emitted JSON) |
1 | compile error (parse or typecheck) or unreadable input file |
2 | usage error (bad arguments) |
Diagnostics
Errors are written twice, for two audiences:
- stderr — human-readable,
file:line:col: error: messageplus a caret snippet. On typecheck failures, every error is printed, not just the first. - stdout — one machine-readable JSON object (the first error):
$ swoftc check e_parse.sw
e_parse.sw:3:22: error: Unexpected identifier 'ot' at start of statement
send "hello" ot sender
^
$ echo $?
1{ "error": { "message": "Unexpected identifier 'ot' at start of statement", "line": 3, "col": 22 } }Warnings (for example an unknown argument type: warning: unknown type 'Thing', treating as UNKNOWN) go to stderr and do not block compilation — exit code stays 0. The Java runtime forwards them to the server log prefixed [swoftc].
How the runtime finds swoftc
At script load, the Java side (net.swofty.compiler.SwoftcCompiler) resolves the binary in this order:
SWOFTCenvironment variable — absolute path to the binary. Wins if set, executable, and present.swoftconPATH— first executable hit, scanningPATHin order.- Repo-relative build — walks up from the working directory looking for
compiler/_build/default/bin/main.exe(the dune output), so a source checkout just works. - Sidecar fallback — no binary anywhere: a
<script>.sw.jsonfile next to the script is loaded instead.
If all four miss, loading fails with an error listing everything that was checked.
The runtime invokes swoftc compile <absolute-path> and reads stdout; a non-zero exit turns into a load error carrying swoftc's stderr text.
# pin an explicit binary (systemd unit, Docker, etc.)
SWOFTC=/opt/swoftlang/bin/swoftc java -jar server.jarSidecars
The sidecar mechanism decouples compiling from running: production servers don't need OCaml installed.
# on your dev machine / CI — compile every script next to itself
for f in scripts/*.sw; do
swoftc compile "$f" -o "$f.json"
done
# ship scripts/ (both .sw and .sw.json); the server uses the sidecarsRules of thumb:
- The sidecar is used only when no binary resolves — a present
swoftcalways recompiles from source, so stale sidecars can't shadow fresh edits during development. - Regenerate sidecars whenever the
.swchanges; they are plain build artifacts. Committing them is fine (this repo does) — diffs double as an AST changelog. - Each script is compiled once per load and cached; reloads re-run the pipeline.
CI in one line
swoftc check on every .sw file is the whole lint story — types, options, async coloring, property names, GUI slot math, sidebar caps. If CI is green, the scripts load.
Hot reload & --watch
swoftc is the compile half of the dev loop; the runtime is the reload half. Start the server with --watch <dir> and it watches that scripts directory and hot-reloads on every .sw change — no restart, no dropped session:
# recompile + reload scripts/ on any .sw save
java -jar server.jar --watch scriptsOn a save the runtime recompiles the changed file with swoftc (surfacing any error), and on success applies a tick-safe reload. Editor save-bursts are debounced, and a compile error keeps the previous version running — a broken edit never takes the server down. (--debug [port] starts the same watcher alongside the live tracer.)
A reload fully tears down old state
The reason a reload behaves like a clean restart of your program — no ghost handlers, no schedulers firing twice — is that it dismantles every live, program-derived subsystem before loading the new compiled program. A central teardown registry runs each subsystem's cleanup in reverse (last-created-first) order, then the fresh program re-registers everything from scratch. What gets torn down and rebuilt:
- schedulers & tasks — every
every/schedule/repeat, named and anonymous, is cancelled; - event & packet handlers — all listeners are removed, so a renamed or deleted handler leaves nothing behind;
- spawned entities & mobs and script projectiles;
- HUDs & GUIs — open GUI sessions, scoreboards, tablists, and bossbars (their auto-refresh timers cancelled), plus holograms, NPCs, and displays;
- the reactive-instance index and the struct/nominal-type registries;
- the HTTP server and any playing songs.
Persistent state survives
What a reload deliberately keeps is your durable state. The persistence store is not part of the teardown: persistent variables stay in memory across the reload (no flush-and-reload round trip), so counters, homes, guilds, and reactive structs carry straight over. After the new program re-registers, the reactive-instance liveness is re-derived from the surviving persistent roots — the durable actors that were live before the reload are live again, now running the new code. Loaded worlds and session state survive too.
The result is the tight loop persistence was built for: edit a handler, save, and the new logic runs against the same accumulated state, with the old runtime wiring fully gone.