Malte Ubl d4aeeea0f5 Carry typed-array reads as plain numbers where undefined acts like NaN
A typed-array element read answers undefined for an index outside
[0, length), so the default lowering returns a heap-boxed
`number | undefined` and widens every binding, parameter and return the
read reaches. In the tsc-ts scanner that boxed every character read and
turned every character predicate (isLineBreak, isIdentifierPart, ...)
into a function taking a boxed union.

Most consumers cannot tell undefined from NaN: relational, arithmetic
and bitwise operators, truthiness, Math functions, String.fromCharCode,
typed-array indexing, switch over number cases, and equality with a
number that is never undefined. The new analysis (nan-coded-reads.ts)
follows reads through local bindings, parameters of directly called
functions and methods (no overrides, no value uses), and their returns.
A slot is NaN-coded when every value it receives is a NaN-coded read or
slot, or any other number (converted with ToNumber on entry), and either:

- it never holds a genuine NaN (integer elements, integer constants,
  guarded integer arithmetic), so NaN stands for undefined exactly; any
  consumer that takes the value whole (an argument, a stored or returned
  value, a literal element, concatenation) gets the exact
  `number | undefined` rebuilt, and the optional-read analysis treats
  that occurrence as optional, so it lowers as before; or
- every consumer is NaN-insensitive.

Equality of two NaN-coded values that never hold a genuine NaN also
answers true for two NaNs, as undefined === undefined does. Any other
slot keeps the established union lowering; asserted reads (`t[i]!`) are
unchanged.

tsc-ts best-v1 (scriptc main + this commit vs main, Linux sandbox,
Xeon 2.5 GHz, interleaved, medians of 5, diagnostics identical):

| app | parse | total (single-threaded) | wall --checkers 8 |
| --- | --- | --- | --- |
| tRPC | 1.263 -> 0.866 s (-31.4%) | 3.035 -> 2.527 s (-16.7%) | -13.0% |
| Excalidraw | 2.637 -> 1.865 s (-29.3%) | 14.99 -> 14.04 s (-6.3%) | -16.2% |
| Playwright | 3.150 -> 2.202 s (-30.1%) | 12.93 -> 12.14 s (-6.1%) | -12.4% |
| TypeORM | 2.562 -> 2.036 s (-20.5%) | 10.93 -> 10.46 s (-4.3%) | -8.6% |
| self-check | 2.080 -> 1.529 s (-26.5%) | 7.06 -> 6.68 s (-5.4%) | -15.1% |

The tsc-ts IR has 646 instead of 697 scr_union_new_f64 sites (the rest
are cold), 22 instead of 168 typed-read helper calls, and 217 instead of
572 scr_union_get_f64 calls; the scanner's hot functions allocate no
union boxes. Binary size -0.2%.
2026-10-10 14:03:21 -07:00
2026-10-10 10:08:39 -05:00
2026-07-22 18:08:04 -05:00
2026-10-10 10:08:39 -05:00
2026-07-22 18:08:04 -05:00
2026-07-22 18:08:04 -05:00

scriptc

Vercel Labs Experiment npm version: scriptc License: Apache-2.0 npm downloads per month: scriptc

scriptc compiles TypeScript and JavaScript to native executables and WebAssembly. It uses TypeScript's type information to compile supported code to native instructions. Static builds run without Node.js or a JavaScript engine.

For npm dependencies and any-typed code, enable --dynamic to include the embedded quickjs-ng engine. Dependency JavaScript is included at build time.

scriptc is experimental and supports a subset of JavaScript, TypeScript, and Node.js APIs. See the limitations and Node.js compatibility reference before using it for an existing project.

Installation

Install with Node.js 24 or later and npm:

$ npm install -g scriptc

The installed compiler runs natively on supported macOS, Linux, and Windows hosts. Native compilation and execution do not require Node.js. Standalone compiler archives are also available from GitHub Releases.

Executable builds require a platform linker and SDK/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}`);

Compile and run it in one step:

$ scriptc run hello.ts
hello, world

Or write a standalone executable:

$ scriptc build hello.ts -o hello
$ ./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 which operations compile statically, require dynamic execution, or are unsupported. 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
$ ./server
listening on http://localhost:8080

Use npm packages

Pass --dynamic to embed an npm package's JavaScript in the executable. The result does not 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 --dynamic -o cli
$ ./cli
hello from scriptc

Enabling --dynamic does not add support for unsupported statically typed calls. See npm dependencies for package resolution and the boundary between static and dynamic code.

Documentation

Contributing

See CONTRIBUTING.md for source setup, testing, native artifact builds, and docs development.

S
Description
GitHub Trending: vercel-labs/scriptc
Readme Apache-2.0
167 MiB
Languages
TypeScript 67.9%
C 24%
JavaScript 7.7%
C++ 0.3%