Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ jobs:
- run: brew install llvm@22
- run: pnpm install --frozen-lockfile
- run: pnpm --filter @scriptc/llvm-darwin-arm64 build:native
- run: pnpm --filter @scriptc/runtime-darwin-arm64 build:native
- run: pnpm build
# Separate vitest invocations because the shard axes must not mix: a file
# lands in exactly ONE --shard slice, so an env-sharded file behind
Expand Down Expand Up @@ -135,6 +136,7 @@ jobs:
- run: brew install llvm@22
- run: pnpm install --frozen-lockfile
- run: pnpm --filter @scriptc/llvm-darwin-arm64 build:native
- run: pnpm --filter @scriptc/runtime-darwin-arm64 build:native
- run: pnpm build
- name: No-clang assembly/object contract
if: matrix.shard == 1
Expand All @@ -144,7 +146,9 @@ jobs:
run: >-
pnpm test
packages/compiler/test/native-codegen-integration.test.ts
packages/compiler/src/backend/runtime-pack.test.ts
packages/cli/test/native-link-info.test.ts
packages/cli/test/runtime-pack.test.ts
tests/harness/native-object-example.test.ts
- name: LLVM-tier helper object differential (${{ matrix.shard }}/3)
env:
Expand All @@ -160,11 +164,13 @@ jobs:
if: matrix.shard == 1
run: |
pnpm --dir packages/runtime pack --pack-destination "$RUNNER_TEMP" --silent
pnpm --dir packages/runtime-darwin-arm64 pack --pack-destination "$RUNNER_TEMP" --silent
pnpm --dir packages/compiler pack --pack-destination "$RUNNER_TEMP" --silent
pnpm --dir packages/cli pack --pack-destination "$RUNNER_TEMP" --silent
PREFIX="$RUNNER_TEMP/installed-scriptc"
npm install --prefix "$PREFIX" --ignore-scripts \
"$RUNNER_TEMP/scriptc-runtime-$(node -p "require('./packages/runtime/package.json').version").tgz" \
"$RUNNER_TEMP/scriptc-runtime-darwin-arm64-$(node -p "require('./packages/runtime-darwin-arm64/package.json').version").tgz" \
"$RUNNER_TEMP/scriptc-llvm-darwin-arm64-$(node -p "require('./packages/llvm-darwin-arm64/package.json').version").tgz" \
"$RUNNER_TEMP/scriptc-compiler-$(node -p "require('./packages/compiler/package.json').version").tgz" \
"$RUNNER_TEMP/scriptc-$(node -p "require('./packages/cli/package.json').version").tgz"
Expand Down
10 changes: 6 additions & 4 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -79,9 +79,9 @@ jobs:

# Publishing uses npm trusted publishing (OIDC): the job's id-token
# permission lets npm mint short-lived credentials, so no npm token
# secret exists anywhere in this repo. All four packages —
# @scriptc/runtime, @scriptc/llvm-darwin-arm64, @scriptc/compiler,
# and scriptc — must each be
# secret exists anywhere in this repo. All five packages —
# @scriptc/runtime, @scriptc/runtime-darwin-arm64,
# @scriptc/llvm-darwin-arm64, @scriptc/compiler, and scriptc — must each be
# configured on npmjs.com with a GitHub Actions trusted publisher
# pointing at repository vercel-labs/scriptc, workflow release.yml,
# environment Release. A package missing that configuration fails
Expand All @@ -92,12 +92,13 @@ jobs:
run: |
pnpm install --frozen-lockfile
pnpm --filter @scriptc/llvm-darwin-arm64 build:native
pnpm --filter @scriptc/runtime-darwin-arm64 build:native
pnpm -r build

- name: Check version sync
run: |
VERSION="${{ needs.check-release.outputs.version }}"
for pkg in packages/runtime packages/llvm-darwin-arm64 packages/compiler packages/cli; do
for pkg in packages/runtime packages/runtime-darwin-arm64 packages/llvm-darwin-arm64 packages/compiler packages/cli; do
V=$(node -p "require('./$pkg/package.json').version")
if [ "$V" != "$VERSION" ]; then
echo "Version mismatch: $pkg is $V, expected $VERSION"
Expand Down Expand Up @@ -149,6 +150,7 @@ jobs:
}

