Perry is a native TypeScript compiler developed in Rust, designed to convert TypeScript source code into standalone executables without the need for Node.js or runtime dependencies. This tool enables developers to create cross-platform applications with enhanced performance and efficiency.
Key Features:
SWC Parsing: Utilizes SWC for rapid and efficient TypeScript parsing.
LLVM Code Generation: Employs LLVM as its backend, ensuring highly optimized native code since version 0.5.0.
Performance Excellence: Outperforms Node.js, Bun, and other tools in benchmarks, offering faster execution speeds.
Cross-Platform Support: Generates binaries compatible with multiple platforms, eliminating the need for platform-specific coding.
Lightweight Binaries: Produces small, efficient executables that enhance user experience and simplify distribution.
Optimization Techniques: Implements advanced optimizations like scalar replacement and escape analysis to minimize overhead.
Audience & Benefit:
Ideal for TypeScript developers aiming to build high-performance, cross-platform applications. Perry offers the benefit of fast, lightweight binaries with no runtime dependencies, leading to easier deployment and a superior user experience across various platforms. Installation is straightforward via winget, ensuring seamless integration into development workflows.
README
Perry
One codebase. Every platform. Native performance.
Perry is a native TypeScript compiler written in Rust. It takes your TypeScript and compiles it straight to native executables — no Node.js, no Electron, no browser engine. Just fast, small binaries that run anywhere.
> Have something you've built with Perry? Open a PR to add it here!
Performance
> As of v0.5.585, fast-math is opt-in. Perry's default mode emits no reassoc + contract per-instruction FMF flags, so f64 arithmetic is bit-exact with Node. --fast-math (CLI), PERRY_FAST_MATH=1 (env), or "perry": { "fastMath": true } in package.json re-enables the flags. See docs/src/cli/fast-math.md for the discussion of when it does and doesn't matter. The numbers below are Perry's default mode unless noted.
Numbers below are from a 2026-05-14 sweep on macOS ARM64 (M1 Max, RUNS=11 medians, taskpolicy -t 0 -l 0) at Perry v0.5.908 on an otherwise-idle machine. All languages re-measured together this run. Source + methodology in benchmarks/polyglot/.
Benchmark
Perry
Rust
C++
Go
Swift
Java
Node
Bun
What it tests
fibonacci
309
316
309
446
401
278
987
518
Recursive function calls (i64 specialization)
loop_data_dependent
225
226
129
128
225
226
226
230
Multiplicative carry through sum (genuinely-non-foldable f64)
Default Perry runs in the same neighborhood as Rust default -O, C++ -O3, and Swift -O on every row — competitive on integer recursion (fibonacci 309 vs Rust 316 / C++ 309), within a tick of native on object allocation thanks to scalar replacement (object_create), within a few ms on cache-bound work (nested_loops, array_read/array_write), and matching the no-contract compiled pack on genuinely-non-foldable f64 (loop_data_dependent 225 vs Rust 226 / Bun 230 / Node 226). Apple Clang -O3 and Go default win the loop_data_dependent row at 128-129 by fusing sum * a + b into a single FMADDD instruction (FMA contraction is -ffp-contract=fast — a separate knob --fast-math deliberately doesn't toggle). Python column omitted to keep the table readable; full numbers in benchmarks/polyglot/RESULTS.md.
We deliberately don't lead with the trivially-foldable accumulator microbenchmarks (loop_overhead / math_intensive / accumulate) that Perry posted big numbers on through v0.5.584. Those are flag-aggressiveness probes — they measure whether each compiler applied reassoc + autovectorize to a sum += 1.0-shaped loop, not how fast the resulting loop computes under load. Perry default sits in the no-flags pack (97 / 51 / 97 ms in this sweep) on all three; --fast-math recovers 12 / 14 / 34 ms. C++ -O3 -ffast-math matches Perry --fast-math to the millisecond on the same kernels — same LLVM pipeline, one flag. The full breakdown is in benchmarks/README.md and polyglot/RESULTS_OPT.md.
vs Node.js and Bun
Perry's broader benchmark suite covers workloads outside the polyglot set — closures, classes, JSON, prime sieve, etc. Numbers below from the 2026-05-14 v0.5.908 sweep via benchmarks/suite/run_benchmarks.sh (single-run-per-cell, not RUNS=11 medians — see benchmarks/polyglot/ for the rigorous multi-run methodology).
Benchmark
Perry (v0.5.908)
Node.js
Bun
What it tests
factorial
107ms
591ms
97ms
Modular accumulation (integer fast path)
method_calls
9ms
11ms
9ms
Class method dispatch (10M calls)
closure
50ms
304ms
51ms
Closure creation + invocation (10M calls)
binary_trees
2ms
10ms
7ms
Tree allocation + traversal (1M nodes, scalar replacement)
string_concat
0ms
3ms
1ms
100K string appends
prime_sieve
3ms
8ms
7ms
Sieve of Eratosthenes
mandelbrot
28ms
25ms
29ms
Complex f64 iteration (800x800)
matrix_multiply
28ms
34ms
34ms
256x256 matrix multiply
json_roundtrip (lazy tape, gen-gc)
83ms
377ms
249ms
50× JSON.parse + JSON.stringify on a ~1MB, 10K-item blob
closure and factorial are still slower than the older v0.5.173 baseline (10 → 50 ms, 31 → 107 ms). The v0.5.585 fast-math opt-in flip accounts for factorial (integer modulo plus an FP-tail reduction that the old default-on fast-math collapsed); closure regression is tracked as a follow-up. method_calls is back at baseline this sweep (9 ms) — yesterday's 25 ms reading was single-run noise from concurrent CPU load. The wins on binary_trees / string_concat / prime_sieve / mandelbrot / matrix_multiply against Node/Bun hold steady. Single-run cells are noisier than RUNS=11 medians; the lower-noise multi-run polyglot table above remains the canonical comparison.
Perry compiles to native machine code via LLVM — no JIT warmup, no interpreter overhead. Key optimizations that apply in both modes: scalar replacement of non-escaping objects (escape analysis eliminates heap allocation entirely — object fields become registers), inline bump allocator for objects that do escape, i32 loop counters for bounded array access, integer-modulo fast path (fptosi → srem → sitofp instead of fmod), elimination of redundant js_number_coerce calls on numeric function returns, and i64 specialization for pure numeric recursive functions.
Run benchmarks yourself: cd benchmarks/suite && ./run_benchmarks.sh (requires node, cargo; optional: bun, shermes).
Binary Size
Perry produces small, self-contained binaries with no external dependencies at run time:
Program
Binary Size
console.log("Hello, world!")
~330KB
hello world + fs / path / process imports
~380KB
full stdlib app (fastify, mysql2, etc.)
~48MB
with --enable-js-runtime (V8 embedded)
+~15MB
Perry automatically detects which parts of the runtime your program uses and only links what's needed.
Installation
npm / npx (any platform)
Perry ships as a prebuilt-binary npm package — the fastest way to try it, and the only install path that works on all seven supported platforms (macOS arm64/x64, Linux x64/arm64 glibc + musl, Windows x64) with one command:
# Project-local (recommended — pins Perry's version alongside your deps)
npm install @perryts/perry
npx perry compile src/main.ts -o myapp && ./myapp
# Global
npm install -g @perryts/perry
perry compile src/main.ts -o myapp
# Zero-install, one-shot
npx -y @perryts/perry compile src/main.ts -o myapp
@perryts/perry is a thin launcher; npm automatically picks the matching prebuilt via optionalDependencies (@perryts/perry-darwin-arm64, @perryts/perry-linux-x64-musl, etc.) based on your os / cpu / libc. Requires Node.js ≥ 16 and a system C toolchain for linking (same as any Perry install — see Requirements).
The Scoop manifest declares main/llvm as a dependency, so scoop install automatically pulls the official MSVC-default LLVM toolchain Perry needs for Windows-native object emission. Verify with perry doctor after install.
curl -fsSL https://raw.githubusercontent.com/PerryTS/perry/main/packaging/install.sh | sh
From source
git clone https://github.com/PerryTS/perry.git
cd perry
cargo build --release
# Binary at: target/release/perry
Requirements
Perry requires a C linker to link compiled executables:
macOS: Xcode Command Line Tools (xcode-select --install)
Linux: GCC or Clang (sudo apt install build-essential)
Windows: MSVC (Visual Studio Build Tools)
Run perry doctor to verify your environment.
Quick Start
# Initialize a new project
perry init my-project
cd my-project
# Compile and run
perry compile src/main.ts -o myapp
./myapp
# Or compile and run in one step
perry run .
# Check TypeScript compatibility
perry check src/
# Diagnose environment
perry doctor
Real-World Example: API Server with ESM Modules
Perry supports standard ES module imports and npm packages. Here's a real-world API server with multi-file project structure:
git clone https://github.com/PerryTS/perry-examples
cd perry-examples/fastify-redis-mysql
npm install
perry compile src/index.ts -o server && ./server
Native UI
Perry includes a declarative UI system (perry/ui) that compiles directly to native platform widgets — no WebView, no Electron. The programming model is SwiftUI-like: compose native widgets with stack-based layout, alignment, and distribution — not CSS/HTML.
These packages are natively implemented in Rust — no Node.js required:
Category
Packages
HTTP
fastify, axios, node-fetch, ws (WebSocket)
Database
mysql2, pg, ioredis
Security
bcrypt, argon2, jsonwebtoken
Utilities
dotenv, uuid, nodemailer, zlib, node-cron
Compiling npm Packages Natively
Perry can compile pure TypeScript/JavaScript npm packages directly to native code instead of routing them through the V8 runtime. Add a perry.compilePackages array to your package.json:
Then compile with --enable-js-runtime as usual. Packages in the list are compiled natively; all others use the V8 runtime.
Good candidates: Pure math/crypto libraries, serialization/encoding, data structures with no I/O.
Keep as V8-interpreted: Packages using HTTP/WebSocket, native addons, or unsupported Node.js builtins.
Compiler Optimizations
Scalar Replacement — escape analysis identifies non-escaping objects (let p = new Point(x, y); sum += p.x + p.y); fields are decomposed into stack allocas that LLVM promotes to registers — zero heap allocation
NaN-Boxing — all values are 64-bit words (f64/u64); no boxing overhead for numbers
Garbage Collection — mark-sweep GC with conservative stack scanning, arena block walking, 8-byte GcHeader per allocation
Single-Threaded by Default — async I/O on Tokio workers, callbacks on main thread. Use perry/thread for explicit multi-threading.
No Runtime Type Checking — types erased at compile time. Use typeof and instanceof for runtime checks.
Small Binaries — ~330KB hello world, ~48MB with full stdlib. Automatically stripped.
Development
cargo build --release # Build everything
cargo build --release -p perry-runtime -p perry-stdlib # Rebuild runtime (after changes)
cargo test --workspace --exclude perry-ui-ios # Run tests
cargo run --release -- compile file.ts -o out && ./out # Compile and run
cargo run --release -- compile file.ts --print-hir # Debug HIR
Adding a new feature
HIR — add node type to crates/perry-hir/src/ir.rs
Lowering — handle AST→HIR in crates/perry-hir/src/lower.rs
Codegen — generate LLVM IR in crates/perry-codegen/src/codegen.rs
Runtime — add runtime functions in crates/perry-runtime/ if needed
Test — add test-files/test_feature.ts
Releasing Perry
Release cadence: patch releases (0.5.118 → 0.5.119) ship weekly-ish behind the
macOS CI gate. Major releases — any bump of the major or minor number
(e.g. 0.5.x → 0.6.0, and the upcoming 1.0.0) — must be verified on every
supported platform before the tag is pushed. Patch releases only require the
default CI gate.
1. Pre-release checklist (every release)
Run on macOS (the canonical dev host):
# Full rebuild — runtime/stdlib/UI libs must match the compiler version.
cargo build --release
# Core gates.
cargo test --workspace --exclude perry-ui-ios --exclude perry-ui-tvos \
--exclude perry-ui-watchos --exclude perry-ui-gtk4 \
--exclude perry-ui-android --exclude perry-ui-windows
./run_parity_tests.sh # Perry vs node stdout parity
./scripts/run_doc_tests.sh # Compile + run every docs/examples/*.ts
Then bump and tag:
# Edit Cargo.toml workspace.package.version + CLAUDE.md "Current Version".
# Add a "Recent Changes" entry in CLAUDE.md.
git commit -am "release: v0.x.y"
git tag v0.x.y && git push --tags
The release-packages.yml workflow fires on the pushed tag and builds the
cross-platform matrix (see §3).
2. Major-release verification (all platforms)
Before tagging a major/minor bump, these must all pass:
Platform
What to run
Runs in CI?
macOS (arm64 + x86_64)
cargo test + run_parity_tests.sh + scripts/run_doc_tests.sh
Yes, test.yml (arm64 only)
Linux glibc (x86_64 + aarch64)
Same, under xvfb-run -a for UI; apt install libgtk-4-dev libadwaita-1-dev xvfb first
Partial — release build only
Linux musl (x86_64 + aarch64)
Release build via release-packages.yml; spot-check a compiled hello.ts runs on Alpine
Build only
Windows (x86_64 MSVC)
scripts/run_doc_tests.ps1; smoke-test perry compile hello.ts -o hello.exe && .\hello.exe
perry compile --target android examples/widget_demo.ts; install APK on emulator
No (NDK required)
Web / WASM
perry compile --target web examples/wasm_ui_demo.ts, open out.html in a browser
No
Home-screen widgets
perry compile --target widgetkit ... && perry publish ios
No
For v1.0, expect to spend half a day spinning through the four OS VMs locally.
Linux + Windows doc-tests are automated in test.yml; the mobile/watch/web
lanes remain manual pending tier-2 simulator orchestration.
2a. Simulator-run recipe (iOS / tvOS)
perry-ui-ios and perry-ui-tvos honor PERRY_UI_TEST_MODE=1 — when set,
the app renders one frame, optionally writes a screenshot to
$PERRY_UI_SCREENSHOT_PATH, and exits cleanly. Combine with
xcrun simctl to verify a doc-example runs without a human:
# Compile for the simulator
perry compile --target ios-simulator docs/examples/ui/counter.ts -o counter.app
# Boot a device (one-time; reuse the UDID across runs)
xcrun simctl boot "iPhone 15"
open -a Simulator
# Install + launch with test mode
xcrun simctl install booted counter.app
PERRY_UI_TEST_MODE=1 \
PERRY_UI_TEST_EXIT_AFTER_MS=500 \
PERRY_UI_SCREENSHOT_PATH="$PWD/counter-ios.png" \
xcrun simctl launch --console booted com.example.counter
# App exits 0 after rendering; screenshot lands at counter-ios.png
Same recipe works for tvos-simulator + "Apple TV" device. On watchOS the
Rust Tier-3 toolchain requires +nightly -Zbuild-std — see the
watchos-simulator row in the matrix above.
3. What CI does on the tag
The Release Packages workflow (.github/workflows/release-packages.yml)
triggers on a published GitHub Release or manual workflow_dispatch. Matrix
runners build:
macos-14 / macos-15 — arm64 + x86_64 Darwin binaries
hub.perryts.com — worker notification so cloud build workers refresh
A tag push with a failing platform build aborts the publish step for that
platform only; fix-forward with a new patch tag (e.g. v0.6.1) rather than
amending the existing one.
4. Release gates (what blocks a release)
Parity tests must clear the threshold in test-parity/threshold.json
cargo test --workspace (macOS excluded list as above) must be green
compile-smoke must compile every file under test-files/
doc-tests must compile + run every example under docs/examples/
Benchmark regressions in benchmark.yml hard-fail on release tags (warn only
on main-branch pushes)
5. If a release goes wrong
Wrong artifact published: tag a new patch release with the fix; npm
rejects re-publishes of the same version anyway.
Broken binary on one platform: the release-packages.yml matrix is not
fail-fast: true, so other platforms still publish. Ship a follow-up patch
for the broken one.
CI hook failed after tag: run workflow_dispatch with
publish_npm: true to retry the npm step.