# scriptc documentation

> scriptc compiles TypeScript and JavaScript to native executables and WebAssembly. Build executables, compile npm dependencies, and check Node.js API support.

## When to use scriptc

- Category: Compiler
Common use cases:

- Compile TypeScript or JavaScript to native executables
- Check a program's compilation coverage
- Build WebAssembly modules

## Documentation

---
title: CLI Reference
url: "https://scriptc.dev/docs/cli"
docs_index: /llms.txt
lastUpdated: 2026-11-12
---

> For an index of all documentation, see [/llms.txt](/llms.txt).

The `scriptc` command has seven subcommands: `build`, `run`, `coverage`, `types`, `cache warm`, `merge-profiles`, and `mcp`. `scriptc --help` prints this summary and the options below, and `scriptc --version` prints the installed version.

```console
$ scriptc --help
scriptc — TypeScript/JavaScript to native and WebAssembly executables (experimental)

Usage:
  scriptc build <file.ts|.js> [options]     compile to an executable or source artifact
  scriptc run <file.ts|.js> [options]       compile and run
  scriptc coverage <file.ts|.js>            how much compiles statically, and why not
  scriptc coverage <file.ts|.js> --external-types <specifier=file.d.ts>
                                            type-resolve an embedder-provided module for analysis
  scriptc build --lib --profile <p.json>    library mode: compile the profile's entry
                                            module to a linkable static archive
                                            (<name>.lib.a), or a Wasm reactor
                                            (<name>.wasm) on wasm32-wasi,
                                            exporting the profile symbols; a profile
                                            with a sidecar section also gets the
                                            contract sidecar JSON beside the archive
  scriptc types patch <package> <selector> <type> [--range <r>] [--dry-run]
                                            fix a wrong declaration of an installed
                                            package: shows what the selector
                                            matches and adds the type patch to
                                            package.json ("| undefined" widens)
  scriptc types list                        the project's type patches and what
                                            they match
  scriptc cache warm [runtime|tls…]         verify and load precompiled runtime families
                                            for the current compiler/SDK/target
  scriptc merge-profiles -o <out.profdata> [--sparse] <profile…>
                                            merge the raw profiles that
                                            --profile-generate runs wrote (or
                                            earlier merged profiles) into one
                                            profile for --profile-use;
                                            --sparse leaves out functions
                                            that never ran (a smaller file)
  scriptc mcp                               serve these commands as MCP tools over stdio
```

## scriptc build

Compiles a TypeScript or JavaScript entry file. By default it writes a native executable, or a WebAssembly module when the target is <code>wasm32-wasi</code>. The program is type-checked first by the TypeScript compiler, honoring the nearest `tsconfig.json`. Anything that can't compile is an error with an `SC`-prefixed code, a code frame, and usually a rewrite hint.

```console
$ scriptc build fib.ts -o fib >/dev/null
$ ./fib
832040
```

`build` prints the path of the file it writes. Without `-o`, the file goes in `.scriptc/` next to the input. Use `--emit` to stop at an earlier stage of compilation:

```console
$ scriptc build fib.ts --emit=ir >/dev/null
$ ls .scriptc/
fib.ir.json
$ scriptc build fib.ts --emit=llvm >/dev/null
$ ls .scriptc/
fib.ir.json
fib.ll
$ scriptc build fib.ts --emit=asm >/dev/null
$ ls .scriptc/
fib.ir.json
fib.ll
fib.s
$ scriptc build fib.ts --emit=obj >/dev/null
$ ls .scriptc/
fib.ir.json
fib.ll
fib.o
fib.s
```

Different output kinds accumulate in `.scriptc/`; rebuilding a kind updates its file.

<code>--emit=exe</code> is the default. Assembly and object output use the LLVM helper installed with scriptc and need no external compiler, linker, or SDK. An object is a relocatable program object that leaves scriptc's runtime functions undefined; it is not a standalone library. Linking objects yourself is experimental and requires the exact runtime pack reported by <code>--print=native-link-info</code>; see <a href="/docs/native-objects">Native Program Objects</a>.

### Library mode

`scriptc build --lib --profile <profile.json>` compiles the entry module named in a library profile into a static archive (`<name>.lib.a`) that a host application links, or into a Wasm reactor (`<name>.wasm`) on `wasm32-wasi`. The profile declares the exported functions and their symbols. Library builds take no input file and don't accept `--emit`, `--print`, `--optimization`, `--strip`, `--ffi`, `--npm-static`, or `--define`; the profile controls emission and optimization. See [WebAssembly Modules](/docs/wasm) for a complete profile and [iOS and Android](/docs/platforms#ios-and-android-arm64-library-mode) for mobile targets.

## scriptc run

Builds the program, then runs it with stdio inherited. For <code>wasm32-wasi</code>, it runs the module with Node.js's WASI Preview 1 implementation, makes the current working directory available as <code>/</code>, and maps the host's temporary directory to <code>/tmp</code>.

```console
$ scriptc run fib.ts
832040
```

`run` does not pass extra command-line arguments to the program. To pass arguments, build an executable and run it directly.

## scriptc coverage

Analyzes the program without building it and reports what compiles and what blocks the rest. See [Coverage Reports](/docs/coverage).

## scriptc types

Writes and lists [type patches](/docs/dependencies#fix-a-packages-types), the project's fixes for wrong declarations of npm packages.

<dl>
  <dt>
    <code>scriptc types patch \<package> \<selector> \<type></code>
  </dt>

  <dd>
    Finds the package installed for the working directory, prints every declaration the selector matches in its declaration files, its JSDoc and its

    <code>@types/</code>

    package with the type before and after, and adds the patch to the nearest

    <code>package.json</code>

    . The patch joins an entry whose range covers the installed version, or gets the range

    <code>^\<installed version></code>

    . Nothing is written when the selector matches nothing.

    <code>--range \<range></code>

    chooses the range, and

    <code>--dry-run</code>

    only prints.
  </dd>

  <dt>
    <code>scriptc types list</code>
  </dt>

  <dd>
    Prints each type patch in the nearest

    <code>package.json</code>

    , whether it applies to the installed version, and what it matches.
  </dd>
</dl>

```console
$ scriptc types list
commander@^15.0.0
  HelpContext.helpWidth: | undefined
    node_modules/commander/lib/command.js:2481  number  ->  number | undefined
```

## scriptc cache warm

Verifies and loads the installed precompiled runtime objects and vendored libraries, and prints the cache directory. Pass `runtime`, `tls`, or both to select which to warm. Ordinary builds don't need this step. With `--sanitize`, it builds the instrumented runtime objects using a C toolchain.

## scriptc merge-profiles

Merges the raw profiles that runs of a `--profile-generate` executable wrote, and any earlier merged profiles, into one profile for `--profile-use`. See [Profile-guided optimization](#profile-guided-optimization).

```console
$ scriptc merge-profiles -o app.profdata profiles/*.profraw
```

With `--sparse`, the profile leaves out the functions the runs never reached. The file is smaller, typically by about two thirds, and those functions are optimized as without a profile instead of as cold code.

## scriptc mcp

Serves the other commands as tools to an AI agent over the [Model Context Protocol](https://modelcontextprotocol.io), on stdin and stdout. Add it to an MCP client's configuration:

```json
{ "mcpServers": { "scriptc": { "command": "scriptc", "args": ["mcp"] } } }
```

Each command is one tool: `scriptc_build`, `scriptc_run`, `scriptc_coverage`, `scriptc_build_lib`, `scriptc_types_patch`, `scriptc_types_list`, `scriptc_cache_warm`, and `scriptc_merge_profiles`. A tool takes the command's arguments by name, with hyphens as underscores, such as `{ "file": "fib.ts", "npm_static": ["auto"], "keep_llvm": false }`, and runs that command as a child process of the same compiler. Relative paths resolve against the directory the server started in.

`scriptc_build` and `scriptc_coverage` return the JSON diagnostics envelope of `--print=diagnostics`; the other tools return the command's exit status and output. `scriptc_run` runs the program with its stdin closed. The server runs at most four tool calls at once, keeps the first 8 MiB of each output stream, and stops a call's processes when the call is cancelled, runs longer than ten minutes, or the server receives SIGTERM or SIGINT. `scriptc_run` executes the program it compiles, so an agent connected to the server can run any code the signed-in user can.

## Options

<dl>
  <dt>
    <code>-o, --out \<path></code>
  </dt>

  <dd>
    Primary artifact path. An explicit path is exact. Defaults are

    <code>.scriptc/\<name>.ir.json</code>

    ,

    <code>.ll</code>

    ,

    <code>.s</code>

    ,

    <code>.o</code>

    , or the platform executable name.
  </dd>

  <dt>
    <code>--emit \<ir|llvm|asm|obj|exe></code>
  </dt>

  <dd>
    Select the primary artifact: serialized typed IR (

    <code>ir</code>

    ), textual LLVM IR (

    <code>llvm</code>

    ), assembly (

    <code>asm</code>

    ), a relocatable object (

    <code>obj</code>

    ), or an executable (

    <code>exe</code>

    , the default). Every kind except

    <code>exe</code>

    is produced without an external compiler, linker, or SDK. Assembly and object output is compiled as one module; only executables are divided into partitions that compile in parallel (see

    <a href="/docs/how-it-works#build-parallelism">Build parallelism</a>

    ).
  </dd>

  <dt>
    <code>--print \<native-link-info|diagnostics></code>
  </dt>

  <dd>
    <code>native-link-info</code>

    : build an object (equivalent to

    <code>--emit=obj</code>

    ) and print its machine-readable external link recipe as JSON instead of printing the artifact path. The document names the exact installed precompiled runtime pack and all link inputs, but does not invoke a linker.

    <code>diagnostics</code>

    : for

    <code>build</code>

    and

    <code>coverage</code>

    , print the versioned JSON diagnostics document described in

    <a href="/docs/coverage#machine-readable-diagnostics">Coverage Reports</a>

    instead of the human output.
  </dd>

  <dt>
    <code>--fail-on \<blockers|divergences|dynamic|dynamic-slots|type-claims></code>
  </dt>

  <dd>
    Coverage only. Exit with status 1 when the entry has blockers that would fail a build, with

    <code>divergences</code>

    , also when it has Node divergence warnings, with

    <code>dynamic</code>

    , also when it has the

    <code>SC6101</code>

    warning for untyped code that likely runs often, with

    <code>dynamic-slots</code>

    , also when it has

    <a href="/docs/coverage#dynamic-slots">dynamic slots</a>

    (

    <code>SC6104</code>

    ), or with

    <code>type-claims</code>

    , also when it has

    <a href="/docs/coverage#type-claims">type claims</a>

    . See

    <a href="/docs/coverage#gating-builds-and-ci">Gating builds and CI</a>

    .
  </dd>

  <dt>
    <code>--coverage \<full|reached></code>
  </dt>

  <dd>
    Coverage only.

    <code>full</code>

    is the default and also lowers code the entry never reaches.

    <code>reached</code>

    skips that pass, for a large program whose analysis would otherwise run out of memory: the report has no unreached group, its totals count reached statements only, and it is not comparable with a full report. Not allowed with

    <code>--explain-dynamic</code>

    . See

    <a href="/docs/coverage#large-programs">Large programs</a>

    .
  </dd>

  <dt>
    <code>--ffi \<file></code>
  </dt>

  <dd>
    Bind signature-only TypeScript declarations to native C ABI symbols and link the manifest's archive, object, and system-library inputs. See

    <a href="/docs/ffi">Native FFI</a>

    .
  </dd>

  <dt>
    <code>--backend \<llvm></code>
  </dt>

  <dd>
    The code generator.

    <code>llvm</code>

    is the only backend and the default.
  </dd>

  <dt>
    <code>--strip</code>
  </dt>

  <dd>
    Remove symbol and debug payload when linking an executable, including cross-compiled targets. This flag preserves code optimization.
  </dd>

  <dt>
    <code>--static</code>
  </dt>

  <dd>
    On Linux, link the C library and the other system libraries into the executable. The executable starts without the dynamic loader, which shortens its startup, and doesn't depend on the run host's glibc version. glibc executables link as static PIE, are about 1 MB larger, and need glibc's static libraries on the build machine (

    <code>libc6-dev</code>

    on Debian and Ubuntu,

    <code>glibc-static</code>

    on Fedora); they link only for the build machine's own glibc target. A program whose executable includes host, user, or group name lookups, which

    <code>dns.lookup</code>

    ,

    <code>net</code>

    and

    <code>http</code>

    connections,

    <code>fetch</code>

    , and

    <code>os.userInfo()</code>

    use, is refused with

    <code>SC3002</code>

    : a static glibc executable loads the NSS modules for those lookups from the run host, which glibc supports only for the glibc version it was linked with. Build such a program without

    <code>--static</code>

    , or for an

    <code>\<arch>-linux-musl</code>

    target. musl executables are always linked statically, and accept the flag. Valid only for executable builds.
  </dd>

  <dt>
    <code>--optimization \<release|dev|speed></code>
  </dt>

  <dd>
    Select native optimization for executable, assembly, or object output. The default is

    <code>release</code>

    (

    <code>-O2</code>

    ), which emits string retains and the retains and releases of dynamic values inline.

    <code>dev</code>

    uses

    <code>-O0</code>

    and emits source locations for TypeScript and JavaScript breakpoints and stack frames. Unless

    <code>--strip</code>

    is passed, the functions of a

    <code>dev</code>

    build keep stack frames with the file, line, and column of the running statement, so an uncaught error, an unhandled rejection, and an abort on Linux and macOS print where they happened; see

    <a href="/docs/limitations#process-arguments-and-errors">Process arguments and errors</a>

    . Run with

    <code>SCRIPTC\_CONVERSION\_TRACE=1</code>

    , such an executable also prints every failed runtime type check with its position, including the ones the program catches; see

    <a href="/docs/limitations#runtime-type-checks">Runtime type checks</a>

    . LLVM builds include source variable names and native storage descriptions; see

    <a href="/docs/limitations#native-debugging">Native debugging</a>

    for the supported representations. macOS executable builds also produce an adjacent

    <code>.dSYM</code>

    bundle.

    <code>speed</code>

    is

    <code>-O2</code>

    plus optimizations that trade binary size and build time for run-time speed: it emits the remaining reference counting inline and, on x86-64, starts every runtime function on a cache-line boundary. Expect larger executables and longer cold builds than

    <code>release</code>

    ; how much faster a program runs depends on how much of its time is spent in the runtime. Where a runtime pack has no speed runtime,

    <code>speed</code>

    links the release runtime and keeps inline reference counting. Selecting

    <code>speed</code>

    never changes what

    <code>release</code>

    builds produce, and the two never share cached artifacts. Library profiles accept

    <code>"optimization": "speed"</code>

    for inline reference counting. In executables large enough to be divided into partitions,

    <code>dev</code>

    compiles each partition on its own, and

    <code>release</code>

    and

    <code>speed</code>

    add a step that lets partitions import small functions from each other; see

    <a href="/docs/how-it-works#build-parallelism">Build parallelism</a>

    .
  </dd>

  <dt>
    <code>--windows-subsystem \<console|gui></code>
  </dt>

  <dd>
    Select the subsystem of a Windows executable.

    <code>console</code>

    is the default;

    <code>gui</code>

    prevents Windows from opening a console window for the app. Valid only for executable builds targeting Windows.
  </dd>

  <dt>
    <code>--detached-exit</code>
  </dt>

  <dd>
    On Linux, when the program calls

    <code>process.exit()</code>

    , hand the release of its memory to a helper process so the parent sees the exit sooner. The kernel otherwise frees the whole address space before the exit is reported, which takes tens of milliseconds per gigabyte. Opt in only where the trade-offs are acceptable: for up to about 100 ms after the exit, the helper keeps the executable mapped (overwriting the binary in place fails with "text file busy"), its memory stays charged to the cgroup, and the teardown CPU time no longer appears in the parent's resource usage. Valid only for executable builds; it has no effect on other platforms.
  </dd>

  <dt>
    <code>--stdout-buffering</code>
  </dt>

  <dd>
    Experimental. When stdout is not a terminal, keep

    <code>console</code>

    and

    <code>process.stdout</code>

    bytes in a 64 KiB buffer instead of writing each chunk before the call returns, which makes line-heavy output to a pipe or file much faster. A terminal keeps the default one-write-per-call behavior. The runtime flushes the buffer when it fills, before any stderr write, when the event loop sleeps, before a blocking stdin read, before

    <code>process.kill</code>

    , on

    <code>process.exit</code>

    and on an uncaught error, and at exit, so ordering under

    <code>2>&1</code>

    and prompt-then-read still match Node; a process that dies without running its exit path, such as one killed by

    <code>SIGKILL</code>

    , loses up to 64 KiB of unsettled output. An interactive reader can therefore see a line later than a default build publishes it. Valid only for executable builds; Windows and WASI builds ignore it. See

    <a href="/docs/limitations#console-and-process-streams">Console and process streams</a>

    .
  </dd>

  <dt>
    <code>--large-pages</code>
  </dt>

  <dd>
    On Linux, when the program starts, move its code onto 2 MiB transparent huge pages, as Node's

    <code>--use-largepages=silent</code>

    does. A large program's code then takes a few instruction TLB entries instead of hundreds, and runs faster. The move applies only when the system grants huge pages to memory that asks for them (

    <code>/sys/kernel/mm/transparent\_hugepage/enabled</code>

    is

    <code>always</code>

    or

    <code>madvise</code>

    ), and only to the code between the first and the last 2 MiB boundary of the executable, so small programs keep their code as it is. Opt in only where the trade-offs are acceptable: the move takes about 3 ms per 10 MiB of code at every start, all of the moved code stays resident and is not shared with other processes running the same executable, and profilers that read symbols from the executable file report the moved code as anonymous memory. If a step of the move fails, the program runs with its code where it was. Valid only for executable builds; it has no effect on other platforms.
  </dd>

  <dt>
    <code>--exceptions \<common|rare></code>
  </dt>

  <dd>
    On Linux, say how often the program throws. With the default,

    <code>common</code>

    , recursive functions and functions up to three calls above a

    <code>throw</code>

    report exceptions to their callers with a status check after each call, which costs a few nanoseconds per call and next to nothing when an exception passes. With

    <code>rare</code>

    , those functions also propagate exceptions by unwinding the native stack: their calls need no check, which makes programs such as type checkers, whose work is deep recursion that seldom throws, a few percent faster. An exception that crosses those functions then costs a native unwind, about 2 µs plus 0.1 to 0.7 µs per frame it crosses, so programs that throw in loops or use exceptions for control flow can become several times slower. Functions with a

    <code>throw</code>

    of their own keep the status check either way. It applies to executables and their LLVM and assembly output; object output and other platforms keep the status checks. A profile for

    <code>--profile-use</code>

    should be collected from a build with the same setting.
  </dd>

  <dt>
    <code>--target-cpu \<baseline|x86-64-v2|x86-64-v3|x86-64-v4|native></code>
  </dt>

  <dd>
    On x86-64 targets, compile the program's code for an x86-64 microarchitecture level. The default,

    <code>baseline</code>

    , runs on any x86-64 CPU. A higher level lets LLVM use more instructions, and the executable then runs only on CPUs that support that level.

    <code>native</code>

    uses the highest level the build machine supports. Valid for executable, assembly, and object output on x86-64 targets. See

    <a href="#target-cpu">Target CPU</a>

    .
  </dd>

  <dt>
    <code>--cpu-dispatch</code>
  </dt>

  <dd>
    On x86-64 targets, compile the program's code twice, for the baseline and for

    <code>x86-64-v3</code>

    , and choose the copy when the program starts. The executable runs on any x86-64 CPU and uses the

    <code>x86-64-v3</code>

    code on CPUs that support it. Not used with a

    <code>--target-cpu</code>

    level above

    <code>baseline</code>

    . Valid only for executable builds. See

    <a href="#runtime-cpu-dispatch">Runtime CPU dispatch</a>

    .
  </dd>

  <dt>
    <code>--profile-generate \<pattern></code>
  </dt>

  <dd>
    On Linux, build an instrumented executable for profile-guided optimization. Each run writes a raw profile to the path pattern, where

    <code>%p</code>

    expands to the process ID; the

    <code>LLVM\_PROFILE\_FILE</code>

    environment variable overrides the pattern at run time. A relative pattern resolves from the current working directory. Instrumented programs run slower. With

    <code>--profile-use</code>

    , the build is optimized by that profile and instrumented after inlining, the second phase of a

    <a href="#context-sensitive-profiles">context-sensitive profile</a>

    . Valid only for executable builds with

    <code>--optimization=release</code>

    or

    <code>speed</code>

    .
  </dd>

  <dt>
    <code>--profile-use \<file.profdata></code>
  </dt>

  <dd>
    Optimize the executable with a profile from

    <code>scriptc merge-profiles</code>

    : code is laid out, inlined, and split by how the profiled runs used it. Code the profile does not describe, or describes from an older version of the program, is optimized as without a profile, so a stale profile only loses part of its gain; a

    <a href="#block-counts">block-count file</a>

    beside the profile gives changed functions their counts again. A new or changed profile compiles every partition of the program again.

    <code>scriptc build</code>

    prints how the profile matched the program's functions on stderr; see

    <a href="#profile-guided-optimization">Profile-guided optimization</a>

    . A profile with

    <a href="#context-sensitive-profiles">context-sensitive counts</a>

    also orders each function's blocks by them on Linux. Valid only for executable builds with

    <code>--optimization=release</code>

    or

    <code>speed</code>

    .
  </dd>

  <dt>
    <code>--npm-static \<pkg\[,pkg…]|auto></code>
  </dt>

  <dd>
    Name npm packages to compile even if they don't ship their own type declarations (repeatable). A trailing

    <code>...</code>

    , as in

    <code>fs-extra...</code>

    , also names every package that package depends on.

    <code>auto</code>

    uses the default automatic selection. Packages that can't compile produce diagnostics. A name the program doesn't load selects nothing, so one list can serve several entry modules; a name that is installed nowhere the program could load it from stops the build with SC8006. See

    <a href="/docs/dependencies#explicit-package-selection">npm Dependencies</a>

    .
  </dd>

  <dt>
    <code>--define \<process.env.NAME=value></code>
  </dt>

  <dd>
    Build the executable to run with the environment variable

    <code>NAME</code>

    set to the value, a JSON string, whatever its environment holds (repeatable):

    <code>--define='process.env.NODE\_ENV="production"'</code>

    . JavaScript

    <code>if</code>

    statements the value decides compile only their taken branch, so a package entry that requires its production or development build by

    <code>NODE\_ENV</code>

    , as React's do, compiles only the build it selects. See

    <a href="/docs/limitations#defined-environment-variables">Defined environment variables</a>

    .
  </dd>

  <dt>
    <code>--provenance-sources</code>
  </dt>

  <dd>
    Experimental. Compile npm dependencies from the source at the commit their provenance attestation names. Packages without a usable attestation compile from their installed files.
  </dd>

  <dt>
    <code>--external-types \<specifier=file.d.ts></code>
  </dt>

  <dd>
    Coverage only. Map an exact bare module specifier to a local declaration file supplied by an embedder. Repeat the option for multiple modules. The declaration supplies checker types so application coverage can continue; runtime imports and values remain explicit

    <code>SC1010</code>

    blockers. Relative paths resolve from the current working directory. Accepted files end in

    <code>.d.ts</code>

    ,

    <code>.d.mts</code>

    , or

    <code>.d.cts</code>

    .
  </dd>

  <dt>
    <code>--tier \<auto|static-only></code>
  </dt>

  <dd>
    The tier policy for code that has no static lowering.

    <code>auto</code>

    is the default.

    <code>static-only</code>

    guarantees that the program contains no dynamic-tier demotions: every such construct stays a compile error in TypeScript and a deferred runtime error in JavaScript, which is how both modes behave today. Use it in CI to keep a program fully static. Library mode is always static-only. See

    <a href="/docs/coverage#dynamic-tier">Dynamic tier</a>

    .
  </dd>

  <dt>
    <code>--explain-dynamic\[=\<name>]</code>
  </dt>

  <dd>
    Coverage only. After the report, list the functions that already operate on values typed only at runtime and the bindings, fields, functions, classes and modules that unsupported constructs would move to the dynamic tier, each with the diagnostic behind it. With a name such as

    <code>x</code>

    ,

    <code>Point</code>

    , or

    <code>Point.x</code>

    , show only the matching units. See

    <a href="/docs/coverage#dynamic-tier">Dynamic tier</a>

    .
  </dd>

  <dt>
    <code>--no-dynamic-tier-warning</code>
  </dt>

  <dd>
    Build and coverage. Leave out

    <code>SC6101</code>

    , the warning that names functions that likely run often and use untyped values. Not allowed with

    <code>--fail-on=dynamic</code>

    . See

    <a href="/docs/coverage#untyped-code-that-likely-runs-often">Untyped code that likely runs often</a>

    .
  </dd>

  <dt>
    <code>--no-copy-warning</code>
  </dt>

  <dd>
    Build and coverage. Leave out

    <code>SC6001</code>

    , the warning at conversions that copy a value the program can tell from the original. Not allowed with

    <code>--fail-on=divergences</code>

    . See

    <a href="/docs/coverage#node-divergences">Node divergences</a>

    .
  </dd>

  <dt>
    <code>--no-key-order-warning</code>
  </dt>

  <dd>
    Build and coverage. Leave out

    <code>SC6014</code>

    , the warning for a record type whose objects create their keys in orders scriptc doesn't follow per object. Not allowed with

    <code>--fail-on=divergences</code>

    . See

    <a href="/docs/coverage#node-divergences">Node divergences</a>

    .
  </dd>

  <dt>
    <code>--wasm \<file.wasm></code>
  </dt>

  <dd>
    Coverage only. Report a WebAssembly module the program loads from a path its sources do not spell out. The report already lists modules whose paths the compiler resolves, modules inlined as base64 literals, and

    <code>.wasm</code>

    files shipped by packages that use WebAssembly; a

    <code>scriptc.wasm</code>

    array in

    <code>package.json</code>

    adds files the same way as this flag. Each module is listed with its size, whether it validates, and the WebAssembly proposals it uses. Builds translate the modules they find to native code; a build reads the

    <code>scriptc.wasm</code>

    array but not this flag. Repeatable.
  </dd>

  <dt>
    <code>--sanitize</code>
  </dt>

  <dd>
    Build an executable with AddressSanitizer and a reference-count audit at exit, the same checks scriptc's own test suite uses. Requires a C compiler and is available only for host builds. Not supported with

    <code>--emit=asm</code>

    or

    <code>--emit=obj</code>

    .
  </dd>

  <dt>
    <code>--emit-ir</code>
  </dt>

  <dd>
    Additive IR side artifact. It is deprecated for executable builds for one release cycle; prefer

    <code>--emit=ir</code>

    when IR is the primary output. Library mode retains

    <code>--emit-ir</code>

    because

    <code>--emit</code>

    does not select library artifacts.
  </dd>

  <dt>
    <code>--emit-lowered \<file></code>
  </dt>

  <dd>
    For work on the compiler itself. Also write the

    <a href="/docs/how-it-works#lowered-programs">lowered program</a>

    to the file: the IR the frontend hands the backend, with the sources, the warnings, and a record of the compiler build, the options, and the inputs it was lowered from. The build is otherwise unchanged. Works only with

    <code>build</code>

    .
  </dd>

  <dt>
    <code>--from-lowered \<file></code>
  </dt>

  <dd>
    For work on the compiler itself. Build from a file that

    <code>--emit-lowered</code>

    wrote instead of type-checking and lowering the sources. The build is refused when the compiler build, an option other than the output paths, a

    <code>SCRIPTC\_\*</code>

    variable the frontend reads, or a file or directory the frontend read differs from what the file records. Such a build takes nothing from the build cache and adds nothing to it. Works only with

    <code>build</code>

    .
  </dd>

  <dt>
    <code>--from-lowered-accept \<kind\[,kind…]></code>
  </dt>

  <dd>
    Build from the file although the named kinds differ:

    <code>compiler</code>

    ,

    <code>options</code>

    , or

    <code>inputs</code>

    . Each accepted difference is reported on stderr. A file written with another IR version is never accepted. Repeatable.
  </dd>

  <dt>
    <code>--keep-llvm</code>

    /

    <code>--no-keep-llvm</code>
  </dt>

  <dd>
    Keep (default) or delete the generated

    <code>.ll</code>

    file next to the executable.
  </dd>

  <dt>
    <code>--lib</code>

    ,

    <code>--profile \<profile.json></code>
  </dt>

  <dd>
    Build in

    <a href="#library-mode">library mode</a>

    from the given profile.

    <code>--lib</code>

    requires

    <code>--profile</code>

    and works only with

    <code>build</code>

    .
  </dd>

  <dt>
    <code>-h, --help</code>
  </dt>

  <dd>
    Show usage.
  </dd>

  <dt>
    <code>-v, --version</code>
  </dt>

  <dd>
    Print the installed version.
  </dd>
</dl>

## Environment variables

<dl>
  <dt>
    <code>SCRIPTC\_CACHE\_DIR</code>
  </dt>

  <dd>
    Override the persistent build-cache root. By default it is

    <code>$XDG\_CACHE\_HOME/scriptc/build</code>

    when set,

    <code>$HOME/Library/Caches/scriptc/build</code>

    on macOS,

    <code>%LOCALAPPDATA%\scriptc\cache\build</code>

    on Windows, and

    <code>$HOME/.cache/scriptc/build</code>

    elsewhere. Cached frontend results and LLVM artifacts, including the compiled partitions of large executables, are verified against their source, helper, target, and compilation settings. Native outputs also depend on the selected runtime pack and linker inputs. FFI objects, archives, and system libraries relink against their current dependencies. Sanitizer development builds additionally cache instrumented runtime objects and verify external compiler and header inputs. An existing POSIX override must already be private; otherwise caching is bypassed without changing the directory's permissions.
  </dd>

  <dt>
    <code>SCRIPTC\_NO\_CACHE</code>
  </dt>

  <dd>
    Set to

    <code>1</code>

    to bypass all build-cache reads and writes. An explicitly empty

    <code>SCRIPTC\_CACHE\_DIR</code>

    has the same effect.
  </dd>

  <dt>
    <code>SCRIPTC\_STATE\_DIR</code>
  </dt>

  <dd>
    Override the compiler-level store's root. It holds what a build derives from the installed files alone, such as the selected runtime pack and the Linux link driver's job, so a build whose build cache starts empty still skips that work. The default is

    <code>$XDG\_CACHE\_HOME/scriptc/compiler</code>

    when set,

    <code>$HOME/Library/Caches/scriptc/compiler</code>

    on macOS,

    <code>%LOCALAPPDATA%\scriptc\cache\compiler</code>

    on Windows, and

    <code>$HOME/.cache/scriptc/compiler</code>

    elsewhere. Each entry is verified against the files it was derived from and is derived again when they change.

    <code>SCRIPTC\_NO\_CACHE=1</code>

    and an explicitly empty

    <code>SCRIPTC\_CACHE\_DIR</code>

    disable the store, and an existing POSIX override must already be private.
  </dd>

  <dt>
    <code>SCRIPTC\_CACHE\_MAX\_MB</code>
  </dt>

  <dd>
    Maximum build-cache size in megabytes. The default is

    <code>4096</code>

    ; the default cache is swept periodically, while an explicitly configured cap is checked after every successful cache write. Least-recently-used entries are removed when the cache exceeds the cap.
  </dd>

  <dt>
    <code>SCRIPTC\_CC</code>
  </dt>

  <dd>
    The C compiler used for

    <code>--sanitize</code>

    builds.

    <code>zigcc</code>

    selects zig's

    <code>cc</code>

    subcommand.
  </dd>

  <dt>
    <code>SCRIPTC\_LINKER</code>
  </dt>

  <dd>
    The linker driver for executable builds, in place of the default (clang on macOS and glibc Linux, zig elsewhere). It receives only object files, archives, and system libraries, and locates the platform SDK and C runtime itself.
  </dd>

  <dt>
    <code>SCRIPTC\_TARGET</code>
  </dt>

  <dd>
    Target triple for cross-compilation, such as

    <code>aarch64-linux-gnu.2.36</code>

    ,

    <code>x86\_64-windows-gnu</code>

    , or

    <code>wasm32-wasi</code>

    . WASI builds default to a

    <code>.wasm</code>

    output name. See

    <a href="/docs/platforms">Platform Support</a>

    .
  </dd>

  <dt>
    <code>SCRIPTC\_RUNTIME\_PACK</code>
  </dt>

  <dd>
    A runtime pack directory to use instead of an installed

    <code>@scriptc/runtime-\<target></code>

    package.
  </dd>

  <dt>
    <code>SCRIPTC\_CHECKER</code>
  </dt>

  <dd>
    The TypeScript 7 API server that checks the program:

    <code>tsc-ts</code>

    (the default), which scriptc includes, or

    <code>tsgo</code>

    , from the

    <code>typescript</code>

    package scriptc also includes. Both give the same diagnostics and generated code.
  </dd>

  <dt>
    <code>SCRIPTC\_CHECKER\_SERVER</code>
  </dt>

  <dd>
    On Linux and macOS, builds check with a tsc-ts server that later builds share. The first build in a working directory starts it: a

    <code>tsc-ts</code>

    process, started with

    <code>--api</code>

    and

    <code>--listen</code>

    , that keeps running in the background with no terminal and keeps the files it parsed, so the next build in that directory loads its program faster. Each user, working directory and scriptc installation gets its own server, and a build connects only to the server of its own scriptc and tsc-ts. The socket is in

    <code>$XDG\_RUNTIME\_DIR/scriptc</code>

    , or

    <code>/tmp/scriptc-\<uid></code>

    , a directory only that user can access. A build that finds the server in use by another build, cannot start or reach it, or loses its connection to it, checks with its own tsc-ts process. The server exits after 10 minutes without a build, or after a build when its resident memory is above 2048 MB (

    <code>TSC\_TS\_LISTEN\_MEMORY</code>

    sets another limit in megabytes). To stop it sooner, end that process, for example with

    <code>pkill -f 'tsc-ts --api .\*--listen'</code>

    . Set to

    <code>0</code>

    to check with a tsc-ts process of each build's own. When

    <code>CI</code>

    is set to a value other than

    <code>0</code>

    or

    <code>false</code>

    , builds use their own process unless this variable is

    <code>1</code>

    . Diagnostics and generated code are the same with and without the server. It applies to the default checker, tsc-ts.
  </dd>
</dl>

```console
$ SCRIPTC_TARGET=aarch64-linux-gnu.2.36 scriptc build fib.ts -o fib-linux
$ file fib-linux
fib-linux: ELF 64-bit LSB executable, ARM aarch64, version 1 (SYSV), dynamically linked, interpreter /lib/ld-linux-aarch64.so.1, for GNU/Linux 2.0.0, with debug_info, not stripped
```

## Profile-guided optimization

A profile records how often each branch and call ran in representative runs of the program. With it, the optimizer inlines the hot calls, keeps hot paths together, and moves cold code out of the way. Profiling takes three steps:

```console
$ scriptc build app.ts --optimization=speed --profile-generate profiles/%p.profraw -o app-instrumented
$ ./app-instrumented typical-input.json
$ scriptc merge-profiles -o app.profdata profiles/*.profraw
$ scriptc build app.ts --optimization=speed --profile-use app.profdata
```

Train on workloads like the ones the program will run: code the training never reached is optimized as without a profile. The profile can be committed and reused as the program changes, and in other checkouts and on other machines: it names functions by the entry's file name, not its directory. Functions whose control flow changed since the profile was collected lose their profile, and the rest keep theirs, so regenerate it when much of the program has changed. [Build parallelism](/docs/how-it-works#build-parallelism) describes what a profile adds to build time.

`scriptc build --profile-use` prints one line on stderr with how the profile matched the program's functions:

```
scriptc: profile app.profdata: 9170 functions annotated, 0 mismatched, 28636 without a record
```

- **annotated:** functions optimized with the profile's counts.
- **mismatched:** functions whose control flow changed since the profile was collected. They are optimized without counts.
- **without a record:** functions the profile does not describe: new functions, and functions the profiled runs never reached.
- When the build reuses cached partitions whose counts were not recorded, the line ends with the number of those partitions.

When more than 10% of the functions the profile describes (annotated and mismatched) are mismatched, the build also prints a warning:

```
scriptc: warning: 3739 of the 8947 functions app.profdata describes (41.8%) changed since it was collected and are optimized without their counts; collect the profile again
```

The build still succeeds. Collecting the profile again with the current program and scriptc version restores the counts.

### Context-sensitive profiles

A profile from the steps above counts each function's branches before the optimizer inlines calls. A second phase counts them again after inlining, separately for every place a function was inlined into:

```console
$ scriptc build app.ts --optimization=speed --profile-use app.profdata --profile-generate profiles-cs/%p.profraw -o app-instrumented-cs
$ ./app-instrumented-cs typical-input.json
$ scriptc merge-profiles -o app-cs.profdata app.profdata profiles-cs/*.profraw
$ scriptc build app.ts --optimization=speed --profile-use app-cs.profdata
```

- **First phase:** `--profile-generate` alone. It instruments the program before inlining and gives the profile of the steps above.
- **Second phase:** `--profile-use` with the first profile and `--profile-generate` together. The build is optimized by the first profile and instrumented after inlining, and `scriptc merge-profiles` adds its counts to the first profile.
- **Optimized build:** `--profile-use` with the merged profile. On Linux, it also orders each function's blocks by the counts and moves blocks the runs never reached into separate sections, so the code that runs sits together.

A profile that already holds context-sensitive counts cannot start a second phase. Merge the second phase's raw profiles into the first-phase profile instead. Both phases run the same workloads.

### Block counts

A profile matches a function only while the function's control flow hashes as it did when the profile was collected, so a change to the program or to scriptc leaves the changed functions without counts. A block-count file beside the profile, with its name and a `.blocks` extension in place of `.profdata` (`app.blocks` beside `app.profdata`), describes each function by the labels of its blocks instead. `--profile-use` reads it when it exists and annotates the functions whose control flow changed (the mismatched ones): blocks that keep their place in the function take their recorded counts, and the counts of the other blocks follow from the counts of the blocks and edges around them. The file also names the functions that never ran while the profile was collected, which a profile merged with `--sparse` has no record of. Those functions are compiled as cold code (an entry count of 0, optimized for size and placed with the other unlikely code), which shortens the build. A function the file does not name, for example one added since the profile was collected, keeps no counts and is optimized as without a profile. The report then adds a line, and the warning counts only the functions that remain without counts:

```
scriptc: profile app-cs.profdata: 0 functions annotated, 1 mismatched, 0 without a record
scriptc: profile app-cs.profdata: 1 function without profile counts annotated from app-cs.blocks
```

A `--profile-use` build writes the block counts of the functions its profile annotates, and the names of the functions it has no record of, when the `SCRIPTC_RECORD_PROFILE_BLOCKS` environment variable names a path; such a build reuses no cached artifacts. For a context-sensitive profile, record them in the second phase, which the first-phase profile annotates:

```console
$ SCRIPTC_RECORD_PROFILE_BLOCKS=app-cs.blocks scriptc build app.ts --optimization=speed --profile-use app.profdata --profile-generate profiles-cs/%p.profraw -o app-instrumented-cs
```

A new or changed block-count file compiles every partition of the program again. Delete it when the profile it was recorded with is replaced.

The file holds the counts before inlining. With a context-sensitive profile, a function whose control flow changed, and a function it is inlined into, also lose their context-sensitive counts and keep the counts from before inlining.

## Target CPU

By default, x86-64 executables run on any x86-64 CPU, so the compiler uses only the baseline instruction set (SSE2). `--target-cpu` compiles the program's code for a higher x86-64 microarchitecture level. Each level includes the ones below it:

<table>
  <thead>
    <tr>
      <th>
        Level
      </th>

      <th>
        Adds
      </th>
    </tr>
  </thead>

  <tbody>
    <tr>
      <td>
        <code>x86-64-v2</code>
      </td>

      <td>
        SSE3, SSSE3, SSE4.1, SSE4.2, POPCNT
      </td>
    </tr>

    <tr>
      <td>
        <code>x86-64-v3</code>
      </td>

      <td>
        AVX, AVX2, BMI1, BMI2, F16C, FMA, LZCNT, MOVBE
      </td>
    </tr>

    <tr>
      <td>
        <code>x86-64-v4</code>
      </td>

      <td>
        AVX-512 (F, BW, CD, DQ, VL)
      </td>
    </tr>
  </tbody>
</table>

```console
$ scriptc build app.ts --target-cpu=x86-64-v3 -o app
```

Loops over typed arrays and numbers gain the most, because LLVM vectorizes them with wider registers. Some loops run slower at a higher level, so measure the program on the machines it will run on.

With `x86-64-v3`, the program's functions are also compiled for the baseline. A function runs its baseline code when its `x86-64-v3` code has vectors of at most 128 bits, none wider, and no rounding or bit counting in a loop, as with [runtime CPU dispatch](#runtime-cpu-dispatch). Its calls still go to the `x86-64-v3` code of the functions it calls. A function keeps its `x86-64-v3` code when its baseline code calls a function that is compiled only for `x86-64-v3`, such as one of the string helpers scriptc adds to the program, because baseline code cannot inline it. The baseline copies make the executable larger and the first build slower.

An executable built for a level runs only on CPUs that support that level. On an older CPU, it crashes with an illegal-instruction signal (`SIGILL`) when it reaches an instruction the CPU lacks. A named level is portable to every machine of that level. `native` selects the highest level the build machine supports, so use it only for executables that run on the machine that built them or on identical machines.

The level applies to the program's own code. The runtime pack keeps its baseline code. Builds for different levels never share cached artifacts, and `baseline` builds are the same as builds without the option.

### Runtime CPU dispatch

`--cpu-dispatch` keeps the executable portable and still uses the higher level where the CPU has it. The program's functions are compiled for the baseline and for `x86-64-v3`. At startup, the executable checks the CPU and the operating system's register support, and then runs the `x86-64-v3` code on CPUs that support every feature of that level. Calls between the program's functions stay within the chosen copy.

```console
$ scriptc build app.ts --optimization=speed --cpu-dispatch -o app
```

The second copy makes the executable larger and the first build of the program slower, since LLVM compiles the program's code twice. Unchanged rebuilds are not affected. Both copies compute the same results: neither contracts floating-point operations into fused multiply-adds.

A function's `x86-64-v3` copy runs its baseline code when its optimized code has vectors of at most 128 bits, none wider, and no rounding or bit counting in a loop. In such code, `x86-64-v3` only lets LLVM's vectorizers move scalar work onto vector instructions, which can run slower. Its calls still go to the `x86-64-v3` copies of the functions it calls.

The `SCRIPTC_CPU_LEVEL` environment variable caps the level the executable uses, for example to test the baseline code on a newer machine. It takes a level from `0` to `4`, `baseline`, or a level name such as `x86-64-v3`; any other value selects the baseline code.

```console
$ SCRIPTC_CPU_LEVEL=baseline ./app
```

## LLVM backend

scriptc generates LLVM IR and compiles it to native code. Executable builds link the result with the matching precompiled runtime pack. Use <code>--emit=llvm</code> to inspect the generated code.

For source breakpoints in Xcode or LLDB, build with <code>--optimization=dev</code>. In an Xcode custom build rule, list the executable and its <code>.dSYM</code> bundle as outputs. Keep the bundle next to the executable and retain the original source files at their build paths. LLVM preserves source locations across imported modules.

## Tool requirements

<table>
  <thead>
    <tr>
      <th>
        Operation
      </th>

      <th>
        Needs
      </th>
    </tr>
  </thead>

  <tbody>
    <tr>
      <td>
        Installing with npm
      </td>

      <td>
        Node.js 24 or later
      </td>
    </tr>

    <tr>
      <td>
        <code>coverage</code>

        ,

        <code>--emit=ir|llvm|asm|obj</code>
      </td>

      <td>
        Only the installed compiler
      </td>
    </tr>

    <tr>
      <td>
        <code>--emit=exe</code>

        (the default)
      </td>

      <td>
        A linker and the platform SDK or sysroot: clang and the Xcode Command Line Tools on macOS, clang on glibc Linux, zig on Windows, musl Linux, and for cross-compiled targets
      </td>
    </tr>

    <tr>
      <td>
        Cross-compiling
      </td>

      <td>
        The target's

        <code>@scriptc/runtime-\<target></code>

        pack and zig (see

        <a href="/docs/platforms">Platform Support</a>

        )
      </td>
    </tr>

    <tr>
      <td>
        <code>scriptc run</code>

        on

        <code>wasm32-wasi</code>
      </td>

      <td>
        Node.js 24 or later on

        <code>PATH</code>
      </td>
    </tr>

    <tr>
      <td>
        <code>--static</code>

        on glibc Linux
      </td>

      <td>
        glibc's static libraries (

        <code>libc6-dev</code>

        on Debian and Ubuntu,

        <code>glibc-static</code>

        on Fedora)
      </td>
    </tr>

    <tr>
      <td>
        <code>--sanitize</code>
      </td>

      <td>
        A C compiler and LLVM toolchain
      </td>
    </tr>
  </tbody>
</table>

---

For a semantic overview of all documentation, see [/sitemap.md](/sitemap.md)

For an index of all available documentation, see [/llms.txt](/llms.txt)

For agent-facing discovery, including API and MCP surfaces, see [/agents.md](/agents.md)

---
title: Contract Types
url: "https://scriptc.dev/docs/contracts"
docs_index: /llms.txt
lastUpdated: 2026-11-12
---

> For an index of all documentation, see [/llms.txt](/llms.txt).

Contract types let you state a promise about your code, such as "this function never throws" or "this value is always a 32-bit integer", that scriptc checks at build time and then uses to generate faster code or use less memory. Reach for them in performance-sensitive code once it already compiles.

Contracts are type aliases imported from `@scriptc/types` with `import type`. TypeScript erases them, so Node.js never loads the package and runs the program as if the annotations weren't there. scriptc either proves each contract or fails the build with a diagnostic that explains why it doesn't hold. Contracts don't change what a program computes; they only tell scriptc what it can rely on.

<table>
  <thead>
    <tr>
      <th>
        Contract
      </th>

      <th>
        Promise
      </th>

      <th>
        What scriptc does with it
      </th>
    </tr>
  </thead>

  <tbody>
    <tr>
      <td>
        <code>NoThrow\<T></code>
      </td>

      <td>
        The function never throws.
      </td>

      <td>
        Callers need no exception check after the call.
      </td>
    </tr>

    <tr>
      <td>
        <code>Int32</code>
      </td>

      <td>
        Every value stored is a signed 32-bit integer by construction.
      </td>

      <td>
        The variable, field, parameter or result is stored as a 32-bit integer.
      </td>
    </tr>

    <tr>
      <td>
        <code>Int32\[]</code>
      </td>

      <td>
        Every element stored into the array, through any reference, is a signed 32-bit integer by construction.
      </td>

      <td>
        Integer arithmetic over its elements runs in 32-bit integer operations.
      </td>
    </tr>

    <tr>
      <td>
        <code>Immutable\<T></code>
      </td>

      <td>
        No code writes to the value, through any reference.
      </td>

      <td>
        A module constant built from literals becomes immortal data: never reference counted, never traced by the collector.
      </td>
    </tr>

    <tr>
      <td>
        <code>Acyclic</code>
      </td>

      <td>
        No chain of references leads from an object of the class or type back to itself.
      </td>

      <td>
        Its objects carry no cycle-collector header and are freed by reference counting alone.
      </td>
    </tr>

    <tr>
      <td>
        <code>ProgramLifetime</code>
      </td>

      <td>
        Instances of the class are kept until the program exits.
      </td>

      <td>
        Instances are immortal from construction: no reference counting, no cycle collection, never freed.
      </td>
    </tr>
  </tbody>
</table>

Add `@scriptc/types` to your project's development dependencies. It contains only type declarations.

## Example

```ts title="main.ts"
import type { Immutable, Int32, NoThrow } from "@scriptc/types";

const KEYWORDS: Immutable<Map<string, number>> = new Map([
  ["const", 1],
  ["let", 2],
  ["function", 3],
]);

class Token {
  pos: Int32;
  end: Int32;
  kind: number;
  constructor(pos: Int32, end: Int32, kind: number) {
    this.pos = pos;
    this.end = end;
    this.kind = kind;
  }
}

function isLetter(ch: number): NoThrow<boolean> {
  return (ch >= 97 && ch <= 122) || (ch >= 65 && ch <= 90);
}

function scan(text: string): Token[] {
  const tokens: Token[] = [];
  let pos: Int32 = 0;
  while (pos < text.length) {
    if (!isLetter(text.charCodeAt(pos))) {
      pos = (pos + 1) | 0;
      continue;
    }
    let end: Int32 = pos;
    while (end < text.length && isLetter(text.charCodeAt(end))) end = (end + 1) | 0;
    tokens.push(new Token(pos, end, KEYWORDS.get(text.slice(pos, end)) ?? 0));
    pos = end;
  }
  return tokens;
}

for (const t of scan("const answer = 42; function f() {}")) console.log(t.pos, t.end, t.kind);
```

Node.js 24 runs the program directly:

```console
$ node main.ts
0 5 1
6 12 0
19 27 3
28 29 0
```

The scriptc executable prints the same output. `KEYWORDS` is immortal, `Token.pos`, `Token.end` and the scanner's positions are 32-bit integers, and the calls to `isLetter` carry no exception checks:

```console
$ scriptc build main.ts -o scan >/dev/null
$ ./scan
0 5 1
6 12 0
19 27 3
28 29 0
```

## Where contracts apply

scriptc recognizes a contract where it is written as the annotation of a declaration, and only when the name is imported from `@scriptc/types` (a named import, possibly renamed, or a namespace import such as `sc.Int32`). TypeScript does not keep alias names in inferred types, so a contract never spreads by inference: write it where the value is declared.

<dl>
  <dt>
    <code>NoThrow\<T></code>
  </dt>

  <dd>
    The written return type of a function, method or arrow function.

    <code>NoThrow\<Int32></code>

    and

    <code>NoThrow\<Immutable\<T>></code>

    combine the result contracts.
  </dd>

  <dt>
    <code>Int32</code>
  </dt>

  <dd>
    Parameters, variables, class instance fields and function results whose type is a plain

    <code>number</code>

    .
  </dd>

  <dt>
    <code>Int32\[]</code>
  </dt>

  <dd>
    Parameters, variables, class instance fields and function results whose type is a plain

    <code>number\[]</code>

    , written

    <code>Int32\[]</code>

    ,

    <code>readonly Int32\[]</code>

    ,

    <code>Array\<Int32></code>

    or

    <code>ReadonlyArray\<Int32></code>

    .
  </dd>

  <dt>
    <code>Immutable\<T></code>
  </dt>

  <dd>
    Parameters, variables, class instance fields and function results.
  </dd>

  <dt>
    <code>Acyclic</code>
  </dt>

  <dd>
    Classes (

    <code>class C implements Acyclic</code>

    ), interfaces (

    <code>interface I extends Acyclic</code>

    ) and object type aliases (

    <code>type T = Acyclic\<...></code>

    over an object type).
  </dd>

  <dt>
    <code>ProgramLifetime</code>
  </dt>

  <dd>
    The

    <code>implements</code>

    clause of a class declaration:

    <code>class Node implements ProgramLifetime</code>

    .
  </dd>
</dl>

A contract anywhere else (a type alias, an interface member, a nested array or other type argument such as `Int32[][]` or `Map<string, Int32[]>`, a function type) would promise something scriptc does not check, so the build reports it with SC7006.

## NoThrow

A `NoThrow` function must not contain a path to a throwing operation: a `throw`, a call to a function that may throw, a call through a function value that may hold such a function, an operation that throws in Node.js (such as reading a property of `undefined`), or recursion, whose stack guard can throw `RangeError`. scriptc already computes this for every function to omit exception checks; the contract turns the result into a guarantee and reports the first throwing path when it does not hold.

A `try` statement discharges what its handlers take. A throwing call inside a `try` with a `catch`, or inside a `try` whose `finally` always ends with `return`, `break` or `continue` (which discards the pending exception), cannot leave the function:

```ts title="digits.ts"
import type { NoThrow } from "@scriptc/types";

function parseDigit(ch: number): number {
  if (ch < 48 || ch > 57) throw new RangeError("not a digit");
  return ch - 48;
}

function digitOr(text: string, i: number, fallback: number): NoThrow<number> {
  try {
    return parseDigit(text.charCodeAt(i));
  } catch {
    return fallback;
  }
}

console.log(digitOr("7", 0, -1), digitOr("x", 0, -1));
```

```console
$ node digits.ts
7 -1
$ scriptc build digits.ts -o digits >/dev/null
$ ./digits
7 -1
```

The `catch` and `finally` bodies are checked like any other code: a rethrow, a throwing call in the `catch`, or a `try`/`finally` without `catch` whose `finally` can complete normally (and so re-raises the exception) still fails the contract. A recursive function cannot be `NoThrow`, because its stack guard throws on entry, outside any handler. In programs that use worker threads, `try` does not discharge the contract, since a termination request unwinds through `catch`. `async` functions and generators cannot be `NoThrow`.

## Int32

Each value stored into an `Int32` slot (initializers, assignments, compound assignments, `++` and `--`, arguments and returns) must be an int32 in Node.js semantics, so storing it as a 32-bit integer cannot change it:

- an integer literal in range;
- the result of `|`, `&`, `^`, `~`, `<<`, `>>` or `Math.imul`;
- another `Int32` value;
- arithmetic whose range scriptc can bound, such as `i + 1` under a loop condition `i < text.length`.

`x >>> 0` is a uint32 and is not accepted. Anything else needs an explicit `| 0`, which makes Node.js compute the same 32-bit value. Reads keep JavaScript semantics: `a + b` of two `Int32` values is an ordinary number that may exceed the int32 range.

scriptc refuses an `Int32` slot whose writes it cannot all see (SC7003): a parameter of a function used as a value, or a field of a class whose instances are stored in `any` or `unknown` values.

## Int32 arrays

`Int32[]` promises that every element stored into the array is an int32, so an element read that finds a value reads an int32. Integer kernels over such arrays, such as sums, hashes and prefix sums written with `| 0` or `Math.imul`, then compute in 32-bit integer operations instead of converting every element and intermediate result between doubles and integers:

```ts title="checksum.ts"
import type { Int32 } from "@scriptc/types";

function checksum(data: Int32[]): number {
  let h = 17;
  for (let i = 0; i < data.length; i++) h = (Math.imul(h, 31) + data[i]) | 0;
  return h;
}

const samples: Int32[] = [];
for (let i = 0; i < 1000; i++) samples.push((i * 7919) & 0xffff);
console.log(checksum(samples));
samples[1200] = -1;
console.log(samples.length, samples[1100], checksum(samples));
```

```console
$ node checksum.ts
-220195363
1201 undefined -1
$ scriptc build checksum.ts -o checksum >/dev/null
$ ./checksum
-220195363
1201 undefined -1
```

The contract constrains the values stored, not the array's shape. Holes, present `undefined` and indices past the end read as `undefined`, and as `NaN` in arithmetic, exactly as in Node.js; writing past the end or changing `length` behaves as usual. The elements stay ordinary numbers, so `a[i] + a[j]` can exceed the int32 range.

The promise belongs to the array, not to one variable, so scriptc checks every store into any array that can reach an `Int32[]` slot, through any reference and wherever it happens, including before the array reaches the slot: `a[i] = v`, `push`, `unshift`, `fill`, `with`, `splice` items, array literal elements, and elements copied in by spreads and `concat`. Each stored value must be an int32 in the same sense as for `Int32`, and an element read from another `Int32[]` array counts as one. Arrays flow through parameters, results, variables, class fields, object properties, `Map` values, nested arrays and captured bindings; reads through any of them are faster when only checked arrays can reach them, so the annotation is needed only where the program declares the array:

```ts title="scale.ts"
import type { Int32 } from "@scriptc/types";

function scale(values: number[], factor: number): void {
  for (let i = 0; i < values.length; i++) values[i] = values[i] * factor;
}

const counts: Int32[] = [3, 5, 8];
scale(counts, 1.5);
console.log(counts);
```

```console
$ scriptc build scale.ts
/tmp/app/scale.ts:4:55 - error SC7002: the element stored into 'values' is not provably an int32: 'values[i] * factor' (the array can reach 'counts', declared Int32[])

  3 | function scale(values: number[], factor: number): void {
  4 |   for (let i = 0; i < values.length; i++) values[i] = values[i] * factor;
    |                                                       ^~~~~~~~~~~~~~~~~~
  5 | }

  hint: make the int32 conversion explicit, for example '(values[i] * factor) | 0', so Node stores the same value

1 error.
```

A copy such as `slice()` or a spread is checked as if it were the same array as its source. The results of a callback are followed only when scriptc copies it into the loop that runs the array method: a callback written at a `map` or `filter` call whose captured variables are not reassigned, such as `xs.map((x) => (x * 2) | 0)`. Other callback results are not followed, so for example an array built by `Array.from` with a mapping function cannot be `Int32[]`; build it with a loop that stores `value | 0`. scriptc refuses an `Int32[]` slot whose array can come from code it cannot see, such as `JSON.parse`, dynamic values or the callers of a function used as a value, and an array that can reach one and escapes to such code, such as a call through a function value (SC7003).

## Immutable

`Immutable<T>` is a deep read-only view, so TypeScript already rejects direct writes such as `KEYWORDS.set(...)`. scriptc also checks the writes TypeScript cannot see:

- a method that writes `this`, called on the value;
- a function that writes a parameter, given the value;
- a cast such as `value as number[]`, an `any` alias, `Object.assign`, or index-signature writes;
- a value stored into an `Immutable` slot that another reference can still change.

A value stored into an `Immutable` slot must be fresh: a literal, a `new` expression whose constructor keeps `this` to itself, or a function that returns what it builds. Values that are stored in `any` or `unknown`, or passed to functions scriptc cannot see, are refused.

## Acyclic

scriptc frees objects by reference counting and finds unreachable reference cycles with a cycle collector. Objects of a type that can never lie on a cycle need neither the collector's object header nor its bookkeeping. scriptc already proves this for many types; `Acyclic` turns the proof into a guarantee and shows the cycle when it does not hold.

A reference can close a cycle when it is stored after its object was constructed (a later write can point an older object at a newer one), when it sits in an array, `Map` or `Set` (whose contents change), or when a field holds a function, a promise or an `any` or `unknown` value (which can reference anything). Scalar fields never matter. A subclass shares its base class's object layout, so a class hierarchy is acyclic only as a whole.

```ts title="tree.ts"
import type { Acyclic } from "@scriptc/types";

class Tree implements Acyclic {
  readonly left: Tree | null;
  readonly right: Tree | null;
  constructor(left: Tree | null, right: Tree | null) {
    this.left = left;
    this.right = right;
  }
  count(): number {
    return 1 + (this.left?.count() ?? 0) + (this.right?.count() ?? 0);
  }
}

function build(depth: number): Tree {
  return depth === 0 ? new Tree(null, null) : new Tree(build(depth - 1), build(depth - 1));
}

console.log(build(20).count());
```

```console
$ node tree.ts
2097151
$ scriptc build tree.ts -o tree >/dev/null
$ ./tree
2097151
```

When a cycle can form, the diagnostic shows its path:

```ts title="graph.ts"
import type { Acyclic } from "@scriptc/types";

class Graph implements Acyclic {
  readonly nodes: GraphNode[] = [];
}

class GraphNode {
  readonly graph: Graph;
  constructor(graph: Graph) {
    this.graph = graph;
  }
}

const graph = new Graph();
graph.nodes.push(new GraphNode(graph));
console.log(graph.nodes.length);
```

```console
$ scriptc build graph.ts
/tmp/app/graph.ts:3:24 - error SC7007: 'Graph' is declared Acyclic but can lie on a reference cycle: Graph.nodes → GraphNode.graph → Graph (Graph.nodes is an array, whose contents can change)

  2 | 
  3 | class Graph implements Acyclic {
    |                        ^~~~~~~
  4 |   readonly nodes: GraphNode[] = [];

  hint: an array, Map or Set can come to hold an object that refers back; make the element type unable to reach this type, or remove Acyclic

1 error.
```

## ProgramLifetime

`ProgramLifetime` marks a class whose instances a program keeps until it exits, such as the nodes of a syntax tree, the symbols of a symbol table or interned types. scriptc then never reference counts, collects or frees instances of the class and of its subclasses: they are never freed, every retain and release on them compiles to nothing, and the cycle collector neither walks them nor needs to trace the objects that point to them. Node.js behavior is unchanged; only memory use differs, because an instance the program drops is never reclaimed. On Linux, the instances a worker thread creates are returned with the rest of its memory after it exits, unless the worker shared objects with other threads (see [cost](/docs/threads#cost)).

```ts title="tree.ts"
import type { ProgramLifetime } from "@scriptc/types";

abstract class SyntaxNode implements ProgramLifetime {
  parent: SyntaxNode | undefined = undefined;
}

class Identifier extends SyntaxNode {
  readonly text: string;
  constructor(text: string) {
    super();
    this.text = text;
  }
}
```

On Linux and macOS, except in builds with sanitizers, the instances also take less memory. An executable keeps them in one address range that it reserves at startup, below 32 GiB on Linux and from 1 TiB on macOS (only the pages in use take memory), and a field typed `C`, `C | undefined` or `C | null`, where `C` is a program-lifetime class, stores a reference to the instance in 4 bytes instead of an 8-byte address, as does each element of an array of such a type and each value of a `Map` with `string` keys and such a value type. In the example, `parent` takes 4 bytes.

scriptc checks that the promise cannot change what the program computes (SC7008):

- the class is the root of its hierarchy, so every value of any class type in the hierarchy is program-lifetime; built-in bases such as `Error`, `EventEmitter` and streams are refused because they hold runtime state that is released with the object;
- the build is an executable: a library's host decides when the objects it receives are released.

A class can be both `ProgramLifetime` and `Acyclic`. `Acyclic` is still checked against the references the program writes, so a chain of references that passes through a program-lifetime instance and leads back is still a cycle (SC7007).

Instances can be published to other threads with `@scriptc/threads` like other class instances: `publish()` makes them immutable and shares them by reference, writes to a published instance throw the same `TypeError` as under Node.js, and instances that were not published stay writable and are cloned by `postMessage` and `workerData`.

Static code has no weak-reference cleanup: a `WeakRef` keeps its target alive and `deref()` always answers it, a `FinalizationRegistry` never runs its cleanup callbacks, and a `WeakMap` or `WeakSet` entry whose key is never freed is never observable as removed, so nothing else can tell the difference.

### Lifetime regions

A long-running program, such as a compiler in watch mode, can free program-lifetime instances one generation at a time with `LifetimeRegion` from [`@scriptc/experimental`](/docs/experimental). Instances created while `region.run(fn)` runs belong to the region. After `region.release()`, scriptc frees them together once nothing outside the region refers to any of them, and removes the `WeakMap` and `WeakSet` entries they are keys of. Instances created outside every region are never freed, as described above.

### Inline array fields

An array field of a program-lifetime class whose elements are program-lifetime instances can keep its elements in the instance itself, after the other fields, with `InlineArray` from [`@scriptc/experimental`](/docs/experimental). The constructor writes the field once from one of its parameters; reads that index the field, read its `length` or iterate it load the elements from the instance, and other reads get an array of them. The array passed to the constructor is one that nothing else keeps, and the arrays fields hold are never written. scriptc reports a field that does not keep these rules (SC7009).

## Diagnostics

Each failed contract names the declaration and the reason:

```ts title="reject.ts"
import type { Immutable, Int32, NoThrow } from "@scriptc/types";

const LIMITS: Immutable<number[]> = [10, 20];

function widen(limits: number[]): void {
  limits.push(30);
}

function parseDigit(ch: number): number {
  if (ch < 48 || ch > 57) throw new RangeError("not a digit");
  return ch - 48;
}

function digitAt(text: string, i: number): NoThrow<number> {
  return parseDigit(text.charCodeAt(i));
}

function middle(start: Int32, end: Int32): Int32 {
  return (start + end) / 2;
}

widen(LIMITS as number[]);
console.log(digitAt("7", 0), middle(2, 5), LIMITS.length);
```

```console
$ scriptc build reject.ts
/tmp/app/reject.ts:6:3 - error SC7004: 'limits.push(30)' writes to a value that may be Immutable ('limits' may be an Immutable value passed to 'widen' at reject.ts:22:1)

  5 | function widen(limits: number[]): void {
  6 |   limits.push(30);
    |   ^~~~~~~~~~~~~~~
  7 | }

  hint: Immutable values cannot be written through any reference; copy the value before changing it

/tmp/app/reject.ts:14:44 - error SC7001: 'digitAt' is declared NoThrow but may throw: calls 'parseDigit' at reject.ts:15:10 → throw statement at reject.ts:10:27

  13 | 
  14 | function digitAt(text: string, i: number): NoThrow<number> {
     |                                            ^~~~~~~~~~~~~~~
  15 |   return parseDigit(text.charCodeAt(i));

  hint: remove the throwing operation from the path (or the recursion), catch it with try/catch, or remove NoThrow

/tmp/app/reject.ts:19:10 - error SC7002: the value returned from 'middle' (Int32) is not provably an int32: '(start + end) / 2'

  18 | function middle(start: Int32, end: Int32): Int32 {
  19 |   return (start + end) / 2;
     |          ^~~~~~~~~~~~~~~~~
  20 | }

  hint: make the int32 conversion explicit, for example '((start + end) / 2) | 0', so Node computes the same value

3 errors.
```

<table>
  <thead>
    <tr>
      <th>
        Code
      </th>

      <th>
        Meaning
      </th>
    </tr>
  </thead>

  <tbody>
    <tr>
      <td>
        SC7001
      </td>

      <td>
        A

        <code>NoThrow</code>

        function may throw; the message shows the path to the throwing operation.
      </td>
    </tr>

    <tr>
      <td>
        SC7002
      </td>

      <td>
        A value stored into an

        <code>Int32</code>

        slot, or an element stored into an array that can reach an

        <code>Int32\[]</code>

        slot, is not provably an int32; the hint shows the

        <code>| 0</code>

        form.
      </td>
    </tr>

    <tr>
      <td>
        SC7003
      </td>

      <td>
        An

        <code>Int32</code>

        slot has writes scriptc cannot see, or an

        <code>Int32\[]</code>

        array can come from or escape to code scriptc cannot see.
      </td>
    </tr>

    <tr>
      <td>
        SC7004
      </td>

      <td>
        A write can reach an

        <code>Immutable</code>

        value, or a stored value is not fresh.
      </td>
    </tr>

    <tr>
      <td>
        SC7005
      </td>

      <td>
        An

        <code>Immutable</code>

        value escapes to code scriptc cannot check.
      </td>
    </tr>

    <tr>
      <td>
        SC7006
      </td>

      <td>
        A contract is written where scriptc does not check it.
      </td>
    </tr>

    <tr>
      <td>
        SC7007
      </td>

      <td>
        An

        <code>Acyclic</code>

        class or type can lie on a reference cycle; the message shows the cycle.
      </td>
    </tr>

    <tr>
      <td>
        SC7008
      </td>

      <td>
        A

        <code>ProgramLifetime</code>

        class is not the root of its hierarchy, or is part of a library build.
      </td>
    </tr>

    <tr>
      <td>
        SC7009
      </td>

      <td>
        An

        <code>InlineArray</code>

        field is not on a

        <code>ProgramLifetime</code>

        class, is written other than once from a constructor parameter, or is read in a way that could tell its reads apart.
      </td>
    </tr>
  </tbody>
</table>

---

For a semantic overview of all documentation, see [/sitemap.md](/sitemap.md)

For an index of all available documentation, see [/llms.txt](/llms.txt)

For agent-facing discovery, including API and MCP surfaces, see [/agents.md](/agents.md)

---
title: Coverage Reports
url: "https://scriptc.dev/docs/coverage"
docs_index: /llms.txt
lastUpdated: 2026-11-12
---

> For an index of all documentation, see [/llms.txt](/llms.txt).

`scriptc coverage` tells you how much of a program compiles before you build it. Use it when you bring an existing Node.js program to scriptc, add a dependency, or want CI to catch unsupported code early. It analyzes the program without producing an executable, counts the statements that compile, and groups anything unsupported by diagnostic code with a location and a hint.

<span id="static-programs" />

## Run a report

Pass the program's entry file:

```console
$ cat hello.ts
const who: string = process.argv.length > 2 ? process.argv[2] : "world";
console.log(`hello, ${who}`);

$ scriptc coverage hello.ts

  statements analyzed   2
  compile statically    2  (100%)

  fully static — all analyzed statements compile to native code.
```

Every statement in this program compiles, so `scriptc build` will succeed. The counts describe this program only, not JavaScript or Node.js support in general.

<span id="report-fields" />

## Reading the report

When a program has unsupported operations, the report adds a group for each root cause. A group shows the diagnostic code, the reason, the first few sites in source order, and a hint. A generic declaration that fails for several type arguments appears once.

<table>
  <thead>
    <tr><th>Field</th><th>Meaning</th></tr>
  </thead>

  <tbody>
    <tr>
      <td><code>statements analyzed</code></td>

      <td>Executable statements in application code, excluding dependency implementation code.</td>
    </tr>

    <tr>
      <td><code>compile statically</code></td>

      <td>Statements that compile to native instructions.</td>
    </tr>

    <tr>
      <td><code>in dynamic tier</code></td>

      <td>Of those, the statements in functions that operate on values typed only at runtime, such as <code>any</code> values and untyped JavaScript. Shown when there are any. See <a href="#dynamic-tier">Dynamic tier</a>.</td>
    </tr>

    <tr>
      <td><code>blockers</code></td>

      <td>Unsupported operations with counts, diagnostic codes, reasons, locations, and hints.</td>
    </tr>

    <tr>
      <td><code>runs at startup</code></td>

      <td>Deferred operations in a JavaScript module's top-level code, which runs when the module loads: at startup for a module the program imports.</td>
    </tr>

    <tr>
      <td><code>deferred to runtime</code></td>

      <td>Other deferred operations in JavaScript code. Each throws only if it runs.</td>
    </tr>

    <tr>
      <td><code>in unreached code</code></td>

      <td>Blockers in code the entry never reaches. A build never compiles that code, so these cannot fail it.</td>
    </tr>

    <tr>
      <td><code>differs from Node</code></td>

      <td>Sites that compile but can behave differently from Node.js. See <a href="#node-divergences">Node divergences</a>.</td>
    </tr>

    <tr>
      <td><code>trusts unverified types</code></td>

      <td>Checked conversions whose value is typed by something no build can verify. See <a href="#type-claims">Type claims</a>.</td>
    </tr>

    <tr>
      <td><code>npm package compilation</code></td>

      <td>Native package compilation outcomes and refusal reasons.</td>
    </tr>
  </tbody>
</table>

Blockers in unreached code and deferred operations in JavaScript packages are listed separately because they don't fail a build. A deferred operation throws a runtime error if it runs, so a successful build doesn't mean every API of a package works; see [unsupported code inside a package](/docs/dependencies#unsupported-code-inside-a-package).

If a function's signature can't compile, the statements in its body count as not compiling, so the percentage reflects how much of the program compiles. The report still lists the diagnostics of those statements; a use of a parameter whose type can't compile reports `SC2004` back to the signature.

<span id="large-programs" />

## Large programs

A full report lowers the entry path, then lowers the rest of the program to list its diagnostics. On a large program the second pass can need more memory than the analysis has.

Analyze one entrypoint per run, and pass `--coverage=reached` to skip the second pass:

```console
$ scriptc coverage hello.ts --coverage=reached

  statements analyzed   2
  compile statically    2  (100%)

  fully static — all analyzed statements compile to native code.
  note  unreached code was not analyzed; the totals count reached statements only and are not comparable with a full report
```

The report then has no "in unreached code" group, and its totals count only the statements the entry path reaches. A function whose signature can't compile still lists its body's diagnostics when the entry path reaches it. The note marks the totals as not comparable with a full report.

`--coverage=reached` can't be combined with `--explain-dynamic`, which needs the unreached code.

<span id="programs-that-require-dynamic-execution" />

<span id="analyze-with-dynamic" />

<span id="embedded-nodejs-builtins" />

<span id="dynamic-tier" />

## Dynamic tier

Code that operates on values whose types are known only at runtime, such as `any` values, the result of an untyped `JSON.parse`, or untyped JavaScript, compiles into dynamic-tier functions. They are native code too, but they check types and look up properties at runtime, so they are slower than code with static types. The report's `in dynamic tier` line counts their statements.

`--explain-dynamic` lists those functions and the operation that makes each one dynamic. It also lists the units an unsupported construct would move to the dynamic tier: the binding, field, class, function, or module that has no static representation for the construct, with the diagnostic behind each one. These are the constructs the dynamic tier is planned to compile; today they remain blockers in TypeScript and deferred runtime errors in JavaScript. Add a name, such as `--explain-dynamic=Point`, to show only the matching units. With `--print=diagnostics`, the document gains a `tiers` object with the same information.

The report also explains runtime-optional storage. A binding or parameter whose value comes from a read that can miss, such as an array element or a record key, keeps storage that holds `undefined`, while the checker types every read with the declared type. TypeScript's narrowing at those reads therefore does not reach the storage, and lowering extracts the value through a checked narrowing. For each such slot the report names the expression that made it optional, whether the slot is a binding, a pattern binding or a parameter, and the reads where the checker's type is narrower than the storage. With `--print=diagnostics`, the `tiers` object carries the same slots.

`--tier=static-only` keeps every unsupported construct a blocker instead of moving it to the dynamic tier; code that already operates on runtime-typed values compiles as before. Today `auto`, the default, behaves the same way.

### Untyped code that likely runs often

When functions of the program's own files use untyped values and likely run often, `scriptc build` and coverage report them as an `SC6101` warning. For the Benchmarks Game n-body program in plain JavaScript:

```console
$ scriptc build nbody.js -o nbody
warning SC6101: 3 functions that likely run often use untyped values, which runs slower than typed code:
  advance (nbody.js:22): untyped JavaScript file, in a loop
  energy (nbody.js:40): untyped JavaScript file, in a loop
  offsetMomentum (nbody.js:12): untyped JavaScript file, in a loop
  hint: add types to compile these statically; see scriptc coverage --explain-dynamic

The program compiled. Adding types to these functions lets scriptc compile them to faster code.
```

Without a profile, a dynamic-tier function counts as likely to run often when a dynamic operation runs on every iteration of one of its loops (a `for` loop's initializer and a `for...of` loop's iterable run once), when a loop calls it, or when it calls itself. A function is named only when its source says why its values are untyped: a value typed `any` (for a `for...of` variable, the value it iterates), an untyped `JSON.parse` result, or an untyped JavaScript file. Dynamic code that runs once, such as setup code that reads an `any` configuration, is not reported, and neither are functions that are dynamic for an operation with no static lowering, which more types would not change.

The warning names at most five functions, most likely to run often first, and gives the total. A function expression is named by its own name, else by the variable, property or class field it is assigned to, else as `anonymous function`. Dynamic-tier code in npm packages is summarized on one line with each package's function count. A build that comes from the cache repeats the warning. `--no-dynamic-tier-warning` leaves it out, and `--fail-on=dynamic` fails a coverage run on it.

## Dynamic slots

A declared type that has no native representation does not fail the build. The field, parameter, return value, variable or expression that holds it becomes a checked dynamic slot: the parts of the type that compile keep their native layout, and the failing parts hold checked-dynamic values. Coverage lists the slots under the statement counts, and `scriptc build` prints one `SC6104` warning per slot.

```console
$ cat plugins.ts
type Hook = (() => void) | AbortSignal | Map<string, (x: number) => string>;

interface Plugin {
  name: string;
  hook: Hook;
}

const plugins: Plugin[] = [{ name: "log", hook: () => console.log("hook ran") }];
for (const p of plugins) {
  const hook = p.hook;
  if (typeof hook === "function") hook();
  console.log(p.name);
}

$ scriptc coverage plugins.ts

  statements analyzed   6
  compile statically    6  (100%)
    in dynamic tier     1  (operate on values typed only at runtime; --explain-dynamic lists them)
    dynamic slots       1  (declared types with no static lowering, held as checked-dynamic values; SC6104)
      module global 'plugins' (declared 'Plugin[]') (dynamic pieces)  plugins.ts:8:7

  fully static — all analyzed statements compile to native code.
```

Here `Plugin` keeps its native `name` field, and only `hook` is checked-dynamic. Reading a dynamic value into typed code checks it against the type that reads it, so data that contradicts the declaration still throws a `TypeError`. Slots with the most dynamic operations are listed first. `--fail-on=dynamic-slots` fails a coverage run when any slot exists, and `--tier=static-only` reports the original errors instead.

## Type claims

scriptc trusts declared types and checks them where an untyped value enters typed code. A value that contradicts its type throws a `TypeError` at that conversion, where Node.js would run on. Some of those types are claims no build can verify: an assertion over parsed data, the type a variable gives an `any` value, and the declarations of a package whose values your program receives. When a claim is wrong, the build succeeds and the program throws the first time the conversion runs. Coverage lists these conversions before anything runs:

```console
$ cat claims.ts
interface User {
  id: number;
  name: string;
}

const user = JSON.parse('{"id":"41","name":"Ada"}') as User;
const settings: any = JSON.parse('{"retries":3}');
const retries: number = settings.retries;
console.log(user.name, user.id + 1, retries * 2);

$ scriptc coverage claims.ts

  statements analyzed   4
  compile statically    4  (100%)
    in dynamic tier     1  (operate on values typed only at runtime; --explain-dynamic lists them)

  fully static — all analyzed statements compile to native code.

  trusts unverified types   2 checked conversions on 2 claims (compiles; a value that contradicts its claimed type throws a TypeError at the conversion)
      ×1  parsed data held in 'settings', read as number
          at claims.ts:8:25
          fix: validate the parsed data before giving it this type (a schema, or checks on an unknown value), or declare the type the data really has
      ×1  parsed data the program reads as { id: number; name: string }
          at claims.ts:6:14
          fix: validate the parsed data before giving it this type (a schema, or checks on an unknown value), or declare the type the data really has

$ scriptc build claims.ts -o claims >/dev/null
$ ./claims
Uncaught TypeError: expected number at $.id, got string
```

The second claim is the wrong one here: the data holds `id` as a string. The report can't know which claims are wrong, only where they are.

Conversions are grouped by what makes the claim, so one wrong declaration appears once with every conversion that trusts it:

<table>
  <thead>
    <tr><th>Group</th><th>What makes the claim</th><th>What makes it safe</th></tr>
  </thead>

  <tbody>
    <tr>
      <td>A package declaration</td>

      <td>A <code>.d.ts</code> file, an <code>@types/</code> package or JSDoc that types a value your code receives from the package. The row names the package, the declaration, and its file and line.</td>

      <td>The <code>scriptc types patch</code> command the row prints. See <a href="/docs/dependencies#fix-a-packages-types">Fix a package's types</a>.</td>
    </tr>

    <tr>
      <td>A function a Node island calls</td>

      <td>The package's declaration of the parameter your function is passed as. The island calls the function with its own values, and each argument is checked against your function's parameter types.</td>

      <td>The same command, with the parameter's selector.</td>
    </tr>

    <tr>
      <td>A package compiled from its JavaScript</td>

      <td>The package's own JSDoc and the types scriptc infers for its untyped code. All conversions inside the package's files are one row.</td>

      <td>Patch the declaration that is wrong, or run the package in a Node island with <code>--node-island=\<package></code>.</td>
    </tr>

    <tr>
      <td>Parsed data</td>

      <td>The type your program gives the result of <code>JSON.parse</code>.</td>

      <td>Validate the data before you give it the type.</td>
    </tr>

    <tr>
      <td>An untyped value</td>

      <td>The type a variable, parameter or return value gives a value read from a binding declared <code>any</code> or <code>unknown</code>. The row names the binding.</td>

      <td>Declare the binding's real type, or narrow the value before typed code reads it.</td>
    </tr>

    <tr>
      <td>An assertion</td>

      <td>A type assertion over a value the program holds dynamically, such as <code>table() as Catalog</code> where <code>table()</code> returns a <code>Record\<string, object></code>. The row names what the value is read from.</td>

      <td>Check the value before asserting its type, or build it under a declared type so the checker verifies it.</td>
    </tr>
  </tbody>
</table>

Rows are ordered by the number of conversions, after two kinds of rows that come first, and before the rows for packages compiled from their JavaScript, whose counts are of the package's own files.

A Node island copies data to your program and adapts functions, but an object the island made never converts to a type only compiled code can make, such as a `Date`, a `RegExp`, or an `http.Server`. A conversion like that throws every time it runs, unless the value is one your program passed into the island, so those rows come first and say so.

Next come the rows with a conversion whose failure your program would not see: one inside a `try` whose `catch` neither reads the error nor throws. This function is meant to skip malformed lines. Compiled, it also skips every well-formed line that lacks a field `Entry` declares, where Node.js keeps the entry, and nothing reports it:

```console
$ cat entries.ts
interface Usage {
  input: number;
  total: number;
}
interface Entry {
  type: "message";
  usage: Usage;
}

function parse(line: string): Entry | null {
  try {
    return JSON.parse(line) as Entry;
  } catch {
    return null;
  }
}

const lines = [
  '{"type":"message","usage":{"input":1,"total":2}}',
  '{"type":"message","usage":{"input":1}}',
  "not json",
];
for (const line of lines) {
  const entry = parse(line);
  console.log(entry === null ? "skipped" : `${entry.type} ${entry.usage.input} ${String(entry.usage.total)}`);
}

$ scriptc coverage entries.ts | tail -n 5
  trusts unverified types   1 checked conversion on 1 claim (compiles; a value that contradicts its claimed type throws a TypeError at the conversion)
      ×1  parsed data the program reads as { type: string; usage: { input: number; total: number } }
          at entries.ts:12:12
          unseen if it fails: entries.ts:12:12 is in a try whose handler ignores the error, where Node.js keeps the value
          fix: validate the parsed data before giving it this type (a schema, or checks on an unknown value), or declare the type the data really has

$ scriptc build entries.ts -o entries >/dev/null
$ ./entries
message 1 2
skipped
skipped
$ node entries.ts
message 1 2
message 1 undefined
skipped
```

Only a conversion written inside the `try` block is marked, not one in a function the block calls.

A conversion that a kind test of the same value guards is not a claim and is not listed: a read inside `if (typeof value === "string")`, in the matching arm of a conditional, or after `if (typeof value !== "string") return`. A test elsewhere in the function guards nothing, and neither does a test of a variable the function assigns again. Neither is reading back a value that your TypeScript source declares with a type and scriptc holds dynamically, such as a `Response` from `fetch`. In JavaScript files nothing declares a type, so every such read is listed.

This program reads two members of a logger that the `debug` package creates. The package runs in a Node island, and its types come from `@types/debug`:

```console
$ cat logger.ts
import createDebug from "debug";

const log = createDebug("app:main");
log("hidden unless DEBUG is set");
console.log(log.enabled, log.namespace);

$ scriptc coverage logger.ts | tail -n 7
  trusts unverified types   2 checked conversions on 2 claims (compiles; a value that contradicts its claimed type throws a TypeError at the conversion)
      ×1  debug@4.4.3 declares debug.Debugger.enabled as boolean (@types/debug/index.d.ts:44): a Node island value
          at logger.ts:5:13
          fix: if the declaration is wrong, patch it: scriptc types patch debug debug.Debugger.enabled '<type>' (a type that starts with | widens the declared one, as in '| undefined')
      ×1  debug@4.4.3 declares debug.Debugger.namespace as string (@types/debug/index.d.ts:46): a Node island value
          at logger.ts:5:26
          fix: if the declaration is wrong, patch it: scriptc types patch debug debug.Debugger.namespace '<type>' (a type that starts with | widens the declared one, as in '| undefined')

$ scriptc build logger.ts -o logger >/dev/null 2>&1
$ ./logger
Uncaught TypeError: expected boolean at $, got undefined
    debug@4.4.3 declares debug.Debugger.enabled as boolean (@types/debug/index.d.ts:44)
    if that declaration is wrong, patch it: scriptc types patch debug debug.Debugger.enabled '| undefined'
```

The first declaration is wrong: `enabled` is `undefined` when the `DEBUG` environment variable isn't set. The error names the declaration the report listed, with the type that would admit the value. That command fixes it, and the program then prints what it prints under Node.js:

```console
$ scriptc types patch debug debug.Debugger.enabled "| undefined"
@types/debug 4.4.3 (node_modules/@types/debug)
  index.d.ts:44  boolean  ->  boolean | undefined
wrote package.json scriptc.typePatches["debug@^4.4.3"].declarations["debug.Debugger.enabled"]

$ scriptc build logger.ts -o logger >/dev/null 2>&1
$ ./logger
undefined app:main
```

A declaration with type parameters has no patch command, because its type is the one your program instantiates it with, and neither has a member of a mapped or inferred type. The row says so and points to replacing the declaration file instead.

The report describes the program a build compiles, including which packages run in Node islands. If `scriptc build` reports a Node island for a package that coverage compiled, pass the same packages to coverage with `--node-island=<package>` to see the claims of that build. `--fail-on=type-claims` fails a coverage run when the program has any claim.

## Diagnostic codes

Coverage and `scriptc build` report the same diagnostic codes. Common ones include:

- `SC2020`: a declared standard-library or Node.js API has no supported compiler implementation.
- `SC2012`: this API form cannot compile natively.
- `SC2013`: a package cannot compile natively; inspect the package-specific reason and the [dependency guide](/docs/dependencies).
- `SC1100`: a value whose type is only known at runtime, such as `unknown`, is used where scriptc needs a specific type. Narrow or cast the value as the diagnostic suggests.

## Node divergences

Some code compiles but can behave differently than under Node.js. Coverage reports the cases it can detect as `SC6xxx` warnings, and `scriptc build` prints the same warnings after a successful build. They never fail a build, but `--fail-on=divergences` can fail a CI job on them.

- `SC6001`: a conversion to a record, array, or class type with another layout copies a value something else can still reach, and the program writes a field or element of either type, compares values of the converted type by identity, or passes either to code scriptc cannot follow.
- `SC6002`: values of one record type are built in key orders scriptc cannot track per value, so the type keeps its declaration order, an object literal's key order differs from it, and the program observes the order through `JSON.stringify`, `Object.keys`, `Object.values`, `Object.entries`, `for...in`, or console output.
- `SC6003`: `localeCompare` compares a string literal outside the built-in Latin-script collation.
- `SC6004`: a call passes a class instance to a function that casts it to an unrelated class, or a value typed by an interface that extends `Error` is handed to a slot typed by an `Error` class.
- `SC6007`: an object or array made in a Node island is copied where Node.js keeps one object: it is converted to an array type of the program, or to a record type that declares a string-keyed dictionary or a library object (an assertion, a typed binding of an `any` value, a typed parameter of a callback the island calls), or it is stored untyped in a member, an element, a reassigned binding or a container and given a type after it is read back; see [Objects converted from Node islands](/docs/limitations#runtime-differences-from-nodejs).
- `SC6012`: a JavaScript statement that doesn't compile sits in the `try` block of a `try`/`catch` in the same function. It throws when it runs, so the `catch` clause runs even where the statement would have completed under Node.js, and the compiled program [doesn't report it at run time](/docs/dependencies).
- `SC6013`: a statement writes a member that the program added to the library `Promise` type, such as `Promise<T> & { status?: string }`. A compiled promise holds no members of its own, so the write throws a `TypeError` when it runs on a promise the program made. Keep the state beside the promise, in a wrapper record or in a `WeakMap` keyed by the promise.
- `SC6014`: the program observes the key order of a record type whose objects create their keys in orders scriptc doesn't follow per object, such as a spread of a value whose key order isn't known, or `Object.assign` adding two or more optional properties from a value converted at run time. The warning names the type once, by its keys, and the objects the program builds can still list their keys as under Node.js; see [Record identity and property order](/docs/limitations#record-identity-and-property-order).
- `SC6015`: a TypeScript statement doesn't compile and its refusal waits for the statement to run. It throws an error that names the refusal when it runs, where it would have completed under Node.js; a program that never reaches the statement runs as it does under Node.js. The same code names a statement of a generic function that takes a value as the one object type its constraint names while the call binds objects of several layouts: the statement throws a `TypeError` for a value of another layout.

## Gating builds and CI

By default, `scriptc coverage` exits with status 0 whenever the program type-checks, so you can use the report to track progress. To fail a CI job instead, add `--fail-on`:

```console
$ scriptc coverage src/main.ts --fail-on=blockers
```

`--fail-on=blockers` exits with status 1 when the program has blockers that would fail `scriptc build`. Blockers in unreached code and deferred operations don't count. `--fail-on=divergences` also fails on Node divergences, `--fail-on=dynamic` also fails on the `SC6101` warning for [untyped code that likely runs often](#untyped-code-that-likely-runs-often), `--fail-on=dynamic-slots` also fails on [dynamic slots](#dynamic-slots), and `--fail-on=type-claims` also fails on [type claims](#type-claims). A program that doesn't type-check always exits with status 1.

Coverage caches its result. Running it again on an unchanged program, or after edits that only touch comments, reuses the previous verdict. Set `SCRIPTC_NO_CACHE=1` to turn the cache off.

## Machine-readable diagnostics

For editors, CI annotations, and other tools, `scriptc coverage --print=diagnostics` and `scriptc build --print=diagnostics` write one JSON document to stdout instead of the human-readable report. The exit status is unchanged. The document looks like this:

```json
{
  "schema": "scriptc-diagnostics",
  "schemaVersion": 1,
  "compilerVersion": "0.3.0",
  "command": "coverage",
  "entry": "/work/app/src/main.ts",
  "success": false,
  "phase": "compile",
  "stats": {
    "statementsTotal": 13,
    "statementsStatic": 12,
    "statementsDynamic": 0,
    "statementsFailed": 1,
    "functionsSkipped": 0,
    "percentStatic": 92
  },
  "groups": [
    {
      "id": "SC1031-c2cc8425",
      "code": "SC1031",
      "category": "unsupported",
      "severity": "error",
      "scope": "reached",
      "message": "object destructuring of non-record values (the source is array-typed)",
      "hint": "...",
      "count": 1,
      "primary": { "file": "/work/app/src/main.ts", "line": 13, "column": 7 },
      "sites": [{ "file": "/work/app/src/main.ts", "line": 13, "column": 7 }]
    }
  ],
  "diagnostics": [
    {
      "code": "SC1031",
      "category": "unsupported",
      "severity": "error",
      "scope": "reached",
      "file": "/work/app/src/main.ts",
      "line": 13,
      "column": 7,
      "endLine": 13,
      "endColumn": 28,
      "start": 352,
      "end": 373,
      "message": "object destructuring of non-record values (the source is array-typed) are not supported yet",
      "hint": "...",
      "group": "SC1031-c2cc8425"
    }
  ]
}
```

<table>
  <thead>
    <tr>
      <th>
        Field
      </th>

      <th>
        Meaning
      </th>
    </tr>
  </thead>

  <tbody>
    <tr>
      <td>
        <code>schemaVersion</code>
      </td>

      <td>
        Incremented when a field is renamed or removed or its meaning changes. New optional fields, categories, and codes keep the version.
      </td>
    </tr>

    <tr>
      <td>
        <code>success</code>
      </td>

      <td>
        Coverage: no blockers on the entry path (and, with

        <code>--fail-on=divergences</code>

        , no divergences). Build: an artifact was produced, named by

        <code>artifact</code>

        .
      </td>
    </tr>

    <tr>
      <td>
        <code>phase</code>
      </td>

      <td>
        <code>preflight</code>

        when type checking, configuration, or the module graph stopped the command;

        <code>compile</code>

        when blockers remain;

        <code>native</code>

        when native code generation or linking failed; otherwise

        <code>complete</code>

        .
      </td>
    </tr>

    <tr>
      <td>
        <code>stats</code>
      </td>

      <td>
        Coverage only, absent after a preflight failure: the report's statement counts.

        <code>statementsStatic</code>

        counts every statement that compiles to native code;

        <code>statementsDynamic</code>

        is the part of those in dynamic-tier functions.
      </td>
    </tr>

    <tr>
      <td>
        <code>category</code>
      </td>

      <td>
        <code>invalid-source</code>

        (the program has a TypeScript or JSON error),

        <code>unsupported</code>

        (valid code scriptc cannot compile yet),

        <code>divergence</code>

        (compiles, but can behave differently from Node.js),

        <code>performance</code>

        (compiles and behaves as under Node.js, but runs slower than typed code would),

        <code>environment</code>

        (project configuration, scriptc's type world, toolchain, or target), or

        <code>internal</code>

        (a compiler bug).
      </td>
    </tr>

    <tr>
      <td>
        <code>severity</code>
      </td>

      <td>
        <code>error</code>

        , or

        <code>warning</code>

        for divergences.
      </td>
    </tr>

    <tr>
      <td>
        <code>scope</code>
      </td>

      <td>
        <code>preflight</code>

        ,

        <code>reached</code>

        (fails a build),

        <code>unreached</code>

        (code the entry never reaches), or

        <code>deferred</code>

        (a JavaScript statement that compiles to a throw of its diagnostic, which happens only if it executes).
      </td>
    </tr>

    <tr>
      <td>
        <code>line</code>

        ,

        <code>column</code>
      </td>

      <td>
        1-based line and UTF-16 column;

        <code>start</code>

        and

        <code>end</code>

        are 0-based UTF-16 offsets.
      </td>
    </tr>

    <tr>
      <td>
        <code>group</code>
      </td>

      <td>
        The

        <code>id</code>

        of the root-cause group the diagnostic belongs to. A group's

        <code>primary</code>

        site is its first in source order.
      </td>
    </tr>

    <tr>
      <td>
        <code>typeClaims</code>
      </td>

      <td>
        Coverage only, present when the program has

        <a href="#type-claims">type claims</a>

        : one object per row of the report, in the report's order.

        <code>kind</code>

        is

        <code>island</code>

        ,

        <code>declared</code>

        ,

        <code>package</code>

        ,

        <code>parsed</code>

        or

        <code>untyped</code>

        ;

        <code>count</code>

        and

        <code>sites</code>

        give the conversions;

        <code>claimed</code>

        lists the types they check for;

        <code>fix</code>

        is the report's fix line. A row about a package declaration has

        <code>declaration</code>

        with its

        <code>package</code>

        ,

        <code>version</code>

        ,

        <code>name</code>

        , file and line (

        <code>at</code>

        ),

        <code>declaredType</code>

        , and the

        <code>selector</code>

        to pass to

        <code>scriptc types patch</code>

        when it has one. A

        <code>package</code>

        row lists those for each of the package's own declarations under

        <code>declarations</code>

        .

        <code>nativeOnly</code>

        counts the conversions that throw whenever they run,

        <code>swallowed</code>

        lists the sites inside a

        <code>try</code>

        that ignores the error,

        <code>callbacks</code>

        the functions a Node island calls, and

        <code>asserted</code>

        the type assertions.
      </td>
    </tr>

    <tr>
      <td>
        <code>startup</code>
      </td>

      <td>
        <code>true</code>

        on a

        <code>deferred</code>

        diagnostic in a module's top-level code, which runs when the module loads. Absent otherwise.
      </td>
    </tr>
  </tbody>
</table>

## Type errors

Coverage needs a program that type-checks. If type checking fails, the report lists the type errors instead of statement counts; fix them first.

scriptc type-checks with its own settings: strict null checks, the ES2025 library, and Node.js types (its bundled declarations when the project has no `@types/node`). An error those settings cause, rather than the project's own `tsc` configuration, carries a hint naming the cause, such as a library member newer than ES2025 or a global of another runtime, and machine-readable output reports it in the `environment` category.

## External host type declarations

When your code will run inside a host application that provides its own modules, those modules aren't in `node_modules`. Use `--external-types` to give coverage their declarations so it can analyze the rest of your code:

```console
$ scriptc coverage src/core.ts --external-types @native-sdk/core=types/native-sdk-core.d.ts
```

The option is repeatable. Relative declaration paths resolve from the current working directory. Supported declaration extensions are `.d.ts`, `.d.mts`, and `.d.cts`. Relative declaration imports and re-exports are included, so the mapped file can be an `index.d.ts` barrel. Project-owned code using these types participates in coverage; type-only imports add no runtime boundary.

The mapping supplies types for analysis, not a runtime implementation. Value imports and uses remain `SC1010` external-host blockers while analysis counts the rest of the application. This option is available only for coverage; builds require a module implementation or runtime integration.

---

For a semantic overview of all documentation, see [/sitemap.md](/sitemap.md)

For an index of all available documentation, see [/llms.txt](/llms.txt)

For agent-facing discovery, including API and MCP surfaces, see [/agents.md](/agents.md)

---
title: npm Dependencies
url: "https://scriptc.dev/docs/dependencies"
docs_index: /llms.txt
lastUpdated: 2026-11-12
---

> For an index of all documentation, see [/llms.txt](/llms.txt).

Programs can import npm packages the same way they do under Node.js. scriptc compiles each package your program uses to native code along with your own code. The executable doesn't read `node_modules` at runtime.

Package support is partial. Packages that scriptc can't compile are reported at build time with a diagnostic, so check new dependencies with [`scriptc coverage`](/docs/coverage) before relying on them.

## Use a package

Create `tool.ts`:

```ts title="tool.ts"
import { Command } from "commander";

const program = new Command();

program
  .name("greet")
  .argument("<name>", "who to greet")
  .option("-u, --upper", "use uppercase")
  .action((name: string, opts: { upper?: boolean }) => {
    const text = `hello, ${name}`;
    console.log(opts.upper ? text.toUpperCase() : text);
  });

program.parse();
```

Install the dependency, then build and run the executable:

```console
$ npm install commander
$ scriptc build tool.ts -o greet >/dev/null
$ ./greet ada --upper
HELLO, ADA
```

Your callback and the commander implementation both compile to native code. No extra flags are needed.

<span id="package-resolution-and-embedding" />

<span id="runtime-behavior" />

<span id="engine-and-builtin-modules" />

## Package resolution

scriptc finds packages in `node_modules` with Node.js's resolution rules, including the export conditions in `package.json`, and compiles their dependencies too. TypeScript workspace packages compile as part of your program, including `.js` import specifiers that resolve to `.ts` sources.

Type-only imports follow Node.js's type stripping. `import type` and `export type … from` statements are erased, so a package or module named only by them supplies types and isn't compiled or loaded; it can even be a package with no JavaScript, such as `type-fest`. This holds wherever the declarations live: an installed package, a workspace package linked into `node_modules`, an `@types` package, an ambient `declare module`, or a declaration file of your own project. Their interfaces and type aliases type the records your program builds, and data cast to them is checked as for a type declared in a `.ts` file. A value still needs an implementation: a class to construct, an enum object, a constant or a function that only a declaration file declares is refused when you build, or, where Node.js can't find the module either, the program ends at start as it does under Node.js. A clause whose specifiers are all marked `type`, such as `import { type Options } from "./config.ts"`, still loads its module, as it does under Node.js.

A project that tsc compiles follows tsc instead. When the nearest `tsconfig.json` above a TypeScript file doesn't set `verbatimModuleSyntax`, scriptc drops the imports and exports that tsc drops for that file: an import whose bindings are never used as values, a clause of `type` specifiers only, and a re-export of a type. A module imported only in those ways isn't loaded, exactly as in the JavaScript tsc emits, so it doesn't run and doesn't take part in an import cycle. `isolatedModules` and `preserveConstEnums` decide whether a use of a const enum keeps its import, as they do for tsc. An export of a const enum stays a value export even where tsc's output has no value for the name, because a file that imports the export and uses a member as a value keeps that use; the importing file's build warns with `SC6008`. With `verbatimModuleSyntax: true`, or with no `tsconfig.json`, the type-stripping rule applies; a plain import of a type then fails when the program starts, as it does under Node.js, and the build warns about it with `SC6201`.

## Which packages compile

scriptc compiles a package automatically when it:

- ships its own type declarations, or is typed by its DefinitelyTyped `@types/` package, or is a dependency of another package that has no declarations at all, and
- ships JavaScript that isn't a webpack bundle. Minified JavaScript compiles like unminified JavaScript.

The declarations check your program; the package's JavaScript compiles with the types scriptc infers from it. A program that doesn't typecheck against the inferred types, for example because an `@types/` package describes another release, fails the build at that use.

A package that doesn't meet these requirements, or that uses something scriptc can't compile, gets a package-specific diagnostic that explains why. The usual fixes are to switch to a version or alternative package that compiles, or to replace the call with code you write yourself.

`graceful-fs` is the exception: it compiles as `node:fs` instead of from its JavaScript, with one [documented difference](/docs/limitations#graceful-fs).

<span id="value-conversion" />

<span id="static-package-compilation-experimental" />

<span id="type-declarations-and-boundary-restrictions" />

## Explicit package selection

Some packages your program imports directly ship no declarations, so their types can only be inferred from their JavaScript. To try compiling one of them, name it with `--npm-static`:

```console
$ scriptc build app.ts --npm-static=three -o app >/dev/null
```

Naming a package skips the declaration requirement above, but the package still has to compile. Add a trailing `...` to select a package together with every package it depends on, for example `--npm-static=semver...`. `--npm-static=auto` uses the default automatic selection. The option accepts a comma-separated list and can be repeated.

## Built-in packages

scriptc implements a few packages itself and compiles that implementation in place of the package's published JavaScript. Which implementation compiles depends on the version your program has installed, so an import resolves the same way it does under Node.js: a package that isn't installed is still a missing package, and a version scriptc doesn't implement compiles from its own files as usual.

`undici` 5.x and 7.x are built in. The implementation covers `request()`, `Dispatcher` (including subclasses that override `dispatch`), `Client`, `Pool` with its `stats`, `Agent` with per-origin `factory` functions and `connect` options, `buildConnector`, the `errors` classes and their `UND_ERR_*` codes, `setGlobalDispatcher` and `getGlobalDispatcher`, `fetch` and `Headers`, and the request events undici publishes on `node:diagnostics_channel`. With 5.x, `ProxyAgent` is built in too: every request travels through an HTTP CONNECT tunnel that an `http:` or `https:` proxy opens, with the `uri`, `token`, `auth`, `headers`, `requestTls` and `proxyTls` options. Requests use HTTP/1.1 with keep-alive connection reuse and undici's headers, body and idle timeouts, and each version keeps its own behavior where 5.x and 7.x differ, such as the error an aborted request rejects with. The package's other exports aren't declared, so using one is a type error at build time. Known differences:

- A response body supports the readable-stream members undici's body mixin uses: `on`, `once`, `pause`, `resume`, `destroy`, `text()`, `json()`, `arrayBuffer()`, `bytes()`, `blob()` and `dump()`. It can't be piped or iterated with `for await`.
- Diagnostics channel messages carry the request but not the socket or connector.
- `fetch` with a dispatcher returns a response whose `url` is empty and whose `redirected` is `false`, and offers only `gzip, deflate` encodings. Without a dispatcher it is the built-in `fetch`. Its init can be undici's `RequestInit` or the global one, and it sends string and byte bodies (a string, an `ArrayBuffer` or one of its views); a stream, `Blob`, `FormData` or `URLSearchParams` body rejects with a `NotSupportedError`. The init members undici acts on and this implementation doesn't (`integrity`, `cache`, `mode`, `referrer`, `referrerPolicy`, `credentials`, `duplex`, `keepalive`, `priority`, `window`) are taken only with a value that asks for nothing, such as an empty `integrity` or `mode: "cors"`; any other value rejects with a `NotSupportedError` that names the member, so a digest is never accepted and then not checked.
- The global `fetch` called with an init whose `dispatcher` is an object of this package (an `Agent`, a `ProxyAgent`, an instance of a class that extends `Dispatcher`) sends the request through that dispatcher, with the same limits as the package's own `fetch`. An object that implements `dispatch()` itself is refused: when the program is built if it is written in the init, otherwise where the call runs, with a `NotSupportedError` that says so.
- `ProxyAgent` dials the proxy once for each tunnel instead of through a pooled client, doesn't take `clientFactory`, and isn't declared for 7.x. Of `requestTls` it applies `servername`, `rejectUnauthorized` and `ca`, of `proxyTls` those and `timeout`, `maxCachedSessions` and `lookup`; another member of either, and a `host`, `content-length` or `connection` header for the tunnel, throws a `NotSupportedError` when the agent is made. It doesn't read `HTTP_PROXY` or its relatives, as in undici: the program passes the proxy.
- Request bodies are strings and bytes; stream, iterable and form-data bodies, `pipelining` above 1, `CONNECT` requests and Unix socket paths aren't supported.

`fs-extra` 8.x through 11.x is built in, and its declarations replace `@types/fs-extra`. The default import, a namespace import and named imports all read the same exports. fs's members are re-exported, and fs-extra's wrapped methods follow its rule: with a callback as the last argument they call back, otherwise they return a promise. The extra methods cover `copy`, `copySync`, `move`, `moveSync`, `remove`, `removeSync`, `emptyDir`, `ensureDir`, `mkdirs`, `mkdirp`, `ensureFile`, `pathExists`, `outputFile`, `readJson`, `readJsonSync`, `writeJson`, `writeJsonSync`, `outputJson` and their aliases, with fs-extra's error messages. Known differences:

- `ensureLink`, `ensureSymlink`, `createLink`, `createSymlink`, `gracefulify`, the promise forms of the descriptor-based methods, and the callback forms of `stat`, `lstat`, `readdir`, `mkdtemp`, `realpath` and `readJson` aren't declared, so using one is a type error at build time. Paths are strings, text is UTF-8, and write flags, string modes and `copyFile` mode flags aren't accepted.
- graceful-fs's retry queue, which fs-extra uses under Node.js, isn't part of the implementation, so a program that runs out of file descriptors fails at the first `EMFILE` error instead of retrying.
- An ES module that imports fs-extra can use any of its exports by name. Node.js only exposes the names its CommonJS lexer finds in fs-extra's source, so a name like `readFile` throws a `SyntaxError` there.

`@hono/node-server` 2.x is built in. `serve`, `createAdaptorServer`, `getRequestListener` and `RequestError` serve a fetch handler such as Hono's `app.fetch` over `node:http` with native `Request` and `Response` objects, and write each response the way the package does under Node.js: the same status line, header names, header order and content length, for buffered and streamed bodies, including the package's shortcut for a response that is still exactly what its constructor received. Request bodies stream to the handler as it reads them, and a body the handler leaves unread is drained after the response, up to 64 MiB or 500 ms, as the package does. Known differences:

- Only HTTP/1.1 servers are declared: the `createServer` and `serverOptions` options for `node:https` and `node:http2`, the `websocket` option and `upgradeWebSocket` aren't declared, so using one is a type error at build time. Subpath modules such as `@hono/node-server/serve-static` compile from the package's own files.
- The fetch handler must return a `Response` or a promise of one; the handler's second argument holds `incoming` and `outgoing`.
- The global `Request` and `Response` stay the built-in classes, so `overrideGlobalObjects` has no effect, and a `Response` reads its default text content type as `text/plain;charset=UTF-8` where the package's replacement reads `text/plain; charset=UTF-8`. Written responses carry the package's spelling.
- A streamed response is written without waiting for `drain`, and a failed handler answers 504 only for an error whose `name` is `TimeoutError`.

Small polyfill packages whose module is exactly a Node.js built-in on Node.js 24 compile as that built-in, at the `require()` that loads them, from your program's CommonJS files, from compiled packages, and from esbuild bundles whose module paths name the package and its version (as pnpm installs do). The package's own files aren't compiled.

<table>
  <thead>
    <tr>
      <th>Package</th>

      <th>Versions</th>

      <th>Compiles as</th>
    </tr>
  </thead>

  <tbody>
    <tr>
      <td><code>isarray</code></td>

      <td>0.x, 1.x, 2.x</td>

      <td><code>Array.isArray</code></td>
    </tr>

    <tr>
      <td><code>safe-buffer</code></td>

      <td>5.x</td>

      <td><code>require("buffer")</code></td>
    </tr>

    <tr>
      <td><code>process-nextick-args</code></td>

      <td>2.x</td>

      <td><code>process</code></td>
    </tr>

    <tr>
      <td><code>inherits</code></td>

      <td>2.x</td>

      <td><code>require("util").inherits</code></td>
    </tr>

    <tr>
      <td><code>util-deprecate</code></td>

      <td>1.x</td>

      <td><code>require("util").deprecate</code></td>
    </tr>

    <tr>
      <td><code>readable-stream</code></td>

      <td>2.x, 3.x, 4.x</td>

      <td><code>require("stream")</code> and its classes</td>
    </tr>
  </tbody>
</table>

`readable-stream` compiles as Node.js's built-in streams, which is its own `READABLE_STREAM=disable` mode for 2.x and 4.x. That can differ from its default behavior under Node.js; see [limitations](/docs/limitations#readable-stream).

## Optional dependencies that never load

Some packages load an optional dependency inside `try`/`catch` and work without it, such as `node-fetch` with `encoding`. To keep such a dependency out of the native build even when it is installed, mark it unloaded in your entry's `package.json`:

```json title="package.json"
{
  "scriptc": {
    "packages": {
      "encoding": "unloaded"
    }
  }
}
```

A `require()` of an unloaded package throws the same `MODULE_NOT_FOUND` error Node.js throws when the package isn't installed, and the package isn't compiled.

## CommonJS packages

CommonJS packages compile with Node.js's module semantics. Each `require()` evaluates its module at the call, once, and later calls return the cached exports. A package can assign `module.exports` and run code before it requires its dependencies. If that code reads a `const` or `let` require binding before its `require` runs, the read throws the same `ReferenceError` as Node.js. A `var` binding that earlier code can reach is reported at build time. A `require()` of a JSON file, such as a package's own `package.json`, returns one object parsed at build time, shared by every module that requires it. A require cycle compiles when each of its modules assigns its exports before requiring anything, as semver's classes and esbuild's per-file output do, and uses the other modules only inside functions that its top-level code can't call. Bundles built by esbuild compile with their bundled CommonJS modules and lazily initialized ES modules, each evaluated once, at its first use, as under Node.js. Other require cycles are reported at build time. The [limitations](/docs/limitations) page lists the supported `require` forms.

Some package entries choose a module by an environment variable, as React's `index.js` requires its production or development build by `NODE_ENV`. Build such a program with [`--define`](/docs/cli), for example `--define='process.env.NODE_ENV="production"'`: the entry compiles only the module the value selects. Without the flag, that conditional `module.exports` assignment is unsupported.

## Unsupported code inside a package

A JavaScript package can contain operations scriptc can't compile, often in functions your program never calls. These don't fail the build: each one compiles to code that throws a runtime error if it ever runs. Coverage reports list them as deferred sites, so review the list and test the parts of the package your program uses. Sites in a module's top-level code are listed under `runs at startup`: that code runs when the module loads, at startup for a module the program imports.

When a deferred operation runs, the error it throws can be caught by your code or by a framework's error handler. So that a caught error doesn't look like an application bug, the program writes to stderr the first time each such site runs, naming the operation, its diagnostic code, and its source position when the site has one. A program that calls `v5` from `uuid` 14.0.3 prints:

```text
scriptc: this program reached code that scriptc could not compile; each such operation throws where it runs and is reported here once ('scriptc coverage' lists them, SCRIPTC_DEFERRED_REPORT=0 silences these lines)
scriptc: not compiled: 'unescape' is part of the standard library types but has no scriptc lowering yet [SC2020]
```

Set `SCRIPTC_DEFERRED_REPORT=0` in the program's environment to silence these lines. A program with no deferred operations never prints them.

A deferred operation that sits in the `try` block of a `try`/`catch` in the same function is different: the program wrote a handler for exactly that statement, as in `try { optional() } catch {}`. The program doesn't print anything for such a site when it runs, so its stderr stays what it is under Node.js. The build names the site instead, with warning `SC6012` at the statement: the `catch` clause runs even where the statement would have completed under Node.js, so the program's output can differ from there on. Set `SCRIPTC_DEFERRED_REPORT=all` to have the program report these sites at run time too. A site in a function that the `try` block calls, in a `catch` or `finally` block, or in a `try` that has only a `finally`, is reported at run time like any other. A library built with `scriptc build --lib` doesn't print them either: a deferred operation in a library throws its error, and nothing is written to the host process's stderr.

## Fix a package's types

scriptc trusts declared types: a package's `.d.ts` files, its `@types/` package, and the JSDoc types in its JavaScript. A value that contradicts its declared type throws a `TypeError` instead of flowing on, so a package whose declarations are wrong can stop a program that runs under Node.js. A common case is a member declared `string` or `number` that is sometimes `undefined`.

When the type comes from a package's declarations, the error names the declaration and the patch that would admit the value:

```text
Uncaught TypeError: expected string at $, got undefined
    profilekit@3.1.0 declares Profile.email as string (index.d.ts:7)
    if that declaration is wrong, patch it: scriptc types patch profilekit "Profile.email" "| undefined"
```

`scriptc coverage` lists the declarations your program trusts before it runs, each with the command that patches it; see [type claims](/docs/coverage#type-claims).

Fix such a declaration in your project with a type patch. `scriptc types patch` shows what a declaration path matches in the installed package and adds the patch to your `package.json`:

```console
$ scriptc types patch commander "HelpContext.helpWidth" "| undefined"
commander 15.0.0 (node_modules/commander)
  lib/command.js:2481  number  ->  number | undefined
wrote package.json scriptc.typePatches["commander@^15.0.0"].declarations["HelpContext.helpWidth"]
```

The patch it writes looks like this:

```json title="package.json"
{
  "scriptc": {
    "typePatches": {
      "commander@^15.0.0": {
        "declarations": {
          "HelpContext.helpWidth": "| undefined"
        }
      }
    }
  }
}
```

Each key names a package and a semver range of the installed versions the patch describes. `declarations` maps a declaration to a type:

<dl>
  <dt>
    `"| undefined"`
  </dt>

  <dd>
    A type that starts with

    `|`

    widens the declared type, here to

    `number | undefined`

    .
  </dd>

  <dt>
    `"string | null"`
  </dt>

  <dd>
    Any other type replaces the declared type.
  </dd>
</dl>

A declaration is named by its path: `Options.name` for a member of a type, `parse()` for a function's return type, `parse(text)` for a parameter, `list()[]` for an array's elements, and `Env[string]` for an index signature. `export=()` names the return type of the function the module exports, `default` its default export, and a file prefix such as `index.js:parse()` names a declaration inside one of the package's files. One patch applies to the package's own declarations, its JSDoc, and its `@types/` package. The patched type then holds everywhere, so your code has to handle the `undefined` that a widened declaration allows. An optional `"note"` string can link the upstream issue.

For larger fixes, replace a whole declaration file, or give a package without declarations its own:

```json title="package.json"
{
  "scriptc": {
    "typePatches": {
      "some-client@^3.2.0": {
        "files": { "dist/types/options.d.ts": "./type-patches/options.d.ts" }
      },
      "untyped-helper@1.x": {
        "types": "./type-patches/untyped-helper.d.ts"
      }
    }
  }
}
```

`files` maps a declaration file inside the package, or inside its `@types/` package written as `@types/<name>/<path>`, to a local `.d.ts` file. `types` supplies the package's declaration entry. Paths of local files are relative to the `package.json`, and a replaced file can import the package's other declaration files with the same relative paths as the file it replaces.

Patches change only declarations, never the package's code, and values are still checked against the patched types. A build warns when a patch doesn't apply as written: when no range covers the installed version, when a declaration path matches nothing, when the declaration already has the type, or when a path is ambiguous. Report wrong declarations to the package too, so the patch can be removed later.

## Provenance sources (experimental)

`--provenance-sources` compiles a package from the source at the commit its npm provenance attestation names, instead of from its published JavaScript. This lets packages written in TypeScript compile from their original TypeScript. Packages without a usable attestation compile from their installed files as usual.

## Upgrading from scriptc 0.2

scriptc 0.2 had a `--dynamic` mode that embedded a JavaScript engine in the executable to run code that didn't compile, including packages that failed native compilation. scriptc 0.3 removed `--dynamic` and the embedded engine, so all application and package code now compiles to native code.

If you built with `--dynamic`, remove the flag and run `scriptc coverage` on your entry file. The report lists the operations and packages that need a change; rewrite those sites or replace the package. If you link [native program objects](/docs/native-objects) into your own builds, use the runtime pack that matches the 0.3 compiler.

---

For a semantic overview of all documentation, see [/sitemap.md](/sitemap.md)

For an index of all available documentation, see [/llms.txt](/llms.txt)

For agent-facing discovery, including API and MCP surfaces, see [/agents.md](/agents.md)

---
title: Dual-Run Testing
url: "https://scriptc.dev/docs/dual-run"
docs_index: /llms.txt
lastUpdated: 2026-11-12
---

> For an index of all documentation, see [/llms.txt](/llms.txt).

Compiled tests should report what the same tests report under Node.js. Dual-run testing runs each test file both ways, once under Node.js and once as the executable scriptc builds, compares the two runs, and fails when they differ. It is opt-in: it runs only when you add the reporter or call the program API. Run it in CI, where a divergence fails the change that introduced it rather than waiting for a user to notice it.

Add both packages as development dependencies:

```console
$ npm install --save-dev scriptc @scriptc/experimental
```

Dual-run builds through the `scriptc` command and its executable cache, so the second run of an unchanged file reuses the executable it built the first time. It uses the command named by `SCRIPTC_BIN` when that is set, otherwise the nearest `node_modules/.bin/scriptc` from the working directory upward, otherwise `scriptc` on `PATH`.

## Run a suite with the reporter

Pass the reporter to the Node.js test runner in place of its own. For every test file the runner finds, the reporter builds the file, runs the binary, and compares the two runs. It prints one line per file, shaped like this run on a suite of four files, one of which fails an assertion, one whose report differs, and one whose Node.js output varies between runs:

```console
$ node --test --test-reporter=@scriptc/experimental/dual-run/reporter --test-reporter-destination=stdout
✖ test/format.test.ts (1 passed, 1 failed)
  ✖ formats thousands: Expected values to be strictly equal: (line 8)
✔ test/parse.test.ts (2 passed)
✖ test/point.test.ts "reports a thrown point": native stdout differs at line 15
@@ stdout line 15
   
   test at test/point.test.ts:12:1
   ✖ reports a thrown point (Xms)
 -   Point { x: 1, y: 2 }
 +   [object Object]
   
ℹ test/watch.test.ts: the Node run differs between two runs (flaky), not compared
```

- A `✔` line means every test in the file behaves the same in both runs.
- A `✖` line that names a test and shows a diff means the compiled binary's output differs from Node.js, and the diff starts at the line where it does.
- A `✖` line with test counts means Node.js reported a failed test in the file. The failing tests and their messages follow, so the report explains the exit code on its own. A test declared in a module the file imports counts toward the file Node.js ran, so its failure appears on that file's line and the imported module is not compared as a file of its own.
- A `✖` line without counts is a file Node.js itself could not run, for example because its TypeScript loader rejects the file's syntax; the failure Node.js reported follows, and the file is still compared.
- An `ℹ` line marks a file whose output differed between two Node.js runs. The reporter does not blame the binary for that, and the comparison does not fail the run.

The runner's exit code is nonzero when a file's reports differ or when a test fails. Files that fail to build are reported with the compiler's diagnostics and also fail the run.

The report does not repeat the test files' own output. Program output appears in a diff when the two runs differ; for the full Node.js report, run the file with `node` directly.

## Compare one file from a test runner

Vitest and jest run JavaScript on Node.js, so scriptc cannot compile them, but they can call the program API. `dualRun` runs one file twice and returns both runs:

```ts title="tests/dual-run.test.ts"
import { expect, test } from "vitest";
import { dualRun } from "@scriptc/experimental/dual-run";

test("compiled serialization matches Node.js", async () => {
  const result = await dualRun("test/serialize.test.ts");
  expect(result.diff).toBe("");
  expect(result.equal).toBe(true);
});
```

A failing comparison keeps the first difference in `result.diff`, so `expect(result.diff).toBe("")` prints it in the test failure report. When the file does not compile, `dualRun` rejects with a `DualRunBuildError` whose `stderr` holds the compiler's diagnostics.

## What is compared

Both runs render a Node.js spec report, and the two reports are compared as text, together with the program's output and exit code. The differences that are inherent to the two lanes are normalized before the comparison:

- a duration in a test result line or in the suite summary takes a placeholder, because the runners measure different times;
- stack frames, the `N lines matching cause stack trace` fold and the property block that follows them are dropped, because they name runner-internal code and objects;
- stderr is compared only when both runs exit 0, because an uncaught error's report format differs between the two (stdout, the report and the exit code are still compared).

Everything else must match: test names, pass, fail, skip and todo results, assertion messages, the failing-tests section, and the program's own stdout and stderr.

Both lanes run with `TZ=UTC` and `LANG=C`, so dates and collation do not depend on the machine. They also run without `NODE_TEST_CONTEXT`, which `node --test` sets for each test file it starts and which would make the Node.js lane answer with binary test events instead of the spec report. The Node.js lane also runs with `--experimental-transform-types`, which a compiled program does not need, and with `--disallow-code-generation-from-strings`, the same refusal compiled programs apply to `eval` and `Function`. Node's warnings about the transform flag and about a `.ts` file in a package that does not set `"type": "module"` are turned off, because they carry content the binary never prints, the process id included.

## API

<dl>
  <dt>
    <code>dualRun(entry, options?)</code>
  </dt>

  <dd>
    Runs

    <code>entry</code>

    under Node.js and as its compiled executable and returns a promise of

    <code />

    . Each run has

    <code>stdout</code>

    ,

    <code>stderr</code>

    and

    <code>exitCode</code>

    (

    <code>null</code>

    when a signal ended it).

    <code>equal</code>

    is true when the runs match after the normalization above, and

    <code>diff</code>

    is the first difference as readable text,

    <code>""</code>

    when there is none. Rejects with a

    <code>DualRunBuildError</code>

    when the program does not compile.
  </dd>

  <dt>
    <code>options.args</code>
  </dt>

  <dd>
    Arguments passed to both runs.
  </dd>

  <dt>
    <code>options.stdin</code>
  </dt>

  <dd>
    A string or

    <code>Uint8Array</code>

    written to both runs' standard input, which is closed afterwards.
  </dd>

  <dt>
    <code>options.env</code>
  </dt>

  <dd>
    Variables added to both runs' environment. An entry set to

    <code>undefined</code>

    removes the variable.
  </dd>

  <dt>
    <code>options.cwd</code>
  </dt>

  <dd>
    Working directory of both runs and of the build. Defaults to the current working directory.
  </dd>

  <dt>
    <code>options.timeoutMs</code>
  </dt>

  <dd>
    Bound on each run, in milliseconds. Defaults to 120000.
  </dd>

  <dt>
    <code>new DualRunBuildError()</code>
  </dt>

  <dd>
    The build's

    <code>entry</code>

    ,

    <code>cli</code>

    ,

    <code>stderr</code>

    and

    <code>exitCode</code>

    , and the same text in

    <code>message</code>

    .
  </dd>
</dl>

## Limits

Dual-run builds native executables, so a suite that runs as WebAssembly under `scriptc run` is not covered, and a compiled program cannot import either module: they spawn processes, so they run under Node.js only. A program whose own Node.js run varies is reported as flaky and not compared, so a suite that depends on timing or on the network needs those inputs deterministic before dual-run can say anything about it. The comparison is also blind to a difference that the normalization removes, such as a duration or a stack frame, and to stderr of a run that exits nonzero.

---

For a semantic overview of all documentation, see [/sitemap.md](/sitemap.md)

For an index of all available documentation, see [/llms.txt](/llms.txt)

For agent-facing discovery, including API and MCP surfaces, see [/agents.md](/agents.md)

---
title: Experimental APIs
url: "https://scriptc.dev/docs/experimental"
docs_index: /llms.txt
lastUpdated: 2026-11-12
---

> For an index of all documentation, see [/llms.txt](/llms.txt).

The `@scriptc/experimental` package holds APIs that are still being designed. Each export has a Node.js implementation, so a program that uses them runs unchanged under Node.js, and scriptc compiles them into the program. Their behavior may change between releases.

Add `@scriptc/experimental` to your project's dependencies.

## LifetimeRegion

Instances of [`ProgramLifetime`](/docs/contracts#programlifetime) classes are never freed, which suits a program that builds one syntax tree or symbol table and exits. A long-running program, such as a compiler in watch mode, builds a new generation of these objects on every rebuild. A `LifetimeRegion` lets it free each generation once it is no longer needed.

```ts title="rebuild.ts"
import type { ProgramLifetime } from "@scriptc/types";
import { LifetimeRegion } from "@scriptc/experimental";

class SyntaxNode implements ProgramLifetime {
  readonly children: SyntaxNode[] = [];
  parent: SyntaxNode | undefined = undefined;
}

function parse(depth: number): SyntaxNode {
  const node = new SyntaxNode();
  for (let i = 0; depth > 0 && i < 4; i++) {
    const child = parse(depth - 1);
    child.parent = node;
    node.children.push(child);
  }
  return node;
}

function count(node: SyntaxNode): number {
  return node.children.reduce((sum, child) => sum + count(child), 1);
}

let previous: LifetimeRegion | undefined;
for (let build = 1; build <= 3; build++) {
  const region = new LifetimeRegion();
  const tree = region.run(() => parse(8));
  console.log(`build ${build}: ${count(tree)} nodes`);
  // The previous build's tree is no longer referenced.
  previous?.release();
  previous = region;
}
```

Under Node.js, `run` calls its callback and `release` does nothing:

```console
$ node rebuild.ts
build 1: 87381 nodes
build 2: 87381 nodes
build 3: 87381 nodes
```

The scriptc executable prints the same. With `SCRIPTC_REGION_STATS=1` it also reports each region it frees:

```console
$ scriptc build rebuild.ts -o rebuild >/dev/null
$ SCRIPTC_REGION_STATS=1 ./rebuild
build 1: 87381 nodes
build 2: 87381 nodes
scriptc region 1: freed 87381 objects, 87381 owned objects, 2 chunks (4.0 MiB); cycles 0.0 ms, walk 14.9 ms, free 2.6 ms
build 3: 87381 nodes
scriptc region 2: freed 87381 objects, 87381 owned objects, 2 chunks (4.0 MiB); cycles 0.0 ms, walk 8.9 ms, free 2.4 ms
scriptc regions: 3 created, 2 released, 2 freed, 0 pinned, 0 kept
scriptc region heap passes: 0
```

### API

<dl>
  <dt>
    <code>new LifetimeRegion()</code>
  </dt>

  <dd>
    Creates a region. Creating a region also retries the released regions that could not be freed yet.
  </dd>

  <dt>
    <code>region.run(fn)</code>
  </dt>

  <dd>
    Calls

    <code>fn</code>

    with the region as the current region and returns its result.

    <code>ProgramLifetime</code>

    instances created during the call belong to the region, and so do those that async functions called during it create after an

    <code>await</code>

    (see

    <a href="#async-work">Async work</a>

    ). Calls nest: a

    <code>run</code>

    inside the callback makes another region current until it returns, and the previous region is current again however the callback finishes, including when it throws.
  </dd>

  <dt>
    <code>region.release()</code>
  </dt>

  <dd>
    Requests that the region be freed. scriptc frees it as soon as nothing outside the region refers to any of its instances, which may be at once, at a later release or region creation, or never. A region is never freed while a

    <code>run</code>

    call on it is active, or while async work started inside it has not finished. After

    <code>release()</code>

    ,

    <code>run</code>

    still calls its callback, and the instances it creates belong to the default region.
  </dd>
</dl>

`ProgramLifetime` instances created outside every `run` call belong to the default region, which is never freed. Ordinary objects (instances of classes without the contract, arrays, maps, closures) are reference counted as always; a region frees the ones that only its instances refer to.

### When a region is freed

When a region is released, scriptc checks that every reference to its instances comes from the region itself: from its own instances, or from ordinary objects that only the region refers to. It then frees the instances and those ordinary objects together, and removes the `WeakMap` and `WeakSet` entries keyed by the freed instances.

A released region stays allocated, and its instances stay usable, while anything else refers to one of them:

- a variable, a module variable, or a local of a function that is still running;
- an ordinary object that something outside the region refers to, including an array, `Map` or `Set` element and a closure's captured variable;
- an instance of another region, until that region is freed;
- an instance of the default region, which is never freed;
- a promise, generator or `Error` that holds an instance, until it is no longer referenced.

### Async work

An async function called inside `run` keeps the region for its whole run: the instances it creates after each `await` belong to the region too, as do those of the async functions it calls. A server can give every request its own region, even while requests interleave at their awaits:

```ts title="requests.ts"
import type { ProgramLifetime } from "@scriptc/types";
import { LifetimeRegion } from "@scriptc/experimental";

class Part implements ProgramLifetime {
  readonly text: string;
  previous: Part | undefined = undefined;
  constructor(text: string) {
    this.text = text;
  }
}

const read = (): Promise<void> => new Promise((resolve) => setImmediate(resolve));

async function handle(id: number): Promise<string> {
  let last: Part | undefined;
  for (let i = 0; i < 1000; i++) {
    if (i % 100 === 0) await read(); // the next chunk of the request body
    const part = new Part(`${id}.${i}`);
    part.previous = last;
    last = part;
  }
  return `request ${id}: last part ${last!.text}`;
}

async function serve(id: number): Promise<string> {
  const region = new LifetimeRegion();
  try {
    return await region.run(() => handle(id));
  } finally {
    region.release();
  }
}

Promise.all([1, 2, 3].map(serve)).then((responses) => {
  for (const response of responses) console.log(response);
});
```

Each request's 1000 parts are freed when the request releases its region:

```console
$ scriptc build requests.ts -o requests >/dev/null
$ SCRIPTC_REGION_STATS=1 ./requests >/dev/null
scriptc region 1: freed 1000 objects, 0 owned objects, 1 chunks (2.0 MiB); cycles 0.0 ms, walk 0.0 ms, free 0.0 ms
scriptc region 2: freed 1000 objects, 0 owned objects, 1 chunks (2.0 MiB); cycles 0.0 ms, walk 0.0 ms, free 0.0 ms
scriptc region 3: freed 1000 objects, 0 owned objects, 1 chunks (2.0 MiB); cycles 0.0 ms, walk 0.0 ms, free 0.0 ms
scriptc regions: 3 created, 3 released, 3 freed, 0 pinned, 0 kept
scriptc region heap passes: 0
```

A region released while async work started inside it is still pending stays allocated until that work has finished, and the work keeps creating its instances in the region. Callbacks that the event loop calls (timers, `queueMicrotask`, event listeners) and the bodies of async generators run outside the region; synchronous generators run in the region of the code that resumes them.

### Threads

Each thread has its own regions and its own default region. A region belongs to the thread that created it: on another thread, `run` makes that thread's default region current and `release` does nothing. A region that holds an instance published with [`@scriptc/threads`](/docs/threads) is never freed, because other threads may refer to it.

### Cost

Programs that never construct a `LifetimeRegion` compile exactly as they would without the package. In programs that do, every retain and release of a `ProgramLifetime` instance that belongs to a region updates a count of the region chunk that holds it. Instances of the default region are not counted, and until the program creates its first region, a retain or release only tests whether counting has started.

Freeing a region visits each of its instances and the ordinary objects they own. Each thread keeps up to 8 MiB of the memory of freed regions for its next regions, so a program that frees a region per request or per batch does not map and fault in new pages every time. A region that the check would keep may be kept only by unreachable cycles of ordinary objects that refer into it, so before keeping it scriptc runs the cycle collector over the thread's heap and checks again.

`SCRIPTC_REGION_STATS=1` prints one line per release attempt, freed, `kept` (with the number of references the check could not explain) or `pinned` (published), and at exit a summary per thread and the number of those cycle-collector passes over the thread's heap.

## InternTable

Programs that run the same work on several [worker threads](/docs/threads) often build the same cache on every thread: interned types, strings, resolved modules. An `InternTable` is one named table of [published](/docs/threads) values that every thread of a scriptc program shares, so each value is created once and every thread reads the same object.

```ts title="intern.ts"
import { Worker, isMainThread, parentPort } from "node:worker_threads";
import { InternTable } from "@scriptc/experimental";

class Shape {
  readonly name: string;
  readonly parts: readonly Shape[];
  constructor(name: string, parts: readonly Shape[]) {
    this.name = name;
    this.parts = parts;
  }
}

// Module scope: every thread that evaluates this line opens the same table.
const shapes = new InternTable<string, Shape>("example.shapes");

function shape(name: string, parts: readonly Shape[] = []): Shape {
  return shapes.getOrInsertComputed(name, () => new Shape(name, parts));
}

function build(): string {
  const point = shape("point");
  const line = shape("line", [point, point]);
  const square = shape("square", [line, line, line, line]);
  return `${square.name}: ${square.parts.length} lines, ${shapes.size} shapes`;
}

if (isMainThread) {
  for (let i = 0; i < 2; i++) {
    new Worker(new URL(import.meta.url)).on("message", (text: string) => console.log(text));
  }
} else {
  parentPort!.postMessage(`worker: ${build()}`);
}
```

Each thread that evaluates `new InternTable(...)` opens the same table. Under Node.js every thread has its own table, so each worker builds its own three shapes; in the scriptc executable both workers use one table and read the same shape objects. Both print the same:

```console
$ node intern.ts
worker: square: 4 lines, 3 shapes
worker: square: 4 lines, 3 shapes
$ scriptc build intern.ts -o intern >/dev/null
$ ./intern
worker: square: 4 lines, 3 shapes
worker: square: 4 lines, 3 shapes
```

### Methods

<dl>
  <dt>
    <code>new InternTable\<K, V>(name)</code>
  </dt>

  <dd>
    Opens the table called

    <code>name</code>

    . Within one thread every construction with the same name refers to the same table. In scriptc builds every thread of the process shares it; under Node.js each thread has its own. Keys are strings, numbers, or arrays of numbers or of strings. Strings and numbers compare like

    <code>Map</code>

    keys; arrays compare by their elements in the same way and can have at most 65536 elements.
  </dd>

  <dt>
    <code>table.get(key)</code>

    ,

    <code>table.has(key)</code>

    ,

    <code>table.size</code>
  </dt>

  <dd>
    Read the table.
  </dd>

  <dt>
    <code>table.getOrInsert(key, value)</code>
  </dt>

  <dd>
    Returns the value stored under

    <code>key</code>

    . When there is none, publishes

    <code>value</code>

    , stores it and returns it.
  </dd>

  <dt>
    <code>table.getOrInsertComputed(key, callback)</code>
  </dt>

  <dd>
    Returns the value stored under

    <code>key</code>

    . When there is none, calls

    <code>callback(key)</code>

    , publishes its result and stores it, unless a value was stored under

    <code>key</code>

    meanwhile (by the callback, or in scriptc builds by another thread), and returns the stored value.
  </dd>
</dl>

A key's value never changes once stored, and there is no `set` or `delete`. Stored values and everything they reach are immutable in both runtimes. A value that `publish` refuses makes the call throw the same `TypeError` and stores nothing.

scriptc requires the name to be a string literal and every construction of one name to use the same type arguments, since all threads share the table's layout. Values are numbers, booleans, strings, bigints, arrays, `Map`s, `Set`s, objects and class instances.

### Thread behavior

No lock is held while a callback runs. Two threads that miss the same key at the same time may both call the callback; one result is stored, and both calls return it. A program behaves the same under scriptc and Node.js when:

1. A stored value does not depend on which thread created it, for example because the callback computes it from the key and other stored values.
2. It does not compare identities or ids across threads.
3. A thread does not depend on another thread's inserts: `size` and `has` reflect every thread's inserts in scriptc and the calling thread's under Node.js.

State that a thread computes about a shared value later (lazily resolved members, caches) belongs in thread-local state, such as a [`ThreadSlots`](#threadslots) family, a module-level `Map` keyed by the value or an array indexed by an id the value carries: module state is per thread in both runtimes. In scriptc an array indexed by a dense id is the fastest of these.

### Memory

Lookups take no lock and write nothing. Inserts lock one of 64 segments of the table for a probe and a few stores. Stored values are immortal in scriptc builds: they stay allocated until the process exits, including a value a callback computed for a key that another thread stored first. Programs that never construct an `InternTable` do not run any of its code.

## ThreadSlots

Threads that share values through an `InternTable` often compute more about each shared value later: resolved members, cached results, flags. That state differs per thread, so it cannot live in the shared value, which is immutable. A `ThreadSlots` family holds one value per key and thread, like a `WeakMap` that each thread has its own copy of. A thread sees only the values it set, including for keys that every thread shares.

```ts title="slots.ts"
import { Worker, isMainThread, parentPort, threadId } from "node:worker_threads";
import { InternTable, ThreadSlots } from "@scriptc/experimental";

class Shape {
  readonly name: string;
  constructor(name: string) {
    this.name = name;
  }
}

class ShapeState {
  uses = 0;
  readonly thread: number;
  constructor(thread: number) {
    this.thread = thread;
  }
}

const shapes = new InternTable<string, Shape>("example.slot-shapes");
// Module scope: every thread that evaluates this line opens the same family.
const states = new ThreadSlots<Shape, ShapeState>("example.shape-states");

function use(name: string): ShapeState {
  const shape = shapes.getOrInsertComputed(name, () => new Shape(name));
  let state = states.get(shape);
  if (state === undefined) {
    state = new ShapeState(threadId);
    states.set(shape, state);
  }
  state.uses++;
  return state;
}

if (isMainThread) {
  for (let i = 0; i < 2; i++) {
    new Worker(new URL(import.meta.url)).on("message", (text: string) => console.log(text));
  }
} else {
  use("square");
  const state = use("square");
  parentPort!.postMessage(`worker: ${state.uses} uses, own state ${state.thread === threadId}`);
}
```

In the scriptc executable both workers read the same `Shape` object from the table, and each keeps its own `ShapeState` for it. Both runtimes print the same:

```console
$ node slots.ts
worker: 2 uses, own state true
worker: 2 uses, own state true
$ scriptc build slots.ts -o slots >/dev/null
$ ./slots
worker: 2 uses, own state true
worker: 2 uses, own state true
```

### Slot methods

<dl>
  <dt>
    <code>new ThreadSlots\<K, V>(name)</code>
  </dt>

  <dd>
    Opens the family called

    <code>name</code>

    . Within one thread every construction with the same name refers to the same family.
  </dd>

  <dt>
    <code>slots.get(key)</code>

    ,

    <code>slots.has(key)</code>
  </dt>

  <dd>
    Read the calling thread's value for

    <code>key</code>

    .
  </dd>

  <dt>
    <code>slots.set(key, value)</code>
  </dt>

  <dd>
    Sets the calling thread's value for

    <code>key</code>

    and returns the slots. It never writes the key, so it works on published keys.
  </dd>

  <dt>
    <code>slots.delete(key)</code>
  </dt>

  <dd>
    Removes the calling thread's value for

    <code>key</code>

    and returns whether there was one.
  </dd>
</dl>

Under Node.js each family is a `WeakMap` per thread.

### Keys and values

scriptc requires:

- the name to be a string literal, and every construction of one name to use the same type arguments;
- the key type to be a class declared in the program; keys may be instances of its subclasses;
- the value type to be a class, an object type, an array, a `Map` or a `Set`;
- the methods to be called through a module-level `const` initialized with `new ThreadSlots<K, V>("name")`, as in the example. A family's storage is found by its name when the program is compiled.

### Slot cost

Each instance of a key class, and of every class in its hierarchy, carries a 4-byte slot index, 0 until a value is set for it. The first `set` for a key assigns the index; indices are never reused. `get` and `has` read the index, a page of the thread's family and the entry, with no lock, hash or atomic update. A family stores values in pages of 1024 entries, allocated as the thread sets values in them.

A family holds a reference to each of the thread's values. A value is released when it is replaced or deleted, when its key is freed, and when the thread exits. Keys that are freed with a [`LifetimeRegion`](#lifetimeregion) keep their values until the thread exits, a value that belongs to a region keeps the region allocated while the family holds it, and a value that refers to its own key keeps the key alive. Programs that never construct a `ThreadSlots` do not run any of its code, and their classes have no slot index.

## InlineArray

`InlineArray<T>` marks an array field of a [`ProgramLifetime`](/docs/contracts#programlifetime) class whose elements scriptc keeps in the instance itself, after its other fields, instead of in an array object of their own. It suits a program with millions of short arrays that never change once built, such as the children of each node of a syntax tree: an ordinary array costs a separate allocation with a header, and a reference to it in the instance.

```ts title="lists.ts"
import type { ProgramLifetime } from "@scriptc/types";
import type { InlineArray } from "@scriptc/experimental";

class Token implements ProgramLifetime {
  readonly text: string;
  constructor(text: string) {
    this.text = text;
  }
}

class TokenList implements ProgramLifetime {
  readonly pos: number;
  readonly tokens: InlineArray<Token>;
  constructor(pos: number, tokens: readonly Token[]) {
    this.pos = pos;
    this.tokens = tokens;
  }
}

function width(list: TokenList): number {
  let total = 0;
  for (const token of list.tokens) total += token.text.length;
  return total;
}

const list = new TokenList(0, ["let", "x", "=", "1"].map((text) => new Token(text)));
console.log(list.tokens.length, width(list), list.tokens[0]!.text);
console.log(list.tokens.map((token) => token.text).join(" "));
```

Both runtimes print the same:

```console
$ node lists.ts
4 6 let
let x = 1
$ scriptc build lists.ts -o lists >/dev/null
$ ./lists
4 6 let
let x = 1
```

### Field rules

scriptc requires:

- `T` to be a `ProgramLifetime` class, and the field to be an instance field of a `ProgramLifetime` class;
- the constructor of the class that declares the field to write it once, from one of its parameters. A subclass constructor passes one of its own parameters there through `super(...)`. Nothing else writes the field;
- the array passed to the constructor to be one that nothing else keeps: a new array (an array literal, a copy such as `slice()`, `[...items]` or `Array.from(items)`, or the result of a function that returns only such arrays or its own argument), a local array at its last use (calls that only read it or add to it may come first, and a loop may pass it and then assign a new array to the local), a parameter whose callers each pass such an array, an array that an InlineArray field already holds, or a `const` array that is never written, such as a shared empty array;
- the arrays fields hold never to be written, through a read of the field (which the `readonly` type allows only through a cast) or through any other reference. scriptc follows these arrays through locals, parameters, return values, fields, the elements of arrays, Maps and Sets, and closures, and reports one that reaches a write, a dynamic value (also inside the fields of an object that becomes one), a call through a function value (also inside an array, Map or Set passed there), or a library call other than printing and `JSON.stringify` without a replacer. The report names the field read the array may come from;
- the arrays fields hold, and their reads, not to be compared by identity: with `===` or `!==`, as an `indexOf` or `includes` argument, or as a Map key or Set element. Two reads of the field need not give the same array. Compare the elements instead, or keep a copy made with `slice()` where identity matters;
- instances of the class not to be constructed through a class value. They may become dynamic values, for example through a conversion to `unknown` or an interface several classes implement, when the dynamic code that sees them uses the class's own members and does not read the field: scriptc follows such values through locals, parameters, return values, fields, the elements of arrays, Maps and Sets, the members of dynamic objects and arrays, calls through function values, and the callbacks of array methods such as `map` and `forEach`, and finds the first that reaches a read or write of the field's name or of a computed key, a checked cast to a record, array or collection type that has a record member of the field's name or holds a dynamic value, a call through a value no compiled function reaches (also as an array method's callback), or a library call other than printing, `JSON.stringify` without a replacer, `publish`, a worker's `workerData`, and class tests and casts. A dynamic view of an inline field would read it as a new array each time, so when such an operation exists the field keeps its elements in an ordinary array, which behaves as under Node.js, and scriptc reports a warning (SC6105). The report names the operation.

scriptc reports a field that does not keep these rules (SC7009).

### Reads

Indexing the field, reading its `length` and iterating it with `for...of` read the elements from the instance, directly or through a `const` local used only in those ways. Any other use, such as passing the field to a function or calling one of its methods, gets an array of the elements, which is frozen in scriptc builds. Under Node.js the field holds the array the constructor stored. The two agree because the field rules keep that array from being written.

### Inline array cost

Each element takes 4 bytes in the instance, and the field's own slot holds the count. The constructor copies the elements from the array it is passed, so that array can be released. The elements start at the same offset in every class of the hierarchy, after the largest set of fixed fields. A read that gets an array allocates a small header that refers to the instance's elements, and frees it when the read's value is released.

The elements stay in an ordinary array object, and the field behaves like any other array field, in builds with sanitizers, in programs that use [`LifetimeRegion`](#lifetimeregion), and on platforms other than 64-bit Linux and macOS.

## Load hook

`@scriptc/experimental/register` is a preload that makes Node.js refuse a program the way a scriptc build does, before the program runs. Use it while developing so `node` and `scriptc` agree on what the program may load.

```console
$ node --require @scriptc/experimental/register app.ts
```

`--import` works the same way. Node.js 24 or newer is required.

The hook applies the refusals in `@scriptc/experimental/refusals.json`, the same data the compiler uses:

- Importing a builtin module that scriptc does not support fails with the compiler's code and message (`SC1010`).
- Loading a native addon (`.node`) fails with the compiler's code and message (`SC2020`).
- `process.getBuiltinModule` of an unsupported builtin throws the error a compiled program throws for that call.

A program can therefore run under Node.js with the hook, fail where the build fails, and give the same code and message. Running both in CI catches a refusal before a binary is built or deployed.

scriptc refuses code generation from strings, which Node.js does only with `--disallow-code-generation-from-strings`. The hook cannot set the flag for the process it joins, so add it to `NODE_OPTIONS`:

```console
$ NODE_OPTIONS=--disallow-code-generation-from-strings node --require @scriptc/experimental/register app.ts
```

The hook prints a warning to stderr when the flag is missing, or when the running Node.js version differs from the release the refusal data describes.

---

For a semantic overview of all documentation, see [/sitemap.md](/sitemap.md)

For an index of all available documentation, see [/llms.txt](/llms.txt)

For agent-facing discovery, including API and MCP surfaces, see [/agents.md](/agents.md)

---
title: Native FFI
url: "https://scriptc.dev/docs/ffi"
docs_index: /llms.txt
lastUpdated: 2026-11-12
---

> For an index of all documentation, see [/llms.txt](/llms.txt).

Native FFI lets a compiled program call functions in C libraries. Use it when your program needs an existing native library, a platform API, or a small helper you write in C. You declare the function in TypeScript, describe its C signature in a JSON manifest, and scriptc links the native code into the executable at build time.

## Call a C function

Declare the function in TypeScript without a body. The declaration gives the type checker its signature:

```ts title="main.ts"
declare function nativeScale(value: number): number;

console.log(nativeScale(21));
```

Implement the function in C:

```c title="native.c"
double native_scale(double value) {
  return value * 2.0;
}
```

Connect the two names in a manifest. `params` and `returns` give the C types, and `libraries` lists the objects or archives to link, relative to the manifest:

```json title="ffi.json"
{
  "ffi_format": 1,
  "functions": [
    {
      "name": "nativeScale",
      "symbol": "native_scale",
      "params": ["f64"],
      "returns": "f64"
    }
  ],
  "libraries": ["./libnative.a"],
  "system_libraries": []
}
```

Build the C code into an archive, then pass the manifest to scriptc with `--ffi`:

```console
$ clang -c native.c -o native.o
$ ar rcs libnative.a native.o
$ scriptc build main.ts --ffi ffi.json -o app
$ ./app
42
```

Only direct calls to that exact declaration become native calls. A function with a body, an overload, a generic declaration, an alias such as `const f = nativeScale`, or a shadowing local stays an ordinary TypeScript function.

For C++, export the function with `extern "C"` so it keeps an unmangled name. Pass the same manifest to `scriptc coverage` so the native calls count as compiled.

<span id="replacing-a-node-api-addon" />

## Replace a Node-API addon

Compiled programs can't load Node-API (`.node`) addons, because those depend on Node.js itself. To keep using an addon's native code, move the operation into a plain C function and call it through FFI. If you also publish the addon for Node.js, keep the Node-API registration and value conversion in a separate wrapper.

For example, a program that calls a small numeric helper from an addon:

```ts title="addon-client.ts"
import { createRequire } from "node:module";

const require = createRequire(import.meta.url);
const native = require("./native.node") as {
  scale(value: number): number;
};

console.log(native.scale(21));
```

becomes the `nativeScale` declaration and direct call shown in [Call a C function](#call-a-c-function), with the helper's logic in `native_scale`. Renaming a `.node` file, or linking an archive that still calls `napi_*` functions, doesn't remove its dependency on Node.js. See [native addon limitations](/docs/limitations#native-addons).

## ABI classes

Each parameter and result in the manifest has an ABI class that names its C type. TypeScript has only `number`, so the manifest, not the declaration, decides whether a value crosses as a double or as an integer of a specific width.

<table>
  <thead>
    <tr>
      <th>
        Manifest class
      </th>

      <th>
        TypeScript type
      </th>

      <th>
        C ABI type and behavior
      </th>

      <th>
        Parameter
      </th>

      <th>
        Return
      </th>
    </tr>
  </thead>

  <tbody>
    <tr>
      <td>
        <code>f64</code>
      </td>

      <td>
        <code>number</code>
      </td>

      <td>
        <code>double</code>
      </td>

      <td>
        yes
      </td>

      <td>
        yes
      </td>
    </tr>

    <tr>
      <td>
        <code>f32</code>

        (format 6)
      </td>

      <td>
        <code>number</code>
      </td>

      <td>
        <code>float</code>

        ; rounds to IEEE-754 single precision on the native boundary, widens back to a JavaScript number on return
      </td>

      <td>
        yes
      </td>

      <td>
        yes
      </td>
    </tr>

    <tr>
      <td>
        <code>bool</code>
      </td>

      <td>
        <code>boolean</code>
      </td>

      <td>
        <code>uint8\_t</code>

        ; inputs are 0 or 1, any nonzero return becomes

        <code>true</code>
      </td>

      <td>
        yes
      </td>

      <td>
        yes
      </td>
    </tr>

    <tr>
      <td>
        <code>u8</code>
      </td>

      <td>
        <code>number</code>
      </td>

      <td>
        <code>uint8\_t</code>

        ; inputs use JavaScript's modulo conversion
      </td>

      <td>
        yes
      </td>

      <td>
        yes
      </td>
    </tr>

    <tr>
      <td>
        <code>u32</code>
      </td>

      <td>
        <code>number</code>
      </td>

      <td>
        <code>uint32\_t</code>

        ; inputs use

        <code>ToUint32</code>
      </td>

      <td>
        yes
      </td>

      <td>
        yes
      </td>
    </tr>

    <tr>
      <td>
        <code>i8</code>

        ,

        <code>u16</code>

        ,

        <code>i16</code>

        (format 6)
      </td>

      <td>
        <code>number</code>
      </td>

      <td>
        <code>int8\_t</code>

        ,

        <code>uint16\_t</code>

        ,

        <code>int16\_t</code>

        ; modulo conversion like the corresponding JavaScript typed array
      </td>

      <td>
        yes
      </td>

      <td>
        yes
      </td>
    </tr>

    <tr>
      <td>
        <code>i32</code>
      </td>

      <td>
        <code>number</code>
      </td>

      <td>
        <code>int32\_t</code>

        ; inputs use

        <code>ToInt32</code>
      </td>

      <td>
        yes
      </td>

      <td>
        yes
      </td>
    </tr>

    <tr>
      <td>
        <code>cstring</code>
      </td>

      <td>
        <code>string</code>
      </td>

      <td>
        <code>const char \*</code>

        ; callback input only, copied through lossy UTF-8 decoding
      </td>

      <td>
        callback only (formats 3–7)
      </td>

      <td>
        no
      </td>
    </tr>

    <tr>
      <td>
        <code>string</code>
      </td>

      <td>
        <code>string</code>
      </td>

      <td>
        <code>const uint8\_t \*, size\_t</code>

        ; UTF-8 bytes, length-delimited
      </td>

      <td>
        yes; callback input in formats 3–7
      </td>

      <td>
        no
      </td>
    </tr>

    <tr>
      <td>
        <code>bytes</code>
      </td>

      <td>
        <code>Uint8Array</code>

        or

        <code>Buffer</code>
      </td>

      <td>
        <code>const uint8\_t \*, size\_t</code>

        ; raw bytes, length-delimited
      </td>

      <td>
        yes; callback input in formats 3–7
      </td>

      <td>
        no
      </td>
    </tr>

    <tr>
      <td>
        <code>void</code>
      </td>

      <td>
        <code>void</code>
      </td>

      <td>
        <code>void</code>
      </td>

      <td>
        no
      </td>

      <td>
        yes
      </td>
    </tr>

    <tr>
      <td>
        <code>mutable-bytes</code>

        (format 6)
      </td>

      <td>
        <code>Uint8Array</code>

        or

        <code>Buffer</code>
      </td>

      <td>
        <code>uint8\_t \*, size\_t</code>

        ; a writable borrow of the exact view, including its byte offset and length
      </td>

      <td>
        outbound calls only
      </td>

      <td>
        no
      </td>
    </tr>
  </tbody>
</table>

For ordinary outbound parameters, string and byte pointers are borrowed only for the duration of the call. Native code must not mutate <code>string</code> or <code>bytes</code> spans. Format 6's <code>mutable-bytes</code> permits in-place writes within the supplied length; mutations are visible through the original array and its aliases. Native code must not free or retain any borrowed pointer. Strings may contain embedded NUL bytes, so always use the supplied length; an empty span may have a null pointer. String and byte returns remain unsupported because they need an explicit ownership and allocator contract. Format 7 can return an opaque pointer as a bigint; scriptc does not infer its lifetime, allocation size, or deallocator.

Set <code>ffi\_format</code> to <code>6</code> to use the additional scalar widths or writable spans. The new scalar classes also work in call-scoped, retained, and foreign-thread callback parameters, and in same-thread callback returns. Foreign-thread delivery widens their values before queueing them without losing precision. Formats 1–5 retain their original class sets.

Set <code>ffi\_format</code> to <code>7</code> for <code>i64</code>, <code>u64</code>, and <code>pointer</code>. These classes use TypeScript <code>bigint</code> and the C types <code>int64\_t</code>, <code>uint64\_t</code>, and <code>void \*</code>. Integer inputs use the low 64 bits; pointer inputs use the low pointer-width bits. Results preserve every bit without conversion through a JavaScript number. They work in direct calls and same-thread callbacks, including retained callbacks. Foreign-thread callback parameters using these classes remain refused.

For operating-system helpers, wrap the system call in a fixed-signature C function too. For example, Linux's variadic `prctl` needs a wrapper that supplies its operation and trailing arguments. A wrapper returning C `int32_t` uses the manifest's `i32` return class and a TypeScript `number`; check its error result in TypeScript before using the value. Build the object or archive for the same target as the scriptc binary.

## Callbacks and context pointers

Many C APIs take a function pointer and an opaque `void *` context that they pass back to the callback. Format 2 lets you pass a TypeScript function in that position. The manifest describes the function pointer and the context as separate entries, each in its actual position in the C signature.

For example, this C function takes a callback first, a value second, and its context last. The callback itself receives the context last:

```c title="native.c"
typedef double (*map_callback)(double value, void *context);

double native_map(map_callback callback, double value, void *context) {
  return callback(value, context);
}
```

The TypeScript declaration contains only source values. Context entries are supplied by the compiler, so there is no context parameter in TypeScript:

```ts title="main.ts"
declare function nativeMap(
  callback: (value: number) => number,
  value: number,
): number;

const offset = 7;
console.log(nativeMap((value) => value + offset, 5));
```

The callback id connects the two independently positioned context entries. Both positions are explicit; no adjacency or conventional argument order is assumed.

```json title="ffi.json"
{
  "ffi_format": 2,
  "functions": [
    {
      "name": "nativeMap",
      "symbol": "native_map",
      "params": [
        {
          "callback": {
            "id": "map",
            "params": ["f64", { "context": "map" }],
            "returns": "f64",
            "lifetime": "call"
          }
        },
        "f64",
        { "context": "map" }
      ],
      "returns": "f64"
    }
  ],
  "libraries": ["./libnative.a"]
}
```

A callback descriptor consumes one TypeScript function parameter and one native function-pointer slot. A context entry consumes no TypeScript parameter and one native `void *` slot. Callback parameters can use `f64`, `bool`, `u8`, `u32`, and `i32` from format 2, `cstring`, `string`, and `bytes` from format 3, and the classes added by formats 6 and 7 as described in [ABI classes](#abi-classes), plus at most one context entry. Callback returns are scalar or `void`.

Format 3 string-bearing callback parameters copy native data before the closure runs. `cstring` reads one non-null, NUL-terminated `const char *`. `string` and `bytes` each consume a `const uint8_t *, size_t` pair; a null pointer is valid only when its length is zero. Text is decoded as UTF-8 with malformed sequences replaced by U+FFFD, matching `Buffer.toString("utf8")`. The resulting string or `Uint8Array` is freshly owned scriptc storage, so the closure may retain it without depending on the native buffer's lifetime. An unexpected null `cstring`, or a null non-empty span, traps at the boundary instead of being treated as empty.

For a raw call-scoped C callback type with no userdata, omit the context entry from both parameter lists. scriptc installs that closure in a binding-specific thread-local slot around the native call, so captures and nested calls still work.

### Retained callbacks

Format 4 adds `lifetime: "retained"` for same-thread native APIs that store a callback and invoke it from a later FFI call. Registration pins the closure and its captures. A paired release binding passes the same trampoline and closure context back to native code, then unpins one matching registration after the native call returns:

```ts title="main.ts"
declare function timerAdd(interval: number, tick: () => void): void;
declare function timerRemove(tick: () => void): void;

const tick = () => console.log("tick");
timerAdd(100, tick);
// A later native pump call may invoke tick here.
timerRemove(tick);
```

```json title="ffi.json"
{
  "ffi_format": 4,
  "functions": [
    {
      "name": "timerAdd",
      "symbol": "timer_add",
      "params": [
        "u32",
        {
          "callback": {
            "id": "tick",
            "params": [{ "context": "tick" }],
            "returns": "void",
            "lifetime": "retained"
          }
        },
        { "context": "tick" }
      ],
      "returns": "void"
    },
    {
      "name": "timerRemove",
      "symbol": "timer_remove",
      "params": [
        { "callback": { "release": "timerAdd:tick" } },
        { "context": "timerAdd:tick" }
      ],
      "returns": "void"
    }
  ]
}
```

The release argument must be the same function value used for registration. Registrations to a context-bearing descriptor are counted: registering the same closure twice requires two releases. A raw descriptor's slot has replace semantics instead — every set call supersedes the previous registration, including one that passes the already-registered closure, so exactly one release is ever pending for that descriptor. Releasing an unregistered value traps because native code may still hold the original pointer — the trap fires before the native release call runs, so native code never observes the invalid release. The callback's function type must be exact at retained call sites; an implicit wrapper would create a different pointer and make release identity unsound. An inline function literal as a release argument is rejected for the same reason: it creates a fresh closure at every evaluation, a pointer no registration holds — pass the same named value used to register. Registering an inline literal remains legal; such a registration is simply permanent and is dropped by the exit teardown. A single binding cannot both register and release the same descriptor — the manifest loader rejects a `release` targeting a retained callback declared in the same function's parameter list, because the register-then-release ordering within one call would defeat the pre-call release validation.

Context-bearing descriptors support multiple concurrent closures. A raw retained descriptor has no context pointer, so it has one slot on the script thread with replace semantics: the previous registration stays live and dispatching until the replacing set call returns (a native setter that flushes the outgoing callback mid-replace still reaches the old closure), then it is released and the slot commits to the new closure. Script-thread retained registrations do not keep the event loop alive; format 5 foreign registrations do. At process exit, `process` `'exit'` listeners run first — they may still release registrations or pump script-thread callbacks on every exit path. Foreign posting is disarmed when the loop stops, so a straggling native post is dropped without an error. On exits that run atexit handlers, the runtime then drops the remaining registrations and disarms raw slots; `process.exit()` terminates immediately after its listeners and skips that sweep, leaving remaining registrations to the operating system. A raw-slot invocation after teardown traps instead of reaching a freed closure. A context-bearing registration has no slot to disarm — its trampoline and context pointer dangle once teardown frees the closure — so native code must not invoke one after exit; a library that can fire on its own exit path should have its registrations released from a `process` `'exit'` listener.

Retained identity is scoped to the declaring binding. Every retained callback parameter is its own descriptor: the `<binding>:<callback-id>` pair names one registration ledger, one generated trampoline, and (for a raw descriptor) one slot, and a release binding validates and unpins only registrations made through the binding its `release` reference targets. Two bindings that store into the same native state — a plain setter and a flush-on-replace setter for one native slot, say — are therefore independent descriptors that pass native code two different function pointers. Registering the same function value through both and then releasing it through one is unsound: the release unpins in its own descriptor's ledger, but native code compares stored pointers against the other descriptor's trampoline, so the surviving registration stays armed and keeps dispatching — nothing traps, and the callback keeps firing after the program believes it released it. Keep a function value registered with one such native registration point through exactly one binding at a time, and release it through that binding's paired release.

### Callbacks from other threads

Format 5 adds `invoke: "foreign"` to a retained, context-bearing callback descriptor for libraries that invoke the callback from their own threads:

```json title="ffi.json"
{
  "ffi_format": 5,
  "functions": [
    {
      "name": "timerAdd",
      "symbol": "timer_add",
      "params": [
        "u32",
        {
          "callback": {
            "id": "tick",
            "params": ["cstring", { "context": "tick" }],
            "returns": "void",
            "lifetime": "retained",
            "invoke": "foreign"
          }
        },
        { "context": "tick" }
      ],
      "returns": "void"
    }
  ]
}
```

The native trampoline never runs script code. It copies scalar values and native string/byte memory into plain staging storage, posts to the process event loop, and returns immediately—even if native code happened to invoke it on the script thread. The loop delivers one invocation per turn, FIFO by enqueue order, with microtasks and timers interleaved. Live foreign registrations are ref'd: they keep the loop alive until their paired release binding runs. A callback may release itself; already-enqueued deliveries remain valid and drain before its closure pin is dropped. Throws follow ordinary timer-callback behavior and are uncaught unless the surrounding loop-dispatch semantics catch them.

Foreign delivery is deliberately fire-and-forget. It requires `lifetime: "retained"`, `returns: "void"`, and a context entry. Value-returning foreign callbacks would have to block the library thread on the script loop and are refused as deadlock-prone. Delivery takes at least one loop turn and is not suitable for real-time work such as audio DSP. Direct execution of script closures on native threads remains permanently unsupported because reference counting, exception cells, and fibers are thread-confined. Dereferenceable struct callback parameters are also unsupported; use an opaque native handle with accessor functions when the API permits it.

### Exceptions in callbacks

If a script-thread callback throws, the adapter returns zero (or `void`) to native code and suppresses further script callback execution while the exception is pending. When the outer native function returns, the original exception resumes through scriptc's ordinary catchable unwind path. Native work performed between the callback's return and the outer function's return is not rolled back. A foreign callback's native trampoline has already returned before its closure runs; a throw therefore follows event-loop callback semantics instead.

## Static bindings for node:ffi

JavaScript packages that load `node:ffi` through `createRequire` can run unchanged with a format 7 manifest. Add a `library` string to each function to make it available through `ffi.dlopen`. The string must match the library name the package passes to `dlopen`; the implementation comes from the objects or archives the manifest links, so nothing is loaded at runtime.

```json title="ffi.json"
{
  "ffi_format": 7,
  "libraries": ["./native.o"],
  "functions": [
    {
      "name": "nativeU64",
      "symbol": "native_u64",
      "library": "native",
      "params": ["u64"],
      "returns": "u64"
    }
  ]
}
```

```c title="native.c"
#include <stdint.h>
uint64_t native_u64(uint64_t value) { return value; }
```

```js title="app.mjs"
import { createRequire } from "node:module";
const ffi = createRequire(import.meta.url)("node:ffi");
const { lib, functions } = ffi.dlopen("native", {
  nativeU64: { arguments: ["u64"], return: "u64" }
});
console.log(functions.nativeU64(9007199254740993n));
lib.close();
```

Catalog entries support scalar and pointer signatures. Each requested symbol must have the compiled argument and return types. Calls check arity and integer ranges, and 64-bit arguments must be bigints. In this Node API, `bool` is an alias for `u8`, so its arguments and results are numbers. The direct manifest API retains its boolean convention.

Pointer arguments accept a non-negative bigint address, an ArrayBuffer or view, a NUL-free UTF-8 string, or nullish values for a null pointer. Storage stays alive during the call. `getRawPointer` returns an ArrayBuffer or view's address, including a view's byte offset. `toArrayBuffer(pointer, length)` copies memory; passing `false` as its third argument creates a borrowed view. The caller must keep foreign memory valid for the entire lifetime of a borrowed view and must supply a readable range.

For packages that call `lib.registerCallback`, declare each callback signature in the root `callbacks` array:

```json
"callbacks": [
  { "library": "native", "params": ["u64"], "returns": "u64", "capacity": 16 }
]
```

The compiler emits a fixed pool of native function pointers for each signature. Capacity defaults to 16 and may be 1–256. Registrations occupy independent slots; unregistering a callback or closing its library releases its slots. Exhausting the compiled capacity throws. These callbacks run on the registering script thread; calling one from a foreign thread is unsupported. Native code must stop using a callback pointer before unregistering it or closing its library. Closing a library also makes its saved function wrappers reject further calls.

Callback returns undergo the same scalar range checks; pointer returns must be nullish or a non-negative bigint. Callback exceptions follow scriptc's deferred exception contract: the native call receives a zero result, and the exception reaches the calling script frame when that native call returns.

On Darwin targets, a format 7 manifest may name system frameworks in `frameworks`, such as `["Foundation"]`. Objects and archives still belong in `libraries`, and ordinary system libraries belong in `system_libraries`.

## Manifest fields

<dl>
  <dt>
    <code>ffi\_format</code>
  </dt>

  <dd>
    Required. Format

    <code>1</code>

    supports value parameters; format

    <code>2</code>

    adds callback and context entries; format

    <code>3</code>

    adds copied

    <code>cstring</code>

    , string, and byte callback parameters; format

    <code>4</code>

    adds retained registrations and release references; format

    <code>5</code>

    adds retained callbacks from other threads, delivered on the event loop; format

    <code>6</code>

    adds

    <code>f32</code>

    ,

    <code>i8</code>

    ,

    <code>u16</code>

    ,

    <code>i16</code>

    , and outbound

    <code>mutable-bytes</code>

    spans; format

    <code>7</code>

    adds

    <code>i64</code>

    ,

    <code>u64</code>

    , and

    <code>pointer</code>

    , plus the

    <code>library</code>

    ,

    <code>callbacks</code>

    , and

    <code>frameworks</code>

    fields used by

    <a href="#static-bindings-for-nodeffi"><code>node:ffi</code> bindings</a>

    . Each format accepts everything earlier formats do.
  </dd>

  <dt>
    <code>functions</code>
  </dt>

  <dd>
    Required array. Every entry has

    <code>name</code>

    ,

    <code>symbol</code>

    ,

    <code>params</code>

    , and

    <code>returns</code>

    , and in format 7 an optional

    <code>library</code>

    . Binding names and symbols must be unique. Callback ids must be unique within a function and every context must match exactly one callback or release. A release references a retained

    <code>\<binding>:\<callback-id></code>

    in the same manifest and inherits its callback ABI.
  </dd>

  <dt>
    <code>libraries</code>
  </dt>

  <dd>
    Optional array of archive or object paths. Relative paths are resolved from the manifest directory and appended after the generated program at link time.
  </dd>

  <dt>
    <code>system\_libraries</code>
  </dt>

  <dd>
    Optional array of linker-neutral library names. For example,

    <code>\["m"]</code>

    is emitted as

    <code>-lm</code>

    .
  </dd>

  <dt>
    <code>frameworks</code>
  </dt>

  <dd>
    Format 7, Darwin targets. Optional array of system framework names, such as

    <code>\["Foundation"]</code>

    .
  </dd>

  <dt>
    <code>callbacks</code>
  </dt>

  <dd>
    Format 7. Optional array of callback signatures for

    <code>node:ffi</code>

    <code>registerCallback</code>

    , each with

    <code>library</code>

    ,

    <code>params</code>

    ,

    <code>returns</code>

    , and an optional

    <code>capacity</code>

    from 1 to 256 (default 16).
  </dd>
</dl>

Unknown fields, invalid ABI classes, duplicate names, and signature mismatches fail the build with an `SC5xxx` diagnostic.

## Boundary rules and current limits

- Native calls are synchronous and must return normally. Do not unwind C++ exceptions or `longjmp` across the boundary.
- Native code is outside scriptc's exception, reference-counting, and sanitizer contracts. A bad pointer or mismatched C signature can still corrupt the process.
- `invoke` defaults to `"script-thread"`. Callbacks from other threads (format 5) are asynchronous, return `void`, need a context entry, are released explicitly, and aren't suitable for real-time work; TypeScript code never runs directly on a foreign thread.
- There are no variadic calls, struct-by-value arguments, or owned pointer returns. The `node:ffi` catalog resolves only bindings compiled into the binary; runtime `dlopen`/`dlsym` loading remains unsupported.
- The archive or object must match the build target. Cross-compilation does not translate native inputs.
- Outbound FFI is currently available for executable builds, not `scriptc build --lib`.

---

For a semantic overview of all documentation, see [/sitemap.md](/sitemap.md)

For an index of all available documentation, see [/llms.txt](/llms.txt)

For agent-facing discovery, including API and MCP surfaces, see [/agents.md](/agents.md)

---
title: How It Works
url: "https://scriptc.dev/docs/how-it-works"
docs_index: /llms.txt
lastUpdated: 2026-11-12
---

> For an index of all documentation, see [/llms.txt](/llms.txt).

This page describes how scriptc turns a TypeScript program into an executable: the compiler stages, the outputs you can inspect at each stage, the native runtime that executables link, and how the compiler is tested.

## Compiler pipeline

```text
TypeScript / JavaScript
         |
         v
TypeScript parser and type checker
         |
         v
Lowering and typed IR ----> serialized IR (--emit=ir)
         |
         v
LLVM IR -----------------> LLVM source (--emit=llvm)
         |
         v
LLVM helper -------------> assembly / object (--emit=asm|obj)
         |
         v
Runtime pack and linker -> executable
```

1. **Frontend:** TypeScript parses and checks the program against `es2025`, scriptc's ambient declarations, and available project declarations. `tsconfig.json` controls checker strictness and, through `verbatimModuleSyntax`, whether [imports used only as types are dropped](/docs/dependencies#package-resolution). [Type checking](/docs/limitations#type-checking) lists the settings scriptc takes from it. The compiler uses the checked types and narrowing information to lower supported operations; unsupported operations produce diagnostics. JavaScript files that TypeScript checks (a `// @ts-check` comment, or `checkJs` in `tsconfig.json`) report values that do not match their JSDoc types as errors, and their classes use those types the way TypeScript classes use annotations.
2. **Typed IR:** A validated, serializable intermediate representation connects the frontend and backend. Types are concrete: generic functions are specialized, unions use tags, and closures have explicit captures. `--emit=ir` writes this representation as JSON.
3. **LLVM backend:** `--emit=llvm` writes textual LLVM IR. The matching LLVM 22 helper generates assembly or objects on supported macOS, Linux, and Windows hosts, including output targeting WASI. Executable and library builds use this backend. Large executables, native and WASI alike, are divided into partitions that are optimized and compiled in parallel; see [Build parallelism](#build-parallelism).
4. **Linking:** Runtime packs contain the precompiled C runtime and vendored libraries. A hashed manifest selects the artifacts the program needs for its features, optimization mode, and (for libraries) state model. The target linker and SDK or sysroot produce the executable. Sanitizer builds use an instrumented C runtime.

`comptime` calls run during the frontend stage in a separate evaluator process based on QuickJS. A callback sees the ECMAScript built-ins and no object of the compiler or the host, and each call has a time limit and a memory limit. Their results are embedded in the program as constants. See [build-time evaluation](/docs/limitations#build-time-evaluation) for the rules and limits.

## Build parallelism

Most of a build runs on one CPU core. The LLVM helper uses more cores when it compiles an executable in partitions, and when it generates code for an optimized executable that it optimizes whole.

### Frontend

Parsing, type checking, lowering, and LLVM IR generation run one after another on one thread. The type checker runs in a separate process that the compiler waits on. When none of a build's inputs has changed since an earlier build, the frontend's results come from the build cache and this stage is skipped. A change to any input runs the whole stage again.

### Partitions

The LLVM helper divides an executable's LLVM IR into partitions by its size and the threads the host offers. Up to 128 KiB of LLVM text is one partition. A larger program gets one partition per 4 MiB of LLVM text and two per hardware thread of the host, whichever is more, though a host's threads add no more partitions than the program can fill at 32 KiB of text each. The count is rounded up to a power of two, and is at least 8 and at most 64. `--emit=llvm` writes this text, so its file size and the host's thread count show how many partitions a program gets. `dev` executables, whose partitions are compiled on their own with nothing imported, are divided from 64 KiB: up to 128 KiB is four partitions. An edit compiles again only the partitions whose code it changes, or that inline a function it changes.

<table>
  <thead>
    <tr>
      <th>Hardware threads</th>

      <th>Partitions of a program of 32 MiB or more</th>
    </tr>
  </thead>

  <tbody>
    <tr>
      <td>Up to 4</td>

      <td>8</td>
    </tr>

    <tr>
      <td>5 to 8</td>

      <td>16</td>
    </tr>

    <tr>
      <td>9 to 16</td>

      <td>32</td>
    </tr>

    <tr>
      <td>17 or more</td>

      <td>64</td>
    </tr>
  </tbody>
</table>

A smaller program gets the count its own text fills: on a host with 16 hardware threads, a program of just over 128 KiB gets 8 partitions, one of 512 KiB 16, and one of 1 MiB 32. Two hosts with the same number of hardware threads give a program the same partitions, and build the same executable; a host with more threads divides the program more finely unless its size already asks for more partitions than the threads do. In programs with 32 partitions or more, functions from the same source file are placed in the same partition, so that their calls to each other stay inside it; a file larger than 1 MiB of LLVM text or than an average partition is divided into parts by function name. Other functions, and the functions of smaller programs, are placed by their name, and a small internal function goes with the one function that calls it. An edit leaves almost every other function in its partition.

An optimized partition is optimized twice: on its own, and again after the small functions of other partitions it calls are imported for inlining. So in optimized builds, a program whose largest partition would hold more than two thirds of its function text is optimized whole instead, as one partition, and its code is generated in parts on several threads.

These builds are not divided into partitions and compile on one thread: `--emit=asm`, `--emit=obj`, and `--print=native-link-info` output, library builds (`--lib`; `dev` library builds are compiled in pieces, one after another), and `--sanitize` builds, which compile the LLVM IR with the C compiler.

The helper compiles partitions on a pool with one thread per hardware thread of the host, so an executable with four partitions uses at most four cores in the backend.

How partitions are compiled depends on `--optimization`:

- `dev` compiles each partition on its own, all in parallel.
- `release` and `speed` optimize each partition and write a summary of its functions, all in parallel. Then a serial step reads every summary and decides which small functions each partition imports from the others, so that calls between partitions can still be inlined. Last, the partitions are optimized with their imports and compiled, again in parallel.

Reading the LLVM text and dividing it into partitions, and the import step, run on one thread.

### Code generation of a program optimized whole

A `release` or `speed` executable whose largest partition would hold more than two thirds of its function text is optimized as one module on one thread. The helper then divides the optimized module for code generation into as many parts as the program has partitions and generates machine code for the parts in parallel, on the same pool. Each part holds a run of consecutive functions with about the same amount of code, so the executable keeps the functions in the same order. The module is optimized before it is divided, so each function's machine code is the same as from one thread, except that a jump to a function that another part also uses takes the longer form. When one function holds most of the code, the first part holds the whole module and the others are empty.

An executable of one partition (up to 128 KiB of LLVM text, or 64 KiB in `dev` builds) is compiled on one thread.

### Rebuilds

With two or more partitions, the helper keeps each partition's summary and object in the build cache (see `SCRIPTC_CACHE_DIR` in the [CLI reference](/docs/cli#environment-variables)). A rebuild after an edit compiles again only the partitions whose functions changed and, in `release` and `speed` builds, the partitions that import a changed function; the others come from the cache. Dividing the program, the import step, and linking run on every build. A cold build, with nothing cached, compiles every partition. A program optimized whole is compiled again whole after any edit.

A program of one partition has no partition cache: a rebuild after an edit compiles the whole program again, and a rebuild that leaves the LLVM text unchanged takes the compiled code from the build cache.

The partition count is part of a cached partition's key, and a cached executable records the count it was linked from, so a build cache shared between hosts that divide a program differently serves each host its own builds.

When the LLVM text grows past a partition boundary (128 KiB, 512 KiB, 1 MiB, 2 MiB, and on in 4 MiB steps for large programs), the partition count doubles, functions move to new partitions, and the next build compiles every partition again.

### Profile-guided optimization

Building with a profile adds these costs (see [Profile-guided optimization](/docs/cli#profile-guided-optimization)):

- `--profile-generate` builds an instrumented executable, which runs slower while it records the profile.
- The profile's contents are part of every partition's cache key. A new or changed profile compiles every partition again; builds without a profile keep their own cache entries.
- With a profile, the import step lets a hot call import a function of up to twice the usual size limit. LLVM's default limits for profiled calls are 10 and 100 times as large, and on large programs they tripled the build's time and memory without making the program measurably faster.
- The profile does not change which partition a function is in.

Collecting a profile takes an instrumented build, the training runs, and the merge. With a committed profile, later builds skip those steps and are one optimized build each.

## Inspect compiler output

The following examples use this program:

```ts title="fib.ts"
function fib(n: number): number {
  return n < 2 ? n : fib(n - 1) + fib(n - 2);
}
console.log(fib(30));
```

Select an output stage with `--emit`:

```console
$ scriptc build fib.ts --emit=ir >/dev/null
$ ls .scriptc/
fib.ir.json
$ scriptc build fib.ts --emit=llvm >/dev/null
$ ls .scriptc/
fib.ir.json
fib.ll
$ scriptc build fib.ts --emit=asm >/dev/null
$ ls .scriptc/
fib.ir.json
fib.ll
fib.s
$ scriptc build fib.ts --emit=obj >/dev/null
$ ls .scriptc/
fib.ir.json
fib.ll
fib.o
fib.s
```

Without `-o`, artifacts are written to `.scriptc/` next to the input. Different output kinds accumulate in that directory; rebuilding a kind updates its file.

### Lowered programs

Work on the phases behind the frontend (IR validation, LLVM emission, native code generation, the link) does not need the frontend to run again for each attempt. `--emit-lowered` makes a build also write its lowered program to a file, and `--from-lowered` starts a build from that file:

```console
$ scriptc build fib.ts -o fib --emit-lowered fib.lowered >/dev/null
$ scriptc build fib.ts -o fib --from-lowered fib.lowered >/dev/null
$ ./fib
832040
```

The second build runs no type checker and no lowering, and writes the same executable byte for byte. The file holds the validated IR, the source texts that code frames and debug information read, the warnings the build reported, and the program's Node islands. The build caches cannot do this, because the compiler is one of their keys and a changed compiler is the reason to build again.

The file also records what the program was lowered from, and a build from it checks each kind and names the one that differs:

- **compiler:** the build of the compiler that lowered the program.
- **options:** the entry file, the target platform, the FFI manifest, every option except the output paths, and the `SCRIPTC_*` environment variables the frontend reads.
- **inputs:** every file, directory listing, and path the frontend read, including `tsconfig.json`, `package.json`, and the declaration files of dependencies.

```console
$ echo "// a comment" >> fib.ts
$ scriptc build fib.ts -o fib --from-lowered fib.lowered
/tmp/demo/fib.ts:1:1 - error SC3002: --from-lowered: /tmp/demo/fib.lowered does not match this build's inputs: /tmp/demo/fib.ts changed since it was lowered

  hint: lower the program again with --emit-lowered, or pass --from-lowered-accept=inputs to build from the file anyway

1 error.
```

`--from-lowered-accept` builds from the file anyway and reports the difference on stderr. After a change to the compiler's backend, pass `--from-lowered-accept=compiler`: the sources and options are still checked, and the build uses the program as the earlier compiler lowered it, so a change to the frontend has no effect until the program is lowered again.

```console
$ scriptc build fib.ts -o fib --from-lowered fib.lowered --from-lowered-accept=inputs
scriptc: warning: --from-lowered: building from /tmp/demo/fib.lowered although its inputs differ: /tmp/demo/fib.ts changed since it was lowered
/tmp/demo/fib
```

A build from a file validates the IR again before it emits. An IR artifact written by `--emit=ir` is also accepted as the file; it records none of the three kinds, so the build needs all of them accepted.

### External linking

Program objects define `main` and leave selected `scr_*` runtime functions undefined. A strong reference to `scr_runtime_abi_v21` checks runtime compatibility at link time.

`--print=native-link-info` emits the program object and a JSON recipe containing the runtime pack and link order for external builds. This object ABI is experimental and requires an exact runtime version. See [Native Program Objects](/docs/native-objects) for examples.

## Native runtime

- **Memory:** Values use reference counting. Acyclic values are freed when their last reference is released, and a cycle collector runs at defined collection points. When the compiler proves that a group of objects is released together, such as a tree that a recursive function builds and its caller only reads, the group is allocated in an arena and released at once, without freeing each object or collecting its cycles. Some cycles involving opaque native values can remain allocated. When the main thread's script ends, the process exits without freeing what the program still holds, and the operating system reclaims it; worker threads free their heaps when they exit. Builds with `--sanitize`, and processes run with `SCRIPTC_EXIT_FREE=1` (for external leak checkers), free everything at exit.
- **Concurrency:** An `async` function or async generator that suspends only at its own `await` and `yield` expressions compiles to a stackless coroutine whose heap frame holds the values live across them; other suspending `async` functions and async generators use stackful fibers, as do debug builds, libraries, worker programs and Windows executables. The native event loop schedules microtasks, timers, and I/O, using kqueue on macOS and epoll on Linux.
- **Networking:** Supported `net`, `http`, `https`, `tls`, `dgram`, and `dns` APIs use native implementations. TLS uses vendored mbedTLS.
- **Node islands:** A package that runs in a Node island is loaded by Node.js inside the program's process, the first time the program uses it. The package's JavaScript travels in the executable and is extracted once into the user's cache directory, `$XDG_CACHE_HOME/scriptc/node-islands/<digest>/` or `~/.cache/scriptc/node-islands/<digest>/`, where the digest names the set of files. Beside it, in `<digest>.compile-cache/`, the program keeps Node.js's [module compile cache](https://nodejs.org/docs/latest-v24.x/api/module.html#module-compile-cache) for those files, so a later start loads the compiled code instead of compiling the source again. A program that runs the `typescript` package in an island started in 205 ms without the cache and in 102 ms with it; its first start, which installs the files and fills the cache, took 244 ms instead of 218 ms. A file that changed is compiled again, and a rebuilt program has directories of its own. `NODE_DISABLE_COMPILE_CACHE=1` runs a program without the cache, and `NODE_COMPILE_CACHE=<directory>` makes Node.js use a directory of your choice instead. A program that never starts its island creates neither directory.
- **Numbers:** Values use JavaScript's double-precision representation. Number-to-string formatting uses shortest-roundtrip output and is tested against Node.js.
- **Regular expressions and Unicode:** The runtime uses QuickJS's regular-expression and Unicode libraries, linked into programs that use them.

See [Limitations](/docs/limitations) for runtime differences and the [Node.js compatibility reference](/compatibility) for API support.

## Testing

The test suite runs each test program under Node.js and as a compiled executable, and compares stdout, stderr, and exit codes. Server tests drive both implementations with the same clients. Number formatting is also fuzz-tested against Node.js's `String(x)`.

The same tests also run with AddressSanitizer and a reference-count audit at exit, to catch leaks and memory errors. You can enable these checks in your own executable builds with `--sanitize`.

Test coverage provides evidence for the cases exercised. It does not establish complete compatibility with Node.js.

<span id="embedded-javascript-engine" />

## Repository layout

<table>
  <thead>
    <tr>
      <th>
        Path
      </th>

      <th>
        Purpose
      </th>
    </tr>
  </thead>

  <tbody>
    <tr>
      <td>
        <code>packages/compiler</code>
      </td>

      <td>
        Frontend, typed IR, validation, serialization, LLVM backend, native-helper integration, and coverage analysis.
      </td>
    </tr>

    <tr>
      <td>
        <code>native/llvm-codegen</code>
      </td>

      <td>
        LLVM 22 helper for verification, optimization, assembly, and object output.
      </td>
    </tr>

    <tr>
      <td>
        <code>packages/runtime</code>
      </td>

      <td>
        C runtime, reference counting and cycle collection, fibers, event loop, networking, and the Node.js API implementations.
      </td>
    </tr>

    <tr>
      <td>
        <code>packages/runtime-\*</code>
      </td>

      <td>
        Runtime object packs and manifests for individual targets.
      </td>
    </tr>

    <tr>
      <td>
        <code>packages/cli</code>
      </td>

      <td>
        CLI commands, including

        <code>build</code>

        ,

        <code>run</code>

        , and

        <code>coverage</code>

        .
      </td>
    </tr>
  </tbody>
</table>

See the [repository README](https://github.com/vercel-labs/scriptc) for the development workflow.

---

For a semantic overview of all documentation, see [/sitemap.md](/sitemap.md)

For an index of all available documentation, see [/llms.txt](/llms.txt)

For agent-facing discovery, including API and MCP surfaces, see [/agents.md](/agents.md)

---
title: Introduction
url: "https://scriptc.dev/docs"
docs_index: /llms.txt
lastUpdated: 2026-11-12
---

> For an index of all documentation, see [/llms.txt](/llms.txt).

scriptc is a compiler that turns TypeScript and JavaScript into native executables and WebAssembly modules. It uses the types checked by the TypeScript compiler to generate machine code, and the output runs without Node.js installed.

scriptc is experimental. It supports a subset of JavaScript, TypeScript, and the Node.js APIs, and it tells you at build time when a program uses something outside that subset.

## Features

- **Native output.** `scriptc build` produces an executable for macOS, Linux, or Windows, or a WebAssembly module.
- **Standard TypeScript.** Programs are type-checked by the TypeScript compiler using your `tsconfig.json`. No annotations or language extensions are required.
- **npm dependencies.** Packages your program imports compile to native code together with your own code.
- **Coverage reports.** [`scriptc coverage`](/docs/coverage) shows what compiles and explains anything that doesn't, with a diagnostic code, a source location, and usually a rewrite hint.

## What you can build

- Command-line tools and scripts.
- HTTP and TCP servers built on `node:http`, `node:net`, and TLS, and clients that use `fetch`.
- Parallel programs with [worker threads](/docs/threads).
- [WebAssembly modules](/docs/wasm) that a web or native application calls.
- [Static libraries](/docs/platforms#ios-and-android-arm64-library-mode) for iOS and Android apps, and programs that [call C libraries](/docs/ffi).

## Compile a program

Create a TypeScript file:

```ts title="fib.ts"
function fib(n: number): number {
  return n < 2 ? n : fib(n - 1) + fib(n - 2);
}
console.log(fib(30));
```

`scriptc run` compiles the file and runs the result:

```console
$ scriptc run fib.ts
832040
```

`scriptc build` writes an executable:

```console
$ scriptc build fib.ts -o fib >/dev/null
$ ./fib
832040
```

The [Quickstart](/docs/quickstart) covers installation and a first build.

<span id="compilation-modes" />

## How it works

scriptc type-checks your program with TypeScript, uses the checked types to choose a native representation for every value, and generates machine code with LLVM. The executable links a native runtime that provides memory management, the event loop, and the Node.js APIs your program uses. [How It Works](/docs/how-it-works) describes each stage.

## Language and API support

scriptc compiles supported forms of:

- Classes, inheritance, closures, generics, discriminated unions, destructuring, and spread.
- `async`/`await`, promises, exceptions, iterators, and resource disposal.
- Strings, BigInts, arrays, Maps, Sets, JSON, Math, typed arrays, and Buffers.
- Node.js filesystem, path, process, child-process, crypto, timer, and networking APIs.

Programs are checked against the `es2025` standard library, scriptc's own declarations, and your project's declarations such as `@types/node`. The [Node.js compatibility reference](/compatibility) lists API-level support, and [Limitations](/docs/limitations) describes the remaining restrictions and the places where compiled programs behave differently from Node.js.

## Runtime type checks

Compiled code relies on types, so scriptc checks a value whose type is only known at runtime before using it as a specific type. For example, parsed JSON must match the type you cast it to:

```ts title="cast.ts"
type Config = { port: number };

try {
  const config = JSON.parse('{"port": "eighty"}') as Config;
  console.log(config.port);
} catch (error) {
  if (error instanceof Error) console.log(error.message);
}
```

```console
$ scriptc run cast.ts
expected number at $.port, got string
```

Under Node.js, the same `as Config` assertion performs no check. See [runtime type checks](/docs/limitations#runtime-type-checks) for details.

## Build-time evaluation

`comptime(() => ...)` runs a function while the program compiles and embeds its result as a constant:

```ts title="label.ts"
const label = comptime(() => "hello".toUpperCase());
console.log(label);
```

```console
$ scriptc run label.ts
HELLO
```

## Next steps

- [Quickstart](/docs/quickstart): install scriptc and build your first executable.
- [Coverage Reports](/docs/coverage): check whether an existing program compiles.
- [npm Dependencies](/docs/dependencies): use packages in compiled programs.
- [Platform Support](/docs/platforms): build for other operating systems and WebAssembly.
- [CLI Reference](/docs/cli): every command and option.

---

For a semantic overview of all documentation, see [/sitemap.md](/sitemap.md)

For an index of all available documentation, see [/llms.txt](/llms.txt)

For agent-facing discovery, including API and MCP surfaces, see [/agents.md](/agents.md)

---
title: Limitations
url: "https://scriptc.dev/docs/limitations"
docs_index: /llms.txt
lastUpdated: 2026-11-12
---

> For an index of all documentation, see [/llms.txt](/llms.txt).

scriptc supports a subset of JavaScript, TypeScript, and the Node.js APIs.
This page is a reference for the remaining restrictions and for runtime behavior that differs from Node.js.
You don't need to read it front to back: run [`scriptc coverage`](/docs/coverage) on your program, and come here when a diagnostic or a behavior difference needs more context.
The [Node.js compatibility reference](/compatibility) lists support API by API.

Some entries mention **checked values**.
When the type of a value is only known at runtime, such as `unknown`, `any`, the result of `JSON.parse`, or a value passed through untyped JavaScript, scriptc stores it together with its runtime type and checks it when the program converts it back to a specific type.
Entries that say a value "retains identity through checked storage" mean the same object comes back out, rather than a copy.

<span id="static-compilation" />

## Language and API restrictions

The following sections describe what compiles and what doesn't, area by area.
They are not exhaustive; the compiler's diagnostics are the authority for a specific program.

### Language features

- **Error causes:** Accessor constructor options are unsupported.
  Subclasses may redeclare `cause` as `unknown` or `object` (for example `cause?: unknown`), which defines the own property after `super()` returns as Node does; redeclarations with other types, parameter properties, and accessors named `cause` are unsupported.
  `Error`, `TypeError`, `RangeError`, and `SyntaxError` otherwise accept `{ cause }`, including through inherited constructors.
  Reads, writes, deletion, and `"cause" in error` distinguish an absent cause from an explicitly undefined cause.
  Constructor causes are non-enumerable; assignment to an absent cause creates an enumerable property.
  Property descriptors and checked-value aliases share the cause, including getters and setters installed after construction.
- **Class reflection:** `JSON.stringify` and `Object.keys` require every declared field to have a representation that can pass through runtime-checked storage.
  `JSON.stringify` writes Date fields as their ISO text and Map and Set fields as `{}`, as Node.js does, and refuses classes with RegExp or Promise fields with `SC1090`.
  `Object.keys` raises an error for classes with opaque fields such as native Maps.
  Class fields, inherited accessors, and added properties retain identity through untyped values.
- **Class members:** A method, an accessor, or a static method of a TypeScript class that cannot be compiled is refused by itself: the class, its fields, and its other members compile.
  Using the refused member where the compiler can see it is a compile error at the use, which names the member, and the member's own diagnostic is reported once.
  A call that reaches the member another way throws an error that carries the diagnostic's code at run time: through an untyped value, through an interface, or through a base-class receiver when the member is an override.
  A refused field, constructor, base class, decorator, or index signature still rejects the whole class, as does a member that overrides an async, generator, or generic method of its base class.
  An override may declare a narrower parameter than the method it overrides: one arm or some arms of the base's union, a required parameter where the base's is optional, or a subclass of the base's class.
  A call through the override's own class is direct; a call through the base class, an interface, or an untyped value checks each such argument and throws a `TypeError` naming the method and the parameter when the value is outside the type the override declares.
  Node.js runs the override with that value; a compiled override reads its parameter as the declared type, so the call is refused instead of running with a value the type does not describe.
  An override may also declare fewer parameters than its base: the arguments past its last parameter are dropped, as in Node.js.
  A call through an untyped value must still pass the base's arguments, and a shorter override that a subclass overrides again is callable through its own class only when the dropped parameters are optional.
  An override with fewer parameters whose body reads `arguments`, or with a tuple where the base declares an array, is refused as a member.
- **Method values:** Borrowing a class method onto an unrelated object is unsupported.
  Detached methods that use `this`, builtin method values, abstract declarations, unspecialized generic methods, and incompatible override signatures produce runtime errors or compilation diagnostics.
  Supported class methods can be stored and passed as unbound functions.
  A method of a TypeScript class that mentions `this` or `super` checks its receiver each time it is called through a value, and throws a `TypeError` unless the receiver is an instance of the class.
  A method that tests its receiver (`this === undefined`, `!this`, `this instanceof C`) therefore throws that `TypeError` when it is called without an instance, where Node.js runs the method and takes the method's own branch.
  The same holds when the call supplies another object as the receiver, where Node.js answers the test for that object.
  A method that never mentions `this` runs without a receiver.
  A member called through parentheses, a non-null assertion, or a type assertion (`(obj.method)()`, `obj.method!()`, `(obj.method as F)()`) is called on its object, as in Node.js; an element access in parentheses (`(obj["method"])()`), a parenthesized optional chain, and an optional method declared without a body and called with `!` lose the receiver.
  A field call supplies its containing instance as `this`; `call`, `apply`, and `bind` work when the signature supports runtime-checked conversion.
  Extracted overrides retain the selected method, and receiver validation accepts the declaring class and its subclasses.
- **Loose equality:** Object-to-primitive `==` and `!=` comparisons are unsupported because `valueOf` and `toString` can execute user code.
  Convert the object explicitly before comparing.
  Comparisons between represented primitives and primitive unions support number, string, boolean, and BigInt coercions, including `x == null` and `x != null`.
- **Resource disposal:** Top-level and switch-clause `using` declarations, and `AsyncDisposableStack`, are unsupported.
  Block- and function-scoped `using` and `await using` support compiled classes with zero-parameter disposal methods, object literals and other `Disposable`- or `AsyncDisposable`-typed values, and supported FileHandle, timer, immediate, child-process, and readline handles.
  A class instance reached through a `Disposable`- or `unknown`-typed value throws an SC2020 error when its declaration runs; declare the variable with the class type.
  Array `for (using ... of ...)` loops also compile.
  `SuppressedError` values expose `error` and `suppressed`; `extends SuppressedError` and casting `unknown` to `SuppressedError` are unsupported (narrow with `instanceof`).
  `DisposableStack` supports `use`, `adopt`, `defer`, `move`, `dispose`, `disposed` and `[Symbol.dispose]`, and works with `using`; `use` accepts class instances, object literals and other `Disposable`-typed values, and refuses unions that include class instances (narrow the value first).
  Disposal runs in reverse order and raises Node's `SuppressedError` chain when more than one callback throws.
- **Async iteration:** Async `yield*` delegation, stored Web or Node iterator handles, and general structural async-iterator objects are unsupported.
  Typed async generators support lazy execution, `await`, direct `yield`, queued `.next()`/`.return()`/`.throw()` requests, and `for await`.
  `for await` also accepts Node `Readable` streams, Web `ReadableStream` values, and compiled class iterators whose zero-parameter `next()` returns a promise of a `{ value, done? }` record.
  A class instance whose `[Symbol.asyncIterator]()` returns an object literal, or that has only `[Symbol.iterator]()`, iterates through the checked-dynamic async protocol, and an early exit awaits the iterator's `return()`.
  Abrupt completion runs cleanup, including literal `destroyOnReturn` and `preventCancel` options.
  The `node:timers/promises` `setInterval(delay, value)` iterator compiles without AbortSignal options.
  `Array.fromAsync` accepts arrays of values or of promises and typed async generators, with an optional mapping function; a `thisArg` and arrays that mix values and promises are unsupported.
- **Iterable values:** A value typed `Iterable<T>` can hold an array, a Set, a Map (for `Iterable<[K, V]>`), a string (for `Iterable<string>`), a generator, or a Map, Set or array iterator.
  Class instances with their own `[Symbol.iterator]` method and typed arrays can't be stored in an `Iterable<T>` and are reported at the conversion.
- **Generics:** Generic functions declared inside other functions, generic class expressions, and generic methods requiring dynamic dispatch are unsupported.
  Generic classes can extend generic bases when each specialization has a concrete layout.
  Independent concrete uses have no specialization-count limit; recursively expanding demands and type layouts produce diagnostics.
  Other generic calls compile when the target resolves statically.
  Generic generator methods, sync and async, compile like other generic methods.
  A generic async generator method called through an untyped value, or through an interface whose receiver's class the compiler does not know, throws a coded SC2020 error.
  Generic function values require a concrete signature at use and a binding that is never reassigned.
  Inside a generic instance whose binding holds values of several record layouts, a statement that takes the binding's value as the one record type its constraint names (an argument for a parameter of that type, a function literal's parameter, or a destructuring pattern) builds with warning `SC6015` and throws a `TypeError` when it runs with a value of another layout, where Node completes it.
- **Variadic wrappers:** A generic higher-order wrapper is specialized to each wrapped function's exact signature.
  `Parameters<F>`, `ReturnType<F>`, and rest parameters typed as fixed tuples become that signature's parameter list.
  A function expression with no parameters that reads `arguments` compiles only when its value is asserted to, or contextually typed as, one fixed-arity signature; `arguments` is then that signature's tuple.
  Forwarding it with `fn.apply(this, arguments)` passes `this` through.
  Reading `fn.name` on a wrapped function requires its name to be known where the wrapper is called, such as a function declaration or a `const`-bound function.
  `Parameters<F>` over a signature with optional or array-typed rest parameters is unsupported, because that tuple has no fixed length.
- **Function values:** Immutable aliases of overloaded program functions retain per-call overload resolution and identity.
  A function stored in a slot with a different signature remains the same function for equality, array searches, `length`, and own properties.
  A method value's `length` is the length of the method its object's class declares, whatever type the receiver is read through.
  The `length` of a function declaration, a static method, or a method of a JavaScript class counts a parameter with a default, where Node.js stops counting at the first one.
  Typed rest-parameter functions can be stored and called indirectly, including through a slot with fixed parameters, where the slot's arguments arrive in the rest array.
  That store is refused with `SC1090` when the program hands a function of the slot's type to a caller that passes more arguments than the type names, because the rest array would not receive them.
  Builtin function values have API-specific restrictions; see [Builtin function values](#builtin-function-values).
  A function called as a member of an object, a tuple, or an array (`holder.fn()`, `holder["fn"]()`, `holder.fn?.()`, `list[0]()`) receives that object as `this`, as in Node.js, and writes through `this` reach it.
  A timer calls its callback without a `this`, where Node.js passes the timer object.
  In a TypeScript source, a callback whose own body reads `this`, or whose signature declares a `this` parameter, is refused with `SC1090` when it is passed to a timer, a `process` listener, a `node:crypto` or `node:zlib` callback, or a stream option as a function value, where Node.js passes an object of its own.
  A function stored under a type that declares no `this` parameter is called without a `this` by a timer, an event emitter, a `process` listener, and a stream, where Node.js passes the timer, the emitter, `process`, or the stream.
- **JavaScript parameters:** In a JavaScript file TypeScript does not check, a prototype method (`C.prototype.m = function (...) {}`) takes whatever values its callers pass; its JSDoc types document the usual values without constraining them.
  A sloppy-mode function with named parameters may read `arguments` only where the aliasing of its parameters cannot show: it reads `arguments.length`, or reads elements and never writes a parameter or an element.
  Other uses of `arguments` in such a function are refused with `SC1090`; ES modules, `'use strict'` code, classes, and functions with default, rest, or destructured parameters have no such restriction.
  The `arguments` object is the list of the call's arguments, an array, where Node.js has an object that is not one.
  For the name itself or a constant that holds it, `Array.isArray` answers `false` as in Node.js, and the `concat` of an array or a string, or `JSON.stringify`, over it is refused with `SC1090`.
  `instanceof Array` and `Object.prototype.toString.call` answer as in Node.js in a function that only reads the object.
  These answers are the built-in functions' only: a method named `concat`, `isArray`, `stringify`, `toString`, `call`, or `apply` that the program defines, a binding that shadows `Array`, `Object`, or `JSON`, and a method called through an untyped receiver are called with the array.
  Converted to a string, printed, read for a member such as `constructor` or `slice`, tested with `instanceof` or for its tag in a function that also writes to it or passes it on, or tested by the code it was passed to, it is the array.
  A sloppy-mode function called with `undefined` or `null` as its receiver gets the global object as `this`, as in Node.
  Called with a primitive receiver, its `this` is the primitive itself, where Node passes the primitive's wrapper object (`typeof this` is `"object"` there).
- **Argument spreads:** Non-empty fixed tuples can spread into fixed signatures; arrays, Sets, and compiled class iterables can spread into typed rest parameters.
  Runtime-length spreads into fixed or rest parameters require types that support checked `unknown` conversion.
  Other JavaScript calls use the checked-value path; unsupported static forms produce a diagnostic.
  Calls with no native parameter slot are unsupported for runtime-length spreads.
  Arguments evaluate once, in source order, before defaults run, including surplus arguments.
- **Dynamic imports:** A specifier the build can bound to a finite set compiles to one load per value; the bound covers a string literal, a `const` or never-reassigned binding, string concatenation and template parts, `?:`/`??`/`||` branches, the pure String methods `replace`, `replaceAll`, `slice`, `toLowerCase`, `toUpperCase`, `trim` and `concat` over bounded values, and a parameter of a module-local function whose every call passes one of these.
  A specifier the build cannot bound is refused with `SC1090`, naming the source that stopped it.
  A value naming a supported Node builtin loads it natively.
  A value naming one of the program's own modules compiles that module and answers the one instance, however many static and dynamic edges reach it; a value that names no file rejects at run time with Node's `ERR_MODULE_NOT_FOUND`, so a missing `.js` variant never falls back to the `.ts` beside it.
  Import attributes, CommonJS namespace synthesis, and imports of shipped JavaScript packages are unsupported unless the target is a compiled ESM module.
  Literal `import()` of compiled ESM and TypeScript modules and of supported Node builtins compiles.
  A literal `import()` of a package that nothing imports statically loads the package in a Node island at the call; with `--node-islands=off` it is a build error that names the package.
  Evaluation is lazy and microtask-delayed, top-level `await` is honored, repeated imports share namespace identity, exported mutable bindings stay live, and namespace keys use Node's sorted order.
- **CommonJS `require`:** Literal `require()` calls of compiled modules, supported builtins, and JSON documents compile.
  Reading a top-level `const` or `let` require binding before its `require` runs throws the same `ReferenceError` as Node.js.
  A `var` binding that code above its `require` can reach is unsupported, and so is a read that compiles to no checkable expression, such as exporting the binding before the `require`.
  A `require()` of a relative path that names no file when the program is built throws the same `MODULE_NOT_FOUND` error as Node.js, where the call runs.
  The error has Node's `code` and `message` and no `requireStack` property.
  JSON documents are parsed at build time, and every `require()` of a document returns the same object.
  Destructuring the exports of a module supports plain and renamed properties, plus nested patterns for builtin sub-modules such as `util.types`.
  Computed keys work when they fold to a constant, such as a string literal or a `const` binding.
  A computed key whose value is only known at run time is unsupported.
  Any pattern works on a JSON document.
  Defaults, rest elements, and nested patterns over other modules are unsupported.
  A require cycle compiles when each of its modules assigns all of its exports before its first `require` and uses the other modules of the cycle only inside functions that its top-level code can't call.
  Other require cycles are rejected with `SC1016`.
  A top-level `return` ends the module's evaluation.
  It is unsupported when the module assigns exports after it, or when a function that exists before it runs reads a binding declared after it, such as a callback created above the `return` or a function declaration.
- **JSX:** JSX in `.tsx`, `.jsx` and JavaScript files compiles with the project's `jsx` setting: `"react"` with `jsxFactory`, `jsxFragmentFactory` or `reactNamespace`, and `"react-jsx"` or `"react-jsxdev"` with `jsxImportSource`, as well as the `@jsx`, `@jsxFrag`, `@jsxImportSource` and `@jsxRuntime` pragmas.
  The checker types JSX with these settings, and each element becomes the call TypeScript emits for it, importing the automatic runtime's functions from `<jsxImportSource>/jsx-runtime` or `jsx-dev-runtime`.
  `"jsx": "preserve"` and `"react-native"`, which keep JSX in the output, are refused.
  The call an element becomes must type-check: a factory or runtime function whose parameters do not accept what an element passes is reported at the element.
  With `"react-jsxdev"`, `jsxDEV` receives `undefined` as `self` where `this` has no declared type (in a function that is not a method), which is the value such a function observes when called without a receiver.

### Types and class definitions

- **BigInt:** BigInt filesystem stats are unsupported.
  Typed code cannot construct `BigInt64Array` or `BigUint64Array`; JavaScript that names either one gets the checked-dynamic element kinds, where elements are BigInts.
  Arbitrary-precision BigInts otherwise support literals, operators, conversions, arrays, records, classes, closures, unions, promises, typed Map keys and Set elements, Buffer 64-bit reads/writes, and DataView 64-bit access.
  Native `unknown` and JavaScript `any` storage preserve BigInts, including checked Map keys and Set elements.
  JSON serialization throws a `TypeError` unless a replacer converts the value.
- **Interfaces:** Interfaces explicitly implemented by compiled classes retain live object identity through checked native values.
  Method-signature interfaces and callable-property interfaces structurally implemented by class methods use the same representation.
  A call to a method signature of such an interface calls the class's method directly when the receiver is an instance of a compiled class, and a read of a property signature reads the class's field directly; object literals, accessors, and methods the program replaces on an instance or prototype use dynamic property lookup.
  Arrays of such interface values are native arrays whose elements keep their identity; methods without a native form for them run on the array's dynamic view, which shares its elements.
  Other data interfaces retain native record layouts and their copy-coercion rules.
  Generic methods require a statically resolved body; a `const` interface binding initialized directly with `new` can keep the concrete class for specialization.
  Unsupported member representations produce diagnostics.
- **Record shapes:** Records use exact native layouts.
  Passing `{ a, b }` where `{ a }` is expected can produce `SC2002`.
  Accepted flows into a strict field-subset shape copy the record; see [Record identity and property order](#record-identity-and-property-order).
  A type that reaches a graph of types so large or so entangled that one mapping of it takes more than 500,000 steps ends the build with `SC2016`, which names the type and points at its declaration.
  This holds under every tier, because the checked-dynamic form of the same value maps the same types.
  Declaring the value with a narrower type, or with `unknown` where the graph of types closes, compiles.
- **Unions:** Narrow a union before an operation whose members require incompatible native representations, such as reading `u.length` on `string | string[]`.
  Some union-to-union widening and unions combining function and data members are unsupported.
  Same-representation methods on class unions, `map` and `forEach` on array unions, and shared or joinable field reads compile.
  `console.log(u)` supports printing a whole union.
  A union may hold one promise beside other members, such as `T | Promise<T>` or `Promise<R | void> | R | void`: `await`, an async function's `return`, `Promise.resolve`, and `instanceof Promise` act on the member it holds, with Node's microtask timing.
  `instanceof` against one of the program's classes answers for the member the union holds as well, and is false for the promise.
  A promise member beside a member that declares `then`, two promise members in one union, and `console.log` of a union holding a promise are unsupported.
- **Dynamic slots:** A field, parameter, return value, variable or expression whose declared type has no native representation compiles as a checked dynamic slot instead of failing the build.
  The parts of the type that compile keep their native layout, and only the failing parts hold checked-dynamic values.
  Operations on those values run in the dynamic tier, and reading one into typed code checks it against the type that reads it, so a value that contradicts its declaration throws a `TypeError` when typed code first uses it rather than when it is stored.
  Each slot is an `SC6104` warning; `scriptc coverage --fail-on=dynamic-slots` fails on them, and `--tier=static-only` turns them back into build errors.
  A slot declared with the library's `PromiseLike<T>` is such a slot: it holds a promise or an object of the program with a `then` method, and `await`, `Promise.resolve`, `Promise.all`, `Promise.race`, and a call of its `then` ask the value as Node.js does.
  What a thenable hands over is checked against `T` where typed code reads it.
  A class whose `then` method has type parameters cannot be called through such a slot and throws `SC2020` when it is awaited; a `then` without type parameters, or an object literal, can.
  A promise given to one of the program's own types with a `then` method is refused with `SC2002`; declare the slot `PromiseLike<T>` instead.
  A member that the program adds to the library `Promise` type, such as `Promise<T> & { status?: string }`, cannot be written on a promise the program made: the write throws a `TypeError` when it runs, and the build reports it as an `SC6013` warning.
  Keep such state beside the promise, in a wrapper record or in a `WeakMap` keyed by the promise.
  Values that scriptc cannot produce at all, such as `BigInt64Array` instances, still fail where they are created.
  A promise typed with extra members, such as `Promise<T> & { settle(value: T): void }`, is such a slot: a promise that a Node island hands over keeps its members, while a promise the program made holds none of its own.
  Assigning a member to a promise the program made, as in `const p = new Promise(...) as Promise<T> & { settle(value: T): void }; p.settle = finish`, builds and throws when the statement runs: an `Error` that names the write with `SC1090` where the compiler sees how the promise was made, and a `TypeError` otherwise.
  Return the members beside the promise instead, such as `{ promise, settle }`.
- **Null and optional values:** `undefined` and `null` compile as standalone values and union members.
  Optional record and class fields and optional, default, and rest parameters also compile, including rest parameters typed `unknown[]` or `any[]`, whose functions take any number of arguments.
  A function value whose fixed parameters are all `unknown` or `any`, or that has none, can be stored where such a variadic function type without fixed parameters is expected: each call passes the arguments in order, `undefined` for the ones the caller left out.
  Other function values with fixed parameters cannot be stored there.
  A function value of such a variadic type can be stored where the program declares a function type with fixed parameters (a typed binding, parameter, result, field, member, or element): each call passes the declared arguments, a `void` result is discarded, and any other result is checked.
  That store is refused when the expected type's last parameter is optional, is `unknown` or `any`, or includes `undefined`, because the function would receive `undefined` where the call passes nothing.
  It is also refused with `SC1090` when the program hands a function of the expected type to a caller that can pass more arguments than the type names: a function type with more parameters, or a library function whose callback takes more.
  The stored function would not receive those arguments.
  Untyped code that receives the stored function calls the original, with every argument it passes.
  A variadic function passed directly to a library function that calls its callback with a fixed argument list is refused with `SC1090`.
  Event listeners, promise executors, timers, and `process.nextTick` pass such a function every argument, as do the methods of arrays whose elements are `unknown`.
  In a function expression written where a callback or listener is expected, `arguments.length` is the number of parameters that slot's function type declares.
  Handed where no function type is expected (an `any` or `unknown` slot, a JavaScript module's function, `Reflect.apply`), such a function expression is refused with `SC1090`.
- **Runtime-optional storage:** A binding, field, or parameter whose value comes from a read that can miss — an array element, a record key, or a read asserted with `!` — keeps `undefined` in its storage, and a call site that passes such a read adds that arm to the callee parameter's storage, so the callee receives every value the call site can produce.
  The checker's narrowing does not remove the stored arm: inside a `typeof` or `=== undefined` guard, lowering extracts the stored value through a checked narrowing.
  A parameter widened this way keeps the arm even when the call site's checker type has no `undefined`, so a construct that needs the declared type can refuse while an arm the program never wrote is still stored; the refusal's hint names both the narrowed type and the stored arms that block it.
  Bind the guarded value to a local declared as the narrowed type and pass that local when a construct needs the declared type.
  `scriptc coverage --explain-dynamic` lists each widened slot and the reads where the checker's type is narrower than its storage.
- **Static field updates:** Numeric static fields support `++` and `--` through the declaring class or an immutable alias.
  Updates through a subclass name are unsupported because JavaScript creates a separate property on the subclass.
- **Array subclasses:** A class that extends `Array` compiles: its instances are arrays, and `map`, `filter`, `slice` and the other methods that create arrays return instances of the subclass, as in Node.js.
  A generic subclass (`class Bag<T> extends Array<T>`), a class that extends such a subclass, and a subclass that declares `Symbol.species` are unsupported.
  A method whose result the subclass's constructor cannot construct throws a `TypeError`: the constructor's parameters must take the one length argument, and a `map` to another element type needs a constructor that adds no elements.
- **Local class expressions:** Static fields and static blocks in TypeScript files, decorators, private brands, and generic members are unsupported.
  Static methods read the enclosing function's bindings and are inherited by subclasses.
  A named class expression's methods and static members can refer to the class by its name.
  Supported expressions preserve fresh constructor identity and shared captures across constructors, methods, accessors, and arrow fields.
  Local inheritance requires a compiled base class.
  Constructors retain identity, `typeof`, `name`, and `length` through `unknown`; cast back to their exact type before construction.
  Construction and static-member access directly through `unknown` are unsupported.
- **Constructor-function inheritance:** Module-level JavaScript classes can extend compiled ordinary constructor functions.
  Constructors that return replacement objects, spread `super` arguments, and reflective static inheritance are unsupported.
  Supported `super` calls preserve receiver and field initialization order and capture the base prototype at class evaluation.
- **Symbol fields:** Instance fields require a stable module-level `Symbol()` or `Symbol.for()` binding with a literal description or constant string binding.
  A `[Symbol.toStringTag]` field or getter with a string value sets the tag that `String()`, `Object.prototype.toString` and `util.inspect` print; fields keyed by other well-known symbols are unsupported.
  Reassigned keys, runtime-computed descriptions, distinct symbols with the same description in one class, and copying symbol fields with rest bindings or `Object.assign` are unsupported.
  JavaScript object spreads copy represented own symbol fields.
  Equal registry keys share a field across modules.
  Uninitialized JavaScript fields start as `undefined`; TypeScript fields require an initializer or unconditional constructor assignment.
  Represented symbol fields and methods, including inherited members, remain accessible through checked values.
- **Unassigned fields:** A field that a method reads before the constructor has assigned it holds `undefined`, as in Node.js, when construction calls that method through `this`.
  This covers a method the constructor calls before the assignment, the override a derived class supplies for a method its base constructor calls, and a callback written in either that captures `this`.
  `typeof`, comparisons with `undefined` or `null`, truthiness tests, `??`, string conversion and `console.log` see the `undefined`.
  Arithmetic on an unassigned number field gives `NaN`, and reading a member of an unassigned object, array or string field throws a `TypeError`, as in Node.js.
  Any other use that needs the declared type throws a `TypeError` for an object, array, string or bigint field, and reads `NaN` for a number field and `false` for a boolean field, where Node.js passes `undefined` on.
  When a constructor hands the instance to other code before it has assigned a field, such as `register(this)`, a read of that field in that code does not see `undefined`: a number reads `0`, a boolean reads `false`, and using an object, array or string field stops the program.
  Assign the fields first, or include `undefined` in the field's type.
- **Class index signatures:** A class may declare `[key: string]: any` or `[key: string]: unknown`.
  The signature takes no storage: a name the class does not declare is an own property of the instance, absent until something assigns it, as in Node.js.
  Reads, assignments, computed keys, `in`, `Object.keys`, `JSON.stringify` and untyped views of the instance see the same property.
  An instance lists its declared fields before the added properties, as Node.js does when every added property is written after the fields exist.
  A class that could write one earlier is refused: a field initializer of the class or of a base class that mentions `this` or calls a function, or a base class constructor that calls a function or hands the instance out while a class below it declares fields.
  Assign such a field in the constructor instead.
  A signature with another value type or a number key is refused.
  A subclass of such a class cannot declare instance fields of its own; declare them in the class that has the signature, or assign them through it.
  Assigning a declared field through a computed key checks the value against the field's type and throws a `TypeError` on a mismatch, where Node.js stores the value.
  Compound assignment, `++`, `--`, `delete` and method calls on a name the signature admits, and object spread of the instance, are refused.

`Promise.all([work(1), work(2)])` infers a tuple.
Some tuple operations, such as `.at()` and `.concat()`, are unsupported.
If you need array operations on the result, declare the input as an array:

```ts
const jobs: Promise<number>[] = [work(1), work(2), work(3)];
const results = await Promise.all(jobs); // number[]
```

`Promise.allSettled` and `Promise.any` take the same `Promise<T>[]` input and return `PromiseSettledResult<T>[]` and `T`.

### Builtin APIs

A declaration in the standard library does not guarantee that an API compiles.
Unsupported declared APIs produce `SC2020`, with alternatives where available.
Check the [Node.js compatibility reference](/compatibility) for API-level support.

- **Text encoding:** `TextEncoder` and `TextDecoder` can be stored in fields, arguments, return values, arrays, and closures.
  Encoding supports UTF-8, including `encodeInto` into a Uint8Array or Buffer view.
  `new TextEncoder(...)` evaluates its arguments and ignores them, as Node does, so `new TextEncoder("utf-8")` is a UTF-8 encoder.
  Codec properties expose the canonical encoding and decoder flags.
  Codecs held in `any` or `unknown` storage keep their identity and streaming state, and dynamic code can call `encode`, `encodeInto` and `decode`, with options objects, and read their properties.
  Decoding accepts recognized WHATWG labels, including runtime labels and omitted or undefined encodings.
  Literal constructor options support `fatal` and `ignoreBOM`; literal decode options support streaming UTF-8, UTF-16, and single-byte encodings.
  Other legacy streaming encodings, runtime options objects, inspection, JSON conversion, and module-scope declarations using `var` or lacking an initializer are unsupported.
  Use initialized `let` or `const` bindings for stored codecs.
- **EventEmitter:** Literal event names use fixed argument tuples.
  Number, `null`, `undefined`, and boolean names register under `String(name)`, as in Node.
  Computed string names with a constant prefix or suffix that excludes `error`, meta events, and stream-internal events pass arguments of the exact arity with checked conversion.
  Other names reach an EventEmitter as through an untyped value: a name computed at run time converts with `String(name)`, then the name is matched against the program's literal event names when the call runs.
  The name expression is evaluated before it converts; a name whose static type is a string or number literal is a compile-time key only when reading it runs no program code (a literal, a `null`, `undefined`, `true` or `false` keyword, a binding read, a stored field or record entry read), and a call or an accessor lowers as a run-time name.
  On streams, those names and names that may reach internal events are unsupported.
  Symbol event names are supported: a symbol keys the registry by identity, so two `Symbol("s")` names are different events and a `Symbol.for()` name is one event at every call site.
  `eventNames()` lists symbol names after every string name, and the max-listeners warning renders a symbol name as `Symbol(description)`.
  Registering or removing a listener under a symbol name while a `newListener` or `removeListener` listener is registered is unsupported, as is `eventNames()` on an emitter with a registered `errorMonitor` listener.
  A stream's event names are strings: a symbol name is a type error at a typed call site and throws through an untyped value.
  Once the program registers a listener under an unrestricted name, typed `listeners()`/`rawListeners()` results are unsupported for events that literal names register with typed listeners.
  An emitter reached through an untyped value shares one listener list with its typed uses, so untyped registrations and emits reach typed listeners and typed emits reach untyped listeners.
  A `pipe` or `unpipe` emission reaches such a listener with a live view of the stream (its methods and properties), not the same object identity.
  An untyped `emit` of an event that has a listener with typed parameters converts its arguments to the event's typed tuple before any listener runs, and throws a `TypeError` when they do not convert.
  Through an untyped value, `listeners()` and `rawListeners()` of an event with a listener that has typed parameters are unsupported, and `emit('error')` of an object that is not an Error with no `error` listener is unsupported.
  A `newListener` or `removeListener` listener registered through an untyped value receives `undefined` in place of a listener registered with typed parameters.
  A typed `error` or `errorMonitor` listener throws a `TypeError` when an emitted payload that is not an Error does not convert to its parameter.
  Reading EventEmitter methods as properties through an untyped value (`typeof emitter.listeners`) answers `undefined`.
  `EventEmitter.prototype.<method>.call(receiver, ...)` runs the method typed when `receiver` is statically an emitter instance; a receiver of type `any` runs the untyped method over its JavaScript `_events`, which does not reach a compiled emitter's listeners.
  The method is read from `EventEmitter.prototype` or from an emitter reached through a binding, a stored field, or a record entry; an accessor or a call on that path is unsupported, since skipping it would drop its effects.
  `events.once` takes any options expression and validates it as Node does: `options` must be an object, only its `signal` property is read, and a non-AbortSignal value or an aborted signal rejects the promise.
  An EventEmitter takes a literal, computed, or symbol event name and a stream takes a literal one, including `data`; a computed name on a stream and the `errorMonitor` event are unsupported.
  Every other receiver takes the runtime path: an EventTarget resolves with the dispatched event and any other value rejects as Node does.
  `EventEmitter.captureRejectionSymbol` and `events.captureRejectionSymbol` read as Node's `Symbol.for('nodejs.rejection')` value and work as an event name; the `captureRejections` option and the rejection protocol behind it are unsupported.
  `events.on` lowers over an EventEmitter with a literal, computed, or symbol event name and over any other receiver through a checked-dynamic value, with the iterator's `close` names, `signal` aborts, 'error' handling and watermarks; a stream needs a literal event name and the `errorMonitor` event is unsupported.
  Reading `defaultMaxListeners` through a CommonJS destructured or member binding (`const { defaultMaxListeners } = require("events")`) is unsupported.
- **AsyncLocalStorage:** timer, interval, `setImmediate`, `process.nextTick` and `queueMicrotask` callbacks run with the stores that were active when they were scheduled, as in Node.
  The completion callbacks of `fs`, `crypto` and `zlib` callback APIs, `dns.lookup`, `child_process.execFile`, `server.listen`, `net.connect`, `http.request`, `http.get` and `dgram` run with the stores that were active at the call, as in Node.
  Events of `net`, `tls`, `http` and `https` sockets, servers, requests and responses run with the stores Node runs them with.
  A socket's or client request's events see the context of the call that created it, and a server's events and its requests' and responses' events see the context of its `listen()` call.
  Socket `write` and `end` callbacks, `'finish'` and `server.close(callback)` see the context of their call.
  Stream events and callbacks run with the stores of the code that triggers them, as in Node.
  Stream steps that Node defers with `process.nextTick` see the context that scheduled them, and `stream.finished` and `stream.pipeline` callbacks see the context of their call.
  `fs` read and write streams and `zlib` streams run their events and callbacks in the context that created them.
  Events of `child_process` children, their standard streams and their IPC channels see the context of the `spawn()` or `fork()` call; stdin's `'finish'` and `send()` callbacks see the context of their call.
  A client `http2` session's events see the context of its `http2.connect()` call, and a client stream's events the context of its `request()` call.
  An `http2` server's `'session'` and `'stream'` events and the compatibility API's request handler see the context of its `listen()` call; a server stream's own events, and its compatibility request's and response's, run with no store, as in Node.
  `AsyncResource` has no lowering yet.
- **Streams and readline:** Stream chunks are Buffers or strings, and on an object-mode side any value that converts to a checked-dynamic value.
  `NodeJS.ReadableStream` and `NodeJS.WritableStream`, and `stream.Readable` and `stream.Writable` under `@types/node`, accept every stream: when a program passes file, zlib, or `node:stream` streams through them, child-process pipes and `process.stdout` and `process.stderr` join those streams as `Readable` and `Writable` instances.
  `readable.pipe(process.stdout)` and `readable.pipe(process.stderr)` compile and, as in Node, do not end the process stream.
  When an untyped value reaches `NodeJS.ReadableStream` or `NodeJS.WritableStream`, those two interfaces hold any value, so JavaScript stream objects (constructor-function streams, plain objects with `on` and `pipe`) pass through TypeScript functions typed with them and come back to JavaScript as the same objects.
  Member calls on such a value use the native stream when it is one and call the object's own members otherwise; `readable.pipe()` into a JavaScript destination follows Node's pipe protocol.
  The first `pipeline` stage may arrive as an untyped value that is a native stream, a `Cipheriv` or `Decipheriv` value, or an iterable: a string, an array, a generator object, or any value with a `Symbol.asyncIterator` or `Symbol.iterator` method, which the pipeline pumps into the next stage as Node does.
  It may also be a source function, which the pipeline calls with `{ signal }` and whose iterable result it pumps, or a `{ readable, writable }` pair, whose readable side it pumps.
  Later `pipeline` stages that arrive as untyped values may be native streams, `Cipheriv` and `Decipheriv` values, functions, and iterables.
  A function is called with the previous stage and `{ signal }` as a function stage: before the last stage it must return an async iterable, and as the last stage it may return a promise, whose value resolves the pipeline, or an async iterable, which feeds the PassThrough that `pipeline()` returns.
  An untyped source function, function stage, or destination that returns a stream is unsupported.
  An iterable becomes `Duplex.from` of it, which the next stage reads from while the stages before it are not read, as in Node.
  A pipeline branches at run time on whether each of its first three untyped stages after the first is a function; a function in a later untyped stage, an untyped web stream, and a `{ readable, writable }` pair after the first stage throw a not-supported error.
  `for await` sources that arrive as untyped values must be native streams, or `Cipheriv` and `Decipheriv` values, at run time; other values throw a not-supported error.
  Native streams expose no `emit` to JavaScript, so a JavaScript stream whose own `pipe()` emits on its destination (the legacy `Stream` pipe) cannot pipe into one.
  `Readable.from` accepts strings, Buffers, arrays, Sets, Maps, Map and Set iterators, generators, async generators, and class iterables and async iterables, and in JavaScript any iterable or async iterable.
  In JavaScript, a sync iterator value with a `then` method that is not a promise, and an object whose `[Symbol.asyncIterator]` or `[Symbol.iterator]` method is a generator method, are unsupported.
  The `objectMode` option, and the `readableObjectMode` and `writableObjectMode` options of a `Duplex`, `Transform`, or `PassThrough`, take `true` or `false` literals in an options object literal; options passed as a value are read when the stream is built.
  In typed code, a `write` or `transform` callback, or a `_write` or `_transform` method, may declare its chunk with any type that converts to a checked-dynamic value: the chunk converts when the callback runs, and a value of another type throws a TypeError there.
  An object-mode write passes its encoding argument to `write` and `transform` as Node does (`undefined` when absent, `null` when a callback took its place); a parameter declared `string` receives `""` for `undefined` and `null`, and one declared `string | null | undefined` receives Node's value.
  `readable.read()` on an object-mode stream answers any value it holds.
  `node:stream/consumers` and readline interfaces read object-mode streams of strings and Buffers; other values throw a not-supported error.
  `pipeline` accepts function stages anywhere among its streams: async generator functions (`async function* (source)`, iterating `source` with `for await`), an async generator source function (`async function* ({ signal })`), an async generator function as the destination, and an async function destination whose value resolves the promise form and reaches the callback's second argument.
  A stage is written inline, or named by a function declaration or a `const` bound to a function expression that nothing reassigns.
  The source may also be an array, a string, a `Set`, a `Map`, a generator or async generator object, or a synchronous generator function without parameters; their elements reach the next stage on the same ticks as in Node.
  Stages may take the `{ signal }` argument.
  With a function destination, `pipeline()` returns an object-mode PassThrough that receives an async generator destination's values, or an async function destination's value unless it is `null` or `undefined`.
  `@types/node` types that result as `NodeJS.WritableStream`, so `for await` over it needs the `PassThrough` type (`pipeline(...) as PassThrough`, or a JSDoc `@type`).
  In typed code, a pipeline callback with an async function or async generator destination receives `null` for no error when its error parameter cannot hold `undefined` (@types/node declares it `NodeJS.ErrnoException | null`), and `undefined` when it can, as in Node.
  `Readable.from` accepts `objectMode` and `highWaterMark` options.
  The `Readable` operators (`map`, `filter`, `forEach`, `toArray`, `some`, `find`, `every`, `flatMap`, `drop`, `take`, and `reduce`) accept `concurrency`, `highWaterMark`, and `signal` options.
  Their callbacks may take the `{ signal }` argument, `map` may return any value, and `flatMap` may return any iterable, async iterable, or stream.
  `Duplex.from` accepts an async generator function, which reads what is written to the Duplex, a `Duplex`, `Transform` or `PassThrough`, which it returns, and the values `Readable.from` accepts, which give a Duplex that is not writable.
  It also accepts a lone `Readable` or `Writable` and a `{ readable, writable }` pair, which the Duplex reads and writes through, a promise, whose value it reads, and an async function, which reads what is written and must resolve to `null` or `undefined`.
  A `Duplex.from` body function may take the `{ signal }` argument, which is aborted with the error the Duplex is destroyed with.
  `stream.compose` accepts streams and async generator functions in any position, an async function as the last stage, and an array, a string, or a generator as the first stage; one stage is `Duplex.from` of it.
  A web `ReadableStream` is read as an async iterable by `Readable.from` and as the first stage of `compose` or `pipeline`, and through its reader by `Readable.fromWeb` (with `objectMode`, `highWaterMark`, `encoding`, and `signal` in an options object literal) and `Duplex.from`.
  In TypeScript, `new TransformStream()` without arguments builds the identity transform: its readable side is a web `ReadableStream`, its writable side supports `getWriter`, `abort`, `close`, and `locked`, and the writer supports `write`, `close`, `abort`, `releaseLock`, `ready`, `closed`, and `desiredSize`, with Node's backpressure (a write completes when the readable side reads its chunk).
  In TypeScript, `tee`, `pipeTo`, and `pipeThrough`, the `WritableStream` constructor with an underlying sink, and a `TransformStream` with a transformer or queuing strategies compile: a program that uses one of them builds its `ReadableStream`, `WritableStream`, and `TransformStream` values with the same classes as JavaScript, and a `Response` or `Request` body has those members too.
  A TypeScript program that uses only `getReader`, `cancel`, `locked`, and the identity `TransformStream` keeps the smaller native streams.
  A web stream in a later compose stage is unsupported.
  `stream.isDestroyed`, `isErrored`, `isDisturbed`, `isReadable`, and `isWritable`, and `Readable.isDisturbed`, answer for the `node:stream` classes and their subclasses, for file and zlib streams, for an untyped value that holds one of those, and for primitives.
  For any other value, such as a plain object, a web stream, a socket, an HTTP message, or a process stream, they throw a not-supported error, and for a child-process stream they answer only about the side the stream has.
  `readableAborted`, `readableDidRead`, `readableEncoding`, and `writableAborted` read as in Node, on a typed stream and through an untyped value.
  `writableAborted` is `undefined` on a `Duplex`, `Transform`, or `PassThrough`, as in Node, so a `boolean` slot refuses that read with a TypeError.
  `stream[Symbol.asyncDispose]()` and `await using` work on a value typed as a stream class; on an untyped value they are unsupported.
  `stream.setDefaultHighWaterMark`, `stream.duplexPair`, `readable.wrap`, `Readable.toWeb`, `Writable.toWeb`, `Writable.fromWeb`, `Duplex.fromWeb`, and `Duplex.toWeb` are unsupported.
  The `ERR_INVALID_ARG_VALUE` message for a stream that is no longer readable or writable names the stream's class instead of printing the stream.
  `readable.compose(stage, { signal })` is `stream.compose(readable, stage)` with the signal added; the options are an object literal whose `signal` is a variable or a property read.
  `pipeline` from `node:stream/promises` accepts a trailing `{ signal, end }` object, with or without function stages, and `finished` accepts `readable`, `writable`, `error`, `cleanup`, and `signal` options, with boolean literals for the flags.
  Stream constructors accept a `signal` option in an options object literal, and `stream.addAbortSignal` accepts any stream.
  The `construct` option and `_construct` method, `decodeStrings`, and `defaultEncoding` work on every stream class, and the `writev` option and `_writev` method on every class with a writable side.
  A `decodeStrings` or `defaultEncoding` value known only at run time is unsupported after an option member whose value has effects (a call or an assignment).
  In typed code, a `writev` entry's `encoding` declared as a string receives `""` where Node passes `undefined` or `null` (an object-mode write).
  `setDefaultEncoding()` takes any encoding Node knows.
  `node:readline` and `node:readline/promises` (also reached as `readline.promises`) interfaces, from `createInterface(options)` or `createInterface(input, output, completer, terminal)`, read `process.stdin` or a readable stream: `question`, `prompt`, `write` (with a key object literal of `name`, `ctrl`, `meta`, and `shift`), `pause`, `resume`, `'line'`/`'close'`/`'pause'`/`'resume'`/`'error'`/`'history'`/`'SIGINT'`/`'SIGTSTP'`/`'SIGCONT'` listeners, and `for await` compile.
  `on`, `once`, and `addListener` return the interface, so registrations chain.
  The `history`, `historySize`, `removeHistoryDuplicates`, `completer`, `tabSize`, and `escapeCodeTimeout` options are validated as in Node.
  With `terminal: true`, or with no `terminal` option and `process.stdout` as the output when it is a TTY, the interface runs Node's terminal mode: the editing keys, the kill ring, undo and redo, history recording and recall, Tab completion through the completer, Ctrl+C, Ctrl+D, and Ctrl+Z, `TERM=dumb`, redrawing the prompt and line when the output's `'resize'` event arrives, and the same escape sequences written to the output.
  The cursor never stops between the two UTF-16 units of a supplementary character: where Node's Up or Down movement through a multiline line would, the cursor moves to the character's start or end.
  A `'history'` listener receives the interface's history array itself: the same object `rl.history` answers, in typed and JavaScript code alike.
  A change the listener makes lands in that array, so an entry it removes never reaches recall or the later history.
  `rl.history`, `historySize`, `historyIndex`, `crlfDelay`, `tabSize`, `escapeCodeTimeout`, `line`, `cursor`, and `terminal` read as in Node; `rl.cursor` is `undefined` on an interface that is not a terminal.
  `rl[Symbol.dispose]()` closes the interface, and `instanceof` with `readline.Interface` or `readlinePromises.Interface` answers whether the interface came from that module, as in Node.
  In JavaScript, the input and output can be any expression that evaluates to a stream, such as a function's result or an object's member; a value that is not a stream is unsupported.
  `readline.cursorTo`, `moveCursor`, `clearLine`, and `clearScreenDown` write to `process.stdout`, `process.stderr`, a writable stream, or nothing for `null` and `undefined`; their callbacks take no parameters.
  The `readline/promises` `Readline` class writes to the same outputs.
  `readline.emitKeypressEvents` emits `'keypress'` events on `process.stdin` or a readable stream, shared with terminal interfaces on that stream as in Node; a listener's first parameter must admit `undefined`, which it receives for escape sequences.
  Chained listener registration is unsupported.
  `node:stream/web` exports the same classes as the globals; in TypeScript, `ReadableStream`, `WritableStream`, and `TransformStream` have lowerings.
  In JavaScript, `ReadableStream`, `WritableStream`, and `TransformStream` with their default readers, writers, and controllers, `CountQueuingStrategy`, `ByteLengthQueuingStrategy`, `TextEncoderStream`, and `TextDecoderStream`, as globals and as `node:stream/web` exports, are constructor values with Node's prototypes, brand checks, and errors.
  Their underlying sources, sinks, and transformers, queuing strategies with a `size` function, readers and writers, `tee`, `pipeTo` and `pipeThrough` with their options and an abort signal, `ReadableStream.from`, and async iteration settle promises in Node's order, and the streams take own properties.
  `TextDecoderStream` decodes UTF-8; its other encodings are unsupported.
  A JavaScript `ReadableStream` given as a `Response` or `Request` body, or as `fetch`'s body, is read through a stream of its own: `response.body` is that stream rather than the one passed in, and the stream passed in is locked from the start.
  In JavaScript, a `Response` or `Request` body has the members of a `ReadableStream`, including `tee`, `pipeTo`, `pipeThrough`, `values`, and `for await`, and its lock and `bodyUsed` follow them.
  After one of those members is used, a body given to a new `Response` or `Request` is locked from the start, as a JavaScript `ReadableStream` is.
  In a program that names none of the web stream classes, those members are available when the program's JavaScript names one of them or iterates a body with `for await`; reached only through a computed key, they are not.
  In JavaScript, a byte stream (`type: 'bytes'`), a BYOB reader, a class that extends one of these classes, `Object.getPrototypeOf` on these values, their use as `WeakMap` and `WeakSet` keys, and `ReadableStream.from` of a function that is not iterable are unsupported.
- **HTTP/2:** Server push is not implemented: `pushStream`, `createPushResponse`, and the `'push'` event are refused.
  A client says so in its settings (`ENABLE_PUSH = 0`), so its `localSettings.enablePush` and the server's `remoteSettings.enablePush` for it are `false` where Node reports `true`.
  On the compatibility response, `'finish'` listeners run when `end()` returns rather than when the stream closes, so they run before the request's `'close'` and before an `end()` callback, and a response that never ended emits no `'finish'`.
  Trailers, `goaway()`, timeouts, `respondWithFile`, and the stream and session `state` records are refused; on a session or stream that reached untyped code they throw instead of being ignored.
  A `ping()` callback declares its duration and payload optional, because a cancelled ping calls back with the error alone, as in Node.
  A settings record with `customSettings` is refused, in `session.settings()` and in `getPackedSettings()` (where Node 24 answers `undefined`).
- **CommonJS metadata:** The module graph is fixed at build time.
  Cache deletion or reloading, metadata writes, `module.paths` mutation, and `require.extensions` are unsupported.
  Read-only access to `module.id`, `filename`, `path`, `paths`, `loaded`, `isPreloading`, `parent`, `children`, `require.main`, and `require.cache` lookup/enumeration compiles.
- **ESM metadata:** Native `import.meta.url`, `filename`, `dirname`, and `main` support dot access, statically known keys, and destructuring selected fields.
  Storing or mutating the metadata object, rest bindings, runtime keys, and stored `resolve` callbacks are unsupported.
  Native `import.meta.resolve()` requires a literal specifier.
  Module instances distinguished by URL query strings or fragments are unsupported.
- **Date:** Spread constructor arguments are unsupported; the locale formatters take the locales and options that `Intl.DateTimeFormat` takes (see Intl.NumberFormat and Intl.DateTimeFormat).
  Dates are objects with identity: `===` and `==` compare identity, `new Date(date)` makes a new Date, setters change the one Date every reference holds, and Dates compile in arrays (`includes`, `indexOf`, `lastIndexOf`, `join`, and default `sort` by `toString()` text), in unions with other types, as Map and Set keys and values, and in records, tuples, class fields, closures, promises and generators.
  Thrown and rejected Dates keep their identity; `structuredClone` and `postMessage` copy them.
  Zero-argument, single number/string, and local-time year/month field constructors, `getTime`, `valueOf`, `toISOString`, `toUTCString`, `toJSON` (a `string | null`: null for an invalid date, as in Node), `toString`, `toDateString`, `toTimeString`, `toLocaleString()`, `toLocaleDateString()` and `toLocaleTimeString()` (without arguments: the default locale's ICU patterns over the host time zone, as Node prints them), the local and UTC calendar getters and setters, `setTime`, `getTimezoneOffset`, and single-string `Date.parse` compile.
  Unary `+`, `-` and `Number(date)` read the time value; `String(date)`, template literals and `+` with a string read the `toString()` text; `date[Symbol.toPrimitive](hint)` compiles with a literal hint.
  A Date converted to `unknown` is the same Date object.
  See [Dates and timezones](#dates-and-timezones) for timezone data.
- **URL:** Absolute inputs and relative inputs with runtime string or URL bases, including optional bases, compile.
  `URL.canParse`, `URL.parse`, `toString()`, and `toJSON()` compile; the factories use the same parser as construction, returning `false` or `null` for rejected inputs while preserving conversion errors.
  `href`, `protocol`, `username`, `password`, `host`, `hostname`, `port`, `pathname`, `search`, and `hash` support assignment in native code.
  Invalid component writes are ignored except for invalid `href` values, which throw; credentials are percent-encoded and ports accept valid leading digits.
  Native URL assignments require statically known property keys; `+=` accepts primitive operands, while other assignment operators and object operands for `+=` are refused.
  `origin` is read-only.
  `searchParams` is a live view whose identity survives query and URL replacement.
- **Legacy URL API:** `url.parse()`, `url.format()`, `url.resolve()`, `url.domainToASCII()`, and `url.domainToUnicode()` compile, including Punycode hostnames and the `DEP0169`/`DEP0170` warnings.
  A parsed `Url` object supports reads, writes, and its `format()` method; its `parse()`, `resolve()`, `resolveObject()`, and `parseHost()` methods throw `SC2020`, and `url.Url` and `url.resolveObject()` are unsupported.
  `ERR_INVALID_URL` errors have no `input` property.
- **Deprecated `punycode` and `constants` modules:** `punycode.encode()`, `decode()`, `toASCII()`, `toUnicode()`, `ucs2.decode()`, `ucs2.encode()`, and `version` compile, with the `DEP0040` warning Node.js reports when the module loads.
  A `require("punycode")` made from inside `node_modules` doesn't warn; a `require()` used inside a larger expression, such as `require("punycode").toASCII(name)`, doesn't warn either.
  The `punycode.ucs2` object itself is unsupported as a value.
  The `constants` module is the frozen object Node.js builds from `os.constants` and `fs.constants`, plus the `crypto.constants` of Node.js 24.22.1.
- **Filesystem flags:** `fs.openSync` supports inline bitwise OR expressions of `O_RDONLY`, `O_WRONLY`, `O_RDWR`, `O_CREAT`, `O_EXCL`, `O_NOFOLLOW`, `O_NONBLOCK`, `O_TRUNC`, and `O_APPEND`, with an optional creation mode.
  Computed numeric flags are unsupported because bit values vary by target.
  `fstatSync`, `fchmodSync`, `fsyncSync`, and `linkSync` support direct calls.
  On Windows, numeric opens with `O_NOFOLLOW` and calls to `fchmodSync` throw `ENOSYS`; use a POSIX target if you require these operations.
- **Filesystem URL paths:** `fs` and `fs.promises` functions accept `file:` URL paths and convert them as `fileURLToPath` does; `existsSync` answers `false` and the `fs.promises` functions reject for a URL that does not convert.
  A URL path with options that are not literals is refused where Node checks those options before it converts the URL, except a `readFileSync` encoding.
  `fs.promises` functions also refuse a string path before a URL path; pass `fileURLToPath(url)` instead.
  In JavaScript, an untyped path of a synchronous `fs` function is checked as Node.js checks it: a string, a `file:` URL or a `Uint8Array` is the path, and any other value throws Node's `ERR_INVALID_ARG_TYPE`.
  `readFileSync`, `writeFileSync` and `appendFileSync` take a file descriptor only as a statically typed number; a number passed through an untyped value throws `SC2020`.
  A `Uint8Array` path must hold UTF-8 text; other bytes, which Node.js hands to the system unchanged, throw `SC2020`.
  `existsSync`, `realpathSync`, `truncateSync`, the callback functions and `fs.promises` report an untyped path that is not a string with a `TypeError` that has no `code`.
- **Copy:** `fs.cpSync`, `fs.cp`, and `fs/promises` `cp` support every option and follow Node's two implementations: `cpSync` reports the native binding's errors, and the callback and promise forms report `SystemError`s.
  Files, directories, and symbolic links are copied in directory order, as in Node.
  The callback and promise forms complete the copy before the callback runs or the promise settles.
  An async `filter` must return a promise that is already settled; a pending one throws `SC2020`.
  On Windows, the three functions throw `SC2020`.
- **Glob:** `fs.globSync`, `fs.glob` and `fs/promises` `glob` support every option.
  `withFileTypes` must be a literal `true` or `false`, and `exclude` functions cannot be combined with `withFileTypes`.
  The `fs/promises` iterator is supported only as the source of a `for await` loop, and its walk completes when the loop starts.
  `fs.glob` callbacks receive file names, not Dirents.
- **FileHandle and descriptor I/O:** Static reads and writes accept Buffer and Uint8Array windows, numeric positions, and UTF-8 strings.
  FileHandle reads also accept options records and allocate a buffer when omitted.
  Vector I/O, truncation, syncing, and permission changes compile.
  Ownership changes, timestamps, and bigint Stats remain unsupported; FileHandle streams and `readLines` are covered under File streams.
  Dynamic descriptor I/O accepts other ArrayBuffer views and Buffer encodings; read positions outside the safe integer range are refused.
  File operations complete synchronously before their callbacks or promise continuations.
  Descriptor permission changes on Windows throw `ENOSYS`.
- **File streams:** `fs.createReadStream`, `fs.createWriteStream`, `new fs.ReadStream` and `new fs.WriteStream` (in JavaScript sources), and FileHandle `createReadStream`, `createWriteStream` and `readLines` return Readable and Writable streams.
  Options are checked when the stream is created, with Node's errors and order; `flags` and `mode` are checked when the file opens, so their errors arrive as `'error'` events.
  `bytesRead`, `bytesWritten`, `path`, `pending`, `close()`, `close(callback)` with a `() => void` callback, and the `'open'` and `'ready'` events compile.
  The `signal` option destroys the stream with an `AbortError` when the signal aborts, as `stream.addAbortSignal` does.
  As in Node, a signal that is already aborted destroys the stream when it is created and the file is still opened afterwards, so `'open'` and `'ready'` follow `'close'` and that descriptor stays open.
  A program links that support when the options of one of its file streams write `signal` or have a type that can hold it, `any` and `unknown` included; in any other program, a signal that still reaches a file stream inside a wider object, behind an options type without `signal`, throws when the stream is created.
  Numeric `flags`, the `fs` and `windowsHandle` options, write streams with an encoding other than UTF-8, and `instanceof fs.ReadStream` or `fs.WriteStream` are unsupported.
- **File watching:** `fs.watch` returns an `FSWatcher` whose `on`, `once`, `off`, and `close` take `change`, `rename`, `error`, and `close` listeners.
  A listener may take the event name and the entry name, but the name arrives as a string (Node's default encoding), so a listener annotated with the `Buffer` arm of `@types/node`'s `change` overload has no lowering.
  Watching a directory on macOS with a listener that reads the entry name is refused with `SC2020` because FSEvents is not implemented yet; file watches and listeners that ignore the name work on every target.
- **Stat polling:** `fs.watchFile` polls the resolved path every `interval` milliseconds (default 5007) and calls its listener with `(current, previous)` `Stats`; `fs.unwatchFile` removes the last listener registered with that callback, or every listener when called without one.
  The call must be a statement of its own, because the `StatWatcher` it returns in Node.js is not lowered, and `bigint: true` is refused.
- **Crypto:** Hashing, HMAC, PBKDF2, and HKDF support every digest `crypto.getHashes()` lists in Node 24, with OpenSSL's aliases and object identifiers.
  Hash and Hmac accept every Buffer encoding and any TypedArray or DataView, `createHash` and `hash.copy` take `outputLength`, and `crypto.hash` takes its options object; HMAC, PBKDF2, and HKDF reject the SHAKE and KMAC digests with Node's errors.
  Hash and Hmac values are also Transforms: they can be written, piped into, used as `pipeline()` stages, read, and listened to, and they report Node's errors after the stream flushes.
  `copy()` of a Hash whose stream flushed, and stream use of a value after its stream closed, throw a coded SC2020 error.
  Through `unknown` and `any` slots and untyped JavaScript, Hash and Hmac values keep their identity and support `update()`, `digest()`, and Hash's `copy()`, called or read as values; their stream and event members there throw a coded SC2020 error.
  `timingSafeEqual` compares any view or ArrayBuffer.
  Secret KeyObjects (`createSecretKey`, `generateKey` and `generateKeySync` for `hmac` and `aes`) support `type`, `symmetricKeySize`, `export()`, `equals()`, and `instanceof KeyObject`, and createHmac, hkdf, createCipheriv, and createDecipheriv accept them; `KeyObject.from()` and `toCryptoKey()` are not available.
  `randomFill`, `randomFillSync` (every view kind and ArrayBuffer), `randomInt`, `randomUUID`, and `randomUUIDv7` follow Node 24.22, including callback timing; a `randomInt` callback whose error parameter is typed `Error | null` receives `null` where Node passes `undefined`.
  Scrypt supports its default parameters, numeric options, aliases, and memory limit.
  Both KDFs accept strings, Buffers, numeric typed arrays, DataViews, and fixed-length ArrayBuffers.
  `createCipheriv` and `createDecipheriv` support aes-128, aes-192, and aes-256 in CBC, CTR, GCM, CCM, ECB, CFB, CFB8, CFB1, and OFB modes and as key wrap (the wrap and wrap-pad ciphers and their -inv forms), des-ede3 in CBC, ECB, CFB, CFB8, CFB1, and OFB modes, des-ede in CBC, ECB, CFB, and OFB modes, chacha20, and chacha20-poly1305, on native targets, through `update()`, `final()`, `setAAD()`, `setAutoPadding()`, `getAuthTag()`, and `setAuthTag()`; other ciphers and wasm32-wasi builds are refused.
  `getCipherInfo` answers Node's info for every cipher OpenSSL names, including the ones without a native implementation.
  `getCipherInfo` options whose class defines `keyLength` or `ivLength` throw a coded SC2020 error.
  The result is Node's object; the SIV ciphers and the SM4 GCM, CCM, and XTS ciphers have no `nid`, and reading it where the declared `CipherInfo` type gives `number` throws a TypeError.
  Cipheriv and Decipheriv values are also Transforms: they can be written, piped into, used as `pipeline()` stages, read, iterated with `for await`, and listened to, and their stream properties read as in Node; written chunks go through `update()`, and the end pushes `final()` or reports its error through `'error'`.
  Through `unknown` and `any` slots and untyped JavaScript, in a program that uses streams, their stream properties and their `on`, `once`, `off`, `pipe`, `unpipe`, `read`, `pause`, `resume`, `setEncoding`, `write`, `end`, `destroy`, `cork`, and `uncork` members work; `write` and `end` take no encoding or callback there, and the other stream members throw a coded SC2020 error.
  Hash, Hmac, Cipheriv, and Decipheriv streams count a written string's bytes in `writableLength`, where Node counts the string's length.
  Decrypting chacha20-poly1305 without an authentication tag fails in `final()`; Node 24.22 returns unverified plaintext.
  After `update()` fails, for example past the AES-GCM limit of 2^36 - 32 bytes per message, every later call on that cipher throws; Node still lets `final()` run.
  An output encoding that is a function or an instance of a class defining `toString` throws a coded SC2020 error.
  `createECDH` and `ECDH.convertKey` support prime192v1, secp224r1, prime256v1, secp384r1, secp521r1, secp192k1, secp224k1, secp256k1, brainpoolP256r1, brainpoolP384r1, and brainpoolP512r1, on native targets, through `generateKeys()`, `computeSecret()`, `getPublicKey()`, `getPrivateKey()`, `setPrivateKey()`, and `setPublicKey()`, in compressed, uncompressed, and hybrid forms; the other curves `crypto.getCurves()` lists and wasm32-wasi builds are refused.
  A call on an ECDH value whose arguments no @types/node overload accepts (`getPublicKey("HEX")`) is refused at compile time, as is `computeSecret(view, encoding)`: Node reads that encoding as the input encoding and answers a Buffer where @types/node declares a string.
  ECDH property reads and writes throw a coded SC2020 error.
  `createDiffieHellman`, `getDiffieHellman`, and `createDiffieHellmanGroup` support the modp1, modp2, modp5, and modp14 through modp18 groups, explicit primes and generators, and generated safe primes, on native targets, through `generateKeys()`, `computeSecret()`, `getPrime()`, `getGenerator()`, `getPublicKey()`, `getPrivateKey()`, `setPublicKey()`, `setPrivateKey()`, and `verifyError`, with Node's private key lengths, DH\_check results, and errors; wasm32-wasi builds are refused.
  Primes longer than 8192 bits throw a coded SC2020 error; Node accepts primes up to 10000 bits for key generation and secrets.
  `setPublicKey()` and `setPrivateKey()` keys longer than 80000 bytes, not counting leading zero bytes, throw a coded SC2020 error, as does a generator that long with a prime below 512 bits; Node accepts them.
  A call on a DiffieHellman value whose arguments no @types/node overload accepts, or whose output encoding is typed `any`, is refused at compile time, as are its `Object.prototype` methods; other property reads, and property writes, throw a coded SC2020 error.
  `Object.keys()` and `in` do not see a DiffieHellman's own `verifyError` property, and `JSON.stringify()` and `instanceof` are refused.
  After `createDiffieHellman(primeLength)` throws `Invalid DH parameters`, the next OpenSSL error Node throws can list the failed generation's errors in `opensslErrorStack`; errors in a compiled binary list only their own.
  The error for key material that no decoder accepts has Node's `library`, `reason`, and `opensslErrorStack`, whose last entry is the reported error.
  When a PEM body begins with a valid structure and fails further in, the entries OpenSSL lists before that last one are missing, and a malformed public key or DER input is reported as `ERR_OSSL_UNSUPPORTED` where Node reports the ASN.1 parser's own code.
  `crypto.diffieHellman` computes ECDH and X25519 secrets from EC and X25519 keys, given as KeyObjects or in any form `createPrivateKey` and `createPublicKey` read, with Node's errors; DH keys have no KeyObjects here, and the `DiffieHellman` and `DiffieHellmanGroup` constructors are not available.
  `checkPrime`, `checkPrimeSync`, `generatePrime`, and `generatePrimeSync` support bigint, ArrayBuffer, typed-array, Buffer, and DataView values and the `checks`, `safe`, `add`, `rem`, and `bigint` options on native targets, with Node's errors; the callback forms compute before they return and call back in a later macrotask.
  Candidates and sizes longer than 262144 bits throw a coded SC2020 error, and wasm32-wasi builds are refused.
  Node calls their callbacks with `undefined` as the error, which a callback whose error parameter is typed `Error | null`, as @types/node declares it, receives as `null`.
  RSA, RSA-PSS, EC, Ed25519, and X25519 private and public KeyObjects come from `generateKeyPairSync`, `generateKeyPair`, `createPrivateKey`, and `createPublicKey`, on native targets.
  `generateKeyPairSync` and `generateKeyPair` check their arguments in Node's order, with Node's errors, including the OpenSSL errors for RSA modulus lengths below 512 or above 2147483647 bits and for public exponents of 0, 1, or an even number, which `generateKeyPair` passes to its callback.
  RSA keys generate with modulus lengths from 512 to 8192 bits, an odd length from 2048 bits yielding one bit less as in Node, and odd public exponents up to 4294967295; longer moduli that Node accepts, and `paramEncoding: 'explicit'`, throw a coded SC2020 error.
  The EC curves are prime256v1 (P-256), secp384r1 (P-384), secp521r1 (P-521), and secp256k1.
  Keys read and write PEM and DER as PKCS#1, SEC1, PKCS#8, and SubjectPublicKeyInfo, and JWK (Ed25519 and X25519 keys use PKCS#8, SubjectPublicKeyInfo, and JWK); `createPublicKey` also takes a private key or an X.509 certificate.
  EC, Ed25519, and X25519 keys also read and write the `raw-public` and `raw-private` formats, including compressed EC points.
  Raw X448, Ed448, and post-quantum keys, EC scalars of 0 or at least the curve order, and the EC point at infinity, which Node accepts, throw a coded SC2020 error.
  They support `type`, `asymmetricKeyType`, `asymmetricKeyDetails`, `export()`, `equals()`, and `instanceof KeyObject`.
  `crypto.sign`, `crypto.verify`, `createSign`, and `createVerify` support RSA PKCS#1 v1.5 and PSS padding and ECDSA with DER and IEEE P1363 encodings over SHA-1 and SHA-2 digests, and Ed25519 through `crypto.sign` and `crypto.verify` with a `null` or `undefined` algorithm.
  RSA-PSS keys follow their RSASSA-PSS-params as Node does: PSS padding only, the key's digest and MGF1 digest, and salts of at least the key's length, with OpenSSL's errors; their details name the parameters, they export as PKCS#8 and SubjectPublicKeyInfo only, and digests other than SHA-1 and SHA-2 in their options throw a coded SC2020 error.
  Ed25519 signing, verification, and private keys link when a string literal in the program names `ed25519` (in any case); without one they throw a coded SC2020 error, while Ed25519 public keys still import and export.
  X509Certificate supports `publicKey`, `verify()`, `checkIssued()`, `checkPrivateKey()`, `checkHost()`, `checkEmail()`, `checkIP()`, `toLegacyObject()`, `toString()`, and `toJSON()`, and `String()`, template literals, and `+` give its PEM text as `toString()` does.
  `publicKey`, `verify()`, and `checkPrivateKey()` are refused in wasm32-wasi builds, and an Ed25519 certificate's `verify()` needs the `ed25519` literal like the other Ed25519 operations.
  Own-key walks over a certificate (`Object.keys()`, `Object.entries()`, spread, `for...in`, and `hasOwnProperty`) are refused, since Node keeps its fields as prototype getters.
  `JSON.stringify()` and `console.log()` of a certificate, a certificate stored in an `unknown` or `any` slot, and its methods on a union-typed value are also refused.
  Sign and Verify values are Writable streams: `write()` accumulates data as `update()` does, `end()` finishes the stream, and `pipe()` can pipe into one; they have no Readable side, as in Node.
  Private keys export encrypted (`cipher`, `passphrase`) with aes-128, aes-192, and aes-256 in CBC and ECB modes and with des-ede3-cbc, PKCS#8 as PBES2 with PBKDF2-HMAC-SHA256 and PKCS#1 and SEC1 as traditional PEM, and the ciphers OpenSSL refuses for a form throw its errors; a cipher option can also be an OID; other ciphers Node encrypts with throw a coded SC2020 error.
  Encrypted private keys import with a `passphrase` from PBES2 with HMAC-SHA PRFs and the CBC and ECB ciphers, and from traditional PEM with the CBC ciphers, with Node's errors for a missing or wrong passphrase; other schemes and ciphers throw a coded SC2020 error once a passphrase is given.
  The other key types (`dsa`, `ed448`, `x448`, and the rest) and wasm32-wasi builds throw a coded SC2020 error or are refused.
  Comparing keys of different types with `equals()` answers false; Node also leaves an OpenSSL error queued, which a later key operation can report.
  `publicEncrypt`/`privateDecrypt` remain unsupported.
  The WebCrypto `crypto` global, which node:crypto also exports as `webcrypto`, supports `randomUUID()` and `getRandomValues()`, also read as values from untyped JavaScript.
  Its `subtle` object, also node:crypto's `subtle` export, supports `generateKey()` and `exportKey()` for HMAC and AES keys, `importKey()` for HMAC, AES, PBKDF2, and HKDF keys, `encrypt()` and `decrypt()` with AES-GCM, AES-CBC, and AES-CTR and `wrapKey()` and `unwrapKey()` of secret keys with AES-KW or those ciphers on native targets, `deriveBits()` and `deriveKey()` with PBKDF2 and HKDF, HMAC `sign()` and `verify()`, and SHA-1 and SHA-2 `digest()`.
  Secret keys import and export in the `raw`, `raw-secret`, and `jwk` formats.
  `generateKey()`, `importKey()`, `exportKey()`, `sign()`, and `verify()` also support ECDSA (P-256, P-384, and P-521), Ed25519, RSASSA-PKCS1-v1\_5, and RSA-PSS, with the `spki`, `pkcs8`, and `jwk` formats and `raw` for ECDSA and Ed25519 public keys; ECDSA, Ed25519, and RSA are refused in wasm32-wasi builds.
  ECDSA, Ed25519, and RSA link when a string literal in the program names the algorithm; a name built only at run time throws a coded SC2020 error.
  `wrapKey()` and `unwrapKey()` of ECDSA, Ed25519, and RSA keys throw a coded SC2020 error.
  Other SubtleCrypto methods, algorithms, and key formats are refused or throw a coded SC2020 error.
  Native derivations finish before their deferred callbacks, so computation blocks the caller.
- **Assertions:** Assertion failures throw `assert.AssertionError` with Node's `message`, `generatedMessage`, `code`, `actual`, `expected`, `operator`, and `diff` properties.
  `new AssertionError(options)`, `instanceof`, and `assert.throws(fn, AssertionError)` compile.
  When `strictEqual` compares function values, `actual` and `expected` read as `undefined`; when `assert.throws` or `assert.rejects` reports an error that is not an instance of the expected class, or that does not match an expected object, `expected` reads as `undefined`.
  Values whose type can be cyclic, such as recursive records, also read as `undefined`.
  `assert.AssertionError` used as a value outside `instanceof` throws `SC2020`.
  The comparison methods, `ok`, `fail` and `ifError` are function values that assert as the methods do; called through a stored value, `ok` without a message reports `value == true`, where Node quotes the call, and `fail` takes at most one argument.
  `throws`, `rejects`, `doesNotThrow`, `doesNotReject`, `match` and `doesNotMatch` as values throw `SC2020`.
  `strictEqual` and `notStrictEqual` compare a number, string or boolean that can be `null` or `undefined`, such as `child.signalCode`, with any number, string, boolean, `null` or `undefined`.
  `notStrictEqual` of another value that can be `null` or `undefined`, such as `child.stdin`, against `null` or `undefined` compiles; `strictEqual` of one throws `SC2020`.
  `assert.deepEqual` and `assert.notDeepEqual` follow Node's loose comparison, including Maps, Sets, Dates, regular expressions, typed arrays, and errors; operands whose type can be cyclic or holds functions inside a structure are refused.
  `assert.partialDeepStrictEqual` follows Node's partial comparison over the same values, with the same refusals.
  Checked-dynamic values compared deeply have no cycle detection.
  Errors in failure messages print as `[Name: message]`, where Node prints the stack when only one side is an error.
  `new Assert(options)` compiles when it initializes a `const` with literal `diff`, `strict` and `skipPrototype` options, or none, and the binding is only used to call assertion methods.
  Other instances, such as ones passed as values, destructured, bound with `let`, or built from options that are not a literal, throw `SC2020`.
  With `skipPrototype`, `deepStrictEqual` and `notDeepStrictEqual` compare class instances, and operands of different static types, as checked-dynamic values.
- **Concurrent tests:** with `{ concurrency }` above 1, subtests run on their own fibers, and results are reported in declaration order, as in Node.
  Concurrent bodies resume in Node's order around `beforeEach` hooks and nested suites.
  Other runner steps, such as `afterEach` and `after` hooks, take no microtask turns between bodies, so code that observes the interleaving of concurrent bodies across those steps can see a different order than Node.
- **Snapshots:** `t.assert.snapshot` and `t.assert.fileSnapshot` compile, with Node's default serializer, `JSON.stringify(value, null, 2)`, or an `options.serializers` array literal.
  A compiled test reads the snapshot file Node writes beside the entry file, `<entry file>.snapshot`, at the entry file's path when the program was compiled.
  Running the binary with Node's flag `--test-update-snapshots` as its first argument writes the snapshot files instead; as in Node, the flag is kept out of `process.argv` and appears in `process.execArgv`.
  Snapshot files are read in the form Node writes them, ``exports[`key`] = `value`;`` statements.
  The `snapshot.setDefaultSnapshotSerializers` and `snapshot.setResolveSnapshotPath` module functions throw `SC2020`.
- **Reporters:** the binary writes the spec report, so `--test-reporter=spec` is the only reporter it takes, and any other name ends the run before the tests start with `scriptc: unknown test reporter '<name>': only the spec reporter is supported`.
  `--test-reporter-destination=stdout`, its default, sends the report to stdout, `stderr` sends it to stderr, and any other value names the file the binary writes the report to; the program's own output stays on stdout in every case.
  As in Node, both options are leading arguments, in any order, kept out of `process.argv` and listed in `process.execArgv`.
  A file destination that cannot be opened ends the run the same way, with `scriptc: cannot open test reporter destination '<path>': <reason>`.
- **TestContext values:** `t.filePath` is the entry file's path when the program was compiled.
  `t.attempt` is `0`, since a test runs once.
  `t.passed` is `false` while the body runs and the test's outcome in its `afterEach` and `t.after` hooks.
  `t.workerId` is `Number(process.env.NODE_TEST_WORKER_ID) || undefined`, as in Node.
  `t.tags` is `[]`, since the `tags` test option throws `SC2020`; the array is not frozen as Node's is.
- **Suite and hook contexts:** a `describe` body may take its `SuiteContext`, which has `name`, `fullName`, `filePath`, `attempt`, `passed` and `diagnostic`.
  Hooks may take their context, as in Node: the suite's for `before` and `after`, the test's for `beforeEach`, `afterEach`, `t.before` and `t.after`.
  The `SuiteContext` `signal` and `log` members, and hook `done` callbacks, throw `SC2020`.
- **Module-level hooks:** `before` hooks outside any suite run at their `before()` call, and the first top-level test waits for the async ones; `after` hooks outside any suite run after the last top-level test.
  Both get the root's context, named `<root>`.
  As in Node, a throwing root `before` fails each top-level test with its error, without running its body, and cancels the tests and suites inside each top-level suite.
  A throwing root `after` reports the entry file as a failed result, and the exit code is unchanged.
- **Timeouts:** when a test's `{ timeout }` elapses, its running subtests are cancelled, as in Node.
  Subtests do not take their parent's `timeout` as their own, where Node's do, and the subtests of a timed-out test with `{ concurrency }` above 1 run to completion before the test reports.
  A body that keeps running after its timeout and calls `t.test` again runs that subtest, where Node reports it as not started.
- **Thrown values:** a test that throws a value that is not an `Error` reports it in the failing section as `util.inspect` renders it, as Node does, for strings, numbers, booleans, objects, arrays, `null` and `undefined`.
  A thrown class instance that is not an `Error`, such as `throw new Point(1, 2)`, reports `[object Object]`, where Node prints `Point { x: 1, y: 2 }`.
- **Test names:** a test without a name, or with an empty one, takes its function's name or `<anonymous>`, as in Node, when the function's name is known at compile time: a function expression, a function declaration, or a `const` bound to an arrow or anonymous function.
  A name computed at run time, with a function the program received as a parameter, is used as given, so an empty one stays empty where Node uses the function's name.
  `test(fn)` and `test("", fn)` with such a function throw `SC2020`.
- **Current context:** `getTestContext()` returns the context of the test, suite or hook whose code is running, as in Node: `undefined` at module top level, the root's context in module-level hooks, the suite's in a suite body and in the suite's hooks, and the test's in its body, in the code it awaits, and in the timers, immediates, `process.nextTick` and `queueMicrotask` callbacks it schedules.
  Node returns, for a suite, a `TestContext` that is a different object from the `SuiteContext` passed to the suite body and its `before` and `after` hooks; in a compiled program they are the same object, so comparing them with `===` gives `true`.
  Completion callbacks of callback-style I/O APIs, such as `fs.readFile`, get `undefined`, as AsyncLocalStorage stores do.
- **Test mocks:** `mock.fn`, `mock.method`, `mock.restoreAll` and `mock.reset` compile, on the module's `mock` and on `t.mock`.
  `mock.method` mocks a function-valued field of a record, such as an object literal or an interface-typed object.
  Class instances, builtin objects, and object literals whose methods use `this` throw `SC2020`.
  Calls through the object record it as `this`, including calls of a method read off the object and called on its own, where Node records `undefined`.
  `object.method === mock` is `false`, because the field holds a wrapper of the mock.
  `mock.fn()` without an original returns `undefined` and records the arguments of each call, as in Node; with a function type whose return type does not include `undefined`, such as `mock.fn<() => number>()`, it throws `SC2020`.
  A mock function's `mock` context is a non-enumerable own property of the returned function, so `typeof` is `"function"` and `Object.keys` is empty as in Node; `"mock" in mockFn` and `Object.getOwnPropertyNames` see the property, where Node's proxy hides it.
  `mock.getter`, `mock.setter`, `mock.property`, `mock.module` and mock options throw `SC2020`.
- **Mock timers:** `mock.timers.enable`, `tick`, `runAll`, `reset`, `setTime` and `[Symbol.dispose]()` compile, on the module's `mock` and on `t.mock`, for the `setTimeout`, `setInterval`, `setImmediate`, `Date`, `scheduler.wait` and `AbortSignal.timeout` APIs.
  `enable()` takes no options or an object literal whose `apis`, when given, is an array literal.
  Options and `apis` lists that are not literals, and the `MockTimers` object used as a value, throw `SC2020`.
  As in Node, the global timer functions, `Date.now()`, `new Date()` and `scheduler.wait` are mocked, and other calls through named and namespace imports of `node:timers` and `node:timers/promises` use real timers.
  As in Node, once a reset gives `scheduler.wait` back, calling it throws `ERR_INVALID_THIS`.
  While `scheduler.wait` is mocked, calling it with a `signal` option throws an `SC2020` error.
  A mock timer's handle converts to a negative number, where Node's mock `Timeout` converts to `NaN`.
  When two trackers have timers enabled, clearing a timer through the tracker that did not create it leaves the timer queued, where Node removes the entry at the same queue position in the clearing tracker.
- **Exit status:** Numeric `process.exitCode = value` writes in statement position set the implicit exit status, which `process.exit()` uses when called without an argument.
  `process.exit(code)` and `process.exitCode` throw a `RangeError` (`ERR_OUT_OF_RANGE`) for a code that is not a safe integer, and convert other codes to int32, as Node does.
  Reading `process.exitCode` answers the assigned code, or `undefined` before any assignment; `process.exit(code)` assigns it before the `'exit'` listeners run.
  Resets (`process.exitCode = undefined`) and assignments used as values are unsupported.
- **Process memory:** `process.memoryUsage()` reads `rss` from the operating system and `heapTotal` and `heapUsed` from the runtime's allocator, so the heap figures start near zero and grow as the program allocates.
  `arrayBuffers` is the storage of the ArrayBuffers, typed arrays, Buffers, DataViews and SharedArrayBuffers the program holds, and `external` is the same storage without SharedArrayBuffers, as in Node.
  Node also counts its own startup allocations in both, so a program that holds no such storage reports `0` where Node reports a few megabytes.
  In a program with worker threads, `external` and the non-shared part of `arrayBuffers` count the calling thread's storage.
  The storage comes from the same allocator, so it also counts in `heapTotal` and `heapUsed`.
- **V8 heap statistics:** `node:v8` can be imported.
  `v8.getHeapStatistics()` returns Node's fields in Node's order, computed from the runtime allocator like `process.memoryUsage()`: `total_heap_size` and `total_physical_size` are the bytes the heap holds, `used_heap_size` the bytes allocated, `external_memory` the same figure as `process.memoryUsage().external`, `heap_size_limit` the machine's memory (the heap has no fixed ceiling), and `total_available_size` the limit minus the used bytes.
  Fields that describe V8 itself (`total_heap_size_executable`, `malloced_memory`, `peak_malloced_memory`, the global handle sizes, and `does_zap_garbage`) are `0`, and `number_of_native_contexts` is `1`.
  The values are approximate and are not V8 heap figures.
  The other `node:v8` members observe the V8 engine, such as heap snapshots, profiles, flags, and serialization, and throw `SC2020` where they are used.
- **Performance entries:** `performance.mark()`, `measure()`, and the `getEntries` methods return entries with Node's fields, `toJSON()`, inspection, and identity.
  `Object.keys`, `for...in`, object spread, and `hasOwnProperty` on an entry are unsupported, and an entry stored in an `unknown` slot prints as a plain object.
- **Event-loop metrics:** `performance.eventLoopUtilization()` counts the main thread's CPU time since the event loop started as active time, so blocking system calls count as idle where Node counts them as active.
  `monitorEventLoopDelay()` supports `enable()`, `disable()`, `reset()`, `percentile()`, and the `min`, `max`, `mean`, `stddev`, `count`, and `exceeds` readings; the BigInt readings, `percentiles`, and non-literal option objects are unsupported.
- **Builtin module lookup:** `process.getBuiltinModule()` with a string-literal id that `@types/node` types as a supported module answers that module, as an import of it does, also behind `?.`.
  With any other id it exposes native subsets of `path`, `path/posix`, `path/win32`, `os`, `util/types`, `node:sea`, and current-thread `worker_threads` metadata, and answers `undefined` for an id Node.js does not have.
  Stored module references and functions retain identity.
  Other modules and exports, including stored `Worker` constructors, throw `SC2020`.
  Direct `Worker` imports use the native worker support described below.
  `process.versions` is a shared dictionary containing `node` and `openssl`; other component versions are absent unless defined by the program.
- **Compile cache:** `module.enableCompileCache()` answers `{ status: module.constants.compileCacheStatus.DISABLED, message }` and creates no directory, because a compiled program loads no JavaScript whose compiled form could be cached.
  The `message` is Node.js's own when `NODE_DISABLE_COMPILE_CACHE` is set, and otherwise says that a compiled program has no compile cache.
  `module.getCompileCacheDir()` answers `undefined` and `module.flushCompileCache()` writes nothing.
  The calls validate their arguments as Node.js does, and they never start Node.js in a program that has a Node island.
- **Single executable applications:** a compiled program is not a Node.js single-executable application, so `node:sea` answers what Node.js answers outside one.
  `isSea()` is `false`, and `getAsset()`, `getRawAsset()`, `getAssetAsBlob()`, and `getAssetKeys()` check their arguments and then throw `ERR_NOT_IN_SINGLE_EXECUTABLE_APPLICATION`.
- **ArrayBuffers and typed arrays:** Fixed-length ArrayBuffers support shared typed-array and DataView storage.
  Resizing and transferring are unsupported.
  Typed code cannot construct `BigInt64Array` or `BigUint64Array`; JavaScript that names either one gets the checked-dynamic element kinds, where elements are BigInts.
  `Float16Array` stores IEEE 754 half-precision values, rounding each stored number to nearest-even as Node does; `Math.f16round`, `DataView.prototype.getFloat16` and `setFloat16`, and `util.types.isFloat16Array` compile.
  The typed-array callback methods (`forEach`, `map`, `filter`, `some`, `every`, `find`, `findIndex`, `findLast`, `findLastIndex`, `reduce`, `reduceRight`) compile; `map` and `filter` return the receiver's kind, a Buffer for a Buffer, and a `thisArg` argument is unsupported.
  Numeric typed arrays and Buffers preserve their kind and shared storage through `unknown`, including when nested in records and arrays.
  Numeric typed-array constructors can be stored and used with `new`, including constructors read from an array's `constructor` property.
  JavaScript numeric-array parameters and returns preserve typed-array storage.
  `Buffer.from`, `Buffer.allocUnsafe`, and `Buffer.concat` return views into a per-thread pool of `Buffer.poolSize` bytes (64 KiB by default) when the size is below `Buffer.poolSize >>> 1`, as in Node; a pool slab stays in memory while any of its Buffers does.
  Node's own startup code allocates from the first slab, so the `byteOffset` of Buffers in that slab differs from Node's.
  Buffers returned by other Node APIs, such as `fs.readFileSync` and `zlib`, own their `ArrayBuffer`, where Node allocates small ones from the pool.
- **Blob and File:** `new Blob()` and `new File()` accept arrays, typed arrays, `Set`, and `Map` as sources.
  Other iterables throw `SC2020`.
  `blob.stream()`, `blob.textStream()`, `URL.createObjectURL()`, and `stream/consumers` `blob()` are unsupported.
  Blobs cannot be sent to workers.
- **Prototypes:** Named properties and methods on program class prototypes support replacement, inheritance, and deletion.
  A module-level JavaScript class's prototype can become the prototype of other objects: `Object.create(C.prototype)`, `Object.setPrototypeOf(F.prototype, C.prototype)`, `Object.setPrototypeOf(object, C.prototype)`, and `util.inherits(F, C)` with an ordinary constructor `F`.
  The inherited methods and accessors run with the inheriting object as `this`.
  Methods that use `#private` members or `super` throw a TypeError for a receiver that is not an instance of the class.
  `C.prototype` also reads as a value for `Object.getPrototypeOf` and for `===` and `!==` comparisons.
  Changing the prototype of a class prototype (`Object.setPrototypeOf(C.prototype, X)`, `util.inherits(C, F)`, or `Object.setPrototypeOf` on a class prototype reached through an instance) is unsupported.
  Other bare prototype reflection, TypeScript class prototypes as values, accessor replacement, and prototype storage on local, generic, mixin, or runtime-provided classes are unsupported.
  Plain objects, arrays, functions, class instances, and Date, RegExp, Map, Set, WeakMap, WeakSet, and Promise values inherit `constructor`, `valueOf`, `toString`, `hasOwnProperty`, and `propertyIsEnumerable` from `Object.prototype` as in Node, so `value.constructor === Object` and `value.valueOf === Object.prototype.valueOf` compare identities exactly.
  Named reads such as `value.valueOf` answer these inherited members; computed-key reads such as `value[key]` do not.
  In a TypeScript source, `instance.constructor` on an instance of a program class is the instance's own class, whatever class the static type names.
  Its `name`, comparisons with a class, `new (this.constructor as typeof C)(...)`, and statics read or called through `(this.constructor as typeof C).member` answer as in Node.
  The constructor of a generic class's instance answers its `name` only, and `constructor` is unsupported in a hierarchy that includes a function-local class extending another function-local class.
  Binding `this.constructor as typeof C` to a variable is unsupported when a subclass takes different constructor arguments, and so is `constructor` read through the static type of an array, Map, Set, function, or builtin error.
  Static accessors of TypeScript classes compile, including inherited ones and `super` in a static member; compound assignment and `++` on a static accessor are unsupported.
  `Object.prototype`'s own members read as values; its mutation and other reflection, such as `isPrototypeOf` and `__proto__`, are unsupported.
  The constructors `Object`, `Function`, `Date`, `RegExp`, `Map`, `Set`, `WeakMap`, `WeakSet`, and `Promise` read as function values; through a stored value only `Object` can be called.
  `new` through a stored `Object`, `Date`, `Map`, `Set`, `WeakMap`, or `WeakSet` builds the same value as the `new` expression.
  `new` through a stored `RegExp` or `Promise`, `new` of a stored `Object` with a primitive argument, and `instanceof` against these constructors are unsupported.
  `instanceof Object` and `instanceof Function` on untyped values follow the prototype chain as in Node, so an object built with `Object.create(null)` is not an `Object`.
- **Built-in members:** A program that replaces, deletes or shadows a member of one of the language's built-in objects and also uses that member natively is refused at build time, a deliberate difference from Node.js.
  Compiled code reaches a built-in such as `Array.prototype.push` or `Math.random` directly and never reads it from its object, so that use would keep running the built-in.
  The write is refused with `SC1092`, which names the write and the use: an assignment, `delete`, `Object.defineProperty`, `Object.defineProperties`, `Object.assign`, or a `Reflect` call whose target is a built-in constructor, its `prototype`, `Math`, `JSON`, `Reflect`, or the global object.
  A use is a call of the member through a value of the built-in, a call or a read through the built-in object, another built-in that reads it (`promise.catch()` reads `then`, `String(array)` reads `join`), or a language construct that reads it: a string or numeric conversion (`toString`, `valueOf`, `Symbol.toPrimitive`), an iteration (`Symbol.iterator`), an awaited or resolved value (`then`), or `JSON.stringify` (`toJSON`).
  The write and the use may be in different modules or packages, and the rule applies to TypeScript and JavaScript sources alike.
  Adding `then`, `toJSON`, `Symbol.iterator` or `Symbol.toPrimitive` to a built-in prototype that does not have it counts as such a write.
  A write whose member nothing in the program uses natively compiles as it did before this rule.
  The readers that do see a change are a value read of the member through `Array.prototype` or through an untyped value, `for…of`, array spread and `Array.from` over an untyped value, and the `Map`, `Set`, `WeakMap` and `WeakSet` constructors reading their adder.
  A use that runs before every write of the member, such as `const original = Date.now` at the top of the module that then replaces `Date.now`, is not counted.
  A polyfill guarded by a test of the member, such as `if (!Array.prototype.at) { ... }`, compiles: the member exists in Node 24, so the guarded code never runs, and a program that also changes the tested member is refused.
  Redefining a member with the same value to change its attributes, assigning `Error.stackTraceLimit`, and a write Node itself rejects, such as an assignment to `Math.PI`, are not refused.
  A write the build cannot see, where the object or the key arrives as a value, is not refused: `Math`, `JSON` and `Reflect` members throw at run time, and for other built-ins compiled code keeps calling the built-in.
  To change what a built-in does, for a test mock for instance, call a function of your own and replace that.
  A package that contains a refused write runs in a Node island under the default `--node-islands=auto`.
- **Intl.NumberFormat and Intl.DateTimeFormat:** Formatter objects compile, with their options and `format()`, over ICU 78 (the release Node.js 24 bundles).
  The options object is read when the program runs, by the steps ECMA-402 gives, so it can be a variable or hold computed values, and an invalid option throws the error Node.js throws.
  `Number.prototype.toLocaleString`, `Date.prototype.toLocaleString`, `toLocaleDateString` and `toLocaleTimeString` take a locale and options and build the same formatters.
  The locale is the default locale (no argument, or `undefined`), `"en-US"` or `"en"`; any other locale and a list of locales are refused, because which locale Node.js picks for a requested tag is not modelled.
  `formatToParts`, `formatRange`, `formatRangeToParts`, `resolvedOptions`, `supportedLocalesOf`, detached `format` methods, BigInt and numeric-string inputs, and the `calendar` and `numberingSystem` options of `Intl.DateTimeFormat` are unsupported.
  The other `Intl` constructors (`Collator`, `PluralRules`, `ListFormat`, `RelativeTimeFormat`, `DisplayNames`, `DurationFormat`, `Locale`) and `localeCompare` with a locale argument are unsupported.
  A program that uses these forms links ICU and needs the data file `icudt78l.dat` (18.7 MiB) beside the executable; `scriptc build` puts it there.
  Time zone rules and names are those of the ICU release, which can be older than the time zone data Node.js ships: Node.js 24.22 prints the offset for the names `EST5EDT`, `CST6CDT`, `MST7MDT` and `PST8PDT` and `UTC` for `GMT0` where scriptc prints the zone's name, and the two disagree about `Africa/Casablanca` and `Africa/El_Aaiun` from October 2026 on.
- **Intl.Segmenter:** Unicode grapheme segmentation, iteration, and `containing()` compile, with the default locale or one language tag of the form language\[-script]\[-region] written as a string literal (grapheme clusters do not depend on the locale).
  Word granularity compiles too, over ICU's UAX #29 word iterator at that locale; its segments carry `isWordLike` exactly as Node.js's do.
  In JavaScript dependencies, sentence segmenters construct, and their `segment()` throws an SC2020 error; in TypeScript sources the construction is refused.
  Locale negotiation over several locales, sentence segmentation, `resolvedOptions()`, and detached methods are unsupported.
- **Globals:** Stored `globalThis` references support identity and probes for absent capabilities.
  A name that Node.js 24 defines but scriptc does not implement, such as `WebSocket`, `FormData`, `BroadcastChannel`, or `URLPattern`, is not an absent capability: reading it (`typeof X`, `X`, `globalThis.X`, `globalThis["X"]`, or through a stored `globalThis`) answers Node's constructor rather than `undefined`, so the value carries Node's `name` and `length`, `typeof` answers `"function"`, and `x instanceof X` answers `false` for every value because native code never produces an instance.
  The value carries `name`, `length` and `prototype`; reading any other member, such as the class's static `WebSocket.OPEN` or a method on its prototype, raises `SC2020` naming the global, because native code does not provide that surface.
  Constructing or calling that value raises `SC2020` naming the global, with `host-global` as the Node island reason; in JavaScript the raise is the throw of the reached `new X(...)` or `X(...)` statement, so a module that reads the global at load time still runs.
  Probes for capabilities Node.js 24 also lacks, such as `Bun`, `Deno`, or `Temporal`, still answer `undefined`.
  In JavaScript, reading a builtin global through the global object by name (`globalThis[name]`, or a stored global object) answers what the global's own spelling reads, and builtin module members that are global objects in Node, such as `require("buffer").atob` and `require("timers").setTimeout`, are the same values.
  `WebAssembly` read this way, and builtin members read through a global object that untyped code received, can raise `SC2020`.
  `for (key in globalThis)` visits Node's enumerable globals in Node's order, then the program's own global properties.
  `key in globalThis` and `Reflect.has(globalThis, key)` answer for the names of Node's global object, the names it inherits from `Object.prototype`, and the program's own global properties.
  A global object that untyped code received answers `false` to `in` for every name.
  In JavaScript, a name no declaration binds reads the program's own global property of that name, as in Node: after `globalThis.X = class X {}` or `global.X = function () {}`, in the same module or another, `X`, `new X()`, `X()` and `typeof X` use that value; with no such property, a read throws `ReferenceError: X is not defined` and `typeof X` is `"undefined"`.
  A sloppy-mode function's `this.X = value` write lands on that global object when it runs with no receiver (a plain call, or `call`/`apply` with `undefined` or `null`), so the name reads the property afterwards too.
  `navigator` answers Node's `hardwareConcurrency`, `language`, `languages`, `platform`, and `userAgent`, with `language` derived from `LC_ALL`, `LC_MESSAGES`, or `LANG` as Node does.
  `navigator.locks` raises `SC2020`, as does `navigator.language` for a locale outside the `language_TERRITORY.codeset@modifier` form, or one whose language or territory needs ICU's tables (a three-letter territory, for example).
  `Temporal` is not defined, as in Node.js 24: `typeof Temporal` is `"undefined"`, `globalThis.Temporal` is `undefined`, and any other use throws `ReferenceError: Temporal is not defined`.
  Programs that name it type-check against TypeScript's `Temporal` declarations.
- **Map and Set:** Keys and elements support numbers and strings by value, and records, classes, arrays, functions, symbols, server handles, and unions of these reference types by identity.
  Map values may also be functions.
  `unknown` slots use native checked values, comparing supported primitives by value and retaining supported object references.
  Typed nested collections other than `Map<unknown, unknown>` and `Set<unknown>`, and promises requiring payload adapters, cannot pass into these slots.
  `Promise<unknown>` retains its native representation.
  Stored `keys()`, `values()`, and `entries()` iterators compile, retain element identity, and observe live collection mutations until exhausted.
  They support `.next()`, `for...of`, array spreads, and mapper-less `Array.from`.
  They also support the iterator helper methods, such as `.map()` and `.take()`.
  JavaScript sources compile a method value read from Set, Map, WeakMap, or WeakSet: calling it with `.call` applies the method to that receiver and throws Node's brand-check TypeError when the receiver is not the matching collection.
  Typed sources still refuse a method read as a value with SC1090.
  Constructing a Map, Set, WeakMap or WeakSet, or calling `Object.fromEntries`, with a checked-dynamic iterable (a native iterator, a generator, or an object with a `Symbol.iterator` method) takes all of its values before adding the first one.
  When an entry of a Map seed is not an object, the TypeError comes after the iterable is exhausted; Node closes the iterator at that entry.
  The Set composition methods (`union`, `intersection`, `difference`, `symmetricDifference`, `isSubsetOf`, `isSupersetOf`, `isDisjointFrom`) accept a Set of the same element type or a Map keyed by it; other set-like arguments are unsupported.
- **util.types:** The type predicates answer the value's internal brand, as Node.js's do.
  `isArgumentsObject` answers true asked of `arguments` itself or of a constant holding it.
  An arguments object that has left for an untyped slot, including one reaching a stored predicate value, is the argument list, an array, and answers false where Node.js answers true.
  `isModuleNamespaceObject` answers true asked of an `import * as` binding, a constant holding one, an awaited `import()`, and a program module's namespace that reached an untyped slot in a TypeScript source.
  A builtin module's namespace that has left for an untyped slot answers false there, where Node.js answers true, and so does a program module's namespace in a JavaScript source; ask the question of the binding itself.
  Builtin sub-module members reached through another module, such as `fs.promises` and `util.types`, answer false, as in Node.js.
- **Compression:** The one-shot `deflate`, `inflate`, `deflateRaw`, `inflateRaw`, `gzip`, `gunzip`, and `unzip` functions and their `Sync` forms accept Node's options, validated at run time, including `dictionary`, `maxOutputLength`, and `rejectGarbageAfterEnd`.
  Typed inputs are strings, Buffers, and Uint8Arrays; untyped JavaScript values may also be other TypedArrays, DataViews, and ArrayBuffers.
  A literal `info: true` in an options object answers Node's `{ buffer, engine }` result in the sync, callback and promisified forms, and an options value whose type does not carry the literal `true` still takes the Buffer form and refuses.
  The `Deflate`, `Inflate`, `Gzip`, `Gunzip`, `DeflateRaw`, `InflateRaw`, and `Unzip` streams, from `createGzip()` and the other factories or `new zlib.Gzip()`, accept Node's options and support writes, piping, `pipeline`, async iteration, `flush`, `params`, `close`, `reset`, and `bytesWritten`.
  Each output chunk is its own Buffer rather than a slice of a shared chunk-size buffer.
  `zlib.constants` members and their module-level copies read as Node's values.
  The Brotli one-shots `brotliCompress` and `brotliDecompress` and their `Sync` forms, and the `BrotliCompress` and `BrotliDecompress` streams, accept Node's options, including `params` and `dictionary`, and produce the same bytes as Node.
  The zstd one-shots `zstdCompress` and `zstdDecompress` and their `Sync` forms, and the `ZstdCompress` and `ZstdDecompress` streams, accept Node's options, including `params`, `dictionary`, `pledgedSrcSize`, and `rejectGarbageAfterEnd`, and produce the same frames as Node.
  `util.inspect` is unsupported.
- **Synchronous iteration:** Checked `for...of` follows live iterator methods, including compiled class methods and abrupt-completion cleanup.
  Mapper-less checked `Array.from` accepts those iterators and plain array-like objects.
  Sparse checked arrays and general borrowed iterator factories are unsupported.
  Direct statically typed class loops accept iterators that are class instances or object literals; a loop that exits early calls the iterator's zero-parameter `return()`, as Node's IteratorClose does.
- **Iterator helpers:** `map`, `filter`, `take`, `drop`, `flatMap`, `toArray`, `forEach`, `reduce`, `some`, `every` and `find` compile on generators, on array, typed array, string, Map and Set iterators, and on the objects the helpers return.
  Helper objects support `.next()`, `.return()`, `for...of`, spreads, `Array.from` and `yield*`, pull their source lazily, and close it (run its `return()`) at the same points as Node: when `take` reaches its count, when a short-circuiting method finds its answer, when a callback throws, and when the loop consuming the helper exits early.
  In TypeScript, `h.return!()` and `h.return?.()` compile when `h` is a helper call or a `const` initialized with one.
  `flatMap` callbacks return arrays, generators, Sets or Maps; a string result throws Node's TypeError.
  `Iterator.from` accepts arrays, strings, typed arrays, Sets, Maps and iterator objects such as generators.
  In JavaScript, untyped generators and array, string, Map and Set iterators answer the same methods, and array destructuring of untyped values steps generators, iterators, Maps and Sets the way Node does.
  Helper methods on untyped async generators are unsupported.
  `console.log`, `util.inspect` and `Object.prototype.toString` print iterator objects as Node does: `Object [Iterator Helper] {}`, `Object [Generator] {}`, `Object [Array Iterator] {}`, and Map and Set iterators with the entries they have left.
  `Iterator.from` over an object with its own `next` method or over an untyped value, the `Iterator` global as a value, and `String()` and template literals of typed iterator objects are unsupported.
- **Regular expressions:** Native checked storage preserves identity, `source`, `flags`, flag properties, and stateful `test`, and the flag getters (`global`, `unicodeSets`, and the rest) compile on statically typed regexes.
  Static `exec`, global `match`, and numeric `lastIndex` access use the native matcher.
  Runtime constructors accept checked strings and native regex copies; `replace` and `replaceAll` accept native regex values with string templates or callbacks, including captures, named groups, and UTF-16 offsets.
  Named groups (`groups`) read on matches of a statically known regex: from `exec` and `match`, through optional chains (`m?.groups?.name`), in a `for...of` over `matchAll`, and on the element parameter of an array-method callback over `matchAll` rows.
  The groups object has a null prototype, as in Node.
  Non-numeric `lastIndex`, detached methods, and `exec` through an untyped receiver are unsupported.
  The `v` (unicode sets) flag supports class set operations (`--`, `&&`), nested classes, `\q{...}` string literals, and properties of strings such as `\p{RGI_Emoji}`.
  `new RegExp` with an invalid pattern throws a `SyntaxError` whose message comes from the native engine and can differ from Node's.
  The `d` (match indices) flag is unsupported: regex literals and `new RegExp` calls with a literal flag string containing it fail to compile with SC1120, and `new RegExp` with a computed flag string containing it throws an error naming the flag.
- **Object freezing:** `Object.freeze` and `Object.isFrozen` support plain checked dictionaries and primitives.
  Freezing is shallow and preserves accessors.
  Aliased typed records, arrays, proxies, and other native objects cannot be frozen.
  Class instances support `Object.freeze`, `Object.seal`, `Object.preventExtensions`, `Object.isFrozen`, `Object.isSealed`, and `Object.isExtensible` when the static type of the argument names the class.
  A write to a read-only field throws Node's `TypeError` in strict mode code and leaves the field unchanged in sloppy mode code.
  `Object.defineProperty` with a string-literal key changes the value, `writable`, `enumerable`, and `configurable` attributes of a declared field; an object-literal accessor descriptor (`get` or `set`) for a declared field is refused at compile time with `SC2020`, and another accessor descriptor for a declared field throws `SC2020` when the call runs.
  `Object.freeze`, `Object.seal`, and `Object.preventExtensions` of a class instance held in an untyped value change the instance itself, so writes through its typed references are checked the same way.
  A typed record checked from a frozen untyped object (an object literal frozen in a JavaScript file, or a frozen `JSON.parse` result) is frozen too, and writes through its typed references are checked the same way.
  The `TypeError` for a write to a read-only property that was added to a class instance through an untyped value names `#<Object>` where Node names the class.
  Functions support `Object.freeze`, `Object.seal`, `Object.preventExtensions`, `Object.isFrozen`, `Object.isSealed`, and `Object.isExtensible`; the operations apply to the function's own properties (`name`, `length`, `prototype`, and the ones a program assigns).
  Class constructors are not supported.
  A write to a read-only property of a checked dictionary or a function throws a `TypeError` whose message omits Node's `'x' of object '#<Object>'` (or `of function '…'`) suffix.
  A JavaScript class field that is first created after its instance became non-extensible is added instead of throwing.

### Builtin function values

Some builtins can be stored and passed as functions:

- Lowered `node:path` functions across the bare, POSIX, and win32 modules, including optional `basename` and variadic `join`/`resolve`.
  Called through an untyped value, they validate their arguments with Node's `ERR_INVALID_ARG_TYPE`.
  In JavaScript, a direct call whose arguments are not strings, `join` and `resolve` included, validates them the same way.
  A path module held by untyped code is a plain object with Node's members in Node's order.
- Supported zero-argument `node:os` functions and `querystring.escape`/`unescape`.
- `util.isDeepStrictEqual`, `styleText`, `stripVTControlCharacters`, `toUSVString`, `inherits`, `deprecate`, `callbackify`, `debuglog`, and `debug`.
- `fs.existsSync`, `unlinkSync`, `chmodSync`, `chownSync`, `renameSync`, and `closeSync`, and `fs/promises.unlink`, `chmod`, and `rename`.

`String`, `Number`, `Boolean`, and `BigInt` are function values wherever they are stored, in TypeScript and JavaScript: an option table can hold them as type tags (`{ "--port": Number, "--tag": [String] }`), beside strings and arrays.
A held constructor is the same function everywhere in the program, so `type === Boolean` holds across modules, through `unknown`, and as a `Map` key or `Set` member.
Called through the value, it converts whatever arguments it is given, as the builtin does.
A call through a held constructor converts its arguments to untyped values first; a direct call such as `Number(x)`, and a constructor written as the callback of an array method (`xs.map(Number)`), keep their typed conversion.
A held constructor converts to a function type that has a first parameter (`(value: string, name: string) => unknown`), whoever calls it.
Under a function type with no parameters it is called with no argument, and the store is refused when the program hands a function of that type to a caller that passes arguments, as for any function that reads its argument list.
Through a function type whose first parameter is optional or accepts `undefined`, a call that passes `undefined` there, or omits the argument, throws a `TypeError`: a constructor answers a missing argument differently from `undefined` (`String()` is `""`), and the function type does not say which one the call passed.
The `prototype` of a held constructor and a `Map` or `Set` keyed by a union of several constructor types are refused.
`new` through a held constructor builds with warning `SC6015`, which names the statement, and the statement throws `SC1090` when it runs.
In TypeScript, `util.format("%o", f)` of a function value and `Object.getPrototypeOf` of a held constructor build with the same warning and throw when they run.
A constructor passed into a Node island is the island's own constructor there, and the island's comes back as the program's.
Converted to a string (`String(c)`, a template, `+`, `c.toString()`, `%s`), a held constructor is Node's `function String() { [native code] }`.
A program that hands a held constructor, or a table of them, to something the typed form has no lowering for (printing, `JSON.stringify`, `in`, key listing, an `any` slot, `bind`, an assertion) holds its constructors as untyped values instead, in the whole program, and builds as it did before constructors had a typed form.
`JSON.stringify` and `%j` write a `Map` or `Set` held by an object, an array, or a tuple as `{}`, as Node.js does, whatever it holds.

In JavaScript, `setTimeout`, `setInterval`, `setImmediate`, `clearTimeout`, `clearInterval`, `clearImmediate`, `queueMicrotask`, and `process.nextTick` are function values wherever they are stored, including conditional and logical expressions and assignments: `typeof` answers `"function"`, identity holds across spellings, and calls schedule as the direct calls do, returning the same numeric timer handles.
Calls through these values bypass `node:test` mock timers.
In JavaScript, `atob`, `btoa`, and `structuredClone` are function values the same way, and calls through them run what the direct calls run.
In JavaScript, `parseInt` and `parseFloat` are the `Number.parseInt` and `Number.parseFloat` function values, `isNaN`, `isFinite`, and `Symbol` (with `Symbol.for`, `Symbol.keyFor`, and the well-known symbols) are function values whose calls run what the direct calls run, and `Atomics` is an object whose integer operations run.
Calling `eval` through its value throws the `EvalError` its direct calls throw.
`escape`, `unescape`, and `AsyncDisposableStack` have function values with Node's `name` and `length` that throw `SC2020` when called or constructed.
`globalThis.require`, `module`, `exports`, `__dirname`, and `__filename` are `undefined`, as in Node.
In JavaScript, any other Node function or class stored as a value (a global such as `URL` or `Headers`, a builtin module member such as `module.register` or `dns.lookup`) is a function with Node's `name` and `length`: `typeof` answers `"function"`, identity holds across spellings, and calling or constructing it through that value throws `SC2020`.
`process` and `performance` stored in JavaScript are objects with Node's members: a function member is the function the spelled member reads (`held.nextTick === process.nextTick`), `argv`, `platform`, `arch`, `pid`, `execPath`, `execArgv`, `versions`, `debugPort`, `sourceMapsEnabled` and `performance.timeOrigin` read their values, and reading any other data member through the stored object throws `SC2020`.
Assigning a data member through the stored object (`held.exitCode = 1`) throws `SC2020`; the spelled `process.exitCode = 1` is not affected.
The stored object is frozen: redefining a member with `Object.defineProperty`, replacing a function member or adding a key through it throws a `TypeError`, where Node changes the process object, because the spelled members (`process.pid`, `process.nextTick(f)`) are compiled where they are written and would not follow the change.
Any other Node value that native code does not provide (`process.finalization`, `performance.nodeTiming`) throws `SC2020` where JavaScript reads it, and `typeof` of it answers Node's type without reading it.
A global that Node 24 does not define by default (`localStorage`, `sessionStorage`) is not defined: `typeof` answers `"undefined"`, the global object has no such property, and a read throws `ReferenceError`.
In a JavaScript ES module, `typeof` of `require`, `module`, `exports`, `__dirname`, and `__filename` answers `"undefined"`, as in Node.
An ES module namespace of a builtin module has only the module's exports, so `zlib.Z_NO_FLUSH` through `import * as zlib` is `undefined` in JavaScript, as in Node, while the default import and `require()` have it.
In JavaScript, `require("events")` and the `EventEmitter` class assigned to an untyped binding or stored in an object (`ReleaseEmitter = EventEmitter`) are the untyped `EventEmitter` constructor that constructor functions inherit from (see below): `new` through it builds such an emitter object, and identity and `instanceof` agree with the class.
A class can extend `require("events")` directly.

Other immutable aliases of table-backed Node builtins support direct calls, but cannot be passed elsewhere.
`util.promisify` supports compile-time conversions of `child_process.execFile`, `child_process.exec`, `setTimeout` and `setImmediate` (to the `timers/promises` functions, as in Node), `stream.finished` and `stream.pipeline` (to the `stream/promises` functions, as in Node), the `zlib` callback one-shots `deflate`, `inflate`, `deflateRaw`, `inflateRaw`, `gzip`, `gunzip`, `unzip`, `brotliCompress`, `brotliDecompress`, `zstdCompress` and `zstdDecompress` (with or without options), the `crypto` callback functions `randomBytes`, `pbkdf2`, `scrypt`, `hkdf`, `randomFill`, `randomInt` and `generateKey`, `dns.lookup` (settling with `{ address, family }`, or the address array with `all: true`) and `dns.lookupService` (settling with `{ hostname, service }`), and the `fs` callback functions that have an `fs/promises` counterpart (`access`, `appendFile`, `chmod`, `chown`, `copyFile`, `lchown`, `link`, `lstat`, `lutimes`, `mkdir`, `mkdtemp`, `readFile`, `readdir`, `readlink`, `realpath`, `rename`, `rm`, `rmdir`, `stat`, `statfs`, `symlink`, `truncate`, `unlink`, `utimes`, and `writeFile`); calls through the converted function lower as the `fs/promises` call.

`util.promisify` over an untyped function (a value typed `any`, a function whose parameters are untyped, or any other target in JavaScript) returns a native wrapper that follows Node's callback rules and carries the original's `name` and `length`, and throws Node's `ERR_INVALID_ARG_TYPE` for values that are not functions.
Such a function with other own properties, `util.promisify.custom` among them, throws `SC2020` where `util.promisify` runs, and the wrapper does not report Node's DEP0174 warning when the original returns a promise.

`util.styleText` supports standard formats and aliases, `#RGB` and `#RRGGBB` hex colors, arrays of formats, nested ANSI resets, and explicit color enabling with `{ validateStream: false }`.
Native calls accept standard process streams; custom native streams and mutations to `util.inspect.colors` remain unsupported.

`util.inherits` links the prototypes of constructor functions, and `util.deprecate`, `util.callbackify`, and `util.debuglog` return native function values that forward receivers and arguments.
In JavaScript, a constructor function can inherit from `EventEmitter`, the legacy `Stream` constructor, or a stream class through `util.inherits` or `Object.setPrototypeOf`.
`EventEmitter.call(this)` and `Stream.call(this)` initialize the object as Node does, and its listener methods keep their state in the object's own `_events`, `_eventsCount`, and `_maxListeners` properties.
The `captureRejections` option and `events.errorMonitor` listeners are not supported on such objects.
`Readable.call(this, options)` and the same call on `Writable`, `Duplex`, `Transform`, or `PassThrough` give the object a native stream.
The stream calls the object's `_read`, `_write`, `_final`, `_transform`, `_flush`, and `_destroy` with the object as `this`, and the stream prototypes' methods and accessors work on it.
Its listeners live in the native stream rather than in `_events`, and `_readableState` and `_writableState` are read-only views that answer the commonly read fields.
Typed code that receives such an object as a stream class uses its native stream, and piping a native stream into it works.
The options the native streams refuse (`signal` and others) are refused here too, as are `_writev` on a Transform, `_final` on a Transform, symbol event names, `listeners()`, `rawListeners()`, `wrap()`, and pipes into a destination that overrides `write()` or `end()`.
`_construct` and `_writev` are read when `Readable.call(this, options)` or `Writable.call(this, options)` runs, as Node's constructors read them.
Other class constructors passed to `util.inherits` or `util.deprecate` are refused.
Deprecation warnings are emitted on the next tick and follow assignments to `process.noDeprecation` and `process.throwDeprecation`; compiled binaries take no Node command-line flags such as `--no-deprecation` or `--trace-deprecation`, and `process.traceDeprecation` is unsupported.
A stored `util.deprecate` reports a `length` of 4, where Node reports 3.
In TypeScript, a `util.callbackify` result must reach a callback whose error parameter accepts `null`; the @types/node overloads that type it as non-null are refused.
`util.debuglog` reads `NODE_DEBUG` as the process started.
Its `%s` refuses functions and objects with symbol-keyed members, and object arguments are refused on a color terminal.

`util.isDeepStrictEqual` supports primitives, ordinary records and arrays, cycles, enumerable string and symbol properties, and byte views.
Its optional third argument skips prototype and constructor comparisons.
Structural comparisons of native Maps, Sets, class instances, Errors, proxies, accessor properties, and function-valued own `constructor` properties remain refused.

Synchronous and promise filesystem APIs with trailing arguments that affect behavior remain call-only.
This includes writers, open, directory creation/removal, metadata readers, and directory readers.
Passing them as values could discard modes, flags, encodings, or options during function-signature adaptation.

Native error-first filesystem callbacks can be stored and passed to platform adapters.
Reads support Buffer and UTF-8 results; writes preserve string flags and modes; copy follows `fs.cp`.
Reads and writes reject already-aborted signals, but native file operations complete synchronously before the deferred callback, so cancellation cannot interrupt I/O in progress.
Recursive directory reads and Dirent results report a native limitation.
`fs.glob` compiles only as a direct call; a stored `fs.glob` value throws `SC2020`.

### Native worker threads

`node:worker_threads` runs workers as native threads.
Worker entry points must resolve at build time: use a source-relative `new URL("./worker.ts", import.meta.url)`, the current module URL, an absolute path, or CommonJS `__filename`.
A computed entry the build can bound to a finite set of files (a `new URL` of bounded text against `import.meta.url`, the value a module-local function returns, a branch of `?:`, `??` or `||`) starts the compiled file its value names; a value outside that set throws `ERR_SCRIPTC_WORKER_ENTRY` when the constructor runs, where Node would start that file.
Each worker initializes its own module state, event loop, environment snapshot, and reference-counted heap.
Nested workers inherit their parent's environment snapshot.
Supported constructor options are `workerData`, `argv`, a `transferList` of MessagePorts, `name` (reported by `threadName` and `worker.threadName`), `stdout`, `stderr` and `stdin`, and `resourceLimits.stackSizeMb`, which sets the worker thread's stack size (at least 192 KiB; without it a worker's stack is 8 MiB, where Node's is 4 MiB).
The `resourceLimits` heap limits (`maxOldGenerationSizeMb`, `maxYoungGenerationSizeMb`, and `codeRangeSizeMb`), `worker.resourceLimits` while the worker runs, and `resourceLimits` read inside a worker are refused; in the main thread `resourceLimits` is `{}`, and after a worker exits `worker.resourceLimits` is `{}`, as in Node.
With the `stdout` or `stderr` option, the worker's writes to that stream reach `worker.stdout` or `worker.stderr`, a Readable that ends after the worker's `'exit'` event; its chunks arrive in order with the worker's other events.
Without the option a worker writes to the process's own output, and reading `worker.stdout` or `worker.stderr` is refused.
With the `stdin` option, `worker.stdin` is a Writable whose chunks the worker reads from `process.stdin`, in order with the worker's other events, and `process.stdin` ends when `worker.stdin` ends; without it `worker.stdin` is `null` and the worker's `process.stdin` ends without data, as in Node.
Runtime paths, `eval`, loaders, and shared environments are unsupported.
`setEnvironmentData` and `getEnvironmentData` keep a map per thread with SameValueZero keys; each new Worker starts from a clone of its parent's entries taken by the constructor, which throws `DataCloneError` when an entry cannot be cloned.
Worker argument strings containing null bytes are also unsupported.

Workers support `online`, `message`, `error`, and `exit` events, `postMessage`, `terminate`, `ref`, and `unref`.
`worker[Symbol.asyncDispose]()` terminates the worker and resolves with `undefined` after its `exit` event, so `await using` disposes a Worker at the end of its block.
A worker's uncaught exception reaches its `error` listeners as Node's error serializer sends it: an Error keeps the nearest of `Error` and its six native subclasses (an `AggregateError` arrives as an `Error` named `AggregateError`) along with its name, message, cause, and other data properties and their enumerability, and other thrown values clone as messages do.
An unhandled rejection whose reason is not error-like arrives as an `UnhandledPromiseRejection` Error with code `ERR_UNHANDLED_REJECTION` quoting the reason.
A thrown value that does not clone, such as a function, arrives as an `ERR_WORKER_UNSERIALIZABLE_ERROR` Error, where Node sends its `util.inspect` string.
`new MessageChannel()` from `node:worker_threads` makes two entangled MessagePorts, in one thread or across threads once a port is transferred, with `receiveMessageOnPort`.
Port messages arrive in Node's order: each port's queued messages as one batch before `setImmediate` callbacks, a port's own `'close'` after the poll phase, and the other port's `'close'` after its queued messages while the loop keeps running.
A worker's messages to its parent, and the parent's messages in the worker, also arrive as one batch before `setImmediate` callbacks, each followed by its microtasks, as in Node: the messages queued when delivery starts, and those that arrive meanwhile until the batch holds 1000.
`receiveMessageOnPort(parentPort)` in a worker takes the parent's next message.
A port's `onmessage` property and `addEventListener` are unsupported.
`BroadcastChannel` delivers each message to every other open channel with the same name, in any thread, as a `'message'` `MessageEvent` through `onmessage` and `addEventListener`; an open channel keeps the process alive until `close()` or `unref()`.
A `messageerror` event is never dispatched.
The parent port supports message listeners, `postMessage`, `close`, `start`, `ref`, `unref`, and `hasRef`.
Messages clone primitives, plain object graphs, arrays, errors, ArrayBuffers, typed arrays, and DataViews, preserving cycles and aliases.
Program class instances arrive as plain objects with their own data properties, without their prototype, methods, accessors, or private fields, as in Node.
`markAsUntransferable` marks objects for the current thread, and a marked ArrayBuffer or MessagePort in a transfer list throws `DataCloneError`, as does the ArrayBuffer of a pooled Buffer; marked objects other than ArrayBuffers stay alive until their thread exits.
`markAsUncloneable` marks a plain object, an error, or a class instance, and `structuredClone` and `postMessage` then throw `DataCloneError` for it, also where it is nested; as in Node, the mark has no effect on primitives, arrays, typed arrays, and ArrayBuffers.
A plain object that the compiler copies into a container with a fixed element type, such as an array literal whose elements all have one shape, is a separate object in the copy, which is not marked.
Marked objects stay alive until their thread exits.
A MessagePort travels with a message when the message's transfer list (an array or `{ transfer }`) names it, and the sending port is unusable afterwards; a port in a message without that listing throws `DataCloneError`.
Other transferable values, functions, proxies, and other native handles are refused.
SharedArrayBuffers share their backing storage while retaining separate wrappers in each thread.
Graphs made immutable with `@scriptc/threads` are passed by reference instead of cloned; see [Sharing Data Between Workers](/docs/threads).

SharedArrayBuffer storage is fixed-length.
Atomics supports integer `load`, `store`, `exchange`, `compareExchange`, `add`, `sub`, `and`, `or`, and `xor` over 8-, 16-, and 32-bit integer arrays.
`wait` and `notify` use Int32Array views; BigInt atomics, `waitAsync`, and growable buffers are unsupported.
Atomics operations run as sequentially consistent atomic instructions and take no lock.
Other reads and writes of shared storage access each element atomically; a copy, fill, or conversion that runs while another thread writes the same storage can observe some of those writes, as in Node.
`terminate` unwinds at runtime checkpoints, including loops and parked atomic waits, without running JavaScript catch or finally blocks.
Blocking native calls finish before the next checkpoint.

A worker that reaches a Node island posts each island operation to the thread that started the island, the main thread, and waits there for its answer, so a synchronous island call works from a worker.
The program has one instance of each island module for all its threads, where Node gives every worker an instance of its own: what an island module keeps between calls (a counter, a cache, a registered listener) is shared by the main thread and the workers.
The build warns at each load of an island module in a module that worker threads run (SC6011).
A program module that island code imports too is shared the same way: island code uses the main thread's instance of it whichever thread called the island, while a worker's own code uses that worker's instance, and the build warns for such a module as well (SC6011).
The main thread answers a worker's island operations from its event loop, so a worker's island call waits while the main thread runs synchronous code, and it is never answered when the main thread blocks until that worker is done, as with `Atomics.wait`.
An asynchronous island call from a worker rejects with `ERR_SCRIPTC_ISLAND_WORKER_ASYNC`, an island promise, symbol or byte value (a typed array, a Buffer or an ArrayBuffer) crossing into a worker throws `ERR_SCRIPTC_ISLAND_WORKER_VALUE`, and so does a worker's own symbol or byte value crossing into the island, and a native value the island passes by reference (a function, an object, an array, a Map, a Set, a promise, or an error) crossing between a worker and its island in either direction.
The error's message names the kind of value and the call, read, or write it was part of.
A promise that a worker resolves with an island thenable rejects with the same code, and one resolved with any other island object is fulfilled with it.
In a module that only worker threads run, an island call whose argument has a function, byte, promise, or symbol type is refused at build time (SC8012).
A worker entry must be a compiled module, so a Node island module as the worker's entry is refused at build time (SC8012).

Workers are unsupported in WASI, compiled libraries, and executables that use foreign-thread native callbacks.
Ordinary callbacks that run on their owning script thread remain supported.
Shared byte storage cannot expose unmanaged FFI pointers; copy it into an ordinary Buffer before passing it to native code.

### Child-process workers

`child_process.fork()` requires a worker path resolved at build time from `new URL("./worker.ts", import.meta.url)` or `fileURLToPath(new URL("./worker.ts", import.meta.url))`.
Never-reassigned `const` aliases and statically resolvable template parts are accepted.
The worker and its imports are embedded in the executable, which re-executes itself to start that module.
Worker arguments begin at `process.argv[2]`.

Supported options are default inherited stdio or an inline object with `cwd`, `env`, `silent`, `windowsHide`, or `[stdin, stdout, stderr, "ipc"]`.
`execArgv` is accepted but has no native effect.
Runtime worker paths, custom `execPath`, advanced serialization, transferred sockets or servers, uid/gid, timeout/signal options, and other stdio arrays are unsupported.

Parent and child channels support `send(message, callback?)`, `connected`, `disconnect()`, and `on`/`once` for `message` and `disconnect`.
Message listeners may return `void` or `Promise<void>`.
Messages use newline-delimited JSON, so their static types must be JSON-stringifiable or `unknown` backed by a checked JSON tree.
`send()` returns the channel's backpressure state and invokes its callback later with `null` or an `Error`.
`disconnect()` changes `connected` synchronously, before the later disconnect events.

A `'data'` listener on a child's `stdout` or `stderr` whose chunk is `unknown` or `any` receives strings when a `setEncoding()` statement on the same stream precedes it in its block or an enclosing block, and Buffers when the function never calls `setEncoding()` on that stream.
Any other placement of `setEncoding()` is refused at compile time, and a `setEncoding()` call that runs elsewhere afterwards makes the listener throw a TypeError instead of receiving strings.
`pause()`, `resume()`, `isPaused()` and `readableFlowing` on a child's `stdout` or `stderr` follow Node: a paused stream holds up to 64 KiB, then stops reading, which blocks the child once the pipe is full, and the child's exit resumes the stream.
Pausing the output of an `exec()` or `execFile()` child that has a callback holds back its `'data'` listeners, but the output is still read for the callback, so that child is not blocked.
The call's `maxBuffer` is counted while the stream is paused, so a child that writes past it is ended with the `maxBuffer` error at that point, where Node ends it once the stream is resumed.
A child's output arrives over a pipe where Node uses a socket pair, so the number of bytes a child can write to an unread or paused stream before it blocks differs from Node's.
An inline `'exit'` or `'close'` listener may declare `(code: number, signal: string)`, which TypeScript accepts through the catch-all `on` overload; it receives the `null` values Node passes, and a typed use of a `null` value throws a TypeError.
A function passed by name as an `'exit'` or `'close'` listener may declare its first parameter as `number`; when a signal ended the child, where Node calls it with `null`, it is not called and a TypeError is thrown instead, and a `string` signal parameter of such a function is refused at compile time.

### Untyped values

Supported uses of `any` use native checked values.
Operations without native lowering produce diagnostics such as `SC2011`.
Use `unknown` with a checked cast where an operation needs a concrete type.
Map and Set type arguments may use `any`, with the same checked storage as `unknown`.

Locals, parameters, and returns can carry untyped values; record and tuple fields can hold `unknown`.
JavaScript instance fields and TypeScript class fields declared `unknown` or `object` (also with `null` or `undefined`, or as arrays of those) can also hold native checked values.
Bare fields start as `undefined`, and redeclaring a field with the same checked-value type resets it in initialization order.
Explicit TypeScript `any` class fields, other checked-value static fields, statically typed array elements, and union members are unsupported.

Program class instances retain identity through `unknown` and casts back to their exact class or a base class, including classes with internal collections.
Inspecting dynamic properties throws a catchable `TypeError` if the class's fields cannot pass through checked storage.
Supported native handles also retain identity.
An instance of a class that declares `toString`, converted to a string where its class is not known (an untyped array's `join`, `String()` or a template over such an array, the default sort order), throws `SC2020` instead of running the method; convert the instance where its class is known.
JavaScript argument and return conversions preserve native array references; other JSON-shaped record and array conversions can copy values.

Supported operations on `unknown` include truthiness, `typeof` narrowing, property access, `+`, `switch`, and `throw`.
Cast before using other operations.

In JavaScript, a call whose untyped argument could select an overload with another result type returns a checked value, whichever overload the type checker picks, and so do the unannotated bindings and functions that hold or return it.
In TypeScript, an `any` argument keeps the overload the type checker picks, and a value that selects another overload at runtime throws a `TypeError` where the result is used.

### Type checking

scriptc checks programs against the `es2025` standard library, the ESNext declarations for `Array.fromAsync`, explicit resource management and `Error.isError`, and its own ambient declarations.
Some declarations differ from TypeScript's standard library or `@types/node`: `JSON.parse` returns `unknown`, and a Promise executor's reject reason is typed as `Error`.

A program that passes its project's TypeScript check may still use unsupported operations.
A second preflight using the project's declarations distinguishes those sites from ordinary type errors and reports scriptc diagnostics with rewrite hints.
Declared APIs without a compiler implementation, such as unsupported console members, produce `SC2020`.

From the nearest `tsconfig.json`, scriptc takes the strictness flags, `paths` and `baseUrl`, the JSX settings, the declaration files that `files` and `include` name, the packages that `types` names, `typeRoots`, and `verbatimModuleSyntax`, `isolatedModules` and `preserveConstEnums` for [import elision](/docs/dependencies#package-resolution).
A declaration file of the project, such as a shared `types.d.ts`, a workspace package of types or the typings it writes for an untyped package, declares data types the program builds, like the same types written in a `.ts` file.
A value that a package running in a Node island hands to the program stays that package's object under such a type: the program reads and writes it in place, and copies it only with `structuredClone`, a spread or JSON.
A project that sets `typeRoots` and no `types` has every package directory of a type root outside `node_modules` loaded, as tsc does through TypeScript 5.9.
A type root also answers a `/// <reference types>` directive, and an import of a module for which no `node_modules` directory holds declarations.
In a project that compiles under `moduleResolution: node10` (also spelled `node`, and tsc's choice through TypeScript 5.9 for `module: commonjs` when the setting is absent), a specifier that only the type checker reads (in a declaration file, an `import type` or an `export type`) can name a file that a package's `exports` map does not list, and a package's declarations are also found by path where the map names none, as that project's tsc resolves them.
A value import of a file the map does not list is refused, as Node.js refuses it at startup.
A project whose `target` is below ES2022, or that sets `useDefineForClassFields` to `false`, has its parameter properties assigned before its class field initializers run, as in tsc's output; a field declared without an initializer is still an own property that holds `undefined`.
The other settings keep one meaning.
`lib` does not change the library above, and no other library is loaded into the program that is compiled.
A library that `lib` names beside it, such as `dom`, decides only which names type-check.
A name that only such a library declares is an opaque type in compiled code, and a value of that type is checked dynamically.
As a value the name is absent at run time, as under Node.js: a read throws `ReferenceError`, `typeof` answers `"undefined"`, and `globalThis.document` is `undefined`.
A global that Node.js defines is never treated as absent.
A `/// <reference lib="dom" />` directive in a source file does load the library into the compiled program, where it conflicts with the declarations scriptc bundles for a project without `@types/node`; name the library in `lib` instead.
TypeScript files are ES modules or CommonJS by Node.js's rules (the file extension and the nearest `package.json`), whatever `module` and `moduleResolution` say, so `import x = require()` and `export =` are refused with `SC0016` in a project that sets `module` to `commonjs`.
Legacy decorators (`experimentalDecorators`) are refused with `SC0016`, except a class decorator of one parameter, which behaves the same under both decorator semantics.
A project that does not enable `strict` or `strictNullChecks` is refused with `SC0002`.

TypeScript keeps the type of an assignment across a call, so after `x = undefined` a read of `x` is typed `undefined` even when a function called in between assigns `x` again.
scriptc reads the stored value there.
A function without a declared return type that returns such a read is inferred to return `undefined`; scriptc refuses that return with `SC1090`, and a declared return type compiles.

### Contract types

[Contract types](/docs/contracts) are checked only where they are written as the annotation of a parameter, variable, class instance field or function result, `Acyclic` only on classes, interfaces and object type aliases, and `ProgramLifetime` only in the `implements` clause of a class declaration; other aliases, interface members and nested type positions are reported.
In programs that use worker threads, a surrounding `try`/`catch` does not discharge `NoThrow`.
`Int32` refuses parameters of functions used as values and fields of classes whose instances are stored in `any` or `unknown` values.
`Int32[]` refuses arrays that can come from or escape to code scriptc cannot see, follows callback results only for callbacks written at a `map` or `filter` call that scriptc runs inline, and checks a copy made by `slice` or a spread as if it were its source array.
Element reads that find a value are int32s, but a scalar `Int32` slot still needs `a[i] | 0`, because a read can miss and produce `undefined`.
`Immutable` refuses values stored in `any` or `unknown` or that another reference can still change.
`ProgramLifetime` applies to root classes of executables only; instances the program drops are never reclaimed unless they belong to a released [lifetime region](/docs/experimental#lifetimeregion).

### three.js

With `--npm-static=three`, native and WASI builds support published package imports and tested CPU workflows for math, bounds, procedural geometry, curves, shapes, extrusion, text geometry, scene graphs, cameras, lights, materials, textures, and render-target data.
Mesh workflows include instancing, batching, morph targets, skeletal deformation, LOD, and sprites.
Raycasting includes mesh intersections, sorted results, recursive traversal, layers, distance limits, camera rays, lines, and points.

Animation coverage includes tracks, clips, property binding, mixer playback, blending, and fading.
In-memory loading covers scene, material, geometry, and animation JSON, OBJ, STL, PLY, and self-contained glTF/GLB scenes with buffers, PBR material data, and animation.
This establishes the exercised workflows, not every loader extension or overload.

Tested CPU graph construction from `three/webgpu` and `three/tsl` includes scalar/vector nodes, mutable uniforms, `Fn` layouts and calls, and `MeshBasicNodeMaterial` data.

WebGL/WebGPU rendering, DOM controls, image and video resources, audio, and worker-based decoders require external host bindings.
A browser or native application can render geometry and material state produced by a compiled library and receive its resource lifecycle notifications.
Compressed model and texture decoding belongs to that host; shader execution and rendered output are outside the CPU coverage described here.

## Runtime differences from Node.js

Compiled code has runtime behavior that can differ from Node.js.
Account for the following differences when porting a program.

- **Event order across native code and islands:** Timers, worker output, and other macrotasks from the program and from JavaScript it runs in a Node island share one event loop under Node.js, so their order is fixed.
  A compiled program runs both on the event loop it hosts for Node.js, but it does not merge them into one order, so callbacks from the two sides can run in a different order than under Node.js, and the order can vary between runs.
  Only programs that depend on the ordering between native and island callbacks are affected.
- **Island packages that read the machine when they load:** The build keeps what an island package exports when it loads only when the load read nothing a run could answer differently.
  A load that reads a clock, a random number, the process id, the arguments, the working directory, the user, the host, a file outside the package, or the time zone of a machine whose `TZ` is unset runs when the program imports the package, which starts Node.js there.
  A package imported after any load that runs at its import runs at its import as well: Node.js is running by then, and the executable holds nothing the build computed after that load, such as a value the earlier load read and left in a global.
  A load that reads an environment variable, the default locale, a time zone that `TZ` names, or whether a standard stream is a terminal waits, and runs at the import in a run where that answer differs from the build's.
  A variable that has a value on the build machine keeps the load at the import unless the project lists it in `scriptc.nodeIslands.publicEnv`, so a build machine with `LANG` set does not defer a package that formats a number in the default locale.
  A package marked `"evaluate": "lazy"` waits whatever its load reads.
  The values it computed from the machine, the values of packages imported after it, and its members whose names do not appear in its files are then read from Node.js on first use.
  The build cannot see three things a load can depend on, and keeps the build machine's answer for them: memory the package allocated without initializing it, how deep the stack is, and a value computed from the package's own path that no longer contains the path.
- **Timers across native code and islands:** A timer or immediate the program creates is a number in JavaScript that runs in a Node island, as it is in compiled code.
  The island's `clearTimeout`, `clearInterval` and `clearImmediate` accept that number and clear the program's timer of that kind (`clearTimeout` of an immediate's number clears nothing, as in Node), but `typeof` answers `"number"` there and the value has no `ref`, `unref`, `hasRef`, `refresh` or `close` method.
  A timer the island creates stays the island's `Timeout` or `Immediate` object: the program clears it with the clear functions, calls its methods and passes it back to the island.
  In TypeScript, keep such a timer in a `const`; a variable or field typed as a timer holds only the program's own timers, and storing an island's timer in one is a compile-time error.
- **Abort listeners across native code and islands:** An `AbortSignal` shared between the program and JavaScript running in a Node island is one signal under Node.js, with one list of listeners that run in the order they were added.
  A compiled program keeps a signal on each side, and the two follow each other with the same `reason`; each side keeps its own list of listeners.
  When the signal aborts, the island's listeners run first, in the order they were added, and then the program's, in theirs.
  A signal the program made reports `aborted` on both sides before any listener runs; a signal made in the island reports it natively once the island's listeners have run.
  `getEventListeners` and `listenerCount` on either side count that side's listeners only.
  Only programs that depend on the order between a native and an island listener of one signal, or that count a signal's listeners across the two sides, are affected.
- **Compile cache in Node islands:** Node.js keeps a module compile cache only when `NODE_COMPILE_CACHE` is set, and a compiled program keeps one by default for the JavaScript it runs in a Node island.
  In island code `module.getCompileCacheDir()` therefore returns a directory where Node.js returns `undefined`, and `module.enableCompileCache()` reports that the cache is already enabled.
  The cache is in the user's cache directory, beside the island's extracted files, and the program uses it only when that directory belongs to the user and no one else can write to it.
  The program runs without the cache, and reports nothing, when the directory is not private in that way, cannot be created, or is on a file system with no free space, when the user has no home directory to keep it in, and when the process's effective user or group differs from its real one.
  `NODE_DISABLE_COMPILE_CACHE=1` turns the cache off.
  `NODE_COMPILE_CACHE=<directory>` makes Node.js use that directory as it does on its own, without those checks.
- **Child process event order:** A child's `stdout` and `stderr` chunks, and the `'spawn'`, `'exit'`, and `'close'` events of several children, arrive in the order Node.js delivers them on Linux and macOS.
  The events of one stream relative to the child's exit differ: when a child exits, the rest of its output and each stream's `'end'` are delivered before its `'exit'`, where Node.js fires `'exit'` first when the child's output and its exit arrive in the same turn of the event loop.
  A child that is spawned as the program's first act and writes to `stderr` before `stdout` is delivered in the order written, where Node.js usually delivers `stdout` first because the child finishes before Node.js starts its event loop.
  A child's output pipe does not hold the same amount as the socket Node.js gives it, so a child that fills one stream before it writes to the other can block at a different point and be delivered in a different order.
  On Windows, `stderr` is delivered before `stdout` when both hold output, and the events of several children fire newest child first.
- **Objects converted from Node islands:** An object or array made by JavaScript running in a Node island is one object under Node.js, whatever type the program gives it.
  A compiled program keeps it as the island's object while it is typed `unknown`, `any` or by the island package's own declarations.
  It keeps it too under a record type of the program that the value takes straight from the island: a declared result, an import, a member, a parameter of a callback the island calls, or a type the program asserts it to (`item() as Item`).
  Every value of such a record type then uses checked dynamic storage, which is slower than native record fields, including the records the program builds itself.
  Where the program converts the value to an array type, or to a record type that declares a string-keyed dictionary or a library object such as a `Date`, a `Map` or a `Set`, itself or in a member, the conversion reads the members that type declares and builds a native value from them.
  That value is a copy: a later change made by the island does not show through it, a write made through it does not reach the island, it is not `===` to the island's object, and it holds only the members the type declares.
  An untyped value that the program stores in a member, an array element, a reassigned binding or a container before it gives it a type is copied the same way where it is read back and given a record type that the program's own source files declare.
  A type that a declaration file declares stays a view of the island's object there.
  scriptc reports each conversion it can see, and each such store, as an `SC6007` warning.
  Assert the type where the island hands the value over or declare it in the island package's declarations to keep the island's object, or write the copy yourself (`structuredClone(value) as Item[]`) so that Node.js copies too.
- **Child processes from Node islands:** A `ChildProcess` that JavaScript in a Node island started, for example through a package that wraps `spawn`, and that the program uses at the `ChildProcess` type is the island's own process object: its events arrive in Node.js's order, and its properties, its streams and its listeners are the island object's.
  The following differs from Node.js.
  `once` for `spawn`, `exit`, `close` and `error` registers the listener for every emit of the event, which shows only when the event is emitted more than once.
  When the child has closed, the listeners the program registered on it, on its streams and on its `stdin` are released: they stay registered on the island's object but no longer run, and a listener the program registers afterwards is not registered at all.
  While the program listens to anything on the child, one more `close` listener is registered on the island's object.
  A `data` listener that takes `Buffer` chunks throws a `TypeError` when JavaScript in the island set an encoding on that stream, and one that takes strings throws when no encoding is set, where Node.js hands either listener whatever the stream delivers.
  A write to the child's `stdin` after the child closed reports `ERR_STREAM_DESTROYED` to the write's callback, as under Node.js, and also emits `error` on the stream.
  An `error` event's error has its `name`, `message` and `code`, and its `errno`, `syscall`, `path` and `spawnargs` read as `undefined`.
  A listener runs outside the `AsyncLocalStorage` store that was current when the child was started.

### Array bounds and runtime errors

Typed-array reads at an index that is not an integer in the array's range return `undefined`, as in Node.js, and invalid numeric indexed writes are ignored, while the assignment expression retains its original value.
A non-null assertion on such a read (`bytes[i]!`) yields `NaN` instead of `undefined`.

Ordinary arrays track holes separately from present `undefined` values.
Missing reads and empty `pop()`/`shift()` return `undefined`, indexed writes can grow the array, and length growth creates holes.

Keyed reads of a `Record` or an index signature return `undefined` for a missing key, as in Node.js, with or without `noUncheckedIndexedAccess`.
A missing array element or key that reaches storage that cannot hold `undefined`, such as a class field typed `number`, throws a `TypeError`.
An array converted to an array of another element type, such as an array of narrower records, is copied element by element, and a hole or stored `undefined` that the new element type cannot hold throws a `TypeError`.

Runtime traps cannot be caught.
User-thrown values and supported exceptions, including JSON parse, checked-cast, filesystem, regex, and `String.prototype.repeat` count errors, can be caught; hard traps such as out-of-range reads of statically typed ordinary arrays terminate the process.

Deep recursion in Linux and macOS executables throws Node's catchable `RangeError: Maximum call stack size exceeded`, on the main thread, in worker threads, and inside async functions and generators.
The depth at which it is thrown differs from Node: the main thread uses the process stack limit (usually 8 MiB), worker threads get an 8 MiB stack, async functions and generators that keep a fiber run on 512 KiB stacks, and stackless ones run on the stack that resumes them, so native code usually recurses deeper than Node on threads and somewhat less deeply inside fiber bodies.
Recursion inside runtime library operations, such as serializing deeply nested data, is not checked.
Windows executables, WASI builds and library builds do not check the stack, and exhausting it there terminates the process.

### Runtime type checks

Casts from values whose type is only known at runtime validate the value.
For example, `JSON.parse(s) as Config` throws a catchable error if the data does not match `Config`, identifying the offending path, such as `expected number at $.port, got string`.
A TypeScript assertion alone does not perform this validation when run under Node.js.
A program that catches that error without reading it therefore takes the handler's path for data that Node.js accepts, and prints nothing.
In the program below, the second line is valid JSON without `usage.total`: Node.js prints `message 1` for it, and the compiled program prints `skipped`.

```ts title="log.ts"
interface Usage {
  input: number;
  total: number;
}
interface Entry {
  type: "message";
  usage: Usage;
}
function parse(line: string): Entry | null {
  try {
    return JSON.parse(line) as Entry;
  } catch {
    return null;
  }
}
const lines = [
  '{"type":"message","usage":{"input":1,"total":2}}',
  '{"type":"message","usage":{"input":1}}',
  "not json",
];
for (const line of lines) {
  const entry = parse(line);
  console.log(entry === null ? "skipped" : entry.type + " " + entry.usage.input);
}
```

An executable built with `--optimization dev` and without `--strip` reports such failures when it runs with the environment variable `SCRIPTC_CONVERSION_TRACE` set to a value other than the empty string and `0`.
Each failed check then writes the error and the file, line, and column of the conversion to stderr at the moment it fails, whether or not the program catches the error:

```console
$ scriptc build log.ts --optimization dev -o log
/app/log
$ SCRIPTC_CONVERSION_TRACE=1 ./log > /dev/null
scriptc: checked conversion failed: TypeError: expected number at $.usage.total, got undefined
    at parse (/app/log.ts:11:12)
```

The line that is not JSON fails with a `SyntaxError`, which is not a failed check and is not reported.
The report changes nothing else about the run, a release build has none, and the variable is read only when a check fails.
A record or class instance cast from `unknown` to a dictionary type such as `Record<string, number>` keeps its identity, and the cast does not validate it.
Reading a value that does not match the dictionary's value type throws a `TypeError`, such as `expected number at $, got string`, where Node.js returns the value.

A cast from `unknown` to `undefined` or `void` checks that the value is `undefined`, and so does a cast to a type argument that is one of them, such as `value as T` in a generic function called with `<void>`.
Any other value throws a `TypeError`, such as `expected null | undefined at $, got object`, where Node.js keeps the value.

Casts between program classes are checked too: casting an instance to a class it does not extend throws a `TypeError`, where Node.js reads the members structurally.
`SC6004` reports calls that pass such an instance to a function that casts it.

A value typed by an interface that extends `Error` is an error object or a plain object that has the members.
Handing it to a slot typed by an `Error` class (a parameter, a binding, a return, a member or an element) checks that it is an error and throws a `TypeError` for the plain object, where Node.js reads the members.
`SC6004` reports those sites too.
Inside a generic function or class it reports a value whose type is a type parameter that a call or a `new` expression binds to such an interface, whether the parameter's type is the type parameter itself or holds it, as in `list: T[]`.

A Promise converted from `unknown` to a typed result validates its fulfilled value.
A mismatch rejects with a `TypeError`.
Rejection reasons and reference equality are preserved across conversions.

A rejection handler that declares its parameter's type, such as `(error: Error) => ...`, validates the rejection reason when it runs, whether it is written inline or passed as a function value.
A mismatch rejects the promise that `catch` or `then` returned with a `TypeError`, where Node.js calls the handler with the reason unchanged.

A non-null assertion (`value!`) checks the value when it extracts an object, array or other non-nullable member from a union.
If the value holds `null` or `undefined`, the assertion throws a `TypeError`, even when the result is never used; Node.js continues until a property of the value is read.

A callback of `filter`, `find` or `findLast` that is written as a type predicate, such as `(item): item is Tagged => "tags" in item` or a guard function passed by name, runs as an ordinary test.
The elements it accepts are checked against the predicate's type: an element that is not of that type throws a `TypeError`, where Node.js answers the element unchanged.

### Record identity and property order

A record converted to a record type with a different set of properties, such as a narrower shape or one that adds optional properties, is copied.
Mutations through the converted reference do not affect the original, and the copy is not `===` to it.
Empty record layouts are also snapshots because they have no property storage.
`scriptc coverage` and `scriptc build` report `SC6001` at each conversion that copies a value something else can still reach, in any position: an argument, a return value, a field, an array element, a collection value, a callback parameter.
The report is made when the program writes a field or element of either type, compares values of the converted type by identity (including `includes`, `indexOf`, and Set or Map keys), or passes either type to code scriptc cannot follow.
It is made from the compiled program, so a program that builds without it does not lose such a write or comparison.
It judges types rather than the path one value takes, so it can name a conversion whose copy the program never tells from the original.
A conversion of a new value, such as an object literal, a spread, or the result of a function that returns one, is not reported, because nothing else can reach that value.
The same holds for an array converted to an array of another element type and for a tuple converted to an array.
A record that something else can still reach is refused with `SC1090` where it is converted to a class instance and the program writes either type, because the conversion constructs a new instance where Node.js has the one plain object.
A union value that something else can still reach is refused with `SC2003` under the same condition where one of its members fits several members of the destination union and enters the one its own discriminant names, because that conversion builds a new object where Node.js has one.
Two conversions are not covered and copy without a report.
A promise used as a promise of another record type, such as a `Promise<Wide>` stored as a `Promise<Narrow>`, copies its payload when it settles.
A class stored in a record type, such as `const counter: { count: number } = Counter`, holds copies of the class's static fields.
An `as` assertion that only adds optional members to the properties a record type declares is not such a copy: it is erased, so the value keeps its identity and reading an added member answers what the object carries, as in Node.js.
`scriptc build` reports `SC1031` where such a member is assigned, because a record's layout has no storage for it.
Declaring a record value with a record type that names the same properties, where every declared field fits the value's storage, keeps the value's own layout instead: both names are the same object, as in Node.js.
When a declared field cannot store the values its type admits, such as a declared field that is a wider union type than the value's field, the declaration, a call that passes the value to a parameter the callee writes, or a literal that stores the value in that field is refused with `SC2021` if the program writes through the binding, because the copy would take the write instead of the original.
Assigning an existing value to a binding that keeps its initializer's layout, to one of its fields, or to an element of one of its arrays, is refused with `SC2021`, because the kept layout rebuilds the assigned value, so a later write through it would reach only the copy.
The refusal covers a short-circuit assignment that stores the value (`&&=`, or `??=` on a binding that can be nullish) like the plain assignment it stands for.
A member of another binding that kept its initializer's layout carries that layout into the store, so no refusal is reported when the two layouts already match.
Bracket writes such as `alias["k"] = v` write the field in the kept layout, like their dotted spelling.

Typed records with declared fields, indexed records, and arrays retain identity and mutations through native checked storage.
An empty array literal with no element type of its own, such as the `files` of `{ files: [] }` or `const NONE = [] as const`, takes its element type from the declared types its value reaches and is the same array under every name.
`scriptc build` reports `SC2015` where one such array is used at two element types or reaches a typed array through a computed key; writing the type at the literal (`[] as string[]`) compiles.
A record or class instance converted to a string-keyed dictionary type such as `Record<string, string>`, directly, through a `{}`, `object` or `unknown` binding or parameter, or as an element of a constant `unknown[]` array that holds such records, keeps its identity when the program writes through, compares or inspects values of that dictionary type.
Every dictionary type with the same value type then uses checked dynamic storage, which is slower than a native dictionary.
Class instances retain identity at their exact type or a base class, and at any interface or object type that a class instance is passed, stored, returned, or asserted as when the program could tell a copy from the instance: values of such a type are checked views of the original object, so writes, identity comparisons, Set and Map keys, `util.inspect`, and `JSON.stringify` through them reach the instance, as in Node.js.
These types use checked dynamic storage, which is slower than native record fields, for all of their values, including plain objects.
A type whose values are only read field by field, from fields the class does not write after construction and does not implement as accessors, stays a native record and holds a copy.
Object spread of a class instance is refused.
Dense array literals created directly in checked storage preserve identity, and supported native handles pass by reference.
A type derived from a built-in class whose values are native handles, such as an interface that extends `Response` or an `Omit` or `Pick` of it, an intersection with it, or a subset alias, names the built-in object itself and uses checked dynamic storage, also for an object the program makes for the type.
A member such a type adds or declares again is read from the object by name and checked against its declared type, so reading a required member that the built-in object does not have throws a `TypeError`, where Node.js answers `undefined`.
Assigning a member that the built-in object does not have throws, where Node.js stores it on the object.

`Object.keys`, `Object.values`, `Object.entries`, `for...in`, `JSON.stringify`, and `util.inspect` enumerate a record's keys in the order the object created them, as in Node.js.
scriptc chooses one order per record type from the order in which the program's object literals create the keys, follows object spread, rest destructuring, and record conversions back to the values they copy, and places an optional key that the program assigns after construction after the keys the object was built with.
When the object literals of one record type create its keys in different orders, including literals of two types with the same properties and property types, each value records the order its literal used: copies made by object spread, rest destructuring, and record conversions keep their source's order, and assigning an optional key the object lacks, including after `delete`, moves the key last.
A type keeps the declaration order of its first type, and `SC6002` reports object literals whose order differs where the program observes it, when one of its values gets its keys from a spread whose source order is not known, from a spread that also sets an optional key the source lacks, or from a copy of a value scriptc cannot trace, or when the type's values can take more than 16 orders.
`SC6014` reports such a type once, at a construction its order depends on, when the program observes the type's key order, although the objects the program builds may still list their keys as in Node.js.
`Object.assign` adds its source's properties to a record in the order the source object created them, and `SC6014` reports a call that adds two or more optional properties from a value converted at run time.

A record converted from a checked value, such as `const rows: { score: number }[] = JSON.parse(text)`, keeps the properties its type does not declare.
So does a record converted from an object with more properties, such as `const p: { a: number } = { a: 1, b: 2 }` or an argument passed to a parameter of a narrower type.
`JSON.stringify`, `Object.keys`, `Object.values`, `Object.entries`, `for...in`, `in`, `hasOwnProperty`, `propertyIsEnumerable`, `Object.hasOwn`, `util.inspect`, and untyped views of the record see them, together with the declared fields, in the source object's key order.
String conversion, `+`, and calls of `toString` and `toLocaleString` run the source object's own `toString` or `valueOf` when it has one.
Calling `valueOf` or `isPrototypeOf` by name on a record type that such an object converts into reports `SC1090` when the object has its own property of that name.
`Object.values` and `Object.entries` check those values against their static result type and throw a `TypeError` if one does not fit.
For a record converted from an object with more properties, these surfaces read the object's other properties when they run, so later writes to those properties are visible; the declared fields are a copy taken at the conversion.
Copies of such a record made by object spread, `Object.assign` or rest destructuring contain only the declared fields; a conversion to another record type keeps the properties too.
A record converted from an object keeps a property non-enumerable when the object held it as a non-enumerable data property that is writable and configurable, whether or not the record's type declares it.
`Object.keys`, `Object.values`, `Object.entries`, `for...in`, `JSON.stringify`, `util.inspect`, and untyped views of the record leave the property out, `in`, `Object.hasOwn`, and `Object.getOwnPropertyNames` include it, and its descriptor read through an untyped view reports `enumerable: false`.
A copy of such a record made by object spread, rest destructuring, or `structuredClone` holds a declared non-enumerable property as an ordinary enumerable one, where Node.js leaves it out of the copy, and `Object.assign` from the record throws a `TypeError` when the result's type requires the property.
`propertyIsEnumerable` called through the record's type answers `true` for a declared property that is not enumerable.
A conversion does not keep a declared property's read-only or non-configurable attribute or its accessor: it reads the property once, and typed writes to the field succeed.
A value that a compiled npm package returns into a type its declarations give it is refused with a `TypeError` when the object has an accessor, a read-only or non-configurable property, symbol-keyed properties, or a prototype, or when it is frozen, sealed, or not extensible, or when it has a non-enumerable property and the type keeps its keys without property attributes (an index-signature record).
Index-signature records list declared fields before the other keys.
A rest binding over an index-signature source copies its source's key order, so it lists the remaining declared fields before the other keys even when the source object created the other keys first.

An object literal asserted with `as` to a record type whose required properties it does not write, such as `const point = {} as Point`, keeps its identity, and the properties the program assigns afterwards complete it.
Reading a required property that nothing wrote throws a `TypeError`, where Node.js answers `undefined`.
The same holds for a read through a computed key, the read of a compound assignment such as `row[key] ??= value`, and an object spread of the value, which reads every property.
Freezing such an object freezes it for every reference, whether or not it is complete.
`scriptc build` reports `SC3001` at such an assertion when the object cannot be completed in place: when values of the asserted type may be part of a reference cycle, are one arm of a union of object types, are published or keep more than one property order, or when the literal holds a function.
Write every required property in the literal instead, or collect the properties in a `Record<string, T>` or `Partial<T>` value and assert that value once it is complete.

Optional fields and properties added through a record's `unknown` view can be deleted.
A required field has no absent state, so deleting it through the view throws `TypeError: Cannot delete property 'x' of #<Object>` and leaves the record unchanged, where Node.js removes the property.
A property that a record keeps but its type does not declare can be deleted through an `as` assertion that adds it as an optional member, such as `delete (entry as Entry & { legacy?: number }).legacy`.
`scriptc build` reports `SC1090` for such a delete when a record of a type that declares the property can be converted to the operand's type, because the property is then that record's field, which the delete cannot remove.

Records inherit `toString`, `toLocaleString`, `valueOf`, `hasOwnProperty`, `isPrototypeOf` and `propertyIsEnumerable` from `Object.prototype`.
A call of one of these methods by name runs the built-in with the record as its receiver, unless the record holds an own function under that name.
A keyed read such as `handlers[name]` of a record whose values are functions answers the built-in when the record lacks the key, so truthiness, `typeof` and calls match Node.js.
A keyed read of a record whose values are not functions, such as `counts["toString"]` on a `Record<string, number>`, answers `undefined` where Node.js answers the inherited function, so `counts[word] ?? 0` counts such a word as new.
Calling a built-in read from a record of functions throws a `TypeError` when its answer does not fit the record's function type, for example `valueOf` read from a `Record<string, (x: number) => number>`.
Other inherited names, such as `constructor`, read `undefined` from typed records.

### Buffer construction

`Buffer.from` accepts untyped strings, byte arrays, arrays, and plain array-like or Buffer JSON objects.
Encoding arguments must be supported literals.
`Buffer.from(arrayBuffer, byteOffset, length)` is a view of the ArrayBuffer or SharedArrayBuffer, with Node's offset and length checks.
Custom input `valueOf` hooks and opaque class or native-handle inputs throw an unsupported-operation error.
The deprecated `Buffer()` and `new Buffer()` take up to three arguments, without spreads, and follow Node: the first call made outside `node_modules` reports DEP0005 on the next tick, a number allocates zero-filled bytes, and other arguments convert as `Buffer.from` does.
In JavaScript, `Buffer` read as a value (the global or the buffer module's `Buffer`) is one function with Node's statics and `poolSize`.
Calling or constructing it through a stored value, reading its `prototype`, and its `copyBytesFrom` and `of` statics throw an unsupported-operation error.
`Buffer.prototype` members read as values are functions that run on Buffer receivers; inherited and symbol-keyed members throw an unsupported-operation error.

### readable-stream

`readable-stream` 2.x, 3.x and 4.x compile as Node.js's built-in `stream` module.
For 2.x and 4.x, that is the package's own behavior when the `READABLE_STREAM` environment variable is `disable`.
Without that variable, Node.js runs the package's JavaScript copy of the stream classes, so a native build can differ from Node.js wherever a program depends on that difference: `instanceof` checks against the built-in classes, class identity, error messages, and timing details.
The package's `lib/_stream_*.js` files also compile as the matching built-in classes, although Node.js loads the package's own implementations through them in every environment.
`readable-stream` 3.x is a copy of Node.js 10's streams.
Its own `disable` mode throws on Node.js 24, so no setting makes Node.js run it against the built-in classes; a native build maps the members its entry exports (the stream classes, `Stream`, `finished` and `pipeline`) to the built-in ones.

### HTTP headers

Without `@types/node`, the bundled declarations type every `req.headers` value as a string.
Reading `headers["set-cookie"]` is then refused, and a `{ ...req.headers }` copy holds `set-cookie` as one string joined with `", "`, where Node holds an array.
Read `headersDistinct["set-cookie"]`, or install `@types/node`, whose `headers["set-cookie"]` is `string[]`.

### Request URL credentials

`http.request()`, `http.get()`, `https.request()`, and `https.get()` accept a URL string or URL object in place of the options, with the options and callback after it.
A URL that carries a username or password throws a catchable error before anything is sent.
Node turns those credentials into a Basic `Authorization` header, and scriptc refuses the call rather than send the request unauthenticated.
Pass the header yourself instead, for example `{ headers: { Authorization: "Basic " + Buffer.from("user:pass").toString("base64") } }`.

### JSON callbacks

Function replacers and two-argument revivers support nested replacements, object-property deletion, and thrown exceptions.
Omitting the root through a replacer returns `undefined`.
Array replacers list the object keys to serialize, as in Node.js.
Stored `JSON.stringify` functions accept numeric and string indentation; direct calls require literal indentation.

Reviver source contexts are unsupported.
A reviver that takes a rest parameter or reads `arguments` is refused with `SC2020`, because Node.js passes it the source context as a third argument.
A reviver that deletes an array element throws, because checked arrays cannot represent holes.

Callback values use checked storage.
Typed records and arrays retain identity and mutations; record fields use declaration order; typed-array views retain backing storage and brands.
`Buffer.isBuffer` preserves the Buffer brand through `unknown`.
JavaScript callbacks receive the holder as `this`; TypeScript callbacks that access dynamic `this` are unsupported.

### Strings and sorting

Strings are stored as UTF-8, with lone surrogates kept as their own code units.
Length, string methods and relational comparisons such as `<` and `>` use UTF-16 semantics, including for lone surrogates.
`split` with a string separator and `replace` or `replaceAll` with a string pattern don't match a lone surrogate against half of a surrogate pair; regular-expression patterns do.
Output, file contents, and other encoded text write each lone surrogate as U+FFFD, as Node does.
File system paths and network host names that contain a lone surrogate reach the operating system unconverted.

One-argument `localeCompare` follows Node's default root-locale collation for Latin-script text (ASCII, Latin-1 Supplement, Latin Extended-A, combining diacritical marks, and U+2044 FRACTION SLASH): letters order before case and accents, so `"a".localeCompare("B")` is `-1`.
Strings containing other characters compare by code point, which can differ from Node's ICU collation; `SC6003` reports such string literals.
The locales and options arguments are unsupported.

`sort` and `toSorted` use stable natural merge sort.
Default ordering supports string, number, and boolean arrays, comparing their string forms in UTF-16 order; for example, `[2, 10, 1].sort()` produces `[1, 10, 2]`.
`sort` preserves holes at the end, while `toSorted` produces a fresh array with holes filled by `undefined`.
With an explicit comparator, consistent comparators produce the same sorted results as Node, but callback order and count can differ.

Typed-array `sort` and `toSorted` order numbers by default, with `-0` before `0` and `NaN` last.
With a comparator, they call it in the same order as Node and write the sorted values back after the last call.

### Published graphs

Graphs published with [`@scriptc/threads`](/docs/threads) are immortal: scriptc never frees them, even after every thread has stopped using them.
Publish long-lived data, not per-request messages.

A receiving thread reads the sender's objects, so they keep their classes, methods, and identity across threads and across messages.
Node.js delivers clones without prototypes, a new clone for every message.

Writes into published objects throw Node's frozen-object `TypeError`s.
In Node.js, `o.x = f()` evaluates `f()` before the write fails, and scriptc does the same, except for writes to union-typed class fields and for `Map`, `Set`, and array mutator methods, where the write check runs before the arguments are evaluated.
Private fields (`#x`) of published objects cannot be written either, although Node.js allows writing private fields of frozen objects.

### Dates and timezones

String constructors and `Date.parse` use V8's date-string grammar: ECMAScript date-time strings and the legacy forms Node.js accepts, such as RFC 1123 HTTP dates (`Mon, 01 Jan 2024 00:00:00 GMT`), `Date#toString` output, and `Jan 2, 2024`.
Strings without a timezone, and `new Date(year, monthIndex, ...)` fields, are local time.

Local calendar getters and local-time conversions use the operating system's timezone database.
Historical results can differ from Node.js when timezone data differs.
For valid extreme years outside a platform calendar API's range, scriptc uses a calendar-equivalent year in the 400-year Gregorian cycle to obtain the timezone rule.

`toString`, `toTimeString`, `String(date)` and template literals print the zone's long name after the GMT offset, as Node does (`GMT-0500 (Central Daylight Time)`). scriptc identifies the zone the way Node does, from `TZ` when it names a zone, else from `/etc/localtime`.
Outside Windows, a write to `process.env.TZ` changes the zone for later conversions and getters.
The names are the en-US names for the IANA zones; Node prints them in the language of the locale that `LC_ALL`, `LC_MESSAGES` or `LANG` selects, so with a non-English locale the name differs from Node's.
A `TZ` value that is a file path or a POSIX rule string (such as `EST5EDT,M3.2.0,M11.1.0`) gives offsets and names that differ from Node's, which ignores those forms.
On Windows, the name is the `GMT+hh:mm` form unless `TZ` names a zone.
On `wasm32-wasi`, local time is UTC and the name is `Coordinated Universal Time`.

### Weak collections and memory

`WeakMap` and `WeakSet` accept checked native objects and arrays, functions, numeric typed arrays, ArrayBuffers, native weak collections, and typed records, arrays, tuples, and classes that preserve identity through checked storage.
A promise is a key too: the same promise is one key whether it is held through its typed binding, a wider promise type, or an untyped value.
Entries keyed by a promise are released some time after the promise is gone, when later promise keys are added.
Copied values, other native handles, and symbol keys are unsupported.
Constructor seeds support arrays; custom iterators and detached methods are unsupported.
Weak collections do not retain their keys, but values that refer back to a key can keep it alive until deletion or collection release.

`FinalizationRegistry` keeps its registrations: `register` and `unregister` check their arguments and answer as in Node.js.
Its cleanup callbacks never run, which the ECMAScript specification allows.
Program output matches Node.js, which runs cleanup callbacks only when its garbage collector decides to.
Memory that a package frees only from a cleanup callback is not reclaimed until the program exits.
For example, a wasm-bindgen package frees the WebAssembly memory behind a wrapper object from its cleanup callback, so a long-running server that drops such wrappers without calling their `free()` method keeps that memory.
Call `free()` on wrappers the program no longer needs.
Symbol targets and unregister tokens are unsupported.
`WeakRef` keeps its target alive for as long as the `WeakRef` itself, so `deref()` always answers the target, which is Node.js's answer for a target the collector has not freed.
Constructing one checks its target as Node.js does: an object or a symbol outside the global registry, and a `TypeError` otherwise.

Memory uses reference counting.
Acyclic values are freed deterministically, and cycles are collected at defined collection points.
Native checked objects, arrays, Sets, and captured closures participate in cycle collection.
Cycles through opaque native capsules can remain allocated.

### Foreign-thread callbacks

FFI format 5 accepts retained, context-bearing `void` callbacks invoked by library-owned threads.
The native callback copies arguments and queues work; the script closure runs on the event loop at least one turn later.
These callbacks are not suitable for real-time execution.

Value-returning callbacks and direct script execution on foreign threads are unsupported.
Reference counting and exception state belong to the script thread, and waiting for the event loop can deadlock.

### TLS ciphers

Before the handshake Node's `tlsSocket.getCipher()` returns `undefined`, and after close `null`.
Its declared result is the `{ name, standardName, version }` record, so a typed read there throws a TypeError instead.
After the handshake the record matches Node, with OpenSSL's suite name in `name`.
The suite itself is the one the bundled mbedTLS negotiates, which can differ from OpenSSL's choice for the same peers.

### TLS certificates

`tlsSocket.getPeerCertificate()`, `getCertificate()`, `getPeerX509Certificate()`, and `getX509Certificate()` follow Node, including the `detailed` issuer chain, on sockets whose type is known.
On a TLS socket held in an `unknown` or `any` value, or in an untyped JavaScript parameter, they throw an error that names the call as not supported.
Client certificates follow Node: `tls.createServer`, `https.createServer`, and `new tls.TLSSocket` take `requestCert`, `rejectUnauthorized`, and `ca`, and `tls.connect` and `https.request` take `cert` and `key`.
A server's `authorized` and `authorizationError` report the client certificate's verification as Node does, and a server, client, or `tls.createSecureContext` given a `ca` also sends the chain it completes, as OpenSSL's automatic chain does.
A server verifies client certificates against the `ca` of the context it serves, including contexts from `addContext()` and `setSecureContext()`: an empty `ca` (an empty Buffer or array) trusts no certificate, and an omitted `ca` trusts the default CAs.
The same holds for a client's `ca` in `tls.connect`, `https.request`, and `http2.connect`, where an empty string is no `ca`, as in Node.
In `http2.connect`, a `ca` held in an `unknown` or `any` value that is an empty string is refused.
`requestCert` beside a TLSSocket's `secureContext` or an `SNICallback`, client certificates on http2 secure servers, and, on a tls server without an `SNICallback`, a `cert` without a `key` (or the reverse) are refused.
An `SNICallback` may be the only certificate source: the server options may leave out the default `cert`/`key` pair, and each handshake is then served from the context the callback answers.
`server.addContext()` and `server.setSecureContext()` follow Node on tls and https servers, including wildcard names, which match one label without regard to case.
A name with regular-expression groups or alternation, or one outside ASCII, is refused, as are both methods on a server that is not a tls or https server.
The `'tlsClientError'` event reports Node's error for a failed handshake: OpenSSL's code and reason, or `socket hang up` for a client that leaves.
OpenSSL's messages leave out the thread identifier Node prints before `error:` and the source location after the reason.
A failure whose OpenSSL reason the runtime does not know, such as a malformed client hello, is reported as an error with code `SC2020` that says so.
The X509Certificate those accessors return has the same refusals as any other certificate (see Crypto), so in JavaScript narrow it, for example with `if (cert)`, before reading its fields.

### TLS sessions

TLS servers send no session tickets, so a connection to them cannot be resumed.
A client's `'session'` event fires once per connection, when its handshake finishes and before `secureConnect`.
Node fires it for each ticket the server sends, after `secureConnect`, and OpenSSL servers send two.

### Console and process streams

`console.log`, `info`, and `debug` write to stdout; `error` and `warn` write to stderr.
They accept `unknown`, printing strings verbatim and other values through native `util.inspect`.
Console methods never color their output on their own: where Node colors numbers, booleans and the values inside objects and arrays, on a color terminal or with `FORCE_COLOR` set, the executable prints them plain.
Direct calls support global `console` and `node:console` imports.
Console output, `util.format`, and `util.formatWithOptions` with a format string known only at run time, spread arguments, or an options object format their arguments at run time.
There, `%s` of a function or of a class instance whose class defines `toString` throws `SC2020`.
`util.inspect` with `colors`, with `showHidden`, with option values known only at run time, or in the `inspect(value, showHidden, depth, colors)` form renders the value's untyped view in the runtime with Node's styles.
Colored output of errors, promises, regular expressions, `DataView`, `ArrayBuffer`, weak collections, iterators, and other native objects throws `SC2020`.
Inspect options other than `depth`, `colors`, `customInspect`, `showHidden`, `compact` (a number or `false`), `breakLength`, `maxArrayLength` and `maxStringLength` (a non-negative integer or `null`), `getters` (a boolean, `'get'` or `'set'`), `sorted` (a boolean), and `numericSeparator` (a boolean) throw `SC2020` when they are set to a value other than Node's default.
`util.inspect` calls `[util.inspect.custom]` methods of classes and objects with Node's depth, options, and `util.inspect` arguments, and `console.dir` and `customInspect: false` skip them.
A class instance with such a method that is held as an untyped value throws `SC2020` when it is inspected, and a class whose subclasses declare the method without declaring it itself is refused.
Under `showHidden` — the option, the boolean second argument, and `%o` — arrays print their hidden `[length]` entry, typed arrays their `[BYTES_PER_ELEMENT]`, `[length]`, `[byteLength]`, `[byteOffset]`, and `[buffer]` entries, `ArrayBuffer` and `DataView` their hidden byte fields, and objects their non-enumerable and symbol keys.
Objects also list the properties they inherit from up to three user-defined prototype layers, nearest layer first.
Values this runtime has no hidden view of — functions, errors, `RegExp`, weak collections, and `SharedArrayBuffer` (and views over one) — throw `SC2020` under `showHidden`, and `console.dir` renders the option through the value's checked-dynamic view, refusing the types that have no conversion to it.
`console` is one object and each of `console.log`, `info`, `debug`, `warn`, and `error` is one function, so both can be stored, passed, destructured, bound, compared, and called without a receiver.
A stored method formats the arguments it is called with at run time, as `util.format` does, including format specifiers.
A value the untyped inspector does not render, such as a `RegExp` or a promise, throws `SC2020` when a stored method prints it.
`console` flows into a record type whose members it has, such as `interface Logger { log(...args: unknown[]): void; error(...args: unknown[]): void }`, and each member of the record is console's own function.
The record is a separate value, so comparing it with `console` is refused, and a record type that declares `table`, `dir`, `time`, `trace`, or another member outside the five output methods is refused.
`{ ...console }` copies console's own keys in Node's order.
In TypeScript, keeping a console member other than the five output methods as a value, such as `const table = console.table`, is refused; `typeof console.table` and a truthiness test compile.
Reached through an untyped value, those members answer a function whose call throws `SC2020`.
`promise.catch(console.error)` is refused by the rule for `catch` handlers; `promise.then(undefined, console.error)` compiles.
Replacing a console member, such as `console.log = capture`, is refused in TypeScript and throws `SC2020` through an untyped value, because direct console calls never read the member.
`console` passed into a Node island is the island's own console; a console nested in an object that crosses into an island throws.
Members of custom `Console` instances as values are unsupported.

JavaScript and TypeScript can store `process.stdin`, `stdout`, and `stderr`, replace output `write` methods, and call saved methods with their stream receiver.
The replacement can be assigned through a stored stream as well, such as `out.write = wrapper` where `out` is a `NodeJS.WriteStream` parameter holding `process.stdout`.
`write` is the one member a process stream keeps: an assignment to another member, such as `process.stdout.isTTY = true`, compiles and throws an `SC2020` error when the statement runs, where Node stores the value.
A program that holds `process.stdout` or `process.stderr` as a value shares the stream's `write` method with its Node islands: a replacement installed by either side sees the output of both, and either side can call or restore the method the other saved.
In the islands of such a program `write` is an own accessor property of the two streams, so `Object.hasOwn(process.stdout, "write")` is `true` before any replacement.
There, `Object.defineProperty(process.stdout, "write", …)` throws a `TypeError`, and `delete process.stdout.write` throws one in strict code and answers `false` otherwise, where Node would replace or remove the method.
Writes that island code makes through the shared method do not pass through the island stream's own state, so its `writableLength`, `'drain'` event and `'error'` listeners do not reflect them.
In any other program with islands, a `write` replacement that island code installs sees island output only.
A worker thread's output does not pass through a `write` method replaced on the main thread.
Input supports data/end/error/readable listeners, pause/resume, buffered reads with `read(size)` and `readableLength`, and TTY raw mode.
Output writes are synchronous, with successful callbacks on the next-tick queue; queued backpressure is not modeled.
An executable built with `--stdout-buffering` instead keeps stdout bytes in a 64 KiB buffer whenever stdout is not a terminal, so a pipe or file can receive a line later than the call that wrote it; the runtime settles the buffer when it fills, before a stderr write, when the event loop sleeps, before a blocking stdin read, before `process.kill`, on `process.exit`, on an uncaught error, and at exit, and output that a kill skips those paths for (such as `SIGKILL`) can be lost up to the buffer size.
A terminal keeps one write per call.
Because no write is ever queued, `writableLength` is 0, `writableNeedDrain` is false, `writableHighWaterMark` is 65536, and `'drain'` never fires, so drain listeners register silently and `events.once(process.stdout, "drain")` stays pending, as it does in Node when no write returned false.
Stored streams do not support piping, encoding changes, arbitrary properties, or other events.
`getColorDepth` and `hasColors` exist on `process.stdout` and `process.stderr` only when the stream is a terminal, as in Node, so TypeScript refuses an assertion that the optional call answered, such as `process.stdout.getColorDepth?.() as number` or a trailing `!`, with `SC2020`; test the answer instead, as in `process.stdout.getColorDepth?.() ?? 1`.
A call of either method with an environment that is an array, a `Map`, a `Set`, or a function builds with warning `SC6015` and throws an error that names `SC2020` when it runs on a terminal, and so does a call whose environment is an object literal with the single variable `TERM` set to `constructor` or `__proto__`.
The build cannot see the same `TERM` value in the process's own environment or in an environment passed through a variable, so it prints no warning for it: the call throws the `SC2020` error on a terminal, where Node answers a member of `Object.prototype` instead of a color depth.
A TypeScript `NodeJS.WritableStream` parameter that only receives `process.stdout` or `process.stderr` supports `write(string)`.
When other streams flow into the same parameter, the process streams join them as `Writable` instances with the full writable surface.
`process.stdout` and `process.stderr` also flow into a record type that declares only methods, such as `interface Sink { write(text: string): void }`.
A call through the record runs the stream's current method with the stream as its receiver, so a program that replaces `process.stdout.write` reaches its replacement.
The record is a separate value: it is not identical to the stream, and it exposes only the methods it declares.
A record type that also declares a data member, such as `isTTY`, throws `SC2002`.
An instance of a stream class such as `Writable` throws `SC2002` in a record type of methods as well.
For those, pass an object literal that wraps the stream: `const sink: Sink = { write: (text: string) => { process.stdout.write(text); } }`.

### graceful-fs

The `graceful-fs` package compiles as `node:fs`.
Its imports and `require("graceful-fs")` calls resolve to the builtin module, so the package's JavaScript is not compiled, and `gracefulify(fs)` as a statement does nothing.
Programs behave as with the package except under file-descriptor exhaustion: graceful-fs queues operations that fail with `EMFILE` or `ENFILE` and retries them when a descriptor closes, while a compiled program reports the error to its caller, as `fs` does.
In a compiled program, `require("graceful-fs") === require("fs")` is `true`.

### DNS lookup errors

After a failed `dns.lookup` or `dns.lookupService`, Node calls the callback with the error alone, so the address and family parameters are `undefined`.
A callback whose address parameter is typed `string` (as Node's own type declarations type it) receives `""` and `NaN` there, or an empty array for `all: true`, because a typed string or number cannot hold `undefined`.
An untyped callback receives the error alone, as in Node.
The promise forms in `node:dns/promises` (also reached as `dns.promises`) reject with the error and are unaffected.

### Server and socket errors

`server.listen()` on a server that is already listening ends the process with exit status 1 and reports Node's `ERR_SERVER_ALREADY_LISTEN` as an uncaught error.
Node throws that error at the call, where a `try` statement can catch it.
A server that has closed does not listen again: a later `listen()` call is ignored and its callback never runs.
After a server has closed, another `close(callback)` call runs the callback with `ERR_SERVER_NOT_RUNNING`, as in Node, but the server's `'close'` listeners do not run a second time.
A write callback of a socket that is destroyed before it connects, or whose connection is refused, receives `ERR_SOCKET_CLOSED_BEFORE_CONNECTION`, as in Node.
A write callback of a connected socket that is destroyed while the write is pending runs without an error, and the callback of `socket.end()` does not run; Node passes both the error of the cancelled write.

### Socket data encoding

After `socket.setEncoding()`, an inline `'data'` listener receives strings, as in Node, whatever type its chunk parameter declares.
A listener passed by reference, such as a named function whose parameter is typed `Buffer`, keeps its `Buffer` parameter, so a string chunk throws `TypeError: expected Uint8Array at $[0], got string`.
Declare that parameter as `string` or `string | Buffer`, or pass an inline function.

### UDP socket options

`socket.addMembership()` and `socket.dropMembership()` on a `dgram` socket that is not bound yet throw `SC2020`; Node binds such a socket to an ephemeral port first, so bind the socket before joining a group.
The `SystemError` that the buffer size methods throw (`ERR_SOCKET_BUFFER_SIZE`, for example from `getRecvBufferSize()` before `bind()`) has Node's `message`, `code`, `errno` and `syscall`, but no `info` object.

In JavaScript, `socket.send()` also accepts lists of buffers, sends on a connected socket, and takes a completion callback, which receives `null` and the byte count, or the send's failure instead of an `'error'` event.
The callback runs after the socket's pending events, where Node runs it on the next tick.
In TypeScript, `send()` with a callback, a list, or on a connected socket fails to compile with `SC2020`.
JavaScript can read a socket's `on`, `once`, `addListener`, `removeListener`, `off`, `emit`, `send`, `close`, `bind`, `connect`, `disconnect`, `address`, `ref`, and `unref` as function values, for example `socket.emit.bind(socket)`; reading other members as values is refused with `SC1090`.
Listeners registered that way, or with untyped parameters, take only the `message`, `error`, `close`, `listening`, and `connect` events.
When a socket closes, listeners registered from TypeScript are released, so an `emit()` from JavaScript after `'close'` reaches only listeners registered from JavaScript.
A datagram socket's `'error'` event, for example for an address in use, has Node's `message`, `code`, `errno` and `syscall`, but not the `address` and `port` properties Node also sets; the error a `send()` callback receives has all of them.

### Unix domain sockets and request options

`server.listen(path)` and the `socketPath` option of `http.request()` and `https.request()` use Unix domain sockets on Linux and macOS.
On Windows, where Node maps these paths to named pipes, they fail with `ENOTSUP`.
`server.address()` on a server listening on a Unix socket returns the socket path, as in Node, when the result is typed `string | AddressInfo | null` (the @types/node declaration).
Where it is typed as the `{ address, family, port }` record alone, which cannot hold the path, the call throws an error for such a server.
A `backlog` argument to `server.listen()`, as in `listen(port, host, backlog, callback)` or `listen(path, backlog, callback)`, is refused at compile time; leave it out.
The listen queue length is Node's default, 511.
When the options of `http.request()` come from a variable, or from an object literal that spreads one, they are read at run time with Node's validation errors.
The `auth`, `createConnection`, `family`, `hints`, `insecureHTTPParser`, `joinDuplicateHeaders`, `localAddress`, `localPort`, `lookup` and `uniqueHeaders` options then throw a runtime error when they hold a value, and so do `setHost: false` and `setDefaultHeaders: false`.

### Process arguments and errors

`process.argv[0]` is `"scriptc"`, `process.argv[1]` is the binary's path, and program arguments begin at index 2.
In programs that use `node:test`, a leading `--test-update-snapshots` argument is Node's snapshot update flag, and leading `--test-reporter=spec` and `--test-reporter-destination=...` arguments are Node's reporter options: each is kept out of `process.argv` and appears in `process.execArgv`, in the order given, which is otherwise `[]`.
`process.version` and `process.versions.node` report the Node.js release the runtime is verified against.
The `process.features` object cannot be changed: under Node nine of its flags are writable and a program may add keys, but a compiled binary reads a flag as a constant, so a write to the object throws the `TypeError` of a frozen object and `Object.isFrozen(process.features)` is `true`.

Uncaught exceptions print `Uncaught <value>` to stderr instead of Node.js's source excerpt and stack-trace block; thrown strings and numbers print their value.
An uncaught Error and an unhandled rejection with an Error reason also print the stack frames the Error carries, one `at` line each under that first line.
A release build keeps stack frames only in programs that read an error's `stack`; in every other program its report is the first line alone.
A build with `--optimization dev` and without `--strip` keeps a frame in every function that can raise an error, and each frame names the file, line, and column of the statement that was running: `at decode (/app/src/user.ts:8:3)`.
A function that only computes, and so could fail in no other way than by exhausting the stack, keeps no frame in either build: a stack overflow in it is reported at the statement that called it.
A build with `--sanitize` keeps the frames of a release build.
A frame takes 16 bytes of stack per call, so the deepest recursion of the functions that keep one is slightly lower in a `dev` build.
The position is the start of the statement, where Node.js reports the call or `new` expression inside it.
Such a build also lists constructors, methods, and accessors (`at new Shape`, `at Shape.area`, `at Shape.get sides`) and the top-level code of a module (`at /app/src/main.ts:12:1`), which a release build leaves out.
The frames of an async function or a generator end at that function: the functions awaiting or iterating it are not listed.
On Linux and macOS, a 64-bit `dev` executable that aborts, for example on an array index that a non-null assertion wrongly promised was in range, prints the same frame lines under the runtime's message.
Exit codes and output written before the exception are preserved.
Runtime errors expose `message` and Node's `code` as typed members.
The other own properties Node gives them, such as a system error's `errno`, `syscall`, and `path`, a child process error's `status`, `stdout`, and `stderr`, or an assertion error's `actual` and `expected`, are readable through an untyped view of the error (for example a cast of a caught value) and appear in `util.inspect` output.

Errors carry stack frames that name the function and the source file, without Node.js's internal frames; `file:line:column` positions appear only in the `dev` builds described above.
An error's `stack` text therefore differs between a `dev` build and a release build of one program, and so does what `console.log` prints for an Error: a `dev` build shows its frames where a release build of a program that reads no `stack` prints `[Error: message]`.
A function passed to `Error.captureStackTrace` as the frame to stop at keeps a frame without a position.
A class constructor that passes `this.constructor`, `new.target`, or its own class to `Error.captureStackTrace` keeps no frame, and neither do the constructors of its subclasses, so the trace starts at the function that ran `new`.
`console.log`, `util.format`, and `util.inspect` print an Error from its stack the way Node.js does: the frames under the `name: message` heading, brackets when the stack has no frame line, and a subclass's header named after its class (for example `Bailable [Error]: message`).
Only the frames themselves diverge, so the output agrees with Node.js when `Error.stackTraceLimit` is `0`.

### Module path globals

`__dirname` and `__filename` are compile-time constants: the absolute path of the module's source directory and file at build time, the same strings as `import.meta.dirname` and `import.meta.filename`.
A `.ts` or `.tsx` file keeps them even when it uses `import` syntax, which Node.js would load as an ES module without these globals, because TypeScript projects are commonly shipped as CommonJS bundles where they exist.
In such a file `typeof __dirname` is `"string"`, where Node.js running the source directly reports `"undefined"`.
JavaScript ES modules (`.mjs`, or `.js` in a `"type": "module"` package) do not have them, as in Node.js.

### Defined environment variables

A `--define` value replaces what the executable's environment holds for the variable, so the program and its child processes see the defined value whatever the executable is started with.
The `if` statements of JavaScript files that the value decides are resolved at build time, so assigning or deleting a defined variable is a build error, and a change made another way, such as a write through a name computed at run time or `Object.assign(process.env, ...)`, doesn't change which branch runs.
The other conditions the value decides (the `if` statements of TypeScript files, conditional expressions, and the right operands of `&&` and `||`) are resolved at build time too, except in a program that writes or deletes an environment variable through a name computed at run time, and in a program with code that runs in a Node island, which shares the environment and can change the variable.
In those programs they are evaluated when the program runs.
Named imports of a CommonJS module that picks its exports this way see the names the selected module exports.
Node.js takes those names from the module that the last such `module.exports = require()` assigns, whatever the condition, so the two differ when the modules export different names.

<span id="embedded-javascript-engine" />

## Runtime code evaluation

A compiled program embeds no JavaScript engine, so it cannot run JavaScript source at runtime.
`eval`, indirect `eval`, `new Function`, and `Function` behave as they do under `node --disallow-code-generation-from-strings`, a deliberate difference from default Node.js: every call throws a catchable `EvalError` with the message `Code generation from strings disallowed for this context`.
The arguments are evaluated first, and the Function constructor converts them to strings before it throws, as Node.js does.
Packages that test for code generation before using it, such as zod 4, take their interpreted path.
Code that needs to evaluate source at runtime must be rewritten; to compute a value from code at build time, use `comptime`.
The `vm` module is refused for the same reason.
See [npm Dependencies](/docs/dependencies#which-packages-compile) for which packages compile.

## Build-time evaluation

`comptime(() => ...)` runs its callback while the program compiles, in an evaluator process based on QuickJS, and embeds the returned value as a constant.
The callback must be written inline in the call, must not be `async` or a generator, and must not refer to bindings outside itself, to `console`, or to another `comptime` call.
It sees the ECMAScript built-ins and nothing of the compiler or the machine: `process`, `require`, module loading, timers, file and network access, `Intl` and `WebAssembly` do not exist there, and using one is a build error.
The evaluator is a separate process that holds no object of the compiler, and only source text goes in and only the result's data comes back.
It is not a hardened sandbox: its protection rests on the JavaScript engine having no memory-safety bugs, so build only code you would run.
Each callback has 2 seconds and 256 MB, and exceeding either is a build error that names the limit.
Promise reactions never run, so a callback that leaves one pending, or leaves a rejected promise unhandled, is a build error.
The result must be a finite number, a string, a boolean, or arrays, records and unions with `null` or `undefined` of those, and its type must not contain itself.
Only a result's own enumerable properties are read, and an array hole reads as `undefined`.
A result that contains itself, is nested more than 512 levels deep, has an array of more than 1,000,000 elements, or is larger than 64 MB is a build error.
Two places in a result that hold the same object become two separate constants.
`Math` members give the values Node.js computes.
Members whose Node.js result depends on locale data throw at build time: `toLocaleString`, `localeCompare`, `toLocaleLowerCase`, `toLocaleUpperCase`, and `toString`, `toTimeString`, `toLocaleDateString` and `toLocaleTimeString` of a `Date`.
`Number.prototype.toString` with a radix other than 10 throws at build time unless the number is a safe integer.
`Date.now()`, `Math.random()` and the local time zone are the build machine's at build time.
The message of an error the engine itself throws, such as the `TypeError` for reading a property of `undefined`, is QuickJS's wording, not Node.js's.

## Process events

Static signal listeners resolve names on the target platform, including `SIGWINCH` on POSIX.
`on`, `once`, `off`, and `removeListener` accept computed signal names and deliver the signal name and number.
Computed names for other event families and chained registrations are unsupported.
Windows registration is limited to CRT signals; WASI has no OS signals.

Literal `uncaughtException` and `uncaughtExceptionMonitor` registrations compile.
Handled exceptions resume queued native work; monitors alone do not suppress failure.
Error objects and primitive exceptions can reach listeners, while arbitrary native objects remain subject to exception-value restrictions.
Exception capture callbacks are unsupported.

`NODE_NO_WARNINGS=1` leaves out the default report of a process warning on stderr once the program has called `process.emitWarning` or registered a `'warning'` listener; listeners still run.
Before that, and in a program that does neither, the runtime's own warnings (the listener-leak warning of `EventEmitter` and `EventTarget`, the timer duration warnings, the `dgram` deprecation) are still printed.
The variable is read when a warning is reported, so an assignment to `process.env.NODE_NO_WARNINGS` changes later reports, where Node reads it once at startup.

## Native addons

Node-API (N-API) and V8 `.node` addons depend on Node.js itself, so compiled programs can't load them.
Direct `createRequire` calls to local addons are rejected at compile time.
A package that requires an addon needs a JavaScript implementation that compiles, or a replacement through native FFI.

A package that looks for an optional addon at run time compiles with `--npm-static`.
In JavaScript dependencies, a `createRequire` require call with a computed specifier, and `require.resolve` called with a computed request or read as a value, resolve the specifier with the Node.js algorithm when the program runs.
A `createRequire` loader held as a value, stored in an object or passed to a function, in TypeScript or JavaScript, is a function that resolves its request the same way when it is called.
It has none of the properties that the Node.js `require` function carries: in TypeScript, a statement that reads `resolve`, `cache`, `main`, or `extensions` through a held loader builds with warning `SC6015` and throws an error that names SC2020 when it runs.
`require.resolve` returns the path Node.js returns.
When nothing resolves, both throw the `MODULE_NOT_FOUND` error that Node.js throws, so a package that probes for a missing addon takes the same branch as in Node.js.
When the specifier of a require call resolves to a native addon, a builtin, or a file that is not part of the compiled program, the program prints an SC2020 error that names it and exits with status 1, because Node.js would load that module.
A `catch` clause around the call does not run in that case.
Resolution steps that the runtime does not reproduce, such as package `imports` specifiers, `require.resolve` options, and subpaths that a package does not export, end the program the same way.
Coverage lists these calls as deferred to runtime.
Other computed uses compile when they are inside a `try` block: a require call with spread arguments, a `require.resolve` call with another argument count, and `require.resolve.paths` called with a computed request evaluate their arguments and then throw an error, and `require.resolve.paths` read as a value is a function that throws when called.
Coverage lists these as deferred to runtime as well, and the package's `catch` clause then runs as it does in Node.js when the addon is missing.
Outside a `try` block, these uses keep the package refused.
This applies until compiled programs can load addons through Node.js.

Use [Native FFI](/docs/ffi#replacing-a-node-api-addon) to call a plain C ABI function linked from an object or static archive.
The guide includes a replacement for a small Node-API helper.

## WebAssembly modules

scriptc translates WebAssembly modules to native code when it builds the program; there is no interpreter or JIT in the executable.
A build translates every module whose bytes it can see: files at paths the compiler resolves (`require.resolve`, `createRequire(...).resolve`, `import.meta.resolve`, `new URL("./x.wasm", import.meta.url)`, `path.join(__dirname, ...)`), including `.so` WebAssembly shared libraries such as quickjs-wasi's extensions, modules inlined as base64 or byte-array literals, `.wasm` files shipped by packages whose code uses WebAssembly, and files listed in a `scriptc.wasm` array in `package.json`.
At run time `WebAssembly.compile`, `instantiate`, `validate` and `new WebAssembly.Module` find the translated module by the SHA-256 of the bytes they are given.
Bytes the build did not see, including bytes produced at run time, reject with a `CompileError` that says the module was not translated, and `WebAssembly.validate` answers `false` for them.
`scriptc coverage` lists the modules a build finds, with their size, whether they validate, and the WebAssembly proposals they use.

Translated modules can use the WebAssembly 2.0 feature set: the MVP plus sign-extension operators, non-trapping float-to-int conversions, bulk memory, multi-value, mutable globals, reference types, and extended constant expressions.
Modules that use SIMD, threads, exception handling, tail calls, typed function references or GC, memory64, or multiple memories are not translated, so loading them rejects with a `CompileError` that names the proposal the module uses, and `WebAssembly.validate` answers `false` for them; packages that fall back to a scalar build when that happens, such as undici and es-module-lexer, take their fallback.
Invalid modules the build finds reject with a `CompileError` that includes the validation error.

Supported: `WebAssembly.compile`, `instantiate` (with bytes or a `Module`), `validate`, `new WebAssembly.Module`, `new WebAssembly.Instance`, `new WebAssembly.Memory`, `new WebAssembly.Table`, `new WebAssembly.Global`, `WebAssembly.Module.imports`, `exports` and `customSections`, and `instanceof` for every class; the error classes `CompileError`, `LinkError` and `RuntimeError`, constructed with or without `new`; `globalThis.WebAssembly` and its `compile`, `instantiate` and `validate` functions as values.
Instances expose exported functions, memories (`buffer`, `grow`), tables (`get`, `set`, `grow`, `length`) and globals (`value`, `valueOf`).
Imports can be functions, numbers or BigInts for immutable globals, and memories, tables and globals that the program constructs or other instances export; their limits, types and mutability are checked with Node.js's `LinkError` messages.
Function references from a table or global reach JavaScript as callable functions, and exported functions can be stored in tables and globals, which is what dynamic linkers such as quickjs-wasi's extension loader do.
Traps throw `WebAssembly.RuntimeError` with Node.js's messages, unbounded recursion throws `RangeError`, and an exception thrown by an imported function propagates through the WebAssembly frames unchanged.
A `WebAssembly.Module` can be sent to a worker through `workerData` or `postMessage`.

Not supported yet: the `Tag` and `Exception` constructors, shared memories (`new WebAssembly.Memory({ shared: true, ... })` throws a `TypeError`), `compileStreaming` and `instantiateStreaming`, and the classes used as values other than with `new`, `instanceof` and `WebAssembly.Module`'s static functions.
TypeScript sources fail to build with SC2020 at each such use, and JavaScript sources throw that diagnostic when the statement runs.
Programs built for `wasm32-wasi` can't use the WebAssembly API.

Differences from Node.js:

- Growing a memory, from JavaScript or inside the module, does not detach the previous `ArrayBuffer`.
  The memory never moves, so views created before the growth stay valid but keep their old length; code that reads `memory.buffer` again after growing, as WebAssembly glue code does, behaves as in Node.js.
- `valueOf()` answers a `WebAssembly.Global`'s value only when the receiver's static type is `WebAssembly.Global`; through an untyped value it answers the object, and converting a `Global` to a primitive uses the object rather than its value.
- Instances are never freed, together with their memories, imports and exports.
- A load whose result the module never uses may be removed instead of trapping when its address is out of bounds.
  Loads whose result is dropped explicitly still trap.
- NaN payloads may differ, for example when a signaling NaN passes through arithmetic or a JavaScript number.
- `WebAssembly.compile` and `instantiate` with bytes settle after the current microtasks, as in Node.js, but can settle after `setImmediate` callbacks that Node.js would run later.
- The executable reads `.wasm` files at run time from the paths they had when it was built.

## WASI target

The `wasm32-wasi` executable target supports async/await, promises, generators, timers, portable event-loop work, stdin/readline, filesystem callbacks and promises.

WASI Preview 1 does not provide sockets, process spawning, OS signals, network interfaces, or filesystem notifications.
Networking/fetch, child processes, signal APIs, `os.networkInterfaces()`, the `os` host facts without a WASI equivalent (`cpus()`, `hostname()`, `freemem()`, `loadavg()`, `uptime()`, `getPriority()`), and `fs.watch` are rejected before linking with `SC3002`.
`--sanitize` and native FFI are unavailable.

Library mode emits an async-free Wasm reactor with named host imports.
Native runtime localization and thread instances are unsupported.
Each Wasm instance owns independent state.
Filesystem access is limited to directories opened by the host; `scriptc run` exposes the current working directory and `/tmp`.
See [Platform Support](/docs/platforms) for build and run details.

## Tooling and native FFI

`scriptc run` does not forward extra CLI arguments to the program.
Build an executable and invoke it directly to pass arguments.

Native FFI links C functions declared in a manifest.
Formats 2–5 support call-scoped callbacks, copied string/byte callback parameters, explicitly released retained callbacks, and asynchronous foreign-thread delivery.
Format 6 adds single-precision floats, signed 8-bit and signed/unsigned 16-bit integers, and borrowed writable byte spans.
Variadic calls, structs by value, owned pointer/string/byte returns, runtime dynamic-library loading, and library-mode builds are unsupported.
See [Native FFI](/docs/ffi).

Numbers follow JavaScript's double-precision semantics.

## Browser embedding

Wasm reactors require a host-provided WASI Preview 1 adapter.
A reactor can run supported three.js CPU operations and send animated geometry, material state, and resource lifecycle notifications to an external web application through declared imports.
The application supplies DOM, graphics, media, audio, and decoder bindings, including the renderer.

Library mode excludes promises, async functions, timers.
See [WebAssembly Modules](/docs/wasm) for the embedding contract.

## Native debugging

Executable builds with `--optimization=dev` support source breakpoints and native stack frames for compiled TypeScript and JavaScript.
LLVM builds include source names and lexical scopes for locals, parameters, captured bindings, and module globals.
Release builds and builds with `--strip` omit these source mappings.
On macOS, executable builds produce an adjacent `.dSYM` bundle using the Command Line Tools' `dsymutil`.

Debugger values use native representations.
Numbers and booleans display directly.
Strings expose their `ScrStr` layout, including UTF-8 `data` and byte `len`; tagged unions expose their `tag` and `slot` fields.
Other heap values and scalar bindings captured before initialization can appear as opaque pointers.

Debuggers can't evaluate TypeScript expressions or format values as JavaScript objects.

---

For a semantic overview of all documentation, see [/sitemap.md](/sitemap.md)

For an index of all available documentation, see [/llms.txt](/llms.txt)

For agent-facing discovery, including API and MCP surfaces, see [/agents.md](/agents.md)

---
title: Native Program Objects
url: "https://scriptc.dev/docs/native-objects"
docs_index: /llms.txt
lastUpdated: 2026-11-12
---

> For an index of all documentation, see [/llms.txt](/llms.txt).

A native program object lets your own build system link a compiled program, for example to combine it with other native code in Xcode, CMake, or a custom linker command. `scriptc build --emit=obj` writes one relocatable object file that defines `main`, without running a compiler, linker, or SDK. To build a library that a host application calls into instead, use library mode (`scriptc build --lib --profile`).

## ABI and runtime contract

The object format is experimental and can change in any release before 1.0. Link an object only with the runtime pack from the same scriptc installation, which `--print=native-link-info` reports.

The object leaves the runtime functions it uses undefined, for the runtime pack to provide. It also references a versioned marker symbol that only the matching runtime defines, so linking against a different runtime version fails at link time instead of misbehaving at runtime.

## Machine-readable link information

Add `--print=native-link-info` to write the object and print a JSON link recipe instead of the output path:

```console
$ scriptc build main.ts --print=native-link-info -o app.o > link-info.json
```

The `scriptc.native-link-info.v1` document reports:

- target triple, object format, architecture, minimum OS, and relocation model;
- the `main` entry and the runtime marker symbol;
- the matching installed precompiled runtime-pack root, selected objects and archives, sizes, and hashes;
- ordered program, FFI, runtime, and vendor inputs; and
- required system libraries and frameworks.

Runtime artifact paths are relative to `runtime_pack.root`, and FFI library paths are absolute. Link the inputs in the order the document lists them.

## C compiler as linker driver

The repository's [`examples/native-object`](https://github.com/vercel-labs/scriptc/tree/main/examples/native-object) directory contains a TypeScript program, a C function it calls through [FFI](/docs/ffi), and a small script that links them using the JSON recipe:

```console
$ cd examples/native-object
$ clang -target arm64-apple-macosx14.0.0 -O2 -c native.c -o native.o
$ scriptc build main.ts --ffi ffi.json --print=native-link-info -o app.o > link-info.json
$ node link.mjs cc link-info.json app-cc
$ ./app-cc
42
```

The script passes clang the reported objects, archives, and system libraries. On other targets, use a linker and sysroot for that target: glibc or musl for Linux, zig's MinGW sysroot for Windows x64, and a WASI Preview 1 sysroot for WASI.

## Native Apple linker

The same example can invoke Apple `ld` directly with the precompiled runtime objects:

```console
$ node link.mjs ld link-info.json app-ld
$ ./app-ld
42
```

In this mode the script asks `xcrun` for the macOS SDK and linker, then passes the target's minimum OS version, every object and archive in order, and each reported system library. scriptc produces `app.o`; your toolchain performs the link.

FFI bindings behave the same in program objects as in executables, following the [Native FFI](/docs/ffi) manifest.

---

For a semantic overview of all documentation, see [/sitemap.md](/sitemap.md)

For an index of all available documentation, see [/llms.txt](/llms.txt)

For agent-facing discovery, including API and MCP surfaces, see [/agents.md](/agents.md)

---
title: Platform Support
url: "https://scriptc.dev/docs/platforms"
docs_index: /llms.txt
lastUpdated: 2026-11-12
---

> For an index of all documentation, see [/llms.txt](/llms.txt).

By default, `scriptc build` produces an executable for the machine you are building on. To build for another operating system, another architecture, or WebAssembly, set `SCRIPTC_TARGET` to the target you want. This page lists the supported platforms and shows how to cross-compile for each one.

<span id="owned-llvm-targets" />

## Supported platforms

The compiler runs on:

- macOS 15 or later on arm64 and x64.
- Linux on arm64 and x64, with glibc 2.34 or later or with musl.
- Windows on x64.

It can build:

- Executables for macOS (arm64, x64; macOS 14.0 minimum), Linux (arm64, x64; glibc or musl), and Windows (x64).
- WebAssembly modules for WASI Preview 1.
- Static libraries for iOS and Android apps, in [library mode](#ios-and-android-arm64-library-mode).

Building an executable needs a linker and the platform's SDK or system libraries: clang from the Xcode Command Line Tools on macOS, clang on glibc-based Linux, and [zig](https://ziglang.org) on Windows, on musl-based Linux, and for WASI, Android, and cross-compiled targets. Set `SCRIPTC_LINKER` to use a different linker driver. Assembly and object output (`--emit=asm` or `--emit=obj`) need no external tools.

<span id="cross-compilation-via-zig" />

## Cross-compile

The installed compiler can build for its own platform out of the box. To build for another target:

1. Install [zig](https://ziglang.org), which scriptc uses to link executables for other targets.
2. Add the target's runtime pack to your project, at the same version as `scriptc --version`. Runtime packs are named `@scriptc/runtime-<target>`, for example `@scriptc/runtime-linux-arm64-gnu` or `@scriptc/runtime-wasm32-wasi`.
3. Run `scriptc build` from your project with `SCRIPTC_TARGET` set to the target triple.

```console
$ npm install --save-dev @scriptc/runtime-linux-arm64-gnu@0.3.0
$ SCRIPTC_TARGET=aarch64-linux-gnu.2.36 scriptc build fib.ts -o fib-linux >/dev/null
```

You can also point `SCRIPTC_RUNTIME_PACK` at a runtime pack directory, which is useful with the standalone compiler archives. Adding a pack doesn't require reinstalling the compiler.

### Linux (arm64, x86\_64)

GNU/Linux triples can name the minimum glibc version, `2.34` (the default) or `2.36`:

```console
$ SCRIPTC_TARGET=aarch64-linux-gnu.2.36 scriptc build fib.ts -o fib-linux
$ file fib-linux
fib-linux: ELF 64-bit LSB executable, ARM aarch64, version 1 (SYSV), dynamically linked, interpreter /lib/ld-linux-aarch64.so.1, for GNU/Linux 2.0.0, with debug_info, not stripped
```

`x86_64-linux-gnu.2.36` works the same way. For Alpine and other musl-based systems, use `aarch64-linux-musl` or `x86_64-linux-musl`; these produce statically linked executables. On a glibc-based Linux machine, [`--static`](/docs/cli#options) links a static executable for the machine's own target.

Linux executables use the native Linux implementations of the event loop (epoll), servers, TLS (with the system's CA bundle), and `fs.watch`, and they are tested against Node.js on Linux, including Alpine.

Executables for glibc are position independent, and they resolve the functions they import from the C library when they start, after which the table of those addresses is read-only. Release executables for musl contain no debug information and no symbol table: zig links its own build of musl into them, and its debug information names directories of the machine that built the executable. Build with `--optimization dev` to keep symbols and debug information.

On Linux and macOS, the addresses of a program's objects differ from run to run, by a multiple of 4 MiB. A process whose address-space randomization is turned off gets the same addresses in every run: that is how gdb and lldb start a program, and what `setarch -R` does on Linux.

An executable with at least 4 MiB of program code is linked with 2 MiB segment alignment when lld or zig links it, so the kernel can map its code with 2 MiB pages instead of 4 KiB pages. This takes effect when transparent huge pages are set to `madvise` or `always` and the file system keeps files in the page cache as 2 MiB folios (XFS, for example). scriptc writes such an executable with one write so that the page cache holds it that way; a copy written in small chunks maps with 4 KiB pages until it is read from disk again. The alignment also aligns the executable's load address to 2 MiB, which leaves 19 bits of address randomization for it instead of 28. Smaller executables, and links by GNU ld, keep 4 KiB alignment.

### Windows (x86\_64)

```console
$ SCRIPTC_TARGET=x86_64-windows-gnu scriptc build fib.ts -o fib.exe
$ file fib.exe
fib.exe: PE32+ executable (console) x86-64, for MS Windows
```

Windows executables support `child_process`, `net`, `http`, `https`, `tls`, `http2`, `dgram`, `dns`, and `fetch`, and they are tested against Node.js on Windows.

Executables open a console window by default. For an application that shows its own window, build with `--windows-subsystem=gui` so Windows doesn't open an extra console.

<span id="webassembly-wasi-preview-1" />

### WebAssembly (WASI)

WASI is a cross-compile target, so it needs [zig](https://ziglang.org) and the `@scriptc/runtime-wasm32-wasi` runtime pack, as described in [Cross-compile](#cross-compilation-via-zig). Install the runtime pack in your project at the same version as `scriptc --version`. Without it, the build fails with `SC3003`.

Set `SCRIPTC_TARGET=wasm32-wasi` to build a standalone `.wasm` module:

```console
$ npm install --save-dev @scriptc/runtime-wasm32-wasi@0.3.5
$ SCRIPTC_TARGET=wasm32-wasi scriptc build hello.ts >/dev/null
$ SCRIPTC_TARGET=wasm32-wasi scriptc run hello.ts
hello, world
```

Without `-o`, the module is written to `.scriptc/hello.wasm`. `scriptc run` runs it with Node.js's WASI implementation, so it needs Node.js 24 or later on your `PATH`. It passes through stdio and environment variables, makes the current directory available as `/`, and maps the host's temporary directory to `/tmp`. You can run a built module in any other WASI Preview 1 runtime too.

WASI modules support the same language features as native executables, including async functions, promises, generators, timers, stdin and readline, and filesystem callbacks and promises. WASI Preview 1 has no sockets, process spawning, signals, network interfaces, or file watching, so networking and `fetch`, child processes, signal APIs, `os.networkInterfaces()`, the `os` host facts without a WASI equivalent (`cpus()`, `hostname()`, `freemem()`, `loadavg()`, `uptime()`, `getPriority()`), and `fs.watch` fail the build with `SC3002`. `--sanitize` and native FFI aren't available for WASI.

To build a module that a web or native application calls into, see [WebAssembly Modules](/docs/wasm).

<span id="ios-and-android-arm64-library-mode" />

### iOS and Android (arm64)

On mobile, the app is the executable, so scriptc builds a static library that the app links. Use library mode with one of these targets:

<dl>
  <dt>
    <code>SCRIPTC\_TARGET=aarch64-apple-ios</code>
  </dt>

  <dd>
    iOS devices. Requires a macOS host. Minimum iOS 15.0.
  </dd>

  <dt>
    <code>SCRIPTC\_TARGET=aarch64-apple-ios-simulator</code>
  </dt>

  <dd>
    The iOS simulator on Apple silicon. Requires a macOS host. Minimum iOS 15.0.
  </dd>

  <dt>
    <code>SCRIPTC\_TARGET=aarch64-linux-android</code>
  </dt>

  <dd>
    Android devices, built against API level 26; the app's

    <code>minSdkVersion</code>

    must be 26 or higher. Builds from any supported host with zig.
  </dd>
</dl>

```console
$ SCRIPTC_TARGET=aarch64-apple-ios scriptc build --lib --profile app.profile.json
$ SCRIPTC_TARGET=aarch64-linux-android scriptc build --lib --profile app.profile.json
```

The library profile declares the exported functions, the host callbacks the library can call, and options such as running several independent instances in one app (`abi.localize_runtime`) or one instance per thread (`abi.instance_per_thread`). Xcode or the Android NDK performs the final link against the platform SDK. Building these targets without `--lib` fails with `SC3002`, and library builds don't support async functions, promises, or timers.

## Cross-target limits

- `--sanitize` is available only when building for the host platform.
- Native FFI inputs (objects and archives) must already be built for the target; scriptc doesn't translate them.
- iOS targets build only on macOS hosts.
- WASI builds can't use APIs that need capabilities WASI Preview 1 lacks, as described above.

## Summary

<table>
  <thead>
    <tr>
      <th>
        Platform
      </th>

      <th>
        How to build
      </th>

      <th>
        Notes
      </th>
    </tr>
  </thead>

  <tbody>
    <tr>
      <td>
        macOS arm64 / x64
      </td>

      <td>
        Build on a Mac
      </td>

      <td>
        Needs the Xcode Command Line Tools. Supports

        <code>--sanitize</code>

        .
      </td>
    </tr>

    <tr>
      <td>
        Linux arm64 / x86\_64
      </td>

      <td>
        Build on Linux, or

        <code>SCRIPTC\_TARGET=\<arch>-linux-gnu</code>

        or

        <code>\<arch>-linux-musl</code>
      </td>

      <td>
        Servers, TLS,

        <code>fetch</code>

        ,

        <code>fs.watch</code>

        , and

        <code>child\_process</code>

        . musl builds are statically linked.
      </td>
    </tr>

    <tr>
      <td>
        Windows x86\_64
      </td>

      <td>
        Build on Windows, or

        <code>SCRIPTC\_TARGET=x86\_64-windows-gnu</code>
      </td>

      <td>
        Servers, TLS,

        <code>fetch</code>

        , and

        <code>child\_process</code>

        . Links with zig's Windows sysroot.
      </td>
    </tr>

    <tr>
      <td>
        iOS arm64 (device and simulator)
      </td>

      <td>
        <code>SCRIPTC\_TARGET=aarch64-apple-ios</code>

        or

        <code>aarch64-apple-ios-simulator</code>

        on macOS
      </td>

      <td>
        Library mode only. Static libraries for Xcode projects, iOS 15.0 minimum.
      </td>
    </tr>

    <tr>
      <td>
        Android arm64
      </td>

      <td>
        <code>SCRIPTC\_TARGET=aarch64-linux-android</code>

        with zig
      </td>

      <td>
        Library mode only. Static libraries for Gradle and NDK projects, API level 26 minimum.
      </td>
    </tr>

    <tr>
      <td>
        WebAssembly (WASI Preview 1)
      </td>

      <td>
        <code>SCRIPTC\_TARGET=wasm32-wasi</code>
      </td>

      <td>
        Full language support. APIs that need sockets, processes, signals, or file watching are unavailable.
      </td>
    </tr>
  </tbody>
</table>

---

For a semantic overview of all documentation, see [/sitemap.md](/sitemap.md)

For an index of all available documentation, see [/llms.txt](/llms.txt)

For agent-facing discovery, including API and MCP surfaces, see [/agents.md](/agents.md)

---
title: Quickstart
url: "https://scriptc.dev/docs/quickstart"
docs_index: /llms.txt
lastUpdated: 2026-11-12
---

> For an index of all documentation, see [/llms.txt](/llms.txt).

In this guide you install scriptc, compile a small TypeScript program into an executable, check what compiles, and add an npm dependency.

## Prerequisites

- A supported computer: macOS 15 or later (arm64 or x64), Linux (arm64 or x64), or Windows (x64). See [Platform Support](/docs/platforms) for details.
- Node.js 24 or later and npm, to install scriptc. The compiler itself and the executables it builds run without Node.js. Standalone compiler archives are also available from [GitHub Releases](https://github.com/vercel-labs/scriptc/releases).
- A linker, to build executables. On macOS, install the Xcode Command Line Tools with `xcode-select --install`. On glibc-based Linux, install clang. On Windows and musl-based Linux, install [zig](https://ziglang.org).

## Install

Install the CLI globally with npm:

```console
$ npm install -g scriptc
```

npm installs the native compiler for your platform as an optional dependency. An install script then copies that executable into the `scriptc` package and records where the compiler finds its helper files. The platform package alone does not install a working `scriptc` command, so keep optional dependencies and install scripts enabled. If npm blocks the install script, `scriptc` prints `scriptc: native installation is incomplete; enable install scripts and reinstall scriptc`; reinstall with install scripts enabled to fix it.

Check that it works:

```console
$ scriptc --version
0.3.0
```

## Create a program

Create `hello.ts`. It greets the name passed on the command line, or `world` when there is none:

```ts title="hello.ts"
const who: string = process.argv.length > 2 ? process.argv[2] : "world";
console.log(`hello, ${who}`);
```

## Compile and run

`scriptc run` compiles the program and runs it immediately:

```console
$ scriptc run hello.ts
hello, world
```

`scriptc build` writes an executable instead. It prints the path of the file it writes; the examples in these docs discard that line with `>/dev/null`:

```console
$ scriptc build hello.ts -o hello >/dev/null
$ ./hello scriptc
hello, scriptc
```

The executable runs on its own, without Node.js or scriptc installed. `scriptc run` does not pass extra arguments to the program, so build an executable when you need them.

On Windows, build `hello.exe` and run it with `.\hello.exe`.

## Check compilation support

Before building a larger program, run `scriptc coverage`. It analyzes the program without building it and reports how much compiles:

```console
$ cat hello.ts
const who: string = process.argv.length > 2 ? process.argv[2] : "world";
console.log(`hello, ${who}`);

$ scriptc coverage hello.ts

  statements analyzed   2
  compile statically    2  (100%)

  fully static — all analyzed statements compile to native code.
```

When a program uses an unsupported operation, the report lists each site with a diagnostic code and a hint. See [Coverage Reports](/docs/coverage).

## Use an npm dependency

Create `cli.ts`:

```ts title="cli.ts"
import pc from "picocolors";

console.log(pc.green("hello"));
```

Install the package, then build as usual:

```console
$ npm install picocolors
$ scriptc build cli.ts -o demo >/dev/null
$ ./demo
hello
```

The package compiles into the executable at build time, so the executable doesn't need `node_modules` when it runs. See [npm Dependencies](/docs/dependencies) for which packages compile and how to check them.

## Next steps

- [Coverage Reports](/docs/coverage): find out whether an existing program compiles.
- [Platform Support](/docs/platforms): cross-compile for Linux, Windows, and WebAssembly.
- [CLI Reference](/docs/cli): output formats, optimization levels, and other options.
- [Limitations](/docs/limitations): restrictions and differences from Node.js.

---

For a semantic overview of all documentation, see [/sitemap.md](/sitemap.md)

For an index of all available documentation, see [/llms.txt](/llms.txt)

For agent-facing discovery, including API and MCP surfaces, see [/agents.md](/agents.md)

---
title: Sharing Data Between Workers
url: "https://scriptc.dev/docs/threads"
docs_index: /llms.txt
lastUpdated: 2026-11-12
---

> For an index of all documentation, see [/llms.txt](/llms.txt).

Compiled programs can use `node:worker_threads` to run work in parallel on several CPU cores. As in Node.js, each message a worker receives is a copy (a structured clone) of what was sent, so a large data set sent to four workers is copied four times. See [worker threads](/docs/limitations#native-worker-threads) for the supported worker APIs.

When workers need to read the same large data, use the `@scriptc/threads` package. Its `publish(value)` function makes a graph of objects immutable, so a scriptc executable can hand the same objects to every thread by reference instead of copying them.

Add `@scriptc/threads` to your project's dependencies. scriptc compiles its functions directly into your program, and Node.js runs its JavaScript implementation, so the same program runs under both.

## Example

The main thread builds a ledger of a million orders, publishes it, and sends it to four workers. Each worker sums its share of the orders.

```ts title="main.ts"
import { Worker, isMainThread, parentPort, workerData } from "node:worker_threads";
import { publish, sharesPublishedGraphs } from "@scriptc/threads";

class Order {
  customer: string;
  amount: number;
  constructor(customer: string, amount: number) {
    this.customer = customer;
    this.amount = amount;
  }
}

class Ledger {
  orders: Order[] = [];
  rates = new Map<string, number>();
}

interface Job {
  ledger: Ledger;
  index: number;
  count: number;
}

// Workers read only data: under Node.js they receive a clone without prototypes.
function revenue(ledger: Ledger, index: number, count: number): number {
  let sum = 0;
  for (let i = index; i < ledger.orders.length; i += count) {
    const order = ledger.orders[i]!;
    sum += order.amount * (ledger.rates.get(order.customer) ?? 1);
  }
  return sum;
}

if (isMainThread) {
  const ledger = new Ledger();
  ledger.rates.set("acme", 2);
  for (let i = 0; i < 1_000_000; i++) {
    ledger.orders.push(new Order(i % 3 === 0 ? "acme" : "globex", i % 100));
  }
  publish(ledger); // ledger and everything it reaches are now immutable
  console.log(`shared by reference: ${sharesPublishedGraphs}`);

  const count = 4;
  let total = 0;
  let done = 0;
  for (let index = 0; index < count; index++) {
    const job: Job = { ledger, index, count };
    const worker = new Worker(new URL(import.meta.url), { workerData: job });
    worker.on("message", (part: number) => {
      total += part;
      if (++done === count) console.log(`revenue: ${total}`);
    });
  }

  try {
    ledger.orders.push(new Order("initech", 5));
  } catch (e) {
    console.log(`${(e as Error).name}: ${(e as Error).message}`);
  }
  try {
    ledger.rates.set("globex", 3);
  } catch (e) {
    console.log(`${(e as Error).name}: ${(e as Error).message}`);
  }
} else {
  const job = publish(workerData as Job); // a no-op in scriptc; freezes the clone in Node.js
  parentPort!.postMessage(revenue(job.ledger, job.index, job.count));
}
```

Node.js 24 runs the program directly and clones the ledger for each worker:

```console
$ node main.ts
shared by reference: false
TypeError: Cannot add property 1000000, object is not extensible
TypeError: Cannot modify a published Map
revenue: 66000033
```

The scriptc executable prints the same results, except that every worker reads the main thread's ledger in place:

```console
$ scriptc build main.ts -o ledger >/dev/null
$ ./ledger
shared by reference: true
TypeError: Cannot add property 1000000, object is not extensible
TypeError: Cannot modify a published Map
revenue: 66000033
```

## The publication contract

`publish(value)` returns `value` after making it, and everything it reaches, immutable. Publishing an already published graph returns immediately.

<table>
  <thead>
    <tr>
      <th>
        Values
      </th>

      <th>
        publish
      </th>
    </tr>
  </thead>

  <tbody>
    <tr>
      <td>
        Numbers, strings, booleans, BigInts,

        <code>undefined</code>

        ,

        <code>null</code>
      </td>

      <td>
        Published
      </td>
    </tr>

    <tr>
      <td>
        Plain objects, arrays, class instances, records
      </td>

      <td>
        Published, with their fields and elements
      </td>
    </tr>

    <tr>
      <td>
        <code>Map</code>

        and

        <code>Set</code>
      </td>

      <td>
        Published, with their keys and values
      </td>
    </tr>

    <tr>
      <td>
        Functions, symbols, accessor properties,

        <code>Date</code>

        ,

        <code>RegExp</code>

        ,

        <code>Error</code>

        ,

        <code>Promise</code>

        , typed arrays, other built-in objects
      </td>

      <td>
        Refused with a

        <code>TypeError</code>
      </td>
    </tr>
  </tbody>
</table>

A refusal names the value, for example `Cannot publish a function` or `Cannot publish a Date object`. Only values that are actually present count: a class whose field type could hold an unpublishable value publishes as long as no such value is reached. A refused `publish` leaves the whole graph unchanged and writable, because both implementations check the graph before they change anything.

```ts title="refuse.ts"
import { publish } from "@scriptc/threads";

class Meeting {
  title: string;
  when: Date;
  constructor(title: string, when: Date) {
    this.title = title;
    this.when = when;
  }
}

const meetings = [new Meeting("review", new Date(0))];
try {
  publish(meetings);
} catch (e) {
  console.log(`${(e as Error).name}: ${(e as Error).message}`);
}
// The failed publish left the whole graph unchanged and writable.
meetings.push(new Meeting("retro", new Date(0)));
console.log(meetings.length, meetings[1]!.title);
```

```console
$ scriptc build refuse.ts -o refuse >/dev/null
$ ./refuse
TypeError: Cannot publish a Date object
2 retro
```

### The write guard

Writes into a published graph throw the `TypeError` that Node.js throws for frozen objects, in both implementations:

- Field and property assignment: `Cannot assign to read only property 'title' of object '#<Meeting>'`.
- Adding array elements: `Cannot add property 3, object is not extensible`.
- Removing array elements, for example with `pop()`: `Cannot delete property '2' of [object Array]`.
- Changing an array's length: `Cannot assign to read only property 'length' of object '[object Array]'`.
- `set`, `add`, `delete`, and `clear` on a published `Map` or `Set`: `Cannot modify a published Map` (or `Set`).

In a scriptc executable, every store the compiler emits into a type that the program publishes checks whether its target is published. Objects of those types that were never published stay writable.

### How it differs from structured clone

Node.js keeps cloning published graphs when it posts them; `sharesPublishedGraphs` is `false` there. A scriptc executable passes a published graph to `postMessage` and `workerData` as a pointer, and `sharesPublishedGraphs` is `true`.

<table>
  <thead>
    <tr>
      <th>
        Behavior
      </th>

      <th>
        scriptc executable
      </th>

      <th>
        Node.js
      </th>
    </tr>
  </thead>

  <tbody>
    <tr>
      <td>
        Sending a published graph
      </td>

      <td>
        Constant time; the receiver reads the sender's objects
      </td>

      <td>
        A structured clone per message
      </td>
    </tr>

    <tr>
      <td>
        Received graph
      </td>

      <td>
        Immutable
      </td>

      <td>
        A writable clone, until the receiver publishes it
      </td>
    </tr>

    <tr>
      <td>
        Class instances
      </td>

      <td>
        Keep their class, methods, and

        <code>instanceof</code>

        relations
      </td>

      <td>
        Arrive as plain objects without their prototype
      </td>
    </tr>

    <tr>
      <td>
        Identity across messages
      </td>

      <td>
        Two posts of one graph arrive as the same objects
      </td>

      <td>
        Every post arrives as a new clone
      </td>
    </tr>

    <tr>
      <td>
        Memory
      </td>

      <td>
        Published objects are never freed
      </td>

      <td>
        Garbage collected as usual
      </td>
    </tr>
  </tbody>
</table>

A program behaves the same under both when it follows three rules:

1. A receiver calls `publish` on what it receives. In scriptc this is a no-op; in Node.js it freezes the clone, so writes fail in the same way.
2. A receiver reads data and does not depend on prototypes, methods, or `instanceof`. Error messages that name the receiver's class differ accordingly: `#<Meeting>` in scriptc, `#<Object>` in Node.js.
3. Nothing compares identity across messages.

### Frozen plain data

Without `@scriptc/threads`, a scriptc executable also shares a message of untyped values, such as `JSON.parse` results, by reference when every object and array in it is frozen with `Object.freeze`, ordinary, and holds only primitives, strings, and other such objects and arrays. That data becomes immortal in the same way. Any other message is cloned. Set `SCRIPTC_PUBLISH=0` in the environment to clone every message.

## Cost

Starting a worker thread takes tens of microseconds plus the worker's own module initialization, since every worker initializes the program's modules again, as in Node.js. A worker that exits frees its heap first, which takes time proportional to what it still holds. `SCRIPTC_WORKER_STATS=1` prints, as each worker exits, how long it took to start, to run its script and to exit.

On Linux, a worker that exits without having shared an object it allocated with another thread returns all of its memory, including objects it never freed, such as `ProgramLifetime` instances, once no other worker thread is running. A worker shares objects when, for example, it sends a published value or frozen plain data, which other threads read by reference, or stores a value in an `InternTable`. The memory of a worker that shared stays allocated, as on other platforms. Set `SCR_SA_DISCARD=0` in the environment to keep the memory of every worker.

`publish` visits each object of the graph once and marks it immortal, so publication takes time proportional to the graph. Published objects then cost nothing on later sends, and reference counting and the cycle collector skip them. Stores into the types a program publishes carry one extra load and branch.

Threads that add to one shared collection of published values while they run can use an [`InternTable`](/docs/experimental#interntable) from `@scriptc/experimental`.

See [published graphs](/docs/limitations#published-graphs) for remaining differences from Node.js.

---

For a semantic overview of all documentation, see [/sitemap.md](/sitemap.md)

For an index of all available documentation, see [/llms.txt](/llms.txt)

For agent-facing discovery, including API and MCP surfaces, see [/agents.md](/agents.md)

---
title: WebAssembly Modules
url: "https://scriptc.dev/docs/wasm"
docs_index: /llms.txt
lastUpdated: 2026-11-12
---

> For an index of all documentation, see [/llms.txt](/llms.txt).

A WebAssembly module lets a web page or another application call functions written in TypeScript. Use it when you want to run compiled logic, such as a simulation, a parser, or geometry processing, inside a host that owns the user interface. To build a program that runs on its own under a WASI runtime instead, see [WebAssembly (WASI)](/docs/platforms#webassembly-wasi-preview-1).

You build a module in library mode, with `SCRIPTC_TARGET=wasm32-wasi` and `scriptc build --lib --profile`. The result is a WASI Preview 1 reactor: a `.wasm` file with callable exports, named imports for host callbacks, and its own memory. Your application loads the module, supplies its imports, calls its exports, and handles events, scheduling, and rendering.

Module builds need [zig](https://ziglang.org) and the `@scriptc/runtime-wasm32-wasi` runtime pack in your project, at the same version as `scriptc --version`. See [Cross-compile](/docs/platforms#cross-compilation-via-zig).

```console
$ npm install --save-dev @scriptc/runtime-wasm32-wasi@0.3.5
```

## Define the interface

The library profile names the entry module and its public functions. A reachable callback call becomes a synchronous import from the <code>scriptc</code> namespace.

```ts title="counter.ts"
declare function changed(value: number): void;
let count = 0;
export function step(delta: number): number {
  count += delta;
  changed(count);
  return count;
}
```

```json title="counter.json"
{
  "profile_format": 1,
  "name": "counter",
  "entry": "counter.ts",
  "emission": "llvm",
  "abi": {
    "prefix": "counter_",
    "init_symbol": "counter_init",
    "sink_register_symbol": "counter_sink",
    "callback_register_symbol": "counter_callback"
  },
  "callbacks": [{ "name": "changed", "params": ["f64"], "returns": "void" }],
  "exports": [{ "export": "step", "symbol": "counter_step", "params": ["f64"], "returns": "f64" }]
}
```

Build the module:

```console
$ SCRIPTC_TARGET=wasm32-wasi scriptc build --lib --profile counter.json -o counter.wasm
```

Without `-o`, the output is `.scriptc/counter.wasm` beside the profile. The compiler API's `compileLibrary()` returns this path in `archivePath`, as it does for native libraries.

The profile uses the same schema as native library builds. <code>sink\_register\_symbol</code> and <code>callback\_register\_symbol</code> name native registration functions; a Wasm reactor does not export them. Wasm binds callbacks during instantiation and delivers panics through <code>scriptc.panic</code>.

Set `"optimization": "dev"` in the profile to turn off release optimization. npm dependencies compile automatically, as in executables. Add `"npm_static": ["three"]` to name packages explicitly, like the [`--npm-static`](/docs/dependencies#explicit-package-selection) option. Unsupported code that the module reaches still produces a compiler diagnostic.

## Instantiate and initialize

A browser host needs a WASI Preview 1 adapter as well as the application's callbacks. Browsers do not supply WASI automatically. Inspect <code>WebAssembly.Module.imports(module)</code> to see the exact imports required by your compiled program. Supply real implementations or explicit failures for unsupported operations.

```js title="loader.js"
export async function loadCounter(url, wasi) {
  const module = await WebAssembly.compileStreaming(fetch(url));
  let api;
  const instance = await WebAssembly.instantiate(module, {
    ...wasi.getImportObject(),
    scriptc: {
      changed(value) {
        console.log(value);
      },
      panic(pointer, length) {
        const bytes = new Uint8Array(api.memory.buffer, pointer, length);
        throw new Error(new TextDecoder().decode(bytes));
      },
    },
  });
  api = instance.exports;
  wasi.initialize(instance);
  api.counter_init();
  return api;
}
```

This loader assumes an adapter with <code>getImportObject()</code> and <code>initialize(instance)</code> methods, as provided by Node's <code>node:wasi</code>. Browser adapters may use different method names. Initialization must first connect the adapter to the instance's exported memory and invoke <code>\_initialize</code> once, then call the profile's <code>init\_symbol</code>. Do not call a command-style <code>start()</code>; reactors have no <code>\_start</code> export.

After initialization, <code>counter\_step(2)</code> returns <code>2</code>, and another call returns <code>4</code>. Calling <code>counter\_init()</code> again resets application globals and reevaluates module initialization. Instantiate the module again for independent state and memory.

## Values and memory ownership

<table>
  <thead>
    <tr><th>Profile class</th><th>Wasm boundary</th></tr>
  </thead>

  <tbody>
    <tr>
      <td><code>f64</code></td>

      <td>JavaScript number.</td>
    </tr>

    <tr>
      <td><code>bool</code>, <code>u8</code>, <code>u32</code>, <code>i32</code></td>

      <td>Wasm i32; booleans use 0 or 1. u32 values may appear as signed JavaScript numbers; use <code>value >>> 0</code> to read them unsigned.</td>
    </tr>

    <tr>
      <td><code>i64</code>, <code>u64</code></td>

      <td>JavaScript BigInt. The existing library integer checks and proofs restrict values to the exact JavaScript number range.</td>
    </tr>

    <tr>
      <td><code>string</code>, <code>bytes</code> parameters</td>

      <td>Two i32 arguments: byte offset and byte length. Strings use UTF-8. Entry parameters are copied into runtime-owned values.</td>
    </tr>

    <tr>
      <td><code>string</code>, <code>bytes</code> results</td>

      <td>Two extra i32 arguments identify writable four-byte output slots. The function writes the result pointer and byte length there, in little-endian order.</td>
    </tr>

    <tr>
      <td><code>cstring</code> parameters</td>

      <td>One i32 argument: the byte offset of NUL-terminated UTF-8 text. The text is copied into a runtime-owned string; malformed UTF-8 becomes U+FFFD. A zero pointer traps with <code>SC4012</code>.</td>
    </tr>

    <tr>
      <td><code>cstring</code> results</td>

      <td>An i32 result: the byte offset of NUL-terminated UTF-8 text in the result arena. A result containing U+0000 traps with <code>SC4015</code> instead of being truncated.</td>
    </tr>

    <tr>
      <td><code>void</code></td>

      <td>No result.</td>
    </tr>
  </tbody>
</table>

The native library rules about directions still apply: u8/u32/i32 are export parameters and callback scalars; i64/u64 are export-only. Callback results must be scalars or void. Callback string/byte parameters are borrowed only for the duration of that callback; copy them before retaining them in the host. A <code>cstring</code> callback parameter is one i32 pointer to NUL-terminated text, borrowed the same way; a value containing U+0000 traps with <code>SC4015</code>. The <code>cstring</code> class requires <code>"profile\_format": 2</code>. In native libraries it is a <code>const char \*</code> with the same rules.

Allocate host input buffers and output slots with <code>scriptc\_alloc(size)</code>; a zero result indicates allocation failure. Release each allocation exactly once with <code>scriptc\_free(pointer)</code>. Only free pointers returned by <code>scriptc\_alloc</code>. These allocations remain live across application calls, initialization, and collection until freed.

Result buffers belong to the module. If the profile declares <code>abi.result\_reset\_symbol</code>, results accumulate until that function is called. Otherwise, the next application export resets the result arena. Initialization and the optional <code>abi.collect\_symbol</code> also invalidate results. Allocation and free do not reset the arena. Copy a result into host-owned storage before invalidating it; never pass its pointer to <code>scriptc\_free</code>.

Recreate typed arrays and DataViews from <code>memory.buffer</code> after every Wasm call that may allocate. Memory growth replaces the backing buffer and invalidates older views. Pointers remain offsets into the instance's memory; never use a pointer with another instance.

## Errors and scheduling

A compiled trap or escaped exception calls <code>scriptc.panic(pointer, length)</code> with the native library's structured diagnostic bytes, then traps if the callback returns. Discard the instance after a panic, a Wasm trap, or an exception escaping a host import. Recovery uses a new instance; calling the initialization export does not repair a failed instance.

Host callbacks run synchronously. They must return before calling any application export, initialization, collection, allocation, or free on the same instance. Reentry is rejected. Copy callback data and schedule subsequent work after the outer call returns. The web app can call a frame export from <code>requestAnimationFrame</code> and render its results through Canvas, WebGL, or WebGPU.

See [Limitations](/docs/limitations#browser-embedding) for browser graphics and library-mode restrictions.

---

For a semantic overview of all documentation, see [/sitemap.md](/sitemap.md)

For an index of all available documentation, see [/llms.txt](/llms.txt)

For agent-facing discovery, including API and MCP surfaces, see [/agents.md](/agents.md)