Found by the package audit (compiling real npm packages from source instead of Perry's native bindings) on
Perry e6dcb62 (v0.5.1587), Linux x64. A named export of a Node builtin (crypto, fs, path, ...) that is re-exported from a user/package module is unusable in the importer: the export { x } from "<builtin>" form produces an undefined perry_fn_<module>__<x> symbol at link time, and the import { x } from "<builtin>"; export { x } form links but the imported value returns undefined when called.
Reproduction
Case 1 (link error):
mkdir case1 && cd case1
echo '{"name":"r","private":true,"type":"module"}' > package.json
cat > dep.ts <<'EOF'
export { createHash } from "crypto";
EOF
cat > main.ts <<'EOF'
import { createHash } from "./dep.ts";
console.log(createHash("sha256").update("x").digest("hex"));
EOF
node main.ts
perry compile main.ts -o out && ./out
Case 2 (links, wrong value):
mkdir case2 && cd case2
echo '{"name":"r","private":true,"type":"module"}' > package.json
cat > dep.ts <<'EOF'
import { createHash } from "crypto";
export { createHash };
EOF
cat > main.ts <<'EOF'
import { createHash } from "./dep.ts";
console.log(typeof createHash);
console.log(createHash("sha256").update("x").digest("hex"));
EOF
node main.ts
perry compile main.ts -o out && ./out
Expected (Node 26.5.1)
Case 1:
2d711642b726b04401627ca9fbac32f5c8530fb1903cc4db02258717921a4881
Case 2:
function
2d711642b726b04401627ca9fbac32f5c8530fb1903cc4db02258717921a4881
Actual (Perry)
Case 1 (object-file path shortened, strip-dedup lines omitted):
/usr/bin/ld: node_modules/.cache/perry/objects/host/<hash>.o: in function `main':
perry_module:(.text+0x4a): undefined reference to `perry_fn_dep_ts__createHash'
collect2: error: ld returned 1 exit status
Error: Linking failed
Case 2:
function
TypeError: Cannot read properties of undefined (reading 'update')
at <anonymous>
Same result with the default build and with PERRY_NO_AUTO_OPTIMIZE=1.
Impact
- ethers 6.17.0:
src.ts/crypto/crypto.ts:1-3 is export { createHash, createHmac, pbkdf2Sync, randomBytes } from "crypto"; -> link fails with undefined reference to perry_fn_..._crypto_crypto_ts__createHmac (and pbkdf2Sync, randomBytes, createHash).
- jimp 1.6.1:
@jimp/file-ops/dist/esm/index.js:2 is export { existsSync } from "fs"; (imported by @jimp/core/dist/esm/index.js:4) -> undefined reference to perry_fn_node_modules__jimp_file_ops_dist_esm_index_js__existsSync.
- Any barrel/facade module that forwards builtin functions (a common pattern for isomorphic packages that swap
crypto/fs per platform).
Notes
Variant matrix (all with a local dep module and main.ts importing from it; Node prints the correct value for every row):
| dep module |
Perry |
export { createHash } from "crypto" (.ts or .js) |
link error perry_fn_dep_ts__createHash |
export { existsSync } from "fs" (.js) |
link error perry_fn_dep_js__existsSync |
export { join } from "node:path" |
link error perry_fn_dep_ts__join |
export * from "node:crypto" |
link error __perry_wrap_perry_fn_dep_ts__createHash / perry_fn_dep_ts__createHash |
case-1 dep, but importer uses import * as dep from "./dep.ts" |
links; typeof dep.createHash is undefined |
import { existsSync } from "fs"; export { existsSync } |
links; typeof is function, call returns undefined |
import { join } from "path"; export { join } |
links; typeof is function, call returns undefined |
import * as c from "crypto"; export const createHash = c.createHash |
works |
import { createHash as h } from "crypto"; export function createHash(a) { return h(a); } |
works |
export { hello } from "./impl.ts" (user module, not a builtin) |
works |
builtin used as a value inside one module (const h = createHash; h("sha256")) |
works |
Suspected locations (inferred, not verified):
- Case 1: the driver's re-export propagation only records an origin when the re-export source resolves to a compiled module (
crates/perry/src/commands/compile/run_pipeline.rs:1913-1950, resolve_import_with_context(...) returns nothing for a builtin). The importer then falls back to "the name is a function defined in the immediate module" (run_pipeline.rs:4425-4469: when no origin is known, import_function_prefixes gets the immediate module's prefix), so codegen references perry_fn_dep_ts__createHash, which nothing defines. HIR records the specifier as Export::ReExport (crates/perry-hir/src/lower/module_decl.rs:1491) with no builtin-specific handling.
- Case 2: the local builtin import is exported as a plain
Export::Named (crates/perry-hir/src/lower/module_decl.rs:1562); the exported slot does not hold the callable builtin value.
The same "unresolved named import becomes a perry_fn_ reference" fallback produces #10433.
Found by the package audit (compiling real npm packages from source instead of Perry's native bindings) on
Perry e6dcb62 (v0.5.1587), Linux x64. A named export of a Node builtin (
crypto,fs,path, ...) that is re-exported from a user/package module is unusable in the importer: theexport { x } from "<builtin>"form produces an undefinedperry_fn_<module>__<x>symbol at link time, and theimport { x } from "<builtin>"; export { x }form links but the imported value returnsundefinedwhen called.Reproduction
Case 1 (link error):
Case 2 (links, wrong value):
Expected (Node 26.5.1)
Case 1:
Case 2:
Actual (Perry)
Case 1 (object-file path shortened, strip-dedup lines omitted):
Case 2:
Same result with the default build and with
PERRY_NO_AUTO_OPTIMIZE=1.Impact
src.ts/crypto/crypto.ts:1-3isexport { createHash, createHmac, pbkdf2Sync, randomBytes } from "crypto";-> link fails withundefined reference to perry_fn_..._crypto_crypto_ts__createHmac(andpbkdf2Sync,randomBytes,createHash).@jimp/file-ops/dist/esm/index.js:2isexport { existsSync } from "fs";(imported by@jimp/core/dist/esm/index.js:4) ->undefined reference to perry_fn_node_modules__jimp_file_ops_dist_esm_index_js__existsSync.crypto/fsper platform).Notes
Variant matrix (all with a local
depmodule andmain.tsimporting from it; Node prints the correct value for every row):export { createHash } from "crypto"(.ts or .js)perry_fn_dep_ts__createHashexport { existsSync } from "fs"(.js)perry_fn_dep_js__existsSyncexport { join } from "node:path"perry_fn_dep_ts__joinexport * from "node:crypto"__perry_wrap_perry_fn_dep_ts__createHash/perry_fn_dep_ts__createHashimport * as dep from "./dep.ts"typeof dep.createHashisundefinedimport { existsSync } from "fs"; export { existsSync }typeofisfunction, call returnsundefinedimport { join } from "path"; export { join }typeofisfunction, call returnsundefinedimport * as c from "crypto"; export const createHash = c.createHashimport { createHash as h } from "crypto"; export function createHash(a) { return h(a); }export { hello } from "./impl.ts"(user module, not a builtin)const h = createHash; h("sha256"))Suspected locations (inferred, not verified):
crates/perry/src/commands/compile/run_pipeline.rs:1913-1950,resolve_import_with_context(...)returns nothing for a builtin). The importer then falls back to "the name is a function defined in the immediate module" (run_pipeline.rs:4425-4469: when no origin is known,import_function_prefixesgets the immediate module's prefix), so codegen referencesperry_fn_dep_ts__createHash, which nothing defines. HIR records the specifier asExport::ReExport(crates/perry-hir/src/lower/module_decl.rs:1491) with no builtin-specific handling.Export::Named(crates/perry-hir/src/lower/module_decl.rs:1562); the exported slot does not hold the callable builtin value.The same "unresolved named import becomes a
perry_fn_reference" fallback produces #10433.