You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Tracking issue: what it takes to compile a standard Express app with Perry.
I took a textbook Express 5 app, installed real Express (express@5.2.1, 65 packages in the tree, all plain CommonJS/JS), and drove it through perry compile, removing one blocker at a time to surface the entire chain rather than stopping at the first error.
Headline finding (the good news): Express's whole dependency tree is essentially TypeScript-subset-compatible — all 65 packages route to native compilation; none require the (removed) V8 runtime. Modern JS like Express really does compile the same as TS here. With a handful of escape hatches + two tiny dependency shims, Perry emits an 11.6 MB native binary (COMPILE_EXIT=0).
The catch: that binary is a false positive. The compiler tolerates 49 per-module codegen failures and links the broken objects in anyway, so the binary Bus error: 10 on launch with zero output. The real central blocker is a codegen bug, not the JS/TS divide.
Repro
// app.tsimportexpressfrom'express';constapp=express();app.get('/',(req,res)=>res.send('Hello World!'));app.get('/json',(req,res)=>res.json({message: 'hello',value: 42}));app.listen(3000,()=>console.log('listening on port 3000'));
Tested on main @ bef6c3530, macOS arm64, Apple clang 21.0.0.
The blocker chain (in the order you hit them)
1. Bare import of a JS package → V8-removed gate
With no config, express/index.js (CJS) routes to the removed JS runtime:
Error: JavaScript runtime (V8) support has been removed... pulled in a JS runtime via: express/index.js [express]
2. #497 trust gate
Adding perry.compilePackages: ["express"] then trips:
package 'express' is in compilePackages but not in perry.allow.compilePackages
Fix: also set perry.allow.compilePackages, or PERRY_ALLOW_PERRY_FEATURES=1.
3. Opt-in is not transitive — must enumerate all 65 packages
Even with both flags set for express, all 28+ transitive deps individually route to V8:
still routed to runtime JavaScript because they are not in perry.compilePackages: accepts, body-parser, content-disposition, ... vary
You must list every package in the tree (direct + transitive) in bothcompilePackages and allow.compilePackages.
4. 🐛 The "*" wildcard is broken for compile-routing
compilePackages: ["*"] + allow.compilePackages: ["*"] passes the trust gate (no #497 error) but the wildcard is not honored by native-compile routing — express is treated as un-opted-in and immediately routes to V8:
Compile package: *
JS module: express -> .../express/index.js
Error: JavaScript runtime (V8) support has been removed... express/index.js [express]
So "*" is strictly worse than naming "express". This is a standalone bug worth fixing first — it's the natural "compile everything" ergonomic and it silently does nothing for routing.
5. new Function(...) / Function(...) refused (#1677) — eval-family
With the full tree enumerated, the first hard language error:
depdindex.js:436 — new Function(...) (builds deprecation wrappers from a code string to preserve arity).
function-bindimplementation.js:85 — Function(...). This one is deep and ubiquitous (pulled in by get-intrinsic, which nearly everything depends on).
Both are trivially replaceable with ordinary closures. PERRY_ALLOW_EVAL=1 bypasses but yields non-functional behavior.
After shimming depd: buffer.hasOwnProperty is not implemented (and likely more behind it). PERRY_ALLOW_UNIMPLEMENTED=1 skips the whole category to surface deeper errors.
7. 🐛 CENTRAL BLOCKER — codegen emits references to undefined @perry_closure_* globals
With eval + unimplemented + features all bypassed and depd/function-bind shimmed, the frontend fully succeeds (Found 129 module(s): 129 native, 0 JavaScript), then codegen produces invalid LLVM IR for 49 modules. Every failure is the same shape:
clang -c failed (status=exit status: 1)
.../perry_llvm_*.ll:9153:44: error: use of undefined value '@perry_closure_node_modules_body_parser_lib_read_js__7'
9153 | call void @js_register_function_name(ptr @perry_closure_..._read_js__7, ptr @.str.1, i32 10)
js_register_function_name is handed a @perry_closure_<module>__N global that the module's IR never defines. 49 modules fail this; the build links the surviving objects anyway and reports success → the resulting binary Bus error: 10 immediately.
This is the bug to fix to make Express actually work. Two parts:
(a) Codegen must emit (or not reference) the @perry_closure_* symbol consumed by js_register_function_name. Likely a named-function-expression / closure-naming path that registers a name for a closure global that wasn't materialized in this module.
(b) A per-module clang -c failure must fail the whole compile, not silently drop the object and link a broken binary. Right now COMPILE_EXIT=0 with 49 codegen failures is actively misleading.
The breadth (core utility packages like get-intrinsic/has-symbols/side-channel) suggests this is one codegen bug hit by a common JS pattern, not 30 separate problems.
8. Non-fatal noise (won't block, but worth tracking)
Warning: Could not resolve import 'ejs' — Express's optional view-engine require; harmless for a JSON/text API.
Could not resolve import 'supports-color' — debug's optional color dep.
Many unknown identifier 'window'/'navigator'/'document'/'localStorage'/'global'/'HTMLElement'/'Atomics'/'Float16Array' warnings — universal-module browser branches that don't execute server-side.
Assignment to undeclared variable '$gOPD', creating implicit local (gopd) — likely benign but worth confirming it doesn't change semantics.
What "yes, Express compiles" actually requires
In rough priority:
Fix About README #7(a) — the @perry_closure_* undefined-symbol codegen bug. This is the one that matters. Without it, nothing downstream runs.
Fix About README #7(b) — make per-module clang -c failure abort the build. The silent-link-broken-binary behavior masks About README #7(a) as success.
Ergonomics — transitive opt-in: enumerating 65 packages by hand is impractical. Consider auto-including the transitive closure of a named package, once trusted.
Then re-test runtime: app.listen + routing + res.send/res.json. Raw node:http is already well-supported, so the foundation is there — but it's untested for Express until About README #7 lands.
Notes / methodology
Each layer was verified empirically by re-running the compile with the prior blocker removed (escape-hatch env vars + minimal closure-based shims for depd/function-bind). Logs available on request.
Summary
Tracking issue: what it takes to compile a standard Express app with Perry.
I took a textbook Express 5 app, installed real Express (
express@5.2.1, 65 packages in the tree, all plain CommonJS/JS), and drove it throughperry compile, removing one blocker at a time to surface the entire chain rather than stopping at the first error.Headline finding (the good news): Express's whole dependency tree is essentially TypeScript-subset-compatible — all 65 packages route to native compilation; none require the (removed) V8 runtime. Modern JS like Express really does compile the same as TS here. With a handful of escape hatches + two tiny dependency shims, Perry emits an 11.6 MB native binary (
COMPILE_EXIT=0).The catch: that binary is a false positive. The compiler tolerates 49 per-module codegen failures and links the broken objects in anyway, so the binary
Bus error: 10on launch with zero output. The real central blocker is a codegen bug, not the JS/TS divide.Repro
npm install express # express@5.2.1, 65 packages perry compile app.ts -o app_binTested on
main@bef6c3530, macOS arm64, Apple clang 21.0.0.The blocker chain (in the order you hit them)
1. Bare
importof a JS package → V8-removed gateWith no config,
express/index.js(CJS) routes to the removed JS runtime:2.
#497trust gateAdding
perry.compilePackages: ["express"]then trips:Fix: also set
perry.allow.compilePackages, orPERRY_ALLOW_PERRY_FEATURES=1.3. Opt-in is not transitive — must enumerate all 65 packages
Even with both flags set for
express, all 28+ transitive deps individually route to V8:You must list every package in the tree (direct + transitive) in both
compilePackagesandallow.compilePackages.4. 🐛 The
"*"wildcard is broken for compile-routingcompilePackages: ["*"]+allow.compilePackages: ["*"]passes the trust gate (no #497 error) but the wildcard is not honored by native-compile routing —expressis treated as un-opted-in and immediately routes to V8:So
"*"is strictly worse than naming"express". This is a standalone bug worth fixing first — it's the natural "compile everything" ergonomic and it silently does nothing for routing.5.
new Function(...)/Function(...)refused (#1677) — eval-familyWith the full tree enumerated, the first hard language error:
depdindex.js:436—new Function(...)(builds deprecation wrappers from a code string to preserve arity).function-bindimplementation.js:85—Function(...). This one is deep and ubiquitous (pulled in byget-intrinsic, which nearly everything depends on).Both are trivially replaceable with ordinary closures.
PERRY_ALLOW_EVAL=1bypasses but yields non-functional behavior.6. Unimplemented stdlib surface (#463)
After shimming depd:
buffer.hasOwnProperty is not implemented(and likely more behind it).PERRY_ALLOW_UNIMPLEMENTED=1skips the whole category to surface deeper errors.7. 🐛 CENTRAL BLOCKER — codegen emits references to undefined
@perry_closure_*globalsWith eval + unimplemented + features all bypassed and depd/function-bind shimmed, the frontend fully succeeds (
Found 129 module(s): 129 native, 0 JavaScript), then codegen produces invalid LLVM IR for 49 modules. Every failure is the same shape:js_register_function_nameis handed a@perry_closure_<module>__Nglobal that the module's IR never defines. 49 modules fail this; the build links the surviving objects anyway and reports success → the resulting binaryBus error: 10immediately.This is the bug to fix to make Express actually work. Two parts:
@perry_closure_*symbol consumed byjs_register_function_name. Likely a named-function-expression / closure-naming path that registers a name for a closure global that wasn't materialized in this module.clang -cfailure must fail the whole compile, not silently drop the object and link a broken binary. Right nowCOMPILE_EXIT=0with 49 codegen failures is actively misleading.49 failing modules across 30 packages:
body-parser(6),express(lib/application, request, response, utils, view — 5),negotiator(4),qs(3),router(3),body-parser/lib/types(4),mime-types(2),math-intrinsics(2),has-symbols(2), pluscall-bind-apply-helpers,call-bound,dunder-proto,ee-first,function-bind,get-intrinsic,get-proto,http-errors,iconv-lite,inherits,object-inspect,on-finished,path-to-regexp,proxy-addr,raw-body,send,serve-static,side-channel,side-channel-list,side-channel-map,side-channel-weakmap,statuses(1 each).The breadth (core utility packages like get-intrinsic/has-symbols/side-channel) suggests this is one codegen bug hit by a common JS pattern, not 30 separate problems.
8. Non-fatal noise (won't block, but worth tracking)
Warning: Could not resolve import 'ejs'— Express's optional view-enginerequire; harmless for a JSON/text API.Could not resolve import 'supports-color'— debug's optional color dep.unknown identifier 'window'/'navigator'/'document'/'localStorage'/'global'/'HTMLElement'/'Atomics'/'Float16Array'warnings — universal-module browser branches that don't execute server-side.Assignment to undeclared variable '$gOPD', creating implicit local(gopd) — likely benign but worth confirming it doesn't change semantics.What "yes, Express compiles" actually requires
In rough priority:
@perry_closure_*undefined-symbol codegen bug. This is the one that matters. Without it, nothing downstream runs.clang -cfailure abort the build. The silent-link-broken-binary behavior masks About README #7(a) as success."*"in compile-routing (or reject it loudly so it's not a silent no-op).new Function/Functionarity wrappers indepdandfunction-bind(both are 1-liners replaceable with closures), or document them as known-good shims.buffer.hasOwnPropertyand whatever's behind it once About README #7 is fixed and real modules execute).app.listen+ routing +res.send/res.json. Rawnode:httpis already well-supported, so the foundation is there — but it's untested for Express until About README #7 lands.Notes / methodology