sxn can skip parsing a file entirely and run its already-compiled bytecode
instead. This exists for two different reasons that happen to share one
mechanism:
-
Distribution.
sxn compile app.sxproducesapp.sxbc; ship that instead of the source and there is nothing left to parse on the machine that finally runs it.--stripdrops the compiling machine's own file paths from the output, for when the source shouldn't be reconstructible from a stack trace. -
Startup, on a large file.
sxn --compile-cache app.sxcompiles once, caches the result next to the source, and reuses it on every later launch until the source changes. This is the same idea already applied to this runtime's own bootstrap (see the README's benchmark section), turned into something a user's own script can opt into.
Both produce and consume the same file format, so sxn app.sxbc runs either
one's output directly.
Measured on this runtime's own hardware (Apple M4, Release build), median of 9 runs, whole-process wall clock including startup:
| Script | From source | From bytecode | Saved |
|---|---|---|---|
console.log("hi") |
7.8 ms | 6.9 ms | 12% |
| 32k-line generated file (618 KB) | 15.2 ms | 9.6 ms | 37% |
Skipping the parse always saves something, because there is always a parse to
skip — but it scales with how much there is to parse. A one-line script gets
a small, real win from skipping the tokenizer and AST setup entirely. A
large generated file, a bundled app, or a big TypeScript-emitted script gets
a large one. --compile-cache is the flag to reach for once a script is big
enough, or launched often enough, that the difference shows up in something
you're measuring; for a small script run once, it's not going to move
anything you'd notice.
sxn compile app.sx # writes app.sxbc next to it
sxn compile app.sx -o dist/app.sxbc
sxn compile app.sx --strip -o dist/app.sxbc # no local paths in the outputWorks on anything sxn can run as an entry point: .sx, .ts, .js,
.mjs, .cjs, module or CommonJS, decided the same way running it directly
would decide (spec/NODE.md). The output name defaults to the input's name
with its extension replaced by .sxbc.
--strip removes line-number and local-variable debug tables (so a stripped
error reports a bytecode offset, not a source line) and, separately, embeds
the source's bare filename instead of its full path at compile time, so
nothing about the machine or directory the source lived in survives into the
shipped file. Verify what you're about to ship with strings out.sxbc if
that matters to you.
Runs file exactly as sxn file would, except: before running, it checks
for an .sxbc cache next to the source. If the cache is missing or older
than the source (by mtime), it compiles fresh and writes the cache; either
way, execution then runs from bytecode. A script invoked repeatedly parses
once, not on every launch — the common case for a CLI tool people run
often, or a dev server that restarts on every save without its own source
having changed on most of those restarts.
The cache is invisible to the script itself: process.argv and __filename
still show the original source path, not the internal .sxbc file.
Runs a .sxbc file directly, as if it were the source it was compiled from.
require() and import inside it resolve normally, against the directory
the .sxbc file itself sits in.
A .sxbc file is 5 bytes of header — a magic number this runtime checks
before trusting the rest as bytecode, so a corrupt or foreign file fails with
a clear message rather than a confusing one from deep inside the engine —
followed by QuickJS's own serialized bytecode for either a compiled module or
a compiled CommonJS wrapper function. The format is tied to this runtime's
exact build (the same BC_VERSION dependency the lazily-loaded builtins
have, spec/IMPLEMENTATION.md): a .sxbc compiled by one version of sxn
is not guaranteed to load in another, and a version mismatch is reported
rather than misread.
Only compile trusted code. JS_ReadObject with bytecode enabled is, by
QuickJS's own documentation, not a safe format to parse untrusted input —
unlike source text, a crafted bytecode blob can misdirect the interpreter
directly. Compile your own code, or code you already trust as source; don't
treat a .sxbc from an untrusted party as safer to run than the
.js/.mjs/.cjs it might have come from.
A .sxbc's dependencies (whatever it imports or requires) still resolve
and load as ordinary source at run time — compiling one file does not pull
its dependency tree into the same blob. Compiling a whole app ahead of time
currently means compiling each of its own files individually; there is no
bundler step here.
This page is generated from
spec/BYTECODE.md.
Machine-readable copies of the whole set:
llms.txt,
llms-full.txt.