Skip to content

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 --version

Commands

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.

sh
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.json

swoftc 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.

sh
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.0

swoftc --property-table

Dumps the compiler's property registry as JSON — every owner / name / type / writable row the typechecker uses for dotted paths:

json
[
  { "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.

sh
$ swoftc --check-props
check-props: 0 issues property ownership tables agree with the catalogs

Each 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

CodeMeaning
0success (check passed / compile emitted JSON)
1compile error (parse or typecheck) or unreadable input file
2usage error (bad arguments)

Diagnostics

Errors are written twice, for two audiences:

  • stderr — human-readable, file:line:col: error: message plus 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
json
{ "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:

  1. SWOFTC environment variable — absolute path to the binary. Wins if set, executable, and present.
  2. swoftc on PATH — first executable hit, scanning PATH in order.
  3. 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.
  4. Sidecar fallback — no binary anywhere: a <script>.sw.json file 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.

sh
# pin an explicit binary (systemd unit, Docker, etc.)
SWOFTC=/opt/swoftlang/bin/swoftc java -jar server.jar

Sidecars

The sidecar mechanism decouples compiling from running: production servers don't need OCaml installed.

sh
# 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 sidecars

Rules of thumb:

  • The sidecar is used only when no binary resolves — a present swoftc always recompiles from source, so stale sidecars can't shadow fresh edits during development.
  • Regenerate sidecars whenever the .sw changes; 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:

sh
# recompile + reload scripts/ on any .sw save
java -jar server.jar --watch scripts

On 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.