Perry Compiles TypeScript directly to executables using SWC and LLVM
perryts.com
perryts.com
Edit: as discussed in the thread below, the most likely reason for that is that Express is pure JS with types from @types/express, so the TypeScript compiler bails on it. Reasonable, but still frustrating.
Overall, it seems like every time I decide to try a vibe coded compiler I get this feeling like when you see a plate with fruits on a table but, coming closer, see that they are fake plastic fruits. No, I cannot use it to build a native binary of my project without V8 as easy as shown on the front page. Maybe some other project, yes, but not a real one.
Unrelated: if a project is called Perry, should the icon be a platypus in a hat, you know?
> Perry exposes a faithful subset of Node.js’s stdlib HTTP server modules on top of hyper + rustls + tokio-tungstenite. The whole shape — handler signature, IncomingMessage / ServerResponse properties + methods, TLS opts, ALPN-negotiated HTTP/2, WebSocket upgrade dispatch — works unmodified, so unmodified Node servers (Express / Koa / Polka / hono via @hono/node-server / etc.) compile and run natively[1]
It's pretty standard for "no runtime" to mean nothing on the device you install the compiled target app.
I think iOS development still needs Ruby for Pod installation but no one says Swift apps need a Ruby runtime for example.
$ cat index.ts
import * as express from 'express';
const app = express();
which gives $ perry index.ts
Collecting modules...
JS module: express -> /private/tmp/ex/node_modules/express/index.js
Error: build pulled in `perry-jsruntime` (QuickJS-based eval-equivalent runtime) via the following file(s):
- /private/tmp/ex/node_modules/express/index.js [express]
`perry-jsruntime` is treated as a privileged dependency on par with adding a JIT to the binary — it re-introduces arbitrary runtime code execution and defeats Perry's structural advantage over Node. Refusing to link by default. (#499)
To enable, set `perry.allowJsRuntime: true` in the host package.json, or pass `--enable-js-runtime` on the CLI for a one-off build. (Falls under `--lockdown` deny set when that flag ships — see #496.)
Maybe it's because Express is written in JavaScript with external types from @types/express, that would explain why it might need JS runtime, but it does not make things easier for me.Only by layman that don't understand compilers.
The infrastructure required to support a programming language, startup and shutdown boilerplate, all the required functionality to support standard library features including integration points between language semantics and support code.
Stuff like what code runs before and after main(), trap handlers for floating point arithmetic, handling of thread local storage, bind language heap handling primitives to library code, traps for handling stack overflow errors,....
It's worth being charitable in your interpretation though and recognising "no runtime" probably refers to JVM-shaped or Node-shaped things, not libgcc+crt0-shaped things.
There's practically no difference for the overwhelming majority of software these days. Most people aren't working on embedded systems or operating systems.
Currently tsz passes nearly 100% of TypeScript tests but that is not enough. I want it to be able to type check complex things like type-challenges solutions or complex utility type packages. I'm stress testing it with a repo with 1.5 million lines of code.
I'm constantly assigning AI agents tasks to find bugs in tsz and open issues.
I'll say this is "alpha" when it can do all those things plus matching tsc exactly in thousands of open source projects where tsc reported type errors. It's easy to find CI runs that tsc reported errors. I'll build a database of all the cases I've verified tsz with and will publish those. Hoping that can give folks confidence that tsz is robust
For now, tsz is just a work in progress.
(I'm the founder of Perry) thanks for giving it a try and let me be upfront with you: Compiling express has been one of the hardest challenges for Perry. I'm 99% sure it still does not work out of the box just yet - which is why we pivoted to fastify more than express.
That said, the last few days we made great progress to get Perry to compile packages "lik" Express (lot's of dynamic imports) much better.
I'll retry this once our current batch of Nodejs parity issues are resolved.
Just to be clear though: No v8 runtime, we actually took out the v8 engine we had optionally included thus far because the benefits no longer outweight the disadvantages.
If you want to tag along the progres follow me on X (@proggeramlug), that's where I share the most up to date progress on Perry.
Also, it is definetely a goal to compile apps that use Express and that will happen - not commiting to a timeline but just in the last few days we got so much closer.
Don't get me wrong, I'll be totally excited to replace my card game app backend with a static binary, that's why I immediately went on trying Perry :)
Just to give you an idea what I'm trying to achieve: I'm currently hosting it on a single VPS. Node is handling the average of 30 qps, peaking up to 100 qps, just fine, with P99 latency being safely below 100ms and only occasional spikes of P99 to up to a second. The spikes are probably caused by some VPS sluggishness as I just use filesystem to periodically dump the in-memory games state.
But I expect I might eventually start seeing performance issues, so rewriting to a language that compiles to a native binary is on the table. I was considering Golang or Rust; making the current codebase compile to a binary would be a miracle saving me quite some time, even if I just vibe-code the rewrite :)
Doesn't avoid the runtime, but it made deployment really easy.
Otherwise, I'm just using Go for backend now.
The website doesn't explain how it works in a lot of detail. I am the author of tsonic [1], a TS compiler that produces binaries via Clr NativeAOT (on Linux/Mac). The hardest parts were numbers (TS has no ints or shorts), Generics, and TS Utility Types. I've been on it for the last 6 months (almost every day); getting to near complete TS compatibility is a very long journey because of its expressiveness.
Add: A request is to explain how it works on the website. I did take a look at https://www.perryts.com/en/internals/ but are those techniques described really sufficient to express TS? Based on my experience, I must say I'm surprised. But the proof is in the pudding, and if it's compiling those examples it must be working somehow!
[1]: https://tsonic.org
Two things I found a little confusing from the docs though:
I couldn't easily find a page describing what it can't do yet. I saw that it only works with a "strict, deterministic subset of TypeScript", but is there a page showing what's included and not included in that subset?
Also, what's an "ambient surface" in this context? Is that a compiler term I'm just not familiar with?
I'll add that page, thanks. Today, almost all of idiomatic TS is supported including most of its utility classes. Dynamic JS-style code is not supported, for example adding a function or a field into an object, prototype-based class modifications etc. I'll compile a list, and include it along with the large docs cleanup planned before v1.
> Also, what's an "ambient surface" in this context?
The idea is that when JS gets transpiled into C# (or Rust, upcoming), JS globals and built-ins are invalid. The native "surface" is C#, meaning the string is .Net's string type and the methods that you expect on JS strings would be missing. But when you opt in to a surface, such as the "JS surface", the compiler applies surface defined translations such as substring becoming SubString, either directly or via a companion helper class. This allows you to write against standard JS and Node APIs, instead of relying on the stdlib/builtins of the target framework (currently CLR). And you get the JS "stdlib" - console, JSON, Date, Map, Set etc.
For example, all the projects you see under this use the JS surface: https://github.com/tsoniclang/proof-is-in-the-pudding/tree/m...
FYI, just in case you didn't know, there is an ExpandoObject type in the System.Dynamic namespace that you could use to do this. IDK if you want to, but it's one of those less common .NET features that people tend to not be aware of.
thanks for your comment. I'm Ralph, the founder of Perry (though at this point we are quite a few core contributors + maintainers).
You are right to be skeptical and yes, numbers are the hardest. In fact we started adding some native numbers to make things easier (i64, i32 etc.), types that are not natively available in Javascript/Typescript but make it much easier for the compiler. And they'll internally work as numbers for any other runtime.
The docs I must admit are not quite up to date. We have been pushing hard for Nodejs parity the last few dayys/weeks and are making great progress, however the docs and the site have not been updated in the same time just yet, so apologies for that.
Also, just as a fun fact: I noticed a spike in traffic and found this threat, so happy to take any requests but I'm not the OG of this threat haha.
The easy way to handle that would be to just treat "number" as 64-bit float, since that is semantically how Javascript defines them. But that can hurt performance.
Another option is to define your own integer types
This is what I did. Most int usage is inferred, but if they had to define it explicitly, I make them import { int } from "@tsonic/core/types.js";
That said, the more I think about it the more dubious I am. The site boasts no runtime dependencies but clearly it’s going to need things like a garbage collector, you can’t just magic that requirement away. At a certain point is it just doing what a JS engine’s JIT compilation does… except ahead of time?
Also doesn't inspire confidence that the text on the site is very clearly AI generated and the GitHub log shows an endless stream of AI powered commits. About 15 per hour, every hour? Doesn’t scream stability.
The whole site is very jarring to read.
The most reliable automatic detector right now must be https://www.pangram.com/. If Pangram says something is AI, it is very likely AI. Sometimes it concludes more unusual AI text is human-written; Talkie tricks it! (https://x.com/aliceisplaying/status/2059778915275567129.)
Eta: gradients on text, gradients everywhere, monospace badges, cards for everything, emojis everywhere...
Perry and Hone and the dB meter link all have the same homepage with slightly different spin
X. Y. SUPER Z. heading
X. Y. SUPER Z. in subheading
excessive purple and gradients
--> arrows
cards, cards, cards, cards
doesn't just X, emdash, it [SUPER Y]
more cards
ridiculous awful contrast in copy that makes things unreadable (grey on black etc)
Once you see a few, it becomes obvious
There is also, https://en.wikipedia.org/wiki/Wikipedia:Signs_of_AI_writing
...wait, I went and looked up that file.
"The Three Optimizations That Would Close the Gap"
You're presenting the data from there in an extremely misleading way! They in no way need to drop any Typescript semantics to go faster.
If you encounter code with the same hash as last time, load up the previously generated binary and run that... or is that already happening?
But this case I think they do know the number type for sure, they're just failing to optimize around that fact.
JIT is a technique to accelerate dynamic languages at runtime to near machine performance while keeping dynamic ergonomics; but it can't transcend the AOT / runtime wall.
The point is that you can have dynamic language that executes natively.
You can also compile whole program aot in Julia.
Like you really built a backend that lowers to LLVM, integrated it with a generational gc, wrote a cross-platform reactive runtime, and built support for eleven different targets within like a year? Are you just prompting the model to tack on the next coolest thing or do you understand how these features work?
I worry how many of these kinds of projects will show up now. How do you guarantee stability? If there's a memory corruption error in the GC implementation, who's going to debug it?
Now, to tell you the truth, for a lot of tickets AI does generate legit the better answers than me. And my time is quite limited.
That said, I will always stand behind my posts, AI-assisted or not.
Now the thread you specifically point at here is ironic in many ways. The other guy says "and claude won't tell you that", while in fact Claude said (in my response) the exact thing he claimed Claude would not be able to do. Anyways, the technical merrits of the conversation are certainly worthwhile and important.
Do you somehow find yourself with hundreds of comments to write and nary a moment to spare? Do you genuinely think Claude can better answer questions in your domain of expertise than you can? Is English a second language and not one you're comfortable writing in?
There are courses on how to use an LLM to better market to people, to do PR, to communicate with other people. Grifters don't care about the human aspect or whatever, their goal is to grift. Comments and other text are just a means to an end (money).
thanks for bringing up the concern. I am proggeramlug (or amlug, either way), Ralph, founder of perry.
Yes, Perry is heavily AI-assisted - similar to the Bun to Rust port is too. Your concern is real and I don't claim to have all the answers for that.
I do know that AI is much better at finding bugs when it comes to compilers than most humans will ever be. Decoding bytecode and knowing what it should be is nothing humans are good at - AI does it trivially.
So yes, lots of AI, but this is not a "vibe-coded" slop project either. I have decades of experience in programming (native) and so I'm quite confident that this will work out well - but I totally see your skepticism.
The models are good enough that this works.
You can keep disagreeing for a while, but know that almost all the code in the industry is written like this now.
The concern basically boils down to how large and serious is the team and what if they abandon the project in few weeks or months .
These were always the risks, many here have been burned by betting years of their career building against promising but what turned out to be weak projects
OP is alluding to the fact that today commit frequency, lines of code or how active the contributors in the issue trackers are no longer good signals to use as proxy.
When the underlying project to yours is few million lines of code written by machines only it is not going to be feasible fork and maintain or in-house it if the maintainers abandon it
To be clear users of a library or a tool aren’t owed anything when it available gratis and fully open source .
However not everyone has access to unlimited tokens to disregard the quality (in terms of history and usage ) or size of the underlying project completely
IMO the maintenance story is more or less solved if you can keep AI agents refactoring and improving it in a loop.
> However not everyone has access to unlimited tokens
Apologies. I did not consider this when writing my comment, being spoilt by unlimited 'free' AI.
Free in quotes because, presumably, training agents on AI usage from developers is worth more than the cost of providing free AI.
That’s a weak argument, though, if the future of AI is totally unreliable when it comes to cost and quality. Right now I definitely wouldn’t want to depend on being able to infinitely access AI tools for such an important part of the toolchain.
Aside from that it’s just not attractive to trust a project made by one person.
It was a valid concern last year. We have seen tremendous progress on this in the last 4-6 months.
Even if your initial prototypes are unmaintainable slop, the state of the art models are fairly good at refactoring and fixing things.
In many places AI tools aren't even allowed to touch customer repos.
In compiler speak, a runtime provides all infrastructure required by the language for program startup, shutdown, infrastructure for the standard library execution.
Of course there is a (native) runtime, as there is for all languages (like you said) :)
https://docs.perryts.com/language/supported-features.html#ga...
The Perry runtime is surprisiginly small, especially since we (LLVM rather) strip out all that is no needed in one of the compile steps.
`Error: JavaScript modules found but libperry_jsruntime.a not found. Build it with: cargo build --release -p perry-jsruntime`
Turns out jsruntime was removed one week ago, but the error messages probably not have been updated as they should.
https://github.com/PerryTS/perry/commit/848339fa4ee4b00a53f5...
It looks like others had a similar idea too, adding a "sound mode" to TypeScript, such as this project which is converting tsgo to Rust, also with LLMs.
It's already showing results that is nearly 3x faster than tsgo. For multi-file large projects I have some ideas to implement to make it faster there too.
Once tsz is fast and stable I'll shift focus on making sound mode a reality.
https://www.microsoft.com/en-us/research/publication/static-...
Much saner than a vibe coded compiler.
If you'd like to follow, here's my attempt at converting tsgo to typescript (called tsts [1]). Admittedly there's AI involved, but it's a very mechanical job. Going from golang to ts is not a very difficult problem, the other way around would have been way harder. The plan is to then compile tsts to binary via tsonic.
It's mostly a mechanical port. Hoping to do a Show HN maybe in a month.
What? How is this possible, even with something as simple as:
interface Animal {
speak(): string;
}
class Cat implements Animal {}
class Dog implements Animal {}
function makeSound(animal: Animal) {
return animal.speak();
}[1]: https://github.com/PerryTS/perry/blob/39d5ba2e9db5adf7f7fc90...
switch (animal.type)
case Cat: return cat_speak()
case Dog: return dog_speak()
The generated code has the functions resolved in compile time, there's no function pointer lookup in a table happening. I don't know if this is how this project does it, but this is the commonly used technique when you want to do this.I am still inclined to believe AI just made up the documentation though, because this has its own tradeoffs.
Oh yeah, very often. Especially if it's a loop and resolves the same way very often.
Modern CPUs just blaze through code like this, after three decades optimizing for object oriented and dynamic languages.
Otherwise the best you can do is be solid on common special cases. Which is what a modern (non-static) JS/TS runtime is.
There are a bunch of tools that JIT somewhat optimized assembly.
P.S: does it have PGO for collapsing numbers to ints when possible etc
Is that true? It just goes right into the code with no initialization of any other libraries needed?
Would love to see more about it, or see more about the actual compiler docs.
While the UI framework part is neat, I prefer not to force everything into TS. Combining it means UI definitions and semantics get mixed into AST, making the unbundling of them a humongous task in itself.
Exactly the reason I built my own with pretty similar native UI semantics which supports Rust, Go, Kotlin and more (https://hypen.space) - would love to integrate Perry with it to compile TS apps directly into the runtime - but while the idea itself is great, looking at the documentation makes it hard to implement, and a lot of parts seem confusing.
Can I just use the compiler without the rest of the framework? What is the architecture? What are the limits?
After digging through the documentation, I'm unfortunately just more confused honestly. There are dozens of packages and slop markdown files such as `BUG_STRING_COMPARISON.md` and or `PERRY_UI_IMPLEMENTATION.md` which is an instruction file left for the LLM that just makes me trust the project less.
So while the idea is cool and the performance seems cool, the AI slop presentation would definitely need improvement. Adding a human touch would make it much, much better, as one could actually understand what they are dealing with.
app.get('/api/auth/callback', async (request: any, reply: any) => {
const params: any = request.query || {};
const code = params.code || '';
const state = params.state || '';
// Exchange code for token via curl
const tokenResult = curlExec(
'curl -s -X POST "https://github.com/login/oauth/access_token" -H "Accept: application/json" -d "client_id=' + GITHUB_CLIENT_ID + '&client_secret=' + GITHUB_CLIENT_SECRET + '&code=' + code + '"'
);
Shell injection?