Summary
Node path.resolve() is a lexical path-string operation. It resolves ./.., joins segments, and makes the result absolute, but it does not inspect the filesystem or follow symlinks.
Perry currently calls std::fs::canonicalize() from js_path_resolve(), so existing paths can be rewritten to their realpath. That makes path.resolve() depend on whether the target exists and where symlinks point, which differs from Node.
Node 25.9.0 behavior
const fs = require("node:fs");
const os = require("node:os");
const path = require("node:path");
const dir = fs.mkdtempSync(path.join(os.tmpdir(), "resolve-symlink-"));
fs.mkdirSync(path.join(dir, "real"));
fs.writeFileSync(path.join(dir, "real", "file.txt"), "");
fs.symlinkSync("real", path.join(dir, "link"));
const target = path.join(dir, "link", "file.txt");
console.log(path.resolve(target));
console.log(fs.realpathSync(target));
console.log(path.resolve(target) === fs.realpathSync(target));
On macOS with Node v25.9.0 this prints different strings and false: path.resolve() preserves the lexical link/file.txt path while fs.realpathSync() follows the symlink to real/file.txt (and may also rewrite /var to /private/var).
Perry source evidence
crates/perry-runtime/src/path.rs:505 implements js_path_resolve() by calling std::fs::canonicalize(&path_str) and returning that path when it succeeds.
crates/perry-runtime/src/object/native_module_dispatch.rs:123 chains dynamic path.resolve() arguments and then calls js_path_resolve(result), so the final result is still canonicalized.
crates/perry-codegen/src/expr/array_methods.rs:232 lowers compiled PathResolve to the same js_path_resolve() runtime helper.
Expected
path.resolve() and path.posix.resolve() should be pure lexical operations. They should not call canonicalize, should not require the path to exist, and should not resolve symlinks. Existing paths and missing paths should be handled by the same normalization algorithm.
Duplicate checks
path.resolve canonicalize
path.resolve realpath symlink
node:path resolve filesystem
path resolve symlink
path.resolve symlink canonicalize lexical
These searches did not show an existing node:path issue for lexical path.resolve() semantics; the symlink hits were unrelated node:fs issues.
Summary
Node
path.resolve()is a lexical path-string operation. It resolves./.., joins segments, and makes the result absolute, but it does not inspect the filesystem or follow symlinks.Perry currently calls
std::fs::canonicalize()fromjs_path_resolve(), so existing paths can be rewritten to their realpath. That makespath.resolve()depend on whether the target exists and where symlinks point, which differs from Node.Node 25.9.0 behavior
On macOS with Node v25.9.0 this prints different strings and
false:path.resolve()preserves the lexicallink/file.txtpath whilefs.realpathSync()follows the symlink toreal/file.txt(and may also rewrite/varto/private/var).Perry source evidence
crates/perry-runtime/src/path.rs:505implementsjs_path_resolve()by callingstd::fs::canonicalize(&path_str)and returning that path when it succeeds.crates/perry-runtime/src/object/native_module_dispatch.rs:123chains dynamicpath.resolve()arguments and then callsjs_path_resolve(result), so the final result is still canonicalized.crates/perry-codegen/src/expr/array_methods.rs:232lowers compiledPathResolveto the samejs_path_resolve()runtime helper.Expected
path.resolve()andpath.posix.resolve()should be pure lexical operations. They should not callcanonicalize, should not require the path to exist, and should not resolve symlinks. Existing paths and missing paths should be handled by the same normalization algorithm.Duplicate checks
path.resolve canonicalizepath.resolve realpath symlinknode:path resolve filesystempath resolve symlinkpath.resolve symlink canonicalize lexicalThese searches did not show an existing
node:pathissue for lexicalpath.resolve()semantics; the symlink hits were unrelatednode:fsissues.