Repro
import * as fs from "node:fs";
import * as os from "node:os";
import * as crypto from "node:crypto";
console.log("fs.constants type:", typeof fs.constants);
console.log("fs.constants.F_OK:", fs.constants?.F_OK);
console.log("fs.constants.O_RDONLY:", fs.constants?.O_RDONLY);
console.log("os.constants type:", typeof os.constants);
console.log("os.constants.signals.SIGINT:", os.constants?.signals?.SIGINT);
console.log("os.constants.errno.ENOENT:", os.constants?.errno?.ENOENT);
console.log("os.constants.priority.PRIORITY_NORMAL:", os.constants?.priority?.PRIORITY_NORMAL);
console.log("os.constants.dlopen.RTLD_LAZY:", os.constants?.dlopen?.RTLD_LAZY);
console.log("crypto.constants type:", typeof crypto.constants);
console.log("crypto.constants.RSA_PKCS1_PADDING:", crypto.constants?.RSA_PKCS1_PADDING);
Actual (Perry)
fs.constants type: undefined
fs.constants.F_OK: undefined
fs.constants.O_RDONLY: undefined
os.constants type: undefined
os.constants.signals.SIGINT: undefined
os.constants.errno.ENOENT: undefined
os.constants.priority.PRIORITY_NORMAL: undefined
os.constants.dlopen.RTLD_LAZY: undefined
crypto.constants type: undefined
crypto.constants.RSA_PKCS1_PADDING: undefined
Expected (Node)
fs.constants type: object
fs.constants.F_OK: 0
fs.constants.O_RDONLY: 0
os.constants type: object
os.constants.signals.SIGINT: 2
os.constants.errno.ENOENT: 2
os.constants.priority.PRIORITY_NORMAL: 0
os.constants.dlopen.RTLD_LAZY: 5
crypto.constants type: object
crypto.constants.RSA_PKCS1_PADDING: 1
Impact
These three constant tables are ubiquitous in Node ecosystem code:
fs.constants.F_OK / R_OK / W_OK / X_OK — used by fs.access(path, fs.constants.R_OK) and similar permission probes. ~50 entries total (file mode bits, open flags, copyfile flags, dirent type tags).
os.constants.signals.* — every signal name to number (SIGINT, SIGTERM, SIGHUP, etc.); used by process.kill(pid, signal) and process.on('SIGINT') setups. ~30 entries on POSIX.
os.constants.errno.* — POSIX errno codes (ENOENT, EACCES, etc.); used to compare err.errno in error handlers. ~80 entries.
os.constants.priority.* — process priority levels (PRIORITY_NORMAL, PRIORITY_LOW, etc.) used by os.setPriority.
os.constants.dlopen.* — dlopen flags (RTLD_LAZY, RTLD_NOW, RTLD_GLOBAL, etc.).
crypto.constants.* — RSA padding, SSL_OP_, ENGINE_METHOD_ used by every cipher/sign/verify code path. ~100 entries.
All three are pure static value tables — no runtime logic, no platform branching beyond what's already in crates/perry-runtime. Filling them is mechanical: define the constant maps once, expose them as namespace-keyed lookups when the user imports the parent module.
Currently every consumer that imports node:fs/os/crypto and references .constants.X gets undefined and silently mis-behaves (e.g. fs.access(path, fs.constants.R_OK) becomes fs.access(path, undefined), which Node treats as F_OK).
The signal-name → number table is also blocking test_parity_os from running its full surface — when os.constants.signals is undefined, the next Object.keys(...) call short-circuits the whole test through the catch arm, masking real signal-handling bugs.
Acceptance
The 11-line repro above prints output byte-identical to Node.
Related
Surfaced via test-files/test_parity_fs.ts (fs.constants entirely absent — accounts for ~44 of the test's 409 diff lines), test_parity_os.ts (the test only completes 15 of 60 lines because the constants table is missing), and test_parity_crypto.ts (135 of 199 lines short — partly because crypto.constants.* is referenced throughout the cipher/sign/verify probes).
Repro
Actual (Perry)
Expected (Node)
Impact
These three constant tables are ubiquitous in Node ecosystem code:
fs.constants.F_OK/R_OK/W_OK/X_OK— used byfs.access(path, fs.constants.R_OK)and similar permission probes. ~50 entries total (file mode bits, open flags, copyfile flags, dirent type tags).os.constants.signals.*— every signal name to number (SIGINT,SIGTERM,SIGHUP, etc.); used byprocess.kill(pid, signal)andprocess.on('SIGINT')setups. ~30 entries on POSIX.os.constants.errno.*— POSIX errno codes (ENOENT,EACCES, etc.); used to compareerr.errnoin error handlers. ~80 entries.os.constants.priority.*— process priority levels (PRIORITY_NORMAL,PRIORITY_LOW, etc.) used byos.setPriority.os.constants.dlopen.*— dlopen flags (RTLD_LAZY,RTLD_NOW,RTLD_GLOBAL, etc.).crypto.constants.*— RSA padding, SSL_OP_, ENGINE_METHOD_ used by every cipher/sign/verify code path. ~100 entries.All three are pure static value tables — no runtime logic, no platform branching beyond what's already in
crates/perry-runtime. Filling them is mechanical: define the constant maps once, expose them as namespace-keyed lookups when the user imports the parent module.Currently every consumer that imports
node:fs/os/cryptoand references.constants.Xgetsundefinedand silently mis-behaves (e.g.fs.access(path, fs.constants.R_OK)becomesfs.access(path, undefined), which Node treats asF_OK).The signal-name → number table is also blocking
test_parity_osfrom running its full surface — whenos.constants.signalsis undefined, the nextObject.keys(...)call short-circuits the whole test through the catch arm, masking real signal-handling bugs.Acceptance
The 11-line repro above prints output byte-identical to Node.
Related
Surfaced via
test-files/test_parity_fs.ts(fs.constants entirely absent — accounts for ~44 of the test's 409 diff lines),test_parity_os.ts(the test only completes 15 of 60 lines because the constants table is missing), andtest_parity_crypto.ts(135 of 199 lines short — partly becausecrypto.constants.*is referenced throughout the cipher/sign/verify probes).