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
63 changes: 62 additions & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,9 @@ jobs:
with:
node-version-file: .node-version
cache: pnpm
- run: brew install llvm@22
- run: pnpm install --frozen-lockfile
- run: pnpm --filter @scriptc/llvm-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 @@ -108,6 +110,64 @@ jobs:
pnpm test packages/compiler/test/cc-driver.test.ts
--testNamePattern "host-native clang static build"

llvm_artifacts_macos_arm64:
name: test (macOS arm64 LLVM artifacts, no clang, ${{ matrix.shard }}/3)
runs-on: macos-15
timeout-minutes: 20
strategy:
fail-fast: false
matrix:
shard: [1, 2, 3]
env:
# The helper differential uses the harness's stable per-case sharding,
# keeping each macOS job below its timeout while the matrix union still
# exercises every LLVM-tier program.
SCRIPTC_TEST_SHARD: ${{ matrix.shard }}/3
steps:
- uses: actions/checkout@v4
- uses: pnpm/action-setup@v4
with:
version: 11.1.3
- uses: actions/setup-node@v4
with:
node-version-file: .node-version
cache: pnpm
- run: brew install llvm@22
- run: pnpm install --frozen-lockfile
- run: pnpm --filter @scriptc/llvm-darwin-arm64 build:native
- run: pnpm build
- name: No-clang assembly/object contract
if: matrix.shard == 1
run: pnpm test packages/cli/test/native-output.test.ts
- name: Helper diagnostics and object/link parity
if: matrix.shard == 1
run: pnpm test packages/compiler/test/native-codegen-integration.test.ts
- name: LLVM-tier helper object differential (${{ matrix.shard }}/3)
env:
SCRIPTC_LLVM_HELPER_ONLY: "1"
SCRIPTC_TEST_WORKERS: "4"
run: pnpm test tests/harness/llvm-differential.test.ts
- name: Packaged helper contract
if: matrix.shard == 1
run: |
TARBALL=$(pnpm --dir packages/llvm-darwin-arm64 pack --pack-destination "$RUNNER_TEMP" --silent)
node scripts/verify-llvm-package.mjs "$RUNNER_TEMP/$(basename "$TARBALL")"
- name: Packed npm installation smoke
if: matrix.shard == 1
run: |
pnpm --dir packages/runtime 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-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"
"$PREFIX/node_modules/.bin/scriptc" build tests/corpus/001-hello.ts \
--emit=obj -o "$RUNNER_TEMP/installed.o"
file "$RUNNER_TEMP/installed.o" | grep 'Mach-O 64-bit object arm64'

# Exercises the supported Windows GNU target and the built CLI end to end:
# TS7 must open its synthetic project, ambient files must resolve across
# slash styles, and the default executable must use the .exe suffix.
Expand Down Expand Up @@ -183,12 +243,13 @@ jobs:
# single "test" check (branch protection, badges) keeps resolving. Fails
# unless every matrix shard and both platform integration jobs succeeded.
test:
needs: [tests, linux_host_clang, windows_cli]
needs: [tests, linux_host_clang, llvm_artifacts_macos_arm64, windows_cli]
if: always()
runs-on: ubuntu-latest
steps:
- name: All lanes green
run: |
test "${{ needs.tests.result }}" = success
test "${{ needs.linux_host_clang.result }}" = success
test "${{ needs.llvm_artifacts_macos_arm64.result }}" = success
test "${{ needs.windows_cli.result }}" = success
35 changes: 26 additions & 9 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ jobs:
name: Publish to npm
needs: check-release
if: needs.check-release.outputs.should_release == 'true'
runs-on: ubuntu-latest
runs-on: macos-15
timeout-minutes: 15
environment: Release
permissions:
Expand All @@ -74,10 +74,14 @@ jobs:
node-version: "24"
registry-url: "https://registry.npmjs.org"

- name: Install pinned LLVM
run: brew install llvm@22

# 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 three packages —
# @scriptc/runtime, @scriptc/compiler, scriptc — must each be
# secret exists anywhere in this repo. All four packages —
# @scriptc/runtime, @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 @@ -87,12 +91,13 @@ jobs:
- name: Install and build
run: |
pnpm install --frozen-lockfile
pnpm --filter @scriptc/llvm-darwin-arm64 build:native
pnpm -r build

- name: Check version sync
run: |
VERSION="${{ needs.check-release.outputs.version }}"
for pkg in packages/runtime packages/compiler packages/cli; do
for pkg in packages/runtime 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 All @@ -101,6 +106,13 @@ jobs:
fi
done

- name: Package and verify LLVM helper
run: |
TARBALL=$(pnpm --dir packages/llvm-darwin-arm64 pack --pack-destination "$RUNNER_TEMP" --silent)
HELPER_TARBALL="$RUNNER_TEMP/$(basename "$TARBALL")"
node scripts/verify-llvm-package.mjs "$HELPER_TARBALL"
echo "HELPER_TARBALL=$HELPER_TARBALL" >> "$GITHUB_ENV"

