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
{{ message }}
Repository navigation
link: perry-ext-* staticlibs bundle a feature-stripped perry-runtime that wins the link — regex/Temporal silently degrade (blocks Express boot) #6303
String.prototype.replace(re, fn) invokes the replacer callback zero times for a regex on the fancy-regex fallback path (backreference + lookahead) — but only inside a large module graph. The same code in isolation works. The result is a silent wrong answer ([] instead of the parsed path), not a crash.
This is what currently stops a standard Express app from booting.
Symptom
$ ./app_bin
SyntaxError: intrinsic %% does not exist!
$ curl localhost:3000/ -> status=000 (never listens)
Localization
get-intrinsic's stringToPath uses rePropName, which carries a backreference (\2) and a lookahead — i.e. per CLAUDE.md, perry-runtime/src/regex.rs falls back from the regex crate to fancy-regex for it.
Probe inserted into node_modules/get-intrinsic/index.js:
[P] in="%String.prototype.indexOf%"
[P] uncurried $replace cb=0 result=[] <- callback never fires
[P] direct .replace cb=0 direct=[] <- not the uncurry-bind either
[P] rePropName.exec ="String" source.len=97 flags=g <- regex object is INTACT
[P] FRESH simple regex=["String","prototype","indexOf"] <- replace+callback works generally
Root cause found and fixed in #6313. Two corrections to the framing above, because both mattered for finding it:
It is not fancy-regex-specific, and it is not scale-emergent.
A plain /[^.]+/g fails identically, and a 3-module program reproduces it:
import*ashttpfrom'http';// <- the actual triggerconstre=/[^.]+/g;// plain regex, no backref, no lookaheadconstholder={ re };// read via property => codegen can't type it as RegExpconsole.log('a.b.c'.replace(holder.re,(m)=>m.toUpperCase()));// node: A.B.C perry: a.b.c (callback fired 0 times)
The real discriminator is "does the program link a perry-ext-* archive" — Express does (http.createServer), the 35-module graph didn't. rePropName being fancy is a coincidence of it being a module-level var, which is what routes it through the dynamic dispatcher.
What actually happens
perry-ext-* crates are staticlibs, so each one bundles the perry-runtime rlib objects into libperry_ext_*.a. The workspace dep is default-features = false, so a per-crate cargo build -p perry-ext-http (what release-packages.yml does in its ext loop) bundles a perry-runtime with regex-engine / temporalcompiled out:
strip_bundled_runtime_from_well_known_lib should drop those bundled CGUs, but it matches members by name containment against stdlib — a different feature set means a different CGU hash, so the strip silently no-ops. And ext archives are linked before stdlib/runtime (prefer_well_known_before_stdlib), so the stripped copy wins the link.
The kicker: the dispatchers those features gate are exported unconditionally, with the #[cfg]inside the function body:
pubextern"C"fnjs_string_replace_search_dyn(s,needle,replacement) -> *mutStringHeader{#[cfg(feature = "regex-engine")]// <- gone in the stripped copyifletSome(re) = needle_regex_ptr(needle){returnjs_string_replace_regex_dyn(...);}js_string_replace_string_dyn(s,js_string_coerce(needle), replacement)// <- always taken}
So the RegExp got ToString-coerced and searched for literally. No match => callback never fires => replace returns the subject unchanged => stringToPath returns [] => '%' + '' + '%' => %%.
That also explains the probe results exactly: js_regexp_exec and js_string_replace_regex_fn are #[cfg]-gated, so they don't exist in the stripped copy and always resolve to the real runtime — which is why .exec() and a regex literal at the callsite kept working, and only the runtime-dispatched searchValue shape broke.
Fix (#6313): every perry-ext-* crate that links perry-runtime now requests its default feature set, so the bundled copy can't be a silently-degraded subset. (regex-engine alone isn't enough — native_call_method.rs has the same in-body gate for temporal.) Express 5.2.1 now boots and curl localhost:3000/ returns 200.
Correction — this issue's framing was wrong on both counts
Recording this because both wrong premises were load-bearing and would send the next person down a dead end.
It is NOT fancy-regex-specific, and NOT scale-emergent. Minimal repro is three modules with a plain regex:
import*ashttpfrom'http';// <- the actual triggerconstre=/[^.]+/g;// plain: no backreference, no lookaheadconstholder={ re };// via a property, so codegen can't statically type it as RegExp'a.b.c'.replace(holder.re,(m)=>m.toUpperCase());// node: A.B.C// perry: a.b.c (callback fired 0 times)
The real discriminator is "does the program link a perry-ext-* archive". Express does (http.createServer). The 35-module control graph didn't. Module count was a coincidence, and the backreference/lookahead was a red herring.
Actual root cause — a feature-stripped duplicate runtime wins the link
perry-ext-* crates are staticlibs, so each bundles the perry-runtime rlib objects into libperry_ext_*.a. The workspace dependency is declared default-features = false, so a per-crate cargo build -p perry-ext-http — exactly what release-packages.yml does — bundles a perry-runtime with regex-engine and temporal compiled out.
strip_bundled_runtime_from_well_known_lib is supposed to remove those CGUs, but it matches members by name containment. A different feature set produces a different CGU hash, so the strip silently no-ops. Ext archives are linked before stdlib/runtime, so the crippled copy wins the link.
The kicker, and why this is silent rather than a link error: the dispatchers those features gate are exported unconditionally, with the #[cfg]inside the function body. js_string_replace_search_dyn still exists — just with RegExp detection compiled out — so the RegExp got ToString-coerced and searched for literally.
That also explains every probe in the original report: js_regexp_exec and js_string_replace_regex_fn are #[cfg]-gated, so they don't exist in the stripped copy and always resolve to the real runtime. Which is exactly why .exec() and a regex literal kept working while only the runtime-dispatched shape broke.
Scope
The bug class was latent for Temporal too (native_call_method.rs has the same in-body gate). Fix in #6313 makes all ten ext crates that link perry-runtime request its default feature set.
A second, independent defect of the same duplicate-runtime shape — the ext archive's no-op js_stdlib_init_dispatch stub shadowing perry-stdlib's real one, so node:http servers get no tokio reactor — is filed separately as #6314. #6303 + #6314 together are what a release build needs.
changed the title [-]runtime: String.prototype.replace(re, fn) drops all callback invocations on the fancy-regex path at graph scale (blocks Express boot)[/-][+]link: perry-ext-* staticlibs bundle a feature-stripped perry-runtime that wins the link — regex/Temporal silently degrade (blocks Express boot)[/+]on Jul 12, 2026
Activity
proggeramlug commentedon Jul 12, 2026
Root cause found and fixed in #6313. Two corrections to the framing above, because both mattered for finding it:
It is not fancy-regex-specific, and it is not scale-emergent.
A plain
/[^.]+/gfails identically, and a 3-module program reproduces it:The real discriminator is "does the program link a
perry-ext-*archive" — Express does (http.createServer), the 35-module graph didn't.rePropNamebeing fancy is a coincidence of it being a module-levelvar, which is what routes it through the dynamic dispatcher.What actually happens
perry-ext-*crates arestaticlibs, so each one bundles the perry-runtime rlib objects intolibperry_ext_*.a. The workspace dep isdefault-features = false, so a per-cratecargo build -p perry-ext-http(whatrelease-packages.ymldoes in its ext loop) bundles a perry-runtime withregex-engine/temporalcompiled out:strip_bundled_runtime_from_well_known_libshould drop those bundled CGUs, but it matches members by name containment against stdlib — a different feature set means a different CGU hash, so the strip silently no-ops. And ext archives are linked before stdlib/runtime (prefer_well_known_before_stdlib), so the stripped copy wins the link.The kicker: the dispatchers those features gate are exported unconditionally, with the
#[cfg]inside the function body:So the RegExp got ToString-coerced and searched for literally. No match => callback never fires =>
replacereturns the subject unchanged =>stringToPathreturns[]=>'%' + '' + '%'=>%%.That also explains the probe results exactly:
js_regexp_execandjs_string_replace_regex_fnare#[cfg]-gated, so they don't exist in the stripped copy and always resolve to the real runtime — which is why.exec()and a regex literal at the callsite kept working, and only the runtime-dispatchedsearchValueshape broke.Fix (#6313): every
perry-ext-*crate that links perry-runtime now requests itsdefaultfeature set, so the bundled copy can't be a silently-degraded subset. (regex-enginealone isn't enough —native_call_method.rshas the same in-body gate fortemporal.) Express 5.2.1 now boots andcurl localhost:3000/returns 200.proggeramlug commentedon Jul 12, 2026
Correction — this issue's framing was wrong on both counts
Recording this because both wrong premises were load-bearing and would send the next person down a dead end.
It is NOT fancy-regex-specific, and NOT scale-emergent. Minimal repro is three modules with a plain regex:
The real discriminator is "does the program link a
perry-ext-*archive". Express does (http.createServer). The 35-module control graph didn't. Module count was a coincidence, and the backreference/lookahead was a red herring.Actual root cause — a feature-stripped duplicate runtime wins the link
perry-ext-*crates arestaticlibs, so each bundles the perry-runtime rlib objects intolibperry_ext_*.a. The workspace dependency is declareddefault-features = false, so a per-cratecargo build -p perry-ext-http— exactly whatrelease-packages.ymldoes — bundles a perry-runtime withregex-engineandtemporalcompiled out.strip_bundled_runtime_from_well_known_libis supposed to remove those CGUs, but it matches members by name containment. A different feature set produces a different CGU hash, so the strip silently no-ops. Ext archives are linked before stdlib/runtime, so the crippled copy wins the link.The kicker, and why this is silent rather than a link error: the dispatchers those features gate are exported unconditionally, with the
#[cfg]inside the function body.js_string_replace_search_dynstill exists — just with RegExp detection compiled out — so the RegExp got ToString-coerced and searched for literally.That also explains every probe in the original report:
js_regexp_execandjs_string_replace_regex_fnare#[cfg]-gated, so they don't exist in the stripped copy and always resolve to the real runtime. Which is exactly why.exec()and a regex literal kept working while only the runtime-dispatched shape broke.Scope
The bug class was latent for Temporal too (
native_call_method.rshas the same in-body gate). Fix in #6313 makes all ten ext crates that link perry-runtime request its default feature set.A second, independent defect of the same duplicate-runtime shape — the ext archive's no-op
js_stdlib_init_dispatchstub shadowing perry-stdlib's real one, sonode:httpservers get no tokio reactor — is filed separately as #6314. #6303 + #6314 together are what a release build needs.[-]runtime: String.prototype.replace(re, fn) drops all callback invocations on the fancy-regex path at graph scale (blocks Express boot)[/-][+]link: perry-ext-* staticlibs bundle a feature-stripped perry-runtime that wins the link — regex/Temporal silently degrade (blocks Express boot)[/+]fix(link): ext staticlibs must bundle a full-featured perry-runtime (#…
fix(link): ext staticlibs must bundle a full-featured perry-runtime (#…
fix(regex): keep the UTF-16 index helpers outside the regex-engine gate
fix(regex,hir): UTF-16 indices, own-property method shadowing, RegExp…
test(link): move the ext-runtime-features guard to a per-PR unit test
fix(link): ext staticlibs must not export perry-runtime's stdlib stubs (
chore(ci): refresh workspace-architecture baseline
feat(compile): auto-provision native ext bindings + wasm host
feat(compile): auto-provision native ext bindings + wasm host
feat(compile): auto-provision native ext bindings + wasm host (#7118)