ScrArr stores len and cap as 32-bit fields and no longer stores the state pointer. The dense states share the values' allocation and run backward from its end: the state of slot i is at data + cap * (slot width + 1) - 1 - i. Removing slots from the front (shift, splice(0, n)) advances data and lowers cap, which leaves every remaining state at its address; the head room size, needed only to free or reallocate the block, is kept in the vacated slot before data. On 64-bit targets the header goes from 64 to 48 bytes, and from 36 to 32 bytes on wasm32. Array lengths were already limited to 2^32 - 1. The emitted code loads len and cap as i32 and zero-extends them, and computes state addresses from data and cap. Runtime loops compute the end of the states once per call. Local array reads load data once for both the state and value paths. A local or strict element read whose index is a loop's induction variable reads slots above the hole-free prefix through the runtime: an inline state check there makes LLVM keep -1 - i as a second induction variable, which costs a register in the loop. Bulk operations do not read back states they just wrote: an append to an array whose hole-free prefix reaches its length moves the prefix bound to the new length, a copy from below the source's prefix writes value states without reading the source's, and releasing a string array skips the state checks below the prefix. Copies of up to eight slots and of short state runs use overlapping word moves instead of a call. Emitted element access computes the state address only on the state-checked path. Without these, programs that build many small arrays with split or slice ran 2-4% slower than with the separate state pointer. Integer loops over an array's length compare the counter with the i32 length field in 32 bits; the counter never exceeds 2^32 - 1 there. The array-length stability proof accepts Map and Set reads (get, has, size) and checked union extractions (`x!`), which run no user code, so loops that look up maps keep the dense fast copy, which tests the hole-free prefix once before the loop. Small constructions with the small allocator request 6 slots, so their blocks stay 112 bytes, now with 7 inline slots (6 after a collector header). With 96-byte blocks of 5 slots, programs that free and reuse many small arrays ran 15-20% slower. RUNTIME_ABI_VERSION goes from 22 to 23.
scriptc
scriptc compiles TypeScript and JavaScript to native executables and WebAssembly. It uses the types checked by the TypeScript compiler to generate machine code, and the output runs without Node.js installed. npm packages your program imports compile along with it. Code scriptc can't compile is reported at build time with a diagnostic and, where available, a rewrite hint.
scriptc is experimental and supports a subset of JavaScript, TypeScript, and Node.js APIs. Run scriptc coverage on an existing program to see how much of it compiles, and see the limitations and Node.js compatibility reference for details.
Installation
Install with Node.js 24 or later and npm:
$ npm install -g scriptc
The installed compiler runs natively on macOS, Linux, and Windows. Node.js is needed only to install it and to run WASI modules with scriptc run. Standalone compiler archives are also available from GitHub Releases.
Building executables requires a linker and the platform SDK or sysroot. See the quickstart for prerequisites and platform support for host requirements, cross-compilation, and WebAssembly.
Build a program
Create hello.ts:
const who = process.argv.length > 2 ? process.argv[2] : "world";
console.log(`hello, ${who}`);
scriptc run compiles the file and runs the result:
$ scriptc run hello.ts
hello, world
scriptc build writes an executable:
$ scriptc build hello.ts -o hello >/dev/null
$ ./hello scriptc
hello, scriptc
These examples use POSIX shell syntax. On Windows, build hello.exe and run it with .\hello.exe.
Use scriptc coverage hello.ts to check what compiles before you build. See coverage reports for details.
Use Node APIs
Supported Node APIs compile to the native runtime. For example, server.ts:
import { createServer } from "node:http";
const server = createServer((req, res) => {
res.setHeader("content-type", "application/json");
res.end(JSON.stringify({ path: req.url }));
});
server.listen(8080, () => {
console.log("listening on http://localhost:8080");
});
$ scriptc build server.ts -o server >/dev/null
$ ./server
listening on http://localhost:8080
Use npm packages
npm packages compile into the executable with your code, so it doesn't read node_modules at runtime.
Create cli.ts:
import pc from "picocolors";
console.log(pc.green("hello from scriptc"));
$ npm install picocolors
$ scriptc build cli.ts -o cli >/dev/null
$ ./cli
hello from scriptc
Package support is partial. See npm dependencies for which packages compile and how to check them.
Documentation
- Introduction: what scriptc is and what you can build.
- Quickstart: installation and your first executable.
- Coverage reports: check whether an existing program compiles.
- Platform support: cross-compilation and WebAssembly.
- CLI reference: commands, options, and output formats.
- Native program objects: use compiler output in external builds.
- Native FFI: call C ABI functions from compiled programs.
- WebAssembly modules: build and embed WASI modules.
- Examples: programs and integration examples in this repository.
Contributing
See CONTRIBUTING.md for source setup, testing, native artifact builds, and docs development.