A lean ahead-of-time JavaScript-to-C compiler and runtime. Maligator produces a standalone native executable and keeps optional engine and host surfaces out of builds that do not request them.
Run commands from the project root:
maligator init
maligator doctor
maligator build
maligator run -- first-argument "two words"init creates maligator.build.ts without overwriting an existing file. It selects
the first entry that exists from src/index.ts, src/main.ts, index.ts, and
main.ts; if none exists, it writes src/index.ts as the starting point.
The source-built CLI resolves its runtime tree and eval compiler from its own module installation, not from the working directory. The distributed product CLI instead embeds and materializes those resources, so it is self-contained. In both cases the working directory remains the application project root for config, entries, assets, and output caches. Compiler installation roots are explicit absolute paths passed into the command layer; native stages do not infer runtime ownership from the application.
maligator init
maligator doctor [--verbose] [--target rust-triple]
maligator build [entry] [--production] [--profile[=compiler]] [--artifact directory] [--target rust-triple] [--config path]
maligator run [entry] [--profile[=compiler]] [--config path] [-- args...]
maligator dev [entry] [--profile[=compiler]] [--config path] [-- args...]
maligator test [path ...] [--profile[=compiler]] [--run name] [--shuffle [seed]] [--repeat count] [--bail]
The working directory is always the project root. Relative entries and --config
paths are resolved from it; Maligator does not search ancestor directories. An
explicit entry overrides config.entry. Without a config, an explicit entry uses
the conservative product defaults. build and run fail with an init suggestion
when neither source supplies an entry.
run compiles to a portable development image and executes it in a fresh VM using a
matching compile-time-specialized runtime embedded in the distributed platform CLI.
Locked and mutable primordial profiles with configured assets, the Web and Node
surfaces, and Realm support do not require a C or Rust toolchain when Intl is
disabled. It forwards every
argument after -- without re-parsing it and propagates the application's exit
status or terminating signal.
dev keeps the compiler session alive, watches the application dependency graph,
and restarts a fresh VM after each successful rebuild. Project files are checked
at interactive cadence while dependencies under node_modules are checked less
frequently. A compilation error leaves the watcher running so the next edit can
recover. This is process restart, not in-process hot-module replacement.
Add --profile to any of these four commands for a separately compiled,
production-optimized image with bounded CPU, Poisson-sampled charged allocation,
and GC-pause evidence. run, dev, and test turn the capture into source-ranked
findings and standard profile
artifacts without introducing a separate profiling command. Use
--profile=compiler for the separately instrumented exact source-site census of
executions, fallbacks, allocation, boxing, safepoints, and GC. See
docs/profiling.md for workflows, artifact formats, quality
signals, overhead policy, and current limitations.
Use build --production and launch the reported binary directly for production.
Adding --artifact <directory> creates a deployable artifact and therefore requires
--production. The destination must be absent or empty. Its build-owned layout is:
artifact/
├── artifact.json
├── LICENSE
├── profile.json # only with --profile
├── SHA256SUMS
└── bin/
└── <application>
The manifest records the Maligator version, Rust target triple, production status, binary size, and SHA-256 digest. Release tooling may archive this directory but does not reconstruct its contents.
Use maligator --help and maligator --version for command help and version output.
Unknown options, missing option values, and extra positional arguments are errors.
Ordinary maligator test is an interpreter-only toolchain path. It discovers
*.test.{js,mjs,ts,mts} and *.spec.{js,mjs,ts,mts}, loads a shared dependency
base plus independently cached registration fragments, and runs them in the
interpreter already embedded in the Maligator executable. It never emits C or
invokes a native compiler/linker. The content-addressed cache stores frontend
wire artifacts, not successful results; every selected test executes on every
command.
test --profile is the explicit exception: it compiles the selected graph as one
production AOT image so the profiler observes the code users ship. It retains test
selection and reporting behavior but is intentionally a cold, toolchain-backed
diagnostic path.
import { beforeEach, describe, expect, test } from "maligator:test";
import { createStore } from "./store.ts";
describe("store", () => {
let store: ReturnType<typeof createStore>;
beforeEach(() => {
store = createStore();
});
test("returns inserted values", () => {
store.set("answer", 42);
expect(store.get("answer")).toBe(42);
});
test("loads asynchronously", async () => {
store.set("answer", 42);
await expect(store.load("answer")).resolves.toEqual(42);
});
});The initial API includes nested suites; beforeAll, afterAll, beforeEach, and
afterEach; synchronous and async callbacks; skip, todo, only, and each;
scalar/structural/throw matchers; .not, .resolves, .rejects; and the
any, anything, stringMatching, objectContaining, and arrayContaining
asymmetric matchers. A focused .only run prints a warning.
Selections are stable and serial by default. --run filters hierarchical names,
--shuffle reports its reproducible seed, --repeat reruns the registered suite
without recompiling, and --bail opts out of the default complete policy. The MVP
uses one shared Realm/isolate across files; globals, intrinsic prototypes, host
state, and uncancelled async resources are therefore shared. Per-file Realm
isolation and worker scheduling are deferred rather than simulated.
maligator.build.ts is executable, trusted TypeScript configuration. It is stripped
in place, evaluated on every command invocation, and strictly validated after
evaluation. Ordinary locals, functions, conditions, and environment reads are
allowed. The only supported import is defineBuild from @maligator/cli:
import { defineBuild } from "@maligator/cli";
const productionNodeSurface = process.env.MAL_NODE === "1";
export default defineBuild({
entry: "src/index.ts",
outputName: "example",
engine: {
eval: false,
realms: false,
regexp: true,
temporal: false,
intl: {
enabled: false,
features: [],
languages: [],
},
},
surface: {
webPlatform: false,
node: productionNodeSurface,
maligator: true,
},
});The npm package ships TypeScript declarations for this configuration and for
Maligator's runtime globals. Including maligator.build.ts in the TypeScript project
loads the global mal.assets and Mal.serve types. Projects that exclude the build
file can add @maligator/cli to compilerOptions.types instead. The declarations
describe optional surfaces even when a particular build disables them; the
configuration remains the runtime authority.
All fields are optional. Product defaults are:
| Field | Default | Meaning |
|---|---|---|
entry |
none | Project-relative entry module |
outputName |
inferred | Safe single-component executable name |
assets |
{} |
Unconditionally embedded file and directory resources |
engine.eval |
false |
Runtime-disabled by default; true embeds the compiler; "compile-check" rejects visible uses |
engine.realms |
false |
Include Realm support |
engine.regexp |
true |
Include the RegExp engine |
engine.temporal |
false |
Include Temporal plus calendar and time-zone data |
engine.intl.enabled |
false |
Include Intl and ICU4X data |
engine.intl.features |
[] |
All Intl services when Intl is enabled; a non-empty list selects services |
engine.intl.languages |
[] |
All locales; locale subsetting is not implemented yet |
surface.webPlatform |
false |
Include the WinterTC/web host surface |
surface.node |
false |
Include Maligator's curated node:* compatibility surface |
surface.maligator |
true |
Include the Maligator host surface |
Supported Intl feature names are collator, number-format, date-time-format,
plural-rules, list-format, segmenter, display-names,
relative-time-format, and duration-format. Unknown fields and values fail rather
than being ignored. A non-empty engine.intl.languages currently fails with an
actionable unsupported-feature diagnostic.
Configured assets are captured unconditionally in the native executable after the
trusted configuration has run. File paths resolve from the project root; directory
assets require explicit include patterns (*, ?, and whole-segment **):
export default defineBuild({
assets: {
compilerWire: { type: "file", path: "compiler.malw" },
runtime: {
type: "directory",
path: "runtime",
include: [
"host_main.c",
"test262_main.c",
"src/**",
"rust/Cargo.toml",
"rust/Cargo.lock",
"rust/rust-toolchain.toml",
"rust/src/**",
"rust/include/**",
"vendor/llhttp/include/**",
"vendor/llhttp/src/**",
],
},
},
});Every include pattern must match at least one regular file; symlinks and other
non-regular entries are rejected. At runtime, mal.assets.materialize(name, { baseDirectory? }) writes the captured file or tree atomically and returns its
absolute path. baseDirectory defaults to the operating-system temporary directory.
The immediate child is <content-hash>-<asset-format-version> and a completion
marker makes repeat calls a cheap cache hit. A configured file returns its path
inside that directory; a configured directory returns the directory itself.
Assets require surface.maligator (enabled by default). They are native-executable
resources and are intentionally unsupported by portable --serialize output.
The output name is selected from outputName, then the unscoped portion of
package.json#name, then the working-directory basename. Names cannot be empty,
./.., or contain path separators.
The distributed CLI runs development images and application tests without these
tools. They are required when producing a standalone native binary with build.
Every native build performs the same discovery and capability checks used by
doctor; running doctor first is optional. Runtime translation units are compiled
directly with CC and collected into three static archives with ar. Maligator
requires:
- A C compiler and archive tool with real C23 support — the runtime uses the
bool/true/falsekeywords (no<stdbool.h>),nullptr, and#embed, so Apple clang, clang >= 19, or gcc >= 15 works; gcc <= 14 accepts-std=c2xbut lacks#embedand is rejected by thedoctorC2x probe - Rustup and the
cargo/rustcselected byruntime/rust/rust-toolchain.toml - A C++ compiler/runtime only when
surface.webPlatformis enabled
CC and CXX override compiler selection. Otherwise tools are resolved strictly
from PATH. maligator doctor --verbose reports resolved paths, versions, C and
Rust targets, tested capabilities, and probe-cache status.
An explicit build --target <rust-triple> cross-builds through a detected Zig
installation. The initial supported targets are:
aarch64-apple-darwinx86_64-apple-darwinaarch64-unknown-linux-gnux86_64-unknown-linux-gnu
Maligator maps the Rust triple to Zig's target syntax and consistently uses zig cc, zig c++, and zig ar for the C runtime, Cargo native dependencies, and final
link; optional production stripping is applied by zig cc during that link. Install
the matching Rust standard library first with rustup target add <rust-triple>. Use
maligator doctor --target <rust-triple> --verbose to validate both halves of the
cross toolchain. Cross-built outputs live under
.cache/mal-build/<mode>/<rust-triple>/; maligator run remains a native-host
command.
Discovery, normalized feature booleans, build plan, installation roots, environment
snapshot, and cache root are frozen into one NativeBuildContext. The C archive,
Rust archive, and final linker consume that same context. The final linker returns a
typed result containing the executable path, exact artifact bundle, and context, so
callers such as size tracking cannot accidentally resolve and measure a different
build.
On macOS, install Apple build tools with xcode-select --install. On Linux, install
a C23-capable compiler and select it: on Debian/Ubuntu the stock build-essential
(gcc 12) is too old, so sudo apt install clang-19 and build with
CC=clang-19 CXX=clang++-19 (or use gcc >= 15 where available). Install Rust through
Rustup, then enter runtime/rust and run rustup show to install/select the pinned
toolchain.
The distributed self-hosted CLI embeds the runtime C sources, Rust crate, and a
prebuilt eval compiler wire. It materializes those content-addressed assets on
startup, so the copied compiler can run the full native pipeline outside a Maligator
checkout and without Node.js. npm run selfhost:cli exercises that transfer path.
The initial npm support matrix is macOS and glibc-based Linux on arm64 and x64. Windows is deferred because the native host/runtime currently depends on POSIX APIs. The first alpha binaries are unsigned; macOS artifacts are not notarized. Minimum OS versions will be fixed after the release artifacts have run on the clean-host validation matrix.
@maligator/cli is a small Node.js launcher with exact-version optional dependencies
on these native packages:
@maligator/cli-darwin-arm64@maligator/cli-darwin-x64@maligator/cli-linux-arm64@maligator/cli-linux-x64
There is no source-build fallback. An unsupported host or installation without its optional platform package fails with an actionable message.
package.json is the release version source of truth; src/version.ts is generated
and checked before building or publishing. Every prerelease is published explicitly
under the npm alpha dist-tag. The release automation does not attempt to change or
remove the registry's latest tag.
release:publish verifies every selected tarball against packages.json, publishes
the platform packages first, and publishes @maligator/cli last under the alpha
dist-tag. Each publish is a plain synchronous npm publish with the terminal's
stdin/stdout/stderr inherited, so enter the OTP directly when npm prompts. Build,
pack, and publish log per-target progress and elapsed time.
The Prepare release packages GitHub Actions workflow is the preferred packaging
path. Dispatch it on the exact release commit. It cross-builds all four targets on
macOS ARM, executes the native binary and installed npm launcher on every supported
host, and retains the validated tarballs plus packages.json for seven days. The
intermediate multi-target workspace is deleted after the smoke matrix passes.
Prepare the next numeric alpha with npm run version:alpha, commit the release
preparation, and push that exact commit to main before dispatching the workflow.
Download the successful run's maligator-<version>-packages artifact into
dist/release/packs in a clean checkout. release:create-github then verifies the
commit and tarball checksums, creates the exact v<package.json version> tag and a
draft GitHub prerelease, uploads every npm tarball plus packages.json, verifies the
complete draft, and publishes the GitHub release. Publishing the prerelease triggers
the separate Publish npm alpha workflow.
gh workflow run release-packages.yml --ref main
run_id="$(gh run list --workflow release-packages.yml --commit "$(git rev-parse HEAD)" --event workflow_dispatch --status success --limit 1 --json databaseId --jq '.[0].databaseId')"
version="$(node -p 'require("./package.json").version')"
gh run download "$run_id" --name "maligator-$version-packages" --dir dist/release/packs
npm run release:create-github -- --confirm "$(node -p "require('./package.json').version")"The local single-target build remains available as a fallback:
npm run release:build -- --target aarch64-apple-darwin
npm run release:smoke
npm run release:pack -- --target aarch64-apple-darwin
git push origin main
npm run release:create-github -- --confirm "$(node -p "require('./package.json').version")"The publish workflow only checks out the tagged commit, validates its ancestry and
exact version tag, downloads the prepared assets, rechecks their manifest and
checksums, and publishes through npm trusted publishing. It has id-token: write
permission but no stored npm token. --trusted-publishing is accepted only for the
matching tag in a GitHub Actions OIDC environment; local publishing continues to
require web authentication. If draft creation or asset verification fails, the
GitHub release remains unpublished and the npm workflow does not run.
GitHub release immutability is a required one-time repository setting. Once the draft is published, GitHub then prevents replacement or deletion of its tag and assets. Enable it with an administrative GitHub credential before the first release:
gh api --method PUT -H "X-GitHub-Api-Version: 2026-03-10" \
repos/dirkdev98/maligator/immutable-releasesIf publishing fails after an immutable release is created, repair and push main,
then retry that same release without replacing its tag or assets:
gh workflow run npm-release.yml --ref main -f release_tag=v0.1.0-alpha.8The retry path accepts only the exact version in package.json, checks that the
release is an immutable prerelease whose tag commit is contained in main, and
revalidates every downloaded tarball before publishing.
Before the first workflow release, configure every npm package once for repository
dirkdev98/maligator, workflow filename npm-release.yml, and the npm publish
permission. The CLI equivalent for each package is:
npm trust github @maligator/cli --repo dirkdev98/maligator --file npm-release.yml --allow-publish --yesRepeat that command for the four @maligator/cli-<platform>-<arch> packages listed
above. npm protects this one-time trust change with 2FA; subsequent workflow
publishes use short-lived OIDC credentials and require neither an npm token nor an
OTP.
release:build and release:pack default to aarch64-apple-darwin so a local
release only builds Apple Silicon macOS. Pass -- --all-targets for the complete
matrix or -- --target <rust-triple> for one explicit target. Build and pack must
use the same selection. Packing a native-host target also installs the two tarballs
into a clean temporary project and verifies the installed launcher.
Applications with surface.webPlatform: true link host_main.c, which installs the
web globals and drives the host event loop. Non-web applications retain the lean
synchronous test262_main.c driver; the product CLI itself uses host_main.c for its
hosted command surface.
Development builds use -O2, keep symbols, and do not use LTO. They are intended
for normal iteration and useful native crash diagnostics.
maligator build --production still uses -O2, adds compile/archive/link LTO when
the selected toolchain passes the LTO probe, and strips native symbols after linking
when the host strip probe succeeds. Unsupported LTO or stripping emits a warning and
continues with a valid -O2 executable. Missing required C2x or C++ link capability
is a hard error. Stripping does not remove Maligator's own JavaScript source-position
tables.
Maligator keeps reusable inputs separate from project outputs:
<user-cache>/maligator/v1/actions/ producer/action result manifests
<user-cache>/maligator/v1/blobs/sha256/ immutable content-addressed outputs
<user-cache>/maligator/v1/frontend/ serialized VM definitions
<user-cache>/maligator/v1/toolchains/ tool identity and capability probes
<user-cache>/maligator/v1/work/ disposable compiler/Cargo scratch
<project>/.cache/mal-build/development/ generated C and development executables
<project>/.cache/mal-build/production/ generated C and production executables
The user cache follows the platform convention (~/Library/Caches on macOS,
$XDG_CACHE_HOME on Linux, and LocalAppData on Windows); MALIGATOR_CACHE_DIR
overrides its base. Cache keys include relevant source content, producer protocol,
normalized feature config, target, selected toolchain identity, build environment,
and exact compile or Cargo arguments. Package semver is recorded as metadata, not
used as a blanket invalidation key.
Normal builds restore cached VM definitions before generated-C emission, skipping
unchanged graph, semantic, optimization, allocation, and lowering work while
preserving native-code metadata. Generated translation units and the driver are
then compiled to independently content-addressed objects; unchanged objects are
relinked without invoking their C compilation again. Definitions are assigned to
deterministic, edit-local hash partitions with a 2 MiB soft target and an 8 MiB hard
limit for an indivisible definition; code and data units remain separate, and worker
count affects only scheduling. Normal output reports
frontend, generated-C, toolchain, and native cache hit/miss status plus the final
executable path. Removing
.cache/mal-build forces project output materialization without discarding reusable
artifacts. Use maligator cache clear --all for an intentional full cache reset.
C and Rust outputs are published as immutable blobs only after compilation succeeds; an atomic action manifest makes the result visible. Runtime GC stress/verification flags are execution inputs and reuse the same binary. Build-affecting sanitizer, GC, feature, backend, optimization, target, and toolchain dimensions get distinct action keys. Within those dimensions, each runtime C object is keyed by its actual preprocessed input, so a feature change recompiles only translation units whose code changed. Feature-specific Rust archives share a serialized Cargo target tree and therefore reuse unaffected dependency work. Final products stay statically linked and self-contained. Test/program verdicts and benchmark measurements are always executed and are never cache entries.
no entrypoint ... run 'maligator init': addentryto the config or pass an explicit entry.config file not found:--configis relative to the current project root; no ancestor search occurs.node:* ... surface.node is disabled: setsurface.node: trueonly for programs needing the curated Node surface.eval is disabled: the defaultengine.eval: falsecompiles the call site but throws if it executes. Settrueto embed the runtime compiler, or"compile-check"to reject statically visible uses during the build.RegExp is disabled: removeengine.regexp: falseor avoid regular expressions.Toolchain is not ready: runmaligator doctor --verbose, checkCC/CXX,PATH, and the platform-specific installation suggestions.- A stale or suspect artifact: inspect with
maligator cache status, previewmaligator cache prune --dry-run, or deliberately reset withmaligator cache clear --all; normal identity changes invalidate affected actions automatically. - A custom self-hosted CLI reports a missing
runtimeorcompilerWireasset: build it with the source-tree and prebuilt-wire asset set shown above.
Development requires Node.js 24 or newer, a C/C++ toolchain, and Rustup with the pinned Rust toolchain.
npm ci
# Run the Node-hosted product CLI while working on the compiler.
node ./src/index.ts build path/to/entry.ts
# Default developer gate; smoke is its fast initial fuse.
npm run test:check
# Standalone 20-second warm / four-minute cold fuse.
npm run test:smoke
# Exhaustive gates. Ask before running either command: they include full Test262.
npm run test:full
npm run test:full:report
npm run type-check
npm run lint
npm run bench # production-plan JavaScript + closed HTTP/Express
npm run bench -- javascript --mode open-interpreted
npm run bench -- --full --update # add closed self-compile; hard-cut baseline
npm run bench -- javascript --compare HEAD --runs 5
npm run bench -- --changed --compare HEAD
npm run bench -- self-compile --compare HEAD --runs 1 --max-pairs 1 --budget-seconds 600 --plan=json
# Complete standards reports without baseline updates. Ask before full Test262.
npm run test262:report
npm run test:wpt:report
# Explicit full-corpus baseline replacement; ask before running.
npm run test262:update-baseline
# Targeted lanes remain available while developing.
npm run test:unit # watch mode
npm test run # one-shot unit and native projects
npm run test:native
npm run test:sanitize -- tests/native/example.test.ts
npm run test262:regressions
npm run test262:prepare # populate/repair the pinned full-corpus cache
# Inspect or reclaim Maligator-owned rebuildable caches.
node ./src/index.ts cache status
node ./src/index.ts cache prune --dry-run
node ./src/index.ts cache prune # target 15 GiB, preserving protected entries
node ./src/index.ts cache clear --all
# Check agent sandbox/cache access and inspect exact stage requirements.
npm run env:check -- --json
npm run test:help
npm run test:check -- --list
npm run test:check -- --plan=jsonSee docs/testing.md for tier contents, fail-fast versus
completion policies, paired performance comparisons, full-matrix coverage, and
where new tests belong. See docs/profiling.md for application
performance investigation.
docs/decisions: architecture decisionsruntime: C runtime and Rust FFI shimsrc/compiler/pipeline/compile-core.ts: host-independent semantic-program to VM-definition compiler coresrc/compiler/pipeline/compile-program.ts: module loading and compiler entrypoint orchestrationsrc/native-build-context.ts,src/runtime-build.ts,src/local-build.ts: explicit native context, atomic reusable artifacts, and final linkingsrc/cli-commands.ts: installation-aware command orchestration and web/non-web driver selectionsrc: remaining compiler, CLI, and build toolingtests: unit and native integration coverage
ECMAScript reference: ECMA-262.