publish_dir packages/runtime
publish_dir packages/runtime-darwin-arm64
publish_dir packages/llvm-darwin-arm64 "$HELPER_TARBALL"
publish_dir packages/compiler
publish_dir packages/cli
Expand Down
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,9 @@ node_modules/
!tests/fixtures/node-types/node_modules/
dist/
/packages/llvm-darwin-arm64/bin/
/packages/runtime-darwin-arm64/artifacts/
/packages/runtime-darwin-arm64/runtime-pack.json
/packages/runtime-darwin-arm64/.runtime-pack-*
!tests/fixtures/fetch/node_modules/eventsource-parser/dist/
!tests/fixtures/npm/node_modules/*/dist/
!tests/fixtures/npm/workspace/*/dist/
Expand Down
11 changes: 6 additions & 5 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,12 @@ pnpm install && pnpm -r build # build the workspace
pnpm test:sandbox # full gate: ~4m custom image, ~9m cold managed fallback
```

The ordinary workspace build does not rebuild the packaged macOS LLVM helper.
When changing native assembly/object emission, install CMake, Ninja, and
Homebrew `llvm@22`, then run
`pnpm --filter @scriptc/llvm-darwin-arm64 build:native` explicitly. The macOS
full test suite also needs that generated helper.
The ordinary workspace build does not rebuild packaged macOS native artifacts.
When changing native assembly/object emission or runtime-pack selection,
install CMake, Ninja, and Homebrew `llvm@22`, then run
`pnpm --filter @scriptc/llvm-darwin-arm64 build:native` and
`pnpm --filter @scriptc/runtime-darwin-arm64 build:native` explicitly. The
macOS full test suite also needs those generated artifacts.

Use focused local tests while iterating, then use `pnpm test:sandbox` whenever a
full validation gate is required. It loads Sandbox configuration from the
Expand Down
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ All notable changes to scriptc will be documented in this file.

### Features

- **macOS arm64 executables use release-built runtime packs.** LLVM-tier builds now emit the program object through the bundled helper and link feature-selected, hashed runtime/vendor artifacts without compiling C on the user's machine. Explicit C, LLVM fallback, and sanitizer builds retain the external C-toolchain path.
- **Builds can stop at typed IR, readable C, or textual LLVM IR.** `scriptc build --emit=ir|c|llvm` writes one primary source artifact with stable default suffixes and requires only Node—no external compiler, archiver, linker, or executable cache. `--emit=exe` remains the default, and executable builds retain the former additive `--emit-ir` flag for one release with a deprecation warning; library mode keeps its additive `--emit-ir` option.

<!-- release:start -->
Expand Down
13 changes: 7 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
# scriptc

scriptc compiles TypeScript and JavaScript to typed IR, readable C, textual LLVM IR, native assembly and objects, native executables, and WebAssembly modules. It uses the TypeScript compiler for parsing and type checking. Source outputs require only Node; macOS 15+ arm64 assembly/object output uses scriptc's bundled LLVM helper; executable builds currently use clang to compile/link the runtime.
scriptc compiles TypeScript and JavaScript to typed IR, readable C, textual LLVM IR, native assembly and objects, native executables, and WebAssembly modules. It uses the TypeScript compiler for parsing and type checking. Source outputs require only Node. On macOS 15+ arm64, ordinary LLVM-tier executables use scriptc's bundled helper and precompiled runtime pack; clang is only the platform linker driver and does not compile program or runtime C.

Static builds include a small native runtime, but no Node or JavaScript engine. Code that cannot compile statically is reported as a diagnostic. For npm packages and `any`-typed code, `--dynamic` embeds [quickjs-ng](https://github.com/quickjs-ng/quickjs) explicitly.

scriptc is experimental and targets macOS, Linux, Windows, and WebAssembly via WASI Preview 1.

## Installation

The compiler requires Node.js 24 or newer. `--emit=ir|c|llvm` needs only Node. On macOS 15+ arm64, `--emit=asm|obj` additionally uses the optional platform helper installed with scriptc, but needs no compiler, archiver, linker, or SDK. Executable builds still require clang and the platform SDK. The executables it produces do not require Node.
The compiler requires Node.js 24 or newer. `--emit=ir|c|llvm` needs only Node. On macOS 15+ arm64, `--emit=asm|obj` additionally uses the optional platform helper installed with scriptc, but needs no compiler, archiver, linker, or SDK. Executable builds need a platform linker driver and SDK; explicit C builds, LLVM fallbacks, and `--sanitize` additionally need a C compiler. The executables it produces do not require Node.

```console
$ npm install -g scriptc
Expand Down Expand Up @@ -154,10 +154,11 @@ $ pnpm test:sandbox
```

The normal workspace build needs no local LLVM installation. To rebuild the
optional macOS arm64 assembly/object helper, install CMake, Ninja, and
Homebrew `llvm@22`, then run
`pnpm --filter @scriptc/llvm-darwin-arm64 build:native`. The macOS full test
suite also uses that generated helper.
optional macOS arm64 native artifacts, install CMake, Ninja, and Homebrew
`llvm@22`, then run
`pnpm --filter @scriptc/llvm-darwin-arm64 build:native` and
`pnpm --filter @scriptc/runtime-darwin-arm64 build:native`. The macOS full test
suite also uses those generated artifacts.

`pnpm test:sandbox` loads `.env.local`, preflights Vercel authentication and
project access, and uses the managed `vercel/sandbox/universal` image by
Expand Down
22 changes: 12 additions & 10 deletions RELEASING.md
Original file line number Diff line number Diff line change
@@ -1,29 +1,31 @@
# Releasing

Releases are manual, single-commit affairs. The maintainer controls the changelog voice and format. The four npm packages — `@scriptc/runtime`, `@scriptc/llvm-darwin-arm64`, `@scriptc/compiler`, and `scriptc` — always publish together at the same version.
Releases are manual, single-commit affairs. The maintainer controls the changelog voice and format. The five npm packages — `@scriptc/runtime`, `@scriptc/runtime-darwin-arm64`, `@scriptc/llvm-darwin-arm64`, `@scriptc/compiler`, and `scriptc` — always publish together at the same version.

To prepare a release:

1. Bump the version in `packages/cli/package.json`
2. Run `node scripts/sync-versions.mjs` to stamp the same version into `packages/runtime`, `packages/llvm-darwin-arm64`, and `packages/compiler`, then `pnpm manifest` to restamp `packages/compiler/surface-manifest.json` with the new version, and commit both (the test suite's staleness guard fails on a version drift)
2. Run `node scripts/sync-versions.mjs` to stamp the same version into `packages/runtime`, `packages/runtime-darwin-arm64`, `packages/llvm-darwin-arm64`, and `packages/compiler`, then `pnpm manifest` to restamp `packages/compiler/surface-manifest.json` with the new version, and commit both (the test suite's staleness guard fails on a version drift)
3. Fold the `## Unreleased` section of `CHANGELOG.md` into a new `## <version>` entry (newest first, below `## Unreleased`), and leave `## Unreleased` empty for the next cycle
4. Wrap the new entry in `<!-- release:start -->` and `<!-- release:end -->` markers; this marked block is also the GitHub release body
5. Remove the `<!-- release:start -->` and `<!-- release:end -->` markers from the previous release entry; only the latest release should have markers
6. With Zig on `PATH`, run `SCRIPTC_CROSS=1 pnpm exec vitest run tests/harness/library-cross.test.ts` and require the cross-target library conformance lane to pass
7. Commit to `main`

