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
21 changes: 21 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -201,6 +201,27 @@ jobs:
helper: "false"
- run: pnpm build
- run: pnpm test tests/harness/self-hosting-native-driver.test.ts
env:
SCRIPTC_BOOTSTRAP_PACKAGE_DIR: ${{ runner.temp }}/native-cli-package
- name: Verify packed compiler and Linux ABI
if: matrix.flavor == 'plain'
run: |
pnpm test packages/runtime/test/glibc-random.test.ts
node scripts/verify-native-cli.mjs "$RUNNER_TEMP/native-cli-package" --run
node scripts/smoke-native-install.mjs "$RUNNER_TEMP/native-cli-package"
- name: Install and compile on Amazon Linux 2023 (glibc 2.34)
if: matrix.flavor == 'plain'
run: |
NODE_ROOT=$(dirname "$(dirname "$(command -v node)")")
docker run --rm --volume "$GITHUB_WORKSPACE:/work:ro" \
--volume "$RUNNER_TEMP/native-cli-package:/native:ro" --volume "$NODE_ROOT:/node:ro" \
--env PATH=/node/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin \
--workdir /work amazonlinux:2023 sh -c '
set -eu
dnf install --assumeyes clang tar gzip
test "$(getconf GNU_LIBC_VERSION)" = "glibc 2.34"
node scripts/smoke-native-install.mjs /native
'

# A real host-clang/glibc build, not the zig cross-target lane: catches
# Linux feature-test macro omissions at compile time and separate-libm
Expand Down
12 changes: 12 additions & 0 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -244,6 +244,18 @@ jobs:
pnpm --filter @scriptc/compiler --filter scriptc build
pnpm --filter @scriptc/${{ matrix.cli }} build:native
node scripts/verify-native-cli.mjs packages/${{ matrix.cli }} --run
- name: Verify GNU npm distribution on glibc 2.34
if: matrix.platform == 'linux-x64' || matrix.platform == 'linux-arm64'
run: |
NODE_ROOT=$(dirname "$(dirname "$(command -v node)")")
docker run --rm --volume "$GITHUB_WORKSPACE:/work:ro" --volume "$NODE_ROOT:/node:ro" \
--env PATH=/node/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin \
--workdir /work amazonlinux:2023 sh -c '
set -eu
dnf install --assumeyes clang tar gzip
test "$(getconf GNU_LIBC_VERSION)" = "glibc 2.34"
node scripts/smoke-native-install.mjs packages/${{ matrix.cli }}
'
- name: Verify packed Darwin helper on its build host
if: matrix.platform == 'darwin-arm64'
run: |
Expand Down
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@ scriptc is experimental and targets macOS, Linux, Windows, and WebAssembly via W

The installed compiler runs natively on supported macOS, Linux, and Windows hosts. Compilation, compile-time evaluation, and native execution do not require Node. `--emit=ir|llvm|asm|obj` uses the bundled TypeScript checker and LLVM helper without an external compiler, archiver, linker, or SDK. Executable builds additionally need a platform linker driver and SDK/sysroot; precompiled runtime packs supply the C runtime. Set `SCRIPTC_LINKER` to choose that driver. Runtime development with `--sanitize` additionally needs a C compiler. Node.js 24 or newer is needed for npm installation, development from a source checkout, the JavaScript compiler API, and `scriptc run` of WASI modules.

The native compiler includes its host runtime pack. Cross-compilation uses additional `@scriptc/runtime-<target>` packages installed in your project at the same version as `scriptc --version`. For example, WASI builds use `@scriptc/runtime-wasm32-wasi`. Run the compiler from that project or set `SCRIPTC_RUNTIME_PACK` to the pack directory. Linux GNU distributions require glibc 2.34 or newer; the bundled LLVM helper is statically linked.

