diff --git a/docs/src/generated/node-v24-compatibility-meta.json b/docs/src/generated/node-v24-compatibility-meta.json index 6e92ff856..71a272129 100644 --- a/docs/src/generated/node-v24-compatibility-meta.json +++ b/docs/src/generated/node-v24-compatibility-meta.json @@ -2,6 +2,6 @@ "schemaVersion": 3, "nodeVersion": "24.22.0", "nodeCommit": "f14cd3aa7bf9ca8df2cc5b8bb14beb0f335b1d93", - "artifactVersion": "88ef6fabee3e41b50d74", + "artifactVersion": "35b26942ca49ff006818", "rowCount": 3880 } diff --git a/docs/src/generated/node-v24-compatibility.json b/docs/src/generated/node-v24-compatibility.json index 83e26f998..17b7ce9f7 100644 --- a/docs/src/generated/node-v24-compatibility.json +++ b/docs/src/generated/node-v24-compatibility.json @@ -314,9 +314,9 @@ "apiEntries": 280, "static": { "supported": 7, - "partial": 98, + "partial": 101, "refused": 0, - "not-implemented": 175, + "not-implemented": 172, "by-design": 0, "unreviewed": 0, "not-applicable": 0 @@ -30840,9 +30840,9 @@ "inherited": true }, "static": { - "status": "not-implemented", - "detail": "No static compiler lowering is registered for this API yet.", - "verification": "registry-gap" + "status": "partial", + "detail": "Implemented for the documented scriptc module-loader subset.", + "verification": "test-backed" } }, { @@ -31047,9 +31047,9 @@ "inherited": true }, "static": { - "status": "not-implemented", - "detail": "No static compiler lowering is registered for this API yet.", - "verification": "registry-gap" + "status": "partial", + "detail": "Implemented for the documented scriptc module-loader subset.", + "verification": "test-backed" } }, { @@ -31484,9 +31484,9 @@ "inherited": true }, "static": { - "status": "not-implemented", - "detail": "No static compiler lowering is registered for this API yet.", - "verification": "registry-gap" + "status": "partial", + "detail": "Implemented for the documented scriptc module-loader subset.", + "verification": "test-backed" } }, { diff --git a/internal/compatibility/generated/node-v24-backlog.json b/internal/compatibility/generated/node-v24-backlog.json index a427b558d..380278d84 100644 --- a/internal/compatibility/generated/node-v24-backlog.json +++ b/internal/compatibility/generated/node-v24-backlog.json @@ -8,8 +8,8 @@ "tiers": { "static": { "replace-refusal": 37, - "verify-gap": 2472, - "audit-partial": 876, + "verify-gap": 2469, + "audit-partial": 879, "classify": 2, "implement": 85 } @@ -27353,12 +27353,14 @@ "priority": "high", "tiers": { "static": { - "status": "not-implemented", - "action": "verify-gap", - "verification": "registry-gap", - "confidence": "low", - "source": "compiler-unmatched:fs", - "tests": [] + "status": "partial", + "action": "audit-partial", + "verification": "test-backed", + "confidence": "high", + "source": "compiler-feature:fs.fs.mkdir", + "tests": [ + "tests/corpus/5540-fs-callback-forms.cjs" + ] } } }, @@ -27578,12 +27580,14 @@ "priority": "high", "tiers": { "static": { - "status": "not-implemented", - "action": "verify-gap", - "verification": "registry-gap", - "confidence": "low", - "source": "compiler-unmatched:fs", - "tests": [] + "status": "partial", + "action": "audit-partial", + "verification": "test-backed", + "confidence": "high", + "source": "compiler-feature:fs.fs.readFile", + "tests": [ + "tests/corpus/5540-fs-callback-forms.cjs" + ] } } }, @@ -28084,12 +28088,14 @@ "priority": "high", "tiers": { "static": { - "status": "not-implemented", - "action": "verify-gap", - "verification": "registry-gap", - "confidence": "low", - "source": "compiler-unmatched:fs", - "tests": [] + "status": "partial", + "action": "audit-partial", + "verification": "test-backed", + "confidence": "high", + "source": "compiler-feature:fs.fs.write", + "tests": [ + "tests/corpus/5540-fs-callback-forms.cjs" + ] } } }, diff --git a/internal/compatibility/generated/node-v24-internal.json b/internal/compatibility/generated/node-v24-internal.json index ecd001311..160127242 100644 --- a/internal/compatibility/generated/node-v24-internal.json +++ b/internal/compatibility/generated/node-v24-internal.json @@ -326,9 +326,9 @@ "apiEntries": 280, "static": { "supported": 7, - "partial": 98, + "partial": 101, "refused": 0, - "not-implemented": 175, + "not-implemented": 172, "by-design": 0, "unreviewed": 0, "not-applicable": 0 @@ -43746,6 +43746,7 @@ "tests/corpus/3135-fs-file-handle-controls.ts", "tests/corpus/3136-fs-file-handle-options.ts", "tests/corpus/3137-fs-vector-callbacks.cjs", + "tests/corpus/5540-fs-callback-forms.cjs", "tests/corpus/fs-native-realpath.ts", "tests/corpus/fs-read-encodings.ts", "tests/corpus/node24-fs-links-callbacks.cjs", @@ -46098,8 +46099,11 @@ "inherited": true }, "static": { - "status": "not-implemented", - "evidence": "compiler-unmatched:fs" + "status": "partial", + "evidence": "compiler-feature:fs.fs.mkdir", + "tests": [ + "tests/corpus/5540-fs-callback-forms.cjs" + ] }, "anchorSource": "exact" }, @@ -46305,8 +46309,11 @@ "inherited": true }, "static": { - "status": "not-implemented", - "evidence": "compiler-unmatched:fs" + "status": "partial", + "evidence": "compiler-feature:fs.fs.readFile", + "tests": [ + "tests/corpus/5540-fs-callback-forms.cjs" + ] }, "anchorSource": "exact" }, @@ -46918,8 +46925,11 @@ "inherited": true }, "static": { - "status": "not-implemented", - "evidence": "compiler-unmatched:fs" + "status": "partial", + "evidence": "compiler-feature:fs.fs.write", + "tests": [ + "tests/corpus/5540-fs-callback-forms.cjs" + ] }, "anchorSource": "exact" }, diff --git a/internal/compatibility/static-support.json b/internal/compatibility/static-support.json index 48eb01ba2..18351251d 100644 --- a/internal/compatibility/static-support.json +++ b/internal/compatibility/static-support.json @@ -209,6 +209,7 @@ { "chapter": "fs", "symbols": ["fs.statfs"], "status": "partial", "evidence": ["tests/corpus/node24-fs-statfs-callbacks.cjs", "tests/corpus/node24-fs-statfs-options.cjs", "tests/corpus/node24-fs-statfs-errors.cjs", "tests/fixtures/node-types/fs-statfs.ts"] }, { "chapter": "fs", "symbols": ["fs.StatFs"], "status": "partial", "evidence": ["tests/corpus/node24-fs-statfs.ts", "tests/corpus/node24-fs-statfs-options.cjs", "tests/fixtures/node-types/fs-statfs.ts"] }, { "chapter": "fs", "parent": "Class: `fs.StatFs`", "symbols": ["type", "bsize", "blocks", "bfree", "bavail", "files", "ffree"], "status": "supported", "evidence": ["tests/corpus/node24-fs-statfs.ts", "tests/corpus/node24-fs-statfs-options.cjs", "tests/fixtures/node-types/fs-statfs.ts"] }, + { "chapter": "fs", "signatures": ["`fs.mkdir(path[, options], callback)`", "`fs.readFile(path[, options], callback)`", "`fs.write(fd, string[, position[, encoding]], callback)`"], "status": "partial", "evidence": ["tests/corpus/5540-fs-callback-forms.cjs"] }, { "chapter": "fs", "symbols": ["fs.utimes", "fs.futimes", "fs.lutimes"], "status": "partial", "evidence": ["tests/corpus/node24-fs-timestamps-callbacks.cjs", "tests/corpus/node24-fs-timestamps-symlinks.ts", "tests/corpus/node24-fs-timestamps-errors.cjs"] }, { "chapter": "process", "signatures": ["`process.hrtime([time])`", "`process.hrtime.bigint()`"], "status": "partial", "evidence": ["tests/corpus/process-hrtime.js"] }, { "chapter": "errors", "signatures": ["`stackTraceLimit` Type: {number}"], "status": "partial", "evidence": ["tests/corpus/error-stack-limit.ts"] }, diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5540-fs-callback-forms.cjs.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5540-fs-callback-forms.cjs.json new file mode 100644 index 000000000..bf5842c8f --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5540-fs-callback-forms.cjs.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/5540-fs-callback-forms.cjs" + ], + "diags": [] +} diff --git a/packages/runtime/src/scr_bytes_io.c b/packages/runtime/src/scr_bytes_io.c index cef6d1181..5018f8480 100644 --- a/packages/runtime/src/scr_bytes_io.c +++ b/packages/runtime/src/scr_bytes_io.c @@ -794,9 +794,37 @@ static ScrDyn *scr_fs_cb_invoke(ScrStr *member, ScrDyn *const *args, size_t argc const ScrDyn *buffer = NULL; if (!strcmp(op, "readFile")) { const ScrDyn *options = ARG(1); - if (options->kind != SCR_DYN_STR && (!scr_fs_cb_signal(options) || scr_fs_cb_refuse_option(options, "flag"))) goto done; + if (options->kind != SCR_DYN_STR && !scr_fs_cb_signal(options)) goto done; ScrDyn *encoding = options->kind == SCR_DYN_STR ? scr_dyn_retain((ScrDyn *)options) : scr_fs_cb_option(options, "encoding"); - if (encoding) result = scr_fs_read_file_sync_dyn(path, encoding); + ScrDyn *flag = options->kind == SCR_DYN_STR ? NULL : scr_fs_cb_option(options, "flag"); + bool reading_flag = !flag || flag->kind == SCR_DYN_UNDEF || flag->kind == SCR_DYN_NULL || + (flag->kind == SCR_DYN_STR && flag->v.str->len == 1 && flag->v.str->data[0] == 'r'); + if (encoding && reading_flag) result = scr_fs_read_file_sync_dyn(path, encoding); + else if (encoding) { + /* Node opens the file with options.flag ('w+' truncates first), then + * reads the whole descriptor. */ + double opened = scr_fs_cb_open(path, flag, 0666, "r"); + if (opened >= 0 && !scr_exc_pending()) { + bool text = encoding->kind == SCR_DYN_STR && encoding->v.str->len > 0; + if (text) { + ScrStr *content = scr_fs_read_fd_encoded(opened, encoding->v.str); + if (content) { + result = scr_dyn_new_str(content); + scr_str_release(content); + } + } else { + ScrBytes *content = scr_fs_read_fd_bytes(opened); + if (content) { + result = scr_dyn_new_buffer(content); + scr_bytes_release(content); + } + } + int saved = errno; + close((int)opened); + errno = saved; + } + } + scr_dyn_release(flag); scr_dyn_release(encoding); } else if (!strcmp(op, "writeFile") || !strcmp(op, "appendFile")) { const ScrDyn *options = ARG(2); @@ -828,8 +856,13 @@ static ScrDyn *scr_fs_cb_invoke(ScrStr *member, ScrDyn *const *args, size_t argc else if (!strcmp(op, "mkdir")) { const ScrDyn *options = ARG(1); double mode = options->kind == SCR_DYN_NUM ? options->v.num : scr_fs_cb_option_number(options, "mode", 0777); - if (scr_fs_cb_option_bool(options->kind == SCR_DYN_NUM ? scr_dyn_undefined() : options, "recursive")) scr_fs_mkdir_recursive_mode(path, mode); - else scr_fs_mkdir_mode(path, mode); + if (scr_fs_cb_option_bool(options->kind == SCR_DYN_NUM ? scr_dyn_undefined() : options, "recursive")) { + /* The callback receives the first directory created (undefined when + * the path already existed). */ + ScrStr *first = scr_fs_mkdir_recursive_first(path, mode); + if (first && !scr_exc_pending()) result = scr_fs_cb_string(first); + else if (first) scr_str_release(first); + } else scr_fs_mkdir_mode(path, mode); } else if (!strcmp(op, "mkdtemp")) result = scr_fs_cb_string(scr_fs_mkdtemp(path)); else if (!strcmp(op, "rm")) scr_fs_rm_opts_retry(path, scr_fs_cb_option_bool(ARG(1), "recursive"), scr_fs_cb_option_bool(ARG(1), "force"), scr_fs_cb_option_number(ARG(1), "maxRetries", 0), scr_fs_cb_option_number(ARG(1), "retryDelay", 100)); else if (!strcmp(op, "rmdir")) scr_fs_rmdir(path); @@ -892,6 +925,33 @@ static ScrDyn *scr_fs_cb_invoke(ScrStr *member, ScrDyn *const *args, size_t argc #else scr_fs_throw(ENOSYS, "glob", path); #endif + } else if (!strcmp(op, "write") && ARG(1)->kind == SCR_DYN_STR) { + /* write(fd, string[, position[, encoding]], callback): a non-number + * position writes at the current offset; the encoding defaults to + * utf8 (unknown names write utf8, as the binding does). */ + const ScrDyn *string = ARG(1); + const ScrDyn *position = count >= 3 ? ARG(2) : scr_dyn_undefined(); + const ScrDyn *encoding = count >= 4 ? ARG(3) : scr_dyn_undefined(); + ScrStr *codec = encoding->kind == SCR_DYN_STR && scr_bytes_is_encoding(encoding->v.str) + ? scr_str_retain(encoding->v.str) + : scr_str_new("utf8", 4); + if (codec->len == 3 && memcmp(codec->data, "hex", 3) == 0 && string->v.str->len % 2 != 0) { + /* validateEncoding throws at the call, before any write. */ + char reason[64]; + snprintf(reason, sizeof reason, "is invalid for data of length %zu", (size_t)string->v.str->len); + scr_dyn_arg_value_fail("encoding", reason, encoding); + scr_str_release(codec); + return NULL; + } else { + ScrBytes *bytes = scr_buffer_from_dyn(string, codec); + if (bytes) { + double at = position->kind == SCR_DYN_NUM ? position->v.num : -1; + result = scr_dyn_new_num(scr_fs_write_sync(fd, bytes, 0, (double)bytes->len, at)); + scr_bytes_release(bytes); + } + } + scr_str_release(codec); + buffer = string; } else if (!strcmp(op, "read") || !strcmp(op, "write")) { bool readOp = !strcmp(op, "read"); const ScrDyn *options = ARG(1); @@ -899,8 +959,16 @@ static ScrDyn *scr_fs_cb_invoke(ScrStr *member, ScrDyn *const *args, size_t argc if (readOp && !scr_dyn_bytes_is(options, SCR_BYTES_U8)) { ownedBuffer = scr_fs_cb_option(options, "buffer"); buffer = ownedBuffer; } else buffer = options; ScrBytes *bytes = buffer && buffer->kind == SCR_DYN_BYTES ? scr_dyn_bytes_unbox(buffer) : NULL; - if (!bytes) scr_dyn_arg_type_fail("buffer", "an instance of Buffer, TypedArray, or DataView", buffer ? buffer : scr_dyn_undefined()); - else { + if (!bytes) { + /* The argument check throws at the call (Node validates before the + * request is made). */ + scr_dyn_arg_type_fail("buffer", + readOp ? "an instance of Buffer, TypedArray, or DataView" + : "of type string or an instance of Buffer, TypedArray, or DataView", + buffer ? buffer : scr_dyn_undefined()); + scr_dyn_release(ownedBuffer); + return NULL; + } else { double offset = ownedBuffer ? scr_fs_cb_option_number(options, "offset", 0) : scr_fs_cb_number(ARG(2), "offset", 0); double length = ownedBuffer ? scr_fs_cb_option_number(options, "length", bytes->len - offset) : scr_fs_cb_number(ARG(3), "length", bytes->len - offset); double position = ownedBuffer ? scr_fs_cb_option_number(options, "position", -1) : scr_fs_cb_number(ARG(4), "position", -1); diff --git a/packages/runtime/src/scr_lib.c b/packages/runtime/src/scr_lib.c index 66967078d..82ac517d0 100644 --- a/packages/runtime/src/scr_lib.c +++ b/packages/runtime/src/scr_lib.c @@ -4423,6 +4423,51 @@ void scr_fs_mkdir_recursive_mode(ScrStr *path, double mode) { free(buf); } +/* mkdir(p, { recursive: true })'s answer, Node's MKDirpSync first path: + * the first directory its walk creates, or NULL when the path already + * existed. Node tries mkdir on the path and, on ENOENT, on the path cut + * before its last separator, repeatedly; the first mkdir that succeeds + * (its kernel parent exists) is the answer, spelled as cut. Creates the + * path like scr_fs_mkdir_recursive_mode (the exception pending on + * failure). */ +static bool scr_fs_mkdirp_exists(char *buf, size_t len) { + char saved = buf[len]; + buf[len] = 0; + struct stat st; + int rc = len == 0 ? stat(".", &st) : stat(buf, &st); + int e = errno; + buf[len] = saved; + return rc == 0 || e != ENOENT; +} + +ScrStr *scr_fs_mkdir_recursive_first(ScrStr *path, double mode) { + char *buf = malloc(path->len + 1); + if (!buf) { + scr_trap("scriptc: out of memory\n"); + } + memcpy(buf, path->data, path->len + 1); + size_t cur = path->len, first = 0; + while (cur > 0 && !scr_fs_mkdirp_exists(buf, cur)) { + first = cur; + /* The kernel's parent: trailing separators dropped, then the last + * component and the separators before it ("/" stays). */ + size_t k = cur; + while (k > 1 && scr_fs_sep(buf[k - 1])) k--; + while (k > 0 && !scr_fs_sep(buf[k - 1])) k--; + while (k > 1 && scr_fs_sep(buf[k - 1])) k--; + if (scr_fs_mkdirp_exists(buf, k)) break; + /* ENOENT: Node retries with the path cut before its last separator. */ + size_t cut = cur; + while (cut > 0 && !scr_fs_sep(buf[cut - 1])) cut--; + if (cut == 0) break; + cur = cut - 1; + } + scr_mkdir_rec(buf, path->len, (mode_t)mode); + free(buf); + if (scr_exc_pending() || first == 0) return NULL; + return scr_str_new(path->data, first); +} + /* First failure of an rm walk, recorded instead of thrown so the retry * form can decide (retryable errno + attempts left → sleep and go again) * before anything reaches the exception cell. `path` is +1 when err != 0; diff --git a/packages/runtime/src/scr_lib.h b/packages/runtime/src/scr_lib.h index e3b087d82..5845a584f 100644 --- a/packages/runtime/src/scr_lib.h +++ b/packages/runtime/src/scr_lib.h @@ -287,6 +287,9 @@ void scr_fs_rename_error(int error, const ScrStr *oldpath, const ScrStr *newpath void scr_fs_write_file_mode(ScrStr *path, ScrStr *data, double mode); void scr_fs_mkdir_mode(ScrStr *path, double mode); void scr_fs_mkdir_recursive_mode(ScrStr *path, double mode); +/* The same walk, answering the first directory created (+1) or NULL + * when the path already existed (or on failure, the exception pending). */ +ScrStr *scr_fs_mkdir_recursive_first(ScrStr *path, double mode); /* Sequentially consistent integer operations and shared Int32 wait queues. */ typedef struct ScrBytes ScrBytes; diff --git a/tests/corpus/5540-fs-callback-forms.cjs b/tests/corpus/5540-fs-callback-forms.cjs new file mode 100644 index 000000000..a1f6f8a9e --- /dev/null +++ b/tests/corpus/5540-fs-callback-forms.cjs @@ -0,0 +1,77 @@ +// fs callback forms that pass results: mkdir with { recursive: true } +// gives the first directory it creates (spelled as Node cuts the path, or +// undefined when everything exists), readFile honors options.flag +// ('w+' truncates, 'a+' and 'r+' read), and write takes a string with an +// optional position and encoding, reporting the bytes written and the +// string. Invalid data, buffers and encodings throw at the call. +const fs = require("node:fs"); +const path = require("node:path"); +const os = require("node:os"); + +const dir = fs.mkdtempSync(path.join(os.tmpdir(), "fs-cb-")); +process.chdir(dir); + +function step(i) { + const steps = [ + () => fs.mkdir("a/b/c", { recursive: true }, (e, p) => (console.log("mkdir", e, p), step(i + 1))), + () => fs.mkdir("a/b/c", { recursive: true }, (e, p) => (console.log("mkdir again", e, p), step(i + 1))), + () => fs.mkdir("a/b/d/e/", { recursive: true }, (e, p) => (console.log("trailing", e, p), step(i + 1))), + () => fs.mkdir("./x//y", { recursive: true }, (e, p) => (console.log("doubled", e, p), step(i + 1))), + () => fs.mkdir(path.join(dir, "abs/q"), { recursive: true }, (e, p) => (console.log("absolute", e, p === path.join(dir, "abs")), step(i + 1))), + () => fs.mkdir("plain", (e, p) => (console.log("non-recursive", e, p), step(i + 1))), + () => { + fs.writeFileSync("f.txt", "file text"); + fs.mkdir("f.txt/sub", { recursive: true }, (e, p) => (console.log("under a file", e.code, p), step(i + 1))); + }, + () => fs.readFile("f.txt", { encoding: "utf8", flag: "r" }, (e, t) => (console.log("flag r", e, t), step(i + 1))), + () => fs.readFile("f.txt", { encoding: "utf8", flag: "a+" }, (e, t) => (console.log("flag a+", e, t), step(i + 1))), + () => fs.readFile("f.txt", { flag: "r+" }, (e, b) => (console.log("flag r+", e, b.toString()), step(i + 1))), + () => fs.readFile("new.txt", { encoding: "utf8", flag: "a+" }, (e, t) => (console.log("flag a+ creates", e, JSON.stringify(t), fs.existsSync("new.txt")), step(i + 1))), + () => fs.readFile("f.txt", { encoding: "utf8", flag: "w+" }, (e, t) => (console.log("flag w+", e, JSON.stringify(t), fs.statSync("f.txt").size), step(i + 1))), + () => fs.readFile("missing.txt", { encoding: "utf8", flag: "r+" }, (e) => (console.log("flag r+ missing", e.code), step(i + 1))), + () => { + const fd = fs.openSync("w.txt", "w+"); + fs.write(fd, "hello", (e, n, s) => { + console.log("write string", e, n, s); + fs.write(fd, "WORLD", 0, (e2, n2) => { + console.log("write at 0", e2, n2, fs.readFileSync("w.txt", "utf8")); + fs.write(fd, "686921", 5, "hex", (e3, n3, s3) => { + console.log("write hex", e3, n3, s3, fs.readFileSync("w.txt", "latin1")); + fs.write(fd, "é", null, "latin1", (e4, n4) => { + console.log("write latin1", e4, n4, fs.readFileSync("w.txt").length); + fs.closeSync(fd); + step(i + 1); + }); + }); + }); + }); + }, + () => { + const fd = fs.openSync("w.txt", "r+"); + try { + fs.write(fd, "abc", 0, "hex", () => {}); + } catch (error) { + console.log("odd hex", error.code, error.message); + } + try { + fs.write(fd, 42, () => {}); + } catch (error) { + console.log("bad data", error.code, error.message); + } + try { + fs.read(fd, { buffer: 5 }, () => {}); + } catch (error) { + console.log("bad read buffer", error.code, error.message); + } + fs.closeSync(fd); + step(i + 1); + }, + ]; + if (i < steps.length) steps[i](); + else { + process.chdir(os.tmpdir()); + fs.rmSync(dir, { recursive: true }); + console.log("removed", fs.existsSync(dir)); + } +} +step(0);