probe
JavaScript to native. Write JS, ship a binary.
What it does
probe compiles JavaScript to statically linked native binaries. You give
it a .js file. It gives you an executable. No V8, no JIT, no
garbage collector at runtime. The binary starts in under 5ms and runs at
the speed of compiled C.
It supports the JavaScript you actually write — closures, classes,
async/await, destructuring, iterators, template
literals. It does not support eval, with, or
runtime code generation. If your code does not need to generate code at
runtime, it can probably compile.
Install
$ npm install -g axial-probe
Requires Node 18+ on the host machine for compilation. The output binary has no dependency on Node or any runtime.
Quick start
Compile and run
// hello.js
const name = process.argv[2] || 'world';
console.log(`hello, ${name}`);
$ probe build hello.js
compile hello.js → hello.ir 12ms
link hello.ir → hello 38ms
output hello (41kb, linux-x64)
$ ./hello probe
hello, probe
$ time ./hello
hello, world
real 0m0.003s
41 kilobytes. 3 milliseconds. No runtime.
Using Node APIs
probe implements a substantial subset of the Node.js standard library. Import them the way you normally would:
import { readFileSync } from 'node:fs';
import { join } from 'node:path';
import { createHash } from 'node:crypto';
const file = join(process.cwd(), 'data.bin');
const buf = readFileSync(file);
const hash = createHash('sha256').update(buf).digest('hex');
console.log(hash);
$ probe build hash.js -o hash
$ ./hash
a7ffc6f8bf1ed76651c14756a061d662f580ff4de43b49fa82d80a4b80f8434a
How it works
probe does not interpret JavaScript. It performs whole-program type inference on your source, lowers it to a typed intermediate representation, and compiles that IR to machine code via LLVM. The process:
- Parse the source into an AST
- Run global type inference — every variable, every property, every return value gets a concrete type or a tagged union
- Lower the typed AST to probe IR, a simple SSA form
- Emit LLVM IR from probe IR
- Link against a minimal libc and the probe runtime stubs (syscalls, allocator, I/O)
Memory is managed by region-based allocation. Each function call gets a region. When the call returns, the region is freed in one operation. No tracing collector, no reference counting. This works because probe statically proves that most allocations do not escape their call frame. For those that do, it falls back to reference counting with cycle detection at compile time.
eval,
Proxy, or Reflect. What you get in return is
a 40kb binary that starts before your terminal prompt finishes drawing.
Node.js compatibility
The following Node built-in modules are fully supported:
node:fs— sync and async file operations, includingwatchv0.6node:path— all methodsnode:crypto— hashing, HMAC, randomBytes (backed by libsodium)node:child_process—execSync,spawnnode:os— platform, arch, cpus, homedir, tmpdirnode:events— EventEmitternode:stream— Readable, Writable, Transform, pipelinenode:buffer— Buffer (all encodings)node:net— TCP client and servernode:http— HTTP/1.1 client and server
Partially supported: node:url, node:querystring,
node:util. Not supported: node:vm,
node:worker_threads, node:cluster.
Async support
async/await compiles to a state machine, not
to coroutines or fibers. Each await point becomes a state
transition. The event loop is a simple epoll/kqueue
loop compiled into the binary.
import { readFile } from 'node:fs/promises';
async function main() {
const files = process.argv.slice(2);
const results = await Promise.all(
files.map(async (f) => {
const data = await readFile(f, 'utf-8');
return { file: f, lines: data.split('\n').length };
})
);
console.table(results);
}
main();
$ probe build count-lines.js
$ ./count-lines src/*.js
┌──────────────┬───────┐
│ file │ lines │
├──────────────┼───────┤
│ src/parse.js │ 312 │
│ src/lower.js │ 847 │
│ src/emit.js │ 203 │
└──────────────┴───────┘
CLI reference
probe build <entry>
$ probe build <entry> [--out <name>] [--target <triple>] [--release] [--strip]
--out,-o— output binary name. Defaults to the entry filename without extension.--target— cross-compilation target triple (e.g.aarch64-linux-musl).--release— enable optimizations. Slower compilation, faster binary.--strip— strip debug symbols from the output.
probe check <entry>
Run type inference and report errors without compiling. Useful in CI.
probe inspect <entry>
Print the inferred types for every binding in the program. Useful when the compiler rejects code and you want to understand why.
$ probe inspect server.js
server.js:4 port : number (literal 3000)
server.js:5 handler : (req: HttpRequest, res: HttpResponse) → void
server.js:12 app : HttpServer
server.js:14 middleware : Array<(req, res, next) → void>