```console
$ npm install -g scriptc
```
Expand Down
17 changes: 17 additions & 0 deletions benchmarks/builds/inventory-report/categories.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import type { Category, Product } from "./types.ts";
import { lowStock } from "./stock.ts";
export function categories(products: Product[]): Category[] {
const groups = new Map<string, Category>();
for (const product of products) {
let group = groups.get(product.category);
if (group === undefined) {
group = { name: product.category, products: 0, units: 0, value: 0, lowStock: 0 };
groups.set(product.category, group);
}
group.products++;
group.units += product.quantity;
group.value += product.quantity * product.price;
group.lowStock += lowStock(product) ? 1 : 0;
}
return [...groups.values()];
}
3 changes: 3 additions & 0 deletions benchmarks/builds/inventory-report/config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export const revision = 0;
export const lowStockThreshold = 8;
export const reorderTarget = 30;
3 changes: 3 additions & 0 deletions benchmarks/builds/inventory-report/load.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import { readFileSync } from "node:fs";
import { parse } from "./parse.ts";
export function load(file: string) { return parse(readFileSync(file, "utf8")); }
6 changes: 6 additions & 0 deletions benchmarks/builds/inventory-report/main.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import { options } from "./options.ts";
import { load } from "./load.ts";
import { select } from "./select.ts";
import { report } from "./report.ts";
const args = options(process.argv.slice(2));
console.log(report(select(load(args.file), args.category), args.limit));
3 changes: 3 additions & 0 deletions benchmarks/builds/inventory-report/money.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export function money(cents: number): string {
return (cents / 100).toFixed(2);
}
3 changes: 3 additions & 0 deletions benchmarks/builds/inventory-report/normalize.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export function normalize(value: string): string {
return value.trim().toLowerCase();
}
15 changes: 15 additions & 0 deletions benchmarks/builds/inventory-report/options.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
export interface Options { file: string; category: string; limit: number }
export function options(args: string[]): Options {
let file = "";
let category = "";
let limit = 12;
for (let i = 0; i < args.length; i++) {
const argument = args[i]!;
if (argument === "--category") category = args[++i]!;
else if (argument === "--limit") limit = Number(args[++i]);
else if (file === "") file = argument;
else throw new Error("unexpected argument: " + argument);
}
if (file === "" || !Number.isInteger(limit) || limit < 1) throw new Error("expected an inventory file and positive limit");
return { file, category, limit };
}
17 changes: 17 additions & 0 deletions benchmarks/builds/inventory-report/parse.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import type { Product } from "./types.ts";
import { normalize } from "./normalize.ts";
import { valid } from "./validate.ts";
export function parse(source: string): Product[] {
const products: Product[] = [];
let lineNumber = 0;
for (const line of source.split("\n")) {
lineNumber++;
if (line.trim() === "") continue;
const fields = line.split("\t");
if (fields.length !== 5) throw new Error("invalid inventory line " + lineNumber);
const product = { sku: fields[0]!, category: normalize(fields[1]!), name: fields[2]!, quantity: Number(fields[3]), price: Number(fields[4]) };
if (!valid(product)) throw new Error("invalid product on line " + lineNumber);
products.push(product);
}
return products;
}
9 changes: 9 additions & 0 deletions benchmarks/builds/inventory-report/rank.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import type { Category, Product } from "./types.ts";
import { reorderUnits } from "./stock.ts";
export function rankCategories(rows: Category[]): Category[] {
return rows.sort((a, b) => b.value - a.value || (a.name < b.name ? -1 : a.name > b.name ? 1 : 0));
}
export function rankReorders(products: Product[]): Product[] {
return products.filter(product => reorderUnits(product) > 0).sort((a, b) =>
reorderUnits(b) * b.price - reorderUnits(a) * a.price || (a.sku < b.sku ? -1 : a.sku > b.sku ? 1 : 0));
}
11 changes: 11 additions & 0 deletions benchmarks/builds/inventory-report/render.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import type { Category, Product } from "./types.ts";
import { money } from "./money.ts";
import { reorderUnits } from "./stock.ts";
import { revision } from "./config.ts";
export function render(groups: Category[], reorders: Product[], limit: number): string {
const lines = ["inventory report " + revision, "category,products,units,value,low_stock"];
for (const group of groups) lines.push([group.name, group.products, group.units, money(group.value), group.lowStock].join(","));
lines.push("reorder_sku,units,cost");
for (const product of reorders.slice(0, limit)) lines.push([product.sku, reorderUnits(product), money(reorderUnits(product) * product.price)].join(","));
return lines.join("\n");
}
7 changes: 7 additions & 0 deletions benchmarks/builds/inventory-report/report.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import type { Product } from "./types.ts";
import { categories } from "./categories.ts";
import { rankCategories, rankReorders } from "./rank.ts";
import { render } from "./render.ts";
export function report(products: Product[], limit: number): string {
return render(rankCategories(categories(products)), rankReorders(products), limit);
}
6 changes: 6 additions & 0 deletions benchmarks/builds/inventory-report/select.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import type { Product } from "./types.ts";
import { normalize } from "./normalize.ts";
export function select(products: Product[], category: string): Product[] {
const wanted = normalize(category);
return wanted === "" ? products : products.filter(product => product.category === wanted);
}
4 changes: 4 additions & 0 deletions benchmarks/builds/inventory-report/stock.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
import type { Product } from "./types.ts";
import { lowStockThreshold, reorderTarget } from "./config.ts";
export function lowStock(product: Product): boolean { return product.quantity < lowStockThreshold; }
export function reorderUnits(product: Product): number { return lowStock(product) ? reorderTarget - product.quantity : 0; }
2 changes: 2 additions & 0 deletions benchmarks/builds/inventory-report/types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export interface Product { sku: string; category: string; name: string; quantity: number; price: number }
export interface Category { name: string; products: number; units: number; value: number; lowStock: number }
6 changes: 6 additions & 0 deletions benchmarks/builds/inventory-report/validate.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import type { Product } from "./types.ts";
export function valid(product: Product): boolean {
return product.sku.length > 0 && product.category.length > 0 && product.name.length > 0 &&
Number.isInteger(product.quantity) && product.quantity >= 0 &&
Number.isInteger(product.price) && product.price >= 0;
}
19 changes: 19 additions & 0 deletions benchmarks/builds/log-summary/aggregate.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import { parse } from "./parse.ts";
export interface Route { path: string; count: number; errors: number; milliseconds: number; bytes: number }
export function summarize(source: string, minimum: number): Route[] {
const routes = new Map<string, Route>();
for (const line of source.split("\n")) {
const request = parse(line);
if (request === null || request.status < minimum) continue;
let route = routes.get(request.route);
if (route === undefined) {
route = { path: request.route, count: 0, errors: 0, milliseconds: 0, bytes: 0 };
routes.set(request.route, route);
}
route.count++;
route.errors += request.status >= 400 ? 1 : 0;
route.milliseconds += request.milliseconds;
route.bytes += request.bytes;
}
return [...routes.values()].sort((a, b) => b.count - a.count || (a.path < b.path ? -1 : a.path > b.path ? 1 : 0));
}
9 changes: 9 additions & 0 deletions benchmarks/builds/log-summary/format.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import type { Route } from "./aggregate.ts";
export const revision = 0;
export function format(routes: Route[], limit: number): string {
const lines = ["request report " + revision, "route,count,errors,mean_ms,bytes"];
for (const row of routes.slice(0, limit)) {
lines.push([row.path, row.count, row.errors, Math.round(row.milliseconds / row.count), row.bytes].join(","));
}
return lines.join("\n");
}
6 changes: 6 additions & 0 deletions benchmarks/builds/log-summary/main.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import { readFileSync } from "node:fs";
import { options } from "./options.ts";
import { summarize } from "./aggregate.ts";
import { format } from "./format.ts";
const args = options(process.argv.slice(2));
console.log(format(summarize(readFileSync(args.file, "utf8"), args.minimum), args.limit));
17 changes: 17 additions & 0 deletions benchmarks/builds/log-summary/options.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
export interface Options { file: string; minimum: number; limit: number }
export function options(args: string[]): Options {
let file = "";
let minimum = 200;
let limit = 10;
for (let i = 0; i < args.length; i++) {
const arg = args[i]!;
if (arg === "--min-status") minimum = Number(args[++i]);
else if (arg === "--limit") limit = Number(args[++i]);
else if (file === "") file = arg;
else throw new Error("unexpected argument: " + arg);
}
if (file === "" || !Number.isInteger(minimum) || !Number.isInteger(limit) || limit < 1) {
throw new Error("usage: log-summary <file> [--min-status <number>] [--limit <number>]");
}
return { file, minimum, limit };
}
12 changes: 12 additions & 0 deletions benchmarks/builds/log-summary/parse.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
export interface Request { route: string; status: number; milliseconds: number; bytes: number }
export function parse(line: string): Request | null {
const fields = line.split(" ");
if (fields.length !== 4) return null;
const status = Number(fields[1]);
const milliseconds = Number(fields[2]);
const bytes = Number(fields[3]);
if (!Number.isFinite(status) || !Number.isFinite(milliseconds) || !Number.isFinite(bytes)) return null;
const raw = fields[0]!;
const query = raw.indexOf("?");
return { route: query < 0 ? raw : raw.slice(0, query), status, milliseconds, bytes };
}
4 changes: 4 additions & 0 deletions docs/src/app/platforms/page.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,10 @@ The compiler runs natively and bundles its TypeScript checker and LLVM helper. O