CI (`.github/workflows/release.yml`) compares the version in `packages/cli/package.json` to what `scriptc` has on npm. If it differs, it builds the workspace, verifies all four package versions match (a mismatch fails with a hint to run `scripts/sync-versions.mjs`), and publishes to npm in dependency order — `@scriptc/runtime`, `@scriptc/llvm-darwin-arm64`, `@scriptc/compiler`, then `scriptc` — so each package's dependencies are resolvable the moment it lands. After the publish succeeds, a separate job creates the git tag `v<version>` and the GitHub release with the marked changelog entry as its body, and attaches `surface-manifest.json` — the machine-readable listing of the surface the static tier compiles at that version (stable per-entry ids, so two releases diff mechanically; see `packages/compiler/src/coverage/surface-manifest.ts` for the schema). The job regenerates the manifest from the tree and fails on any byte difference from the committed file before attaching, so the asset is always the manifest of the code being released. The same file ships inside the `@scriptc/compiler` package as `@scriptc/compiler/surface-manifest.json`.
CI (`.github/workflows/release.yml`) compares the version in `packages/cli/package.json` to what `scriptc` has on npm. If it differs, it builds the workspace, verifies all five package versions match (a mismatch fails with a hint to run `scripts/sync-versions.mjs`), and publishes to npm in dependency order — `@scriptc/runtime`, `@scriptc/runtime-darwin-arm64`, `@scriptc/llvm-darwin-arm64`, `@scriptc/compiler`, then `scriptc` — so each package's dependencies are resolvable the moment it lands. After the publish succeeds, a separate job creates the git tag `v<version>` and the GitHub release with the marked changelog entry as its body, and attaches `surface-manifest.json` — the machine-readable listing of the surface the static tier compiles at that version (stable per-entry ids, so two releases diff mechanically; see `packages/compiler/src/coverage/surface-manifest.ts` for the schema). The job regenerates the manifest from the tree and fails on any byte difference from the committed file before attaching, so the asset is always the manifest of the code being released. The same file ships inside the `@scriptc/compiler` package as `@scriptc/compiler/surface-manifest.json`.

