diff --git a/lib/codegen_deno.ml b/lib/codegen_deno.ml index dabbe4fc..09239957 100644 --- a/lib/codegen_deno.ml +++ b/lib/codegen_deno.ml @@ -1326,6 +1326,11 @@ let () = (fun a -> Printf.sprintf "__as_fs().statSync(%s).isFile()" (arg 0 a)); b "statIsDirectory" (fun a -> Printf.sprintf "__as_fs().statSync(%s).isDirectory()" (arg 0 a)); + (* Total existence test: false for a missing path, never a throw. The stat + predicates above keep their ENOENT contract (host_profile pins it); this + is the check to make before them (added for kyX). *) + b "pathExists" + (fun a -> Printf.sprintf "__as_fs().existsSync(%s)" (arg 0 a)); b "args" (fun _ -> "(globalThis.process?.argv?.slice(2) ?? [])"); b "exit" (fun a -> Printf.sprintf "__as_process().exit(%s)" (arg 0 a)); diff --git a/stdlib/Deno.affine b/stdlib/Deno.affine index 15a6cf8f..28693fdc 100644 --- a/stdlib/Deno.affine +++ b/stdlib/Deno.affine @@ -78,6 +78,10 @@ pub extern fn statIsFile(path: String) -> Bool; /// Throws on a missing path (pair with `isNotFound` for the absent case). pub extern fn statIsDirectory(path: String) -> Bool; +/// True if `path` exists (file, directory or other), false if it does not. +/// Never throws: the total check to make before the stat predicates above. +pub extern fn pathExists(path: String) -> Bool; + /// Recursive walk under `root` — every file path beneath it, depth-first. /// Mirrors `std/fs/walk` for the common case (no glob filter; callers /// filter by extension). Throws on a missing root via `Deno.readDirSync`. diff --git a/tests/codegen-bun/host_profile.affine b/tests/codegen-bun/host_profile.affine index 144eba72..c9d3ad60 100644 --- a/tests/codegen-bun/host_profile.affine +++ b/tests/codegen-bun/host_profile.affine @@ -13,6 +13,7 @@ extern fn mkdirRecursive(path: String) -> Int; extern fn statSize(path: String) -> Int; extern fn statIsFile(path: String) -> Bool; extern fn statIsDirectory(path: String) -> Bool; +extern fn pathExists(path: String) -> Bool; extern fn bytesLength(value: Bytes) -> Int; extern fn bytesByteAt(value: Bytes, offset: Int) -> Int; extern fn args() -> [String]; @@ -27,6 +28,7 @@ pub fn make_directory(path: String) -> Int { mkdirRecursive(path) } pub fn file_size(path: String) -> Int { statSize(path) } pub fn is_file(path: String) -> Bool { statIsFile(path) } pub fn is_directory(path: String) -> Bool { statIsDirectory(path) } +pub fn exists(path: String) -> Bool { pathExists(path) } pub fn argument_count() -> Int { len(args()) } pub fn environment_value(name: String) -> String { diff --git a/tests/codegen-bun/host_profile.harness.mjs b/tests/codegen-bun/host_profile.harness.mjs index b8566078..c31aa5e1 100644 --- a/tests/codegen-bun/host_profile.harness.mjs +++ b/tests/codegen-bun/host_profile.harness.mjs @@ -46,6 +46,9 @@ try { missingPathThrew = true; } if (!missingPathThrew) throw new Error("remove did not make path absent"); + // pathExists is the total check: false for the removed file, never a throw. + if (subject.exists(file) !== false) throw new Error("pathExists on a missing path is not false"); + if (subject.exists(nested) !== true) throw new Error("pathExists on a directory is not true"); } finally { rmSync(root, { recursive: true, force: true }); }