## Cross-compilation via zig

The installed compiler includes its host runtime pack. Other runtime packs are optional project dependencies, pinned to the same version as <code>scriptc --version</code>. Install the appropriate <code>@scriptc/runtime-&lt;target&gt;</code> package, such as <code>@scriptc/runtime-linux-arm64-gnu</code> or <code>@scriptc/runtime-wasm32-wasi</code>, and run the compiler from your project. You can also set <code>SCRIPTC_RUNTIME_PACK</code> to a pack directory, including with standalone compiler archives. Adding a pack does not require reinstalling the compiler. Assembly and object output do not require a runtime pack.

GNU/Linux compiler distributions and runtime packs support glibc 2.34 or newer. The LLVM helper is statically linked and does not require the build machine's libc or C++ runtime to be installed.

The installed host LLVM helper emits cross-target program objects and the matching runtime pack supplies precompiled support objects. Install [zig](https://ziglang.org) for Linux, Windows, and WASI cross-linking, then select the output target:

<dl>
Expand Down
37 changes: 27 additions & 10 deletions native/llvm-codegen/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,9 @@ set(SCRIPTC_ALLOWED_TARGETS "arm64-apple-macosx14.0.0,x86_64-apple-macosx14.0.0,
set(SCRIPTC_HELPER_ARCH "arm64" CACHE STRING
"macOS architecture of the helper executable")
# LLVM's official Linux archives use the host libstdc++ and glibc ABIs. The
# musl package therefore ships this process-isolated helper as a fully static
# host executable; its configured output targets and runtime pack remain musl.
option(SCRIPTC_MUSL_BUILD "Configure the Linux musl helper package" OFF)
# Linux packages ship this process-isolated helper as a fully static host
# executable so it runs independently of the destination libc and libstdc++.
option(SCRIPTC_STATIC_LINUX_BUILD "Build a static Linux helper executable" OFF)

# These must be cache variables before project() enables the Apple toolchain.
# A target property named OSX_DEPLOYMENT_TARGET is not recognized by CMake and
Expand All @@ -34,7 +34,7 @@ endif()
project(scriptc_llvm_codegen LANGUAGES C CXX)

if(CMAKE_C_COMPILER_TARGET MATCHES "linux-musl")
set(SCRIPTC_MUSL_BUILD ON)
set(SCRIPTC_STATIC_LINUX_BUILD ON)
endif()

# Official LLVM binary distributions retain zstd as an imported dependency in
Expand All @@ -43,16 +43,16 @@ endif()
find_package(zstd CONFIG QUIET)

# LLVM's Linux archives retain ZLIB and zstd target edges in LLVMSupport. The
# musl package flattens that imported graph below and appends portable library
# static Linux helper flattens that imported graph below and appends portable library
# names explicitly, so use interface placeholders instead of embedding host
# library paths in the flattened graph.
if(SCRIPTC_MUSL_BUILD AND NOT TARGET ZLIB::ZLIB)
if(SCRIPTC_STATIC_LINUX_BUILD AND NOT TARGET ZLIB::ZLIB)
add_library(ZLIB::ZLIB INTERFACE IMPORTED)
endif()
if(SCRIPTC_MUSL_BUILD AND NOT TARGET zstd::libzstd_static)
if(SCRIPTC_STATIC_LINUX_BUILD AND NOT TARGET zstd::libzstd_static)
add_library(zstd::libzstd_static INTERFACE IMPORTED)
endif()
if(SCRIPTC_MUSL_BUILD AND NOT TARGET zstd::libzstd_shared)
if(SCRIPTC_STATIC_LINUX_BUILD AND NOT TARGET zstd::libzstd_shared)
add_library(zstd::libzstd_shared INTERFACE IMPORTED)
endif()

Expand Down Expand Up @@ -126,11 +126,14 @@ target_compile_definitions(scriptc-llvm-codegen PRIVATE
SCRIPTC_ALLOWED_TARGETS="${SCRIPTC_ALLOWED_TARGETS}"
)

if(SCRIPTC_MUSL_BUILD)
if(SCRIPTC_STATIC_LINUX_BUILD)
# CMake's imported LLVM targets carry transitive archive interfaces that
# duplicate most of this large static graph. Flatten it to one absolute
# archive path per target for the resource-constrained musl release link.
# archive path per target for the Linux release link.
function(scriptc_collect_llvm_target target)
if(target STREQUAL "ZLIB::ZLIB" OR target MATCHES "^zstd::")
return()
endif()
get_property(seen GLOBAL PROPERTY SCRIPTC_LLVM_LINK_SEEN)
if(target IN_LIST seen)
return()
Expand All @@ -157,6 +160,17 @@ if(SCRIPTC_MUSL_BUILD)
if(NOT location OR location MATCHES "-NOTFOUND$")
get_target_property(location "${target}" IMPORTED_LOCATION)
endif()
if(NOT location OR location MATCHES "-NOTFOUND$")
# Distribution packages can export RELWITHDEBINFO rather than RELEASE.
get_target_property(configurations "${target}" IMPORTED_CONFIGURATIONS)
foreach(configuration IN LISTS configurations)
string(TOUPPER "${configuration}" configuration)
get_target_property(location "${target}" "IMPORTED_LOCATION_${configuration}")
if(location AND NOT location MATCHES "-NOTFOUND$")
break()
endif()
endforeach()
endif()
if(location AND NOT location MATCHES "-NOTFOUND$")
set_property(GLOBAL APPEND PROPERTY SCRIPTC_LLVM_LINK_LIBS "${location}")
endif()
Expand All @@ -175,6 +189,9 @@ if(SCRIPTC_MUSL_BUILD)
list(REMOVE_DUPLICATES SCRIPTC_LLVM_LIBS)
list(REVERSE SCRIPTC_LLVM_LIBS)
list(REMOVE_DUPLICATES SCRIPTC_LLVM_SYSTEM_LIBS)
# Some distribution builds attach Z3 to LLVMSupport. The helper never uses
# the solver, and its shared library cannot be part of a static executable.
list(FILTER SCRIPTC_LLVM_SYSTEM_LIBS EXCLUDE REGEX "(^|/)(lib)?([zZ]3|z|zstd)([.].*)?$")
list(APPEND SCRIPTC_LLVM_SYSTEM_LIBS z zstd)
else()
llvm_map_components_to_libnames(SCRIPTC_LLVM_LIBS
Expand Down
2 changes: 2 additions & 0 deletions packages/cli/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,8 @@ $ npm install -g scriptc

The installed compiler runs natively on supported macOS, Linux, and Windows hosts. Compilation, compile-time evaluation, and native execution do not require Node. `--emit=ir|llvm|asm|obj` uses the bundled TypeScript checker and LLVM helper without an external compiler, archiver, linker, or SDK. Executable builds additionally need a platform linker driver and SDK/sysroot; precompiled runtime packs supply the C runtime. Set `SCRIPTC_LINKER` to choose that driver. Runtime development with `--sanitize` additionally needs a C compiler. Node.js 24 or newer is needed for npm installation and `scriptc run` of WASI modules.

The native compiler includes its host runtime pack. Cross-compilation uses additional `@scriptc/runtime-<target>` packages installed in your project at the same version as `scriptc --version`. WASI builds use `@scriptc/runtime-wasm32-wasi`, for example. Run the compiler from that project or set `SCRIPTC_RUNTIME_PACK` to the pack directory. Linux GNU distributions require glibc 2.34 or newer.

Builds use a bounded persistent cache. Unchanged source can reuse the validated frontend result and LLVM program objects. Library identity getters occupy a separate LLVM module, so an identity change can reuse the large program object. Runtime objects come from the installed pack. Executable cache entries verify their native dependencies; FFI builds relink against current external inputs. Set `SCRIPTC_NO_CACHE=1` to bypass the cache or `SCRIPTC_CACHE_DIR` to select its location. An existing POSIX override must already be private.

## Commands
Expand Down
Loading
Loading