- name: Publish to npm
run: |
VERSION="${{ needs.check-release.outputs.version }}"
Expand All @@ -123,16 +135,21 @@ jobs:
# Re-runs skip anything already on the registry at this version.
publish_dir() {
dir="$1"
packed="${2:-}"
name=$(node -p "require('./$dir/package.json').name")
if npm view "$name@$VERSION" version >/dev/null 2>&1; then
echo "$name@$VERSION already published, skipping"
return 0
fi
tarball=$(cd "$dir" && pnpm pack --silent | tail -1)
npm publish "$dir/$tarball" $PROVENANCE --access public
if [ -z "$packed" ]; then
tarball=$(cd "$dir" && pnpm pack --silent | tail -1)
packed="$dir/$tarball"
fi
npm publish "$packed" $PROVENANCE --access public
}

publish_dir packages/runtime
publish_dir packages/llvm-darwin-arm64 "$HELPER_TARBALL"
publish_dir packages/compiler
publish_dir packages/cli
env:
Expand All @@ -142,9 +159,9 @@ jobs:
# manifest (packages/compiler/surface-manifest.json — the machine-
# readable listing of the surface the static tier compiles at this
# version, regenerated here and verified against the committed file).
# scriptc has no platform binary assets to stage (programs compile on
# the user's machine), so the job runs AFTER a successful npm publish
# and never gates it. The body is the CHANGELOG.md block between the
# The platform helper ships through its npm package rather than as a GitHub
# release asset, so this job runs AFTER a successful npm publish and never
# gates it. The body is the CHANGELOG.md block between the
# release:start/release:end markers, which RELEASING.md keeps on the
# latest entry only.
github-release:
Expand Down
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ node_modules/
!tests/fixtures/gateway-e2e/node_modules/
!tests/fixtures/node-types/node_modules/
dist/
/packages/llvm-darwin-arm64/bin/
!tests/fixtures/fetch/node_modules/eventsource-parser/dist/
!tests/fixtures/npm/node_modules/*/dist/
!tests/fixtures/npm/workspace/*/dist/
Expand Down
6 changes: 6 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +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.

Use focused local tests while iterating, then use `pnpm test:sandbox` whenever a
full validation gate is required. It loads Sandbox configuration from the
shell and `.env.local`, runs portable coverage across disposable Linux
Expand Down
25 changes: 23 additions & 2 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 executables, and WebAssembly modules. It uses the TypeScript compiler for parsing and type checking. Source outputs require only Node; executable builds currently use clang to compile and link the emitted program and 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; macOS 15+ arm64 assembly/object output uses scriptc's bundled LLVM helper; executable builds currently use clang to compile/link the runtime.

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. Executable builds also require clang; `--emit=ir|c|llvm` does not. 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 still require clang and the platform SDK. The executables it produces do not require Node.

```console
$ npm install -g scriptc
Expand Down Expand Up @@ -50,8 +50,23 @@ hello.c
$ scriptc build hello.ts --emit=llvm >/dev/null
$ ls .scriptc/
hello.ll
$ scriptc build hello.ts --emit=asm >/dev/null
$ ls .scriptc/
hello.s
$ scriptc build hello.ts --emit=obj >/dev/null
$ ls .scriptc/
hello.o
```

`--emit=obj` writes a relocatable program object, not a standalone library. It
has undefined `scr_*` runtime references and a required
`scr_runtime_abi_v1` marker; `scriptc build --lib --profile ...` remains the
self-contained archive interface. The helper runs on macOS 15+ arm64 and emits
artifacts with an `arm64-apple-macosx14.0.0` deployment target. Sanitized
assembly/object
emission is rejected until the helper's AddressSanitizer pipeline matches the
executable path.

## Use Node APIs

Supported Node APIs compile to the native runtime. For example, `server.ts`:
Expand Down Expand Up @@ -131,6 +146,12 @@ $ vercel link && vercel env pull # writes a project-scoped VERCEL_OIDC_TOKEN
$ 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.

`pnpm test:sandbox` loads `.env.local`, preflights Vercel authentication and
project access, and uses the managed `vercel/sandbox/universal` image by
default. It installs the repository-pinned Node, pnpm, and LLVM toolchain plus
Expand Down
20 changes: 15 additions & 5 deletions RELEASING.md
Original file line number Diff line number Diff line change
@@ -1,19 +1,29 @@
# Releasing

Releases are manual, single-commit affairs. The maintainer controls the changelog voice and format. The three npm packages — `@scriptc/runtime`, `@scriptc/compiler`, `scriptc` — always publish together at the same version.
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.

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` 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/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 three package versions match (a mismatch fails with a hint to run `scripts/sync-versions.mjs`), and publishes to npm in dependency order — `@scriptc/runtime`, then `@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 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`.

Two deliberate differences from repositories that ship prebuilt binaries: there are no platform binary assets to build or stage — scriptc compiles programs on the user's machine with the local clang. The npm package's best-effort postinstall warms runtime, TLS, and engine caches against that exact local toolchain; it does not ship foreign objects. The GitHub release is therefore a tag, release notes, and the manifest asset only, and the npm publish never waits on the GitHub release (the release job runs after the publish, not before it).
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.

Publishing uses npm trusted publishing (OIDC) — there is no npm token secret. The one-time setup is already done: each of the three packages is 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 with an OIDC authentication error before anything is uploaded. Re-runs are safe: any package already on the registry at the target version is skipped, so a partially published release can be resumed by re-running the workflow.
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
`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.
Loading
Loading