The release job runs on macOS arm64, builds and strips the pinned LLVM helper,
and publishes its constrained platform package before `@scriptc/compiler`.
Executable/runtime compilation still uses the user's local clang; the helper
owns only assembly/object code generation. The npm package's best-effort
postinstall warms runtime, TLS, and engine caches against that exact local
toolchain. The GitHub release remains a tag, release notes, and the manifest
asset; the npm publish never waits on the GitHub release.
then builds the matching precompiled runtime pack before publishing both
constrained platform packages ahead of `@scriptc/compiler`. Ordinary LLVM-tier
executables use the helper for the program object and the platform pack for
runtime objects; the user's toolchain performs only the final platform link.
Explicit C builds, LLVM refusals, and `--sanitize` retain the external C
toolchain path. npm postinstall skips local runtime-cache compilation when the
platform pack is available. The GitHub release remains a tag, release notes, and the
manifest asset; the npm publish never waits on the GitHub release.

Publishing uses npm trusted publishing (OIDC) — there is no npm token secret.
Each of the four packages must have a GitHub Actions trusted publisher for
Each of the five packages must have a GitHub Actions trusted publisher for
`release.yml` and the `Release` environment. A missing configuration fails
before upload. Re-runs skip package versions already present on npm, so a
partially published release can be resumed safely.
12 changes: 7 additions & 5 deletions docs/src/app/cli/page.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -59,8 +59,10 @@ object, performs no link, and prints a versioned JSON recipe with the target,
libraries. It never reports private scriptc cache paths. See
<a href="/native-objects">Native Program Objects</a> for complete C-driver and
direct-linker examples.
<code>--emit=exe</code> is the default and retains the existing executable
behavior.
<code>--emit=exe</code> is the default. On macOS 15+ arm64, LLVM-tier builds
emit the program object through the helper and link release-built runtime
objects; explicit C, LLVM fallback, and sanitizer builds retain runtime C
compilation.

## scriptc run

Expand All @@ -79,7 +81,7 @@ Analyzes the program without producing a binary and reports, statement by statem

## scriptc cache warm

Prebuilds the release runtime objects and native TLS/dynamic-engine archives against the currently selected compiler, SDK, and target. npm installations run this best-effort automatically; use the explicit command when preparing a container image, CI runner, or installation whose lifecycle scripts were disabled. Pass one or more of `runtime`, `tls`, and `dynamic` to seed only those families. Warming applies to persistently cached native executable targets; WASI and mobile library targets report a target-level error. It also reports an error when mutable toolchain inputs have disabled persistent caching, rather than doing disposable work. The entries use the ordinary strict cache identities, and later builds still revalidate their compiler and dependency inputs.
Prebuilds release runtime objects and native TLS/dynamic-engine archives for targets that still compile runtime C locally. macOS 15+ arm64 installations already carry the release-built runtime pack and skip automatic warming; older macOS hosts retain warming for the source-toolchain path. Use this command when preparing another supported target's container image or CI runner. Pass one or more of `runtime`, `tls`, and `dynamic` to seed only those families.

## Options

Expand All @@ -100,7 +102,7 @@ Prebuilds the release runtime objects and native TLS/dynamic-engine archives aga
<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="/ffi">Native FFI</a>.</dd>

<dt><code>--backend &lt;c|llvm&gt;</code></dt>
<dd>Code generator: <code>llvm</code> (default — emits LLVM IR text, compiled by the same clang) or <code>c</code> (the readable debugging backend). Unset, a native build can fall back to C when the program is outside the LLVM tier. The production <code>wasm32-wasi</code> target never falls back: a missing LLVM lowering is <code>SC3001</code>. Use <code>--backend c</code> explicitly only when inspecting generated C; on WASI that inspection lane accepts async-free programs only and reports <code>SC3001</code> for coroutine-dependent surfaces.</dd>
<dd>Code generator: <code>llvm</code> (default) or <code>c</code> (the readable debugging backend). On macOS arm64, LLVM-tier executable code generation uses the bundled helper and precompiled runtime pack before the platform link. Unset, a native build can fall back to C when the program is outside the LLVM tier. The production <code>wasm32-wasi</code> target never falls back: a missing LLVM lowering is <code>SC3001</code>.</dd>

<dt><code>--npm-static &lt;pkg[,pkg…]|auto&gt;</code></dt>
<dd>EXPERIMENTAL. Compile the named npm packages' shipped JS statically as program modules instead of embedding them for the engine (repeatable; <code>auto</code> opts in every eligible direct import). A package the preflight refuses falls back to the island with a coverage-report note. See <a href="/dependencies">npm Dependencies</a> for maturity notes.</dd>
Expand Down Expand Up @@ -154,7 +156,7 @@ fib-linux: ELF 64-bit LSB executable, ARM aarch64, version 1 (SYSV), dynamically

## Backends

The default backend emits textual LLVM IR, compiled by the same clang that links the runtime. On native targets, a program outside that tier is never miscompiled—the build falls back to the C backend transparently and says so in one stderr line. The production <code>wasm32-wasi</code> target uses LLVM's 32-bit ABI path and never falls back; an LLVM coverage gap is a build diagnostic. Dynamic npm embedding is LLVM surface on every target.
The default backend emits textual LLVM IR. On macOS arm64 it is lowered to an object by the bundled helper and linked with the precompiled runtime pack; other executable targets retain their existing toolchain path. A program outside the LLVM tier is never miscompiled—the native build falls back to the C backend transparently and says so in one stderr line. The production <code>wasm32-wasi</code> target uses LLVM's 32-bit ABI path and never falls back; an LLVM coverage gap is a build diagnostic. Dynamic npm embedding is LLVM surface on every target.

The C backend is a debugging aid: deliberately readable, source-line-annotated output with differential tests against LLVM wherever the two overlap. Pin it when you want to inspect what your program became:

Expand Down
Loading
Loading