How Pseudo handles 11 native targets
How Pseudo handles 11 native targets
Most language designers commit to one runtime. Some commit to two. Pseudo commits to eleven, on v1.0 ship day, with no "best-effort" tier — every target listed in COMPATIBILITY.md ships fully working.
This post explains what those eleven targets are, why each was selected over alternatives, and how the compiler decides which target to emit for a given input.
The eleven
| Target | Min version | Why it ships v1.0 |
|---|---|---|
| TypeScript | 5.8, ES2024 | Default web target — Node 22+, Bun 1.3+, Deno 2; Temporal lands in Node 26 |
| Rust | 1.88, Edition 2024 | Default systems target — Tokio LTS 1.51, jiff date-time, async closures stable |
| Python | 3.13 (3.14 opt-in) | Default data-science target — PEP 695 type syntax, PEP 779 free-threading |
| Go | 1.26 | Default cloud-services target — range-over-func iterators, stdlib post-quantum |
| Swift | 6.2+ (6.3 recommended) | Default Apple target — Hummingbird 2 server, SwiftUI on iOS 26 / macOS 26 |
| Kotlin | 2.3.20+ | Default Android target — Ktor 3 server, Compose 1.9 BOM, Compose Multiplatform iOS |
| C# / .NET | C# 14 + .NET 10 LTS | Default Windows target — ASP.NET Core, WinUI 3 + MAUI, field keyword |
| C++ | C++23 (clang 19+) | Default embedded target — std::expected for Result, opportunistic C++26 |
| WASM / WASI | Preview 2 (wasi 0.2.x) | Default edge / portable-binary target — WIT 0.2, Component Model |
| Shell | POSIX sh + Bash 5+ | Default scripting target — POSIX.1-2024, shellcheck-clean output |
| Ruby | 3.4+ (YJIT default) | Default Rails-ecosystem target — RBS types, Rack/Sinatra, Fiber/Ractor |
This is not a wishlist. Every entry above is a binding v1.0 commitment, locked in COMPATIBILITY.md and traceable per-function via the @target doc comment.
Why eleven and not three
The temptation in language design is to pick a small number of targets, ship those well, and call everything else "future work." We rejected this for two reasons.
First, programmers do not get to pick their runtime. A team writing a Rails app needs Ruby. A team writing an iOS app needs Swift. A team writing a Cloudflare Worker needs WASM. If Pseudo only supported three of those, the language would be unusable for two-thirds of working programmers. Asymptotically, you become a niche.
Second, the cost of supporting a target falls dramatically when the architecture is right. Pseudo's compiler emits source-level code in each target language — TypeScript that a TypeScript engineer would write, Rust that a Rust engineer would write — not bytecode for a Pseudo VM. The emit layer per target is roughly 8,000 lines of Go. Adding the eleventh target (Ruby, per ADR-0039) took less than two weeks once the architecture was stable.
The hard part is not the eleventh target. The hard part is the discipline of saying no to the twelfth, thirteenth, and twentieth targets until v2.0 Sophia.
How the compiler picks a target
The simplest case: you pass --target=rust and the compiler emits Rust. Done.
The interesting case: you do not pass a target, and the compiler infers one. Pseudo's spec covers this in §11. The inference rules are deterministic and follow this priority:
- Explicit
@targetdoc comment on the entry function. If you wrote@target rustonpub fn main, the compiler emits Rust regardless of context. - Project-level
pseudo.tomldefault_targetkey. If the project declares it, that wins. - Manifest-driven inference. If the entry function returns a type that only one target supports (e.g.,
SwiftUI.View), emit that target. If multiple targets support the return type, fall through. - Platform inference. If the build environment is macOS targeting iOS, emit Swift. If it is Windows targeting WinUI, emit C#. If it is generic Linux targeting a server, emit Go (lowest-overhead deployable). If nothing is determinative, fall through.
- TypeScript as the universal fallback. If no rule fires, emit TypeScript. It is the most portable target in the matrix and the most likely to be runnable in whatever environment a human dropped a
.pseudofile into.
The fallback to TypeScript is a design choice. We could have picked Rust (more performant) or Python (easier to read), but TypeScript runs on more substrates than either — browser, Node, Bun, Deno, Workers — and that breadth is what makes it the safest default.
What "fully working" means
Every target in the matrix ships with:
- A complete emit layer in
internal/emit/<target>/covering 100% of the Pseudo language surface - A target-specific conformance test suite under
tests/conformance/v1.0/<target>/— ~1,500 tests per target - A target-specific stdlib with idiomatic implementations of every Pseudo stdlib function
- Documentation in the spec showing the source-to-emit mapping for every grammar production
There is no "supported with caveats" tier. There is no "best-effort." If a target is in the matrix, every Pseudo program that does not use a target-incompatible primitive (e.g., shell does not get Task.spawn) compiles to that target and passes its conformance suite.
This is the discipline. It is harder than shipping a vague compatibility table. It is also why we restricted v1.0 to eleven targets instead of fifteen — every commitment carries a real maintenance cost across the eight AI coding agents who author Pseudo and the human reviewer who signs off.
What is out of scope for v1.0
These are explicitly not shipping in v1.0, and we put them in the spec's "Out of scope" section to be honest rather than soft-list them:
- Java — JVM is well-served by Kotlin emit; we do not need a separate Java emit layer
- Dart / Flutter — package squat exists, but the emit layer is v1.5 work
- Zig — interesting language, but does not yet have the package-ecosystem maturity we need for stdlib reuse
- Lua — useful for embedded scripting, but the shell + WASM combination covers most use cases
- PHP runtime emit — package squat exists for distribution; the runtime emit is v2.0 Sophia
If you are passionate about adding one of these to v1.x, the RFC process is at /rfcs (post-v1.0). For pre-v1.0, the answer is no — we are shipping the eleven we committed to and not chasing scope creep.
What this looks like in the source
Here is a minimal Pseudo program that targets three different runtimes from the same source. The @target annotation on each function pins it; the build command picks which entry point to compile.
/// @intent Compute a SHA-256 hash of a string.
/// @author claude-opus-4-7 (2026-05-15)
/// @target rust, go, swift
pub fn sha256_of(input: string) -> string {
let bytes = input.utf8_bytes()
let digest = crypto.sha256(bytes)
return hex.encode(digest)
}
/// @intent Entry point for the Rust binary.
/// @author claude-opus-4-7 (2026-05-15)
/// @target rust
pub fn main_rust() {
print(sha256_of("hello, rust"))
}
/// @intent Entry point for the Go binary.
/// @author claude-opus-4-7 (2026-05-15)
/// @target go
pub fn main_go() {
print(sha256_of("hello, go"))
}Build pseudoc build --target=rust and you get a Rust binary that calls sha2::Sha256. Build --target=go and you get a Go binary that calls crypto/sha256. Same sha256_of, different idiomatic emit per target.
That is the contract.
The next post in this series will be a deeper look at the comment system — why we made comments grammar-required, how the @conforms trace works, and how the Tier 1 / Tier 2 distinction maps to enforcement.