diff --git a/benchmarks/runtime/README.md b/benchmarks/runtime/README.md index 56d1d3449..23fa17905 100644 --- a/benchmarks/runtime/README.md +++ b/benchmarks/runtime/README.md @@ -14,6 +14,7 @@ Application-shaped workloads for measuring the speed of compiled executables. Ea | `word-graph` | cpu | string-keyed Map/Set, BFS | | `alloc-trees` | cpu | allocation-heavy trees, immutable object-spread updates | | `numeric-kernels` | cpu | sieve, hashing, `number[][]` matmul, `Float64Array` | +| `typed-array-scan`, `typed-array-scan-workers` | cpu | element-by-element scan loops over `Uint8Array`, `Uint16Array`, `Int32Array` and `Float64Array`; the second entry is a worker-enabled executable (it can start a worker), so each access also checks for shared storage | | `value-objects` | cpu | small immutable records and class instances: vector helpers, method chains, particle updates | | `optional-fields` | cpu | records and class instances with `number \| undefined` and other scalar-union fields | | `record-boundaries` | cpu | records built from unchecked array reads passed to and returned from annotated functions | diff --git a/benchmarks/runtime/workloads.json b/benchmarks/runtime/workloads.json index 2d697833d..bb6313cf7 100644 --- a/benchmarks/runtime/workloads.json +++ b/benchmarks/runtime/workloads.json @@ -70,6 +70,20 @@ "0.5" ] }, + { + "name": "typed-array-scan", + "entry": "workloads/typed-array-scan.ts", + "args": [ + "3" + ] + }, + { + "name": "typed-array-scan-workers", + "entry": "workloads/typed-array-scan-workers.ts", + "args": [ + "3" + ] + }, { "name": "value-objects", "entry": "workloads/value-objects.ts", diff --git a/benchmarks/runtime/workloads/typed-array-scan-kernels.ts b/benchmarks/runtime/workloads/typed-array-scan-kernels.ts new file mode 100644 index 000000000..d3664f465 --- /dev/null +++ b/benchmarks/runtime/workloads/typed-array-scan-kernels.ts @@ -0,0 +1,123 @@ +// Scan loops over typed arrays: read an element, branch on it, advance the +// index. Shared by typed-array-scan.ts and typed-array-scan-workers.ts; the +// second entry references node:worker_threads, which compiles the program as +// a worker-enabled executable. + +function makeText(length: number): Uint8Array { + const words = ["const", "value", "=", "index", "+", "1;", "return", "result", "//", "note", "42", "x_y$z"]; + const bytes = new Uint8Array(length); + let state = 12345; + let at = 0; + while (at < length) { + state = (Math.imul(state, 1103515245) + 12345) >>> 0; + const word = words[state % words.length]!; + for (let k = 0; k < word.length && at < length; k++) bytes[at++] = word.charCodeAt(k); + if (at < length) bytes[at++] = state % 7 === 0 ? 10 : 32; + } + return bytes; +} + +function isIdentifierPart(c: number): boolean { + return (c >= 97 && c <= 122) || (c >= 65 && c <= 90) || (c >= 48 && c <= 57) || c === 95 || c === 36; +} + +/** Uint8Array: count lines, words and digit bytes in one pass. */ +function scanBytes(bytes: Uint8Array): number { + let lines = 0; + let words = 0; + let digits = 0; + let inWord = false; + for (let i = 0; i < bytes.length; i++) { + const b = bytes[i]!; + if (b === 10) lines++; + if (b === 10 || b === 32) inWord = false; + else if (!inWord) { + inWord = true; + words++; + } + if (b >= 48 && b <= 57) digits++; + } + return lines * 1000003 + words * 31 + digits; +} + +/** Uint16Array: identifier runs, hashing each run (a tokenizer's inner loop). */ +function scanIdentifiers(chars: Uint16Array, end: number): number { + let pos = 0; + let count = 0; + let mix = 0; + while (pos < end) { + let hash = 0x811c9dc5 | 0; + while (pos < end) { + const c = chars[pos]!; + if (!isIdentifierPart(c)) break; + hash = Math.imul(hash ^ c, 0x01000193); + pos = (pos + 1) | 0; + } + mix = (mix + hash) | 0; + count++; + pos = (pos + 1) | 0; + } + return count * 65599 + (mix >>> 0); +} + +/** Int32Array: longest run of non-negative values and the count of sign changes. */ +function scanSigns(values: Int32Array): number { + let longest = 0; + let run = 0; + let changes = 0; + let previous = 0; + for (let i = 0; i < values.length; i++) { + const v = values[i]!; + if (v >= 0) { + run++; + if (run > longest) longest = run; + } else run = 0; + if ((v < 0) !== (previous < 0)) changes++; + previous = v; + } + return longest * 100003 + changes; +} + +/** Float64Array: threshold crossings and the sum above the threshold. */ +function scanSamples(samples: Float64Array, threshold: number): number { + let crossings = 0; + let above = 0; + let wasAbove = false; + for (let i = 0; i < samples.length; i++) { + const s = samples[i]!; + const isAbove = s > threshold; + if (isAbove !== wasAbove) crossings++; + if (isAbove) above += s - threshold; + wasAbove = isAbove; + } + return crossings + Math.round(above); +} + +export function run(scale: number): void { + const bytes = makeText(1 << 20); + const chars = new Uint16Array(bytes.length); + for (let i = 0; i < bytes.length; i++) chars[i] = bytes[i]!; + const ints = new Int32Array(1 << 20); + const samples = new Float64Array(1 << 20); + let state = 987654321; + for (let i = 0; i < ints.length; i++) { + state = (Math.imul(state, 1664525) + 1013904223) >>> 0; + ints[i] = (state >>> 8) - (1 << 23) + (i % 4096 < 2048 ? 1 << 20 : -(1 << 20)); + samples[i] = Math.sin(i / 97) * 10 + (state % 1000) / 500; + } + const rounds = 12 * scale; + let byteSum = 0; + let charSum = 0; + let intSum = 0; + let sampleSum = 0; + for (let r = 0; r < rounds; r++) { + byteSum += scanBytes(bytes); + charSum += scanIdentifiers(chars, chars.length); + intSum += scanSigns(ints); + sampleSum += scanSamples(samples, 2.5); + } + console.log(`uint8 ${byteSum}`); + console.log(`uint16 ${charSum}`); + console.log(`int32 ${intSum}`); + console.log(`float64 ${sampleSum}`); +} diff --git a/benchmarks/runtime/workloads/typed-array-scan-workers.ts b/benchmarks/runtime/workloads/typed-array-scan-workers.ts new file mode 100644 index 000000000..a4462bbb7 --- /dev/null +++ b/benchmarks/runtime/workloads/typed-array-scan-workers.ts @@ -0,0 +1,10 @@ +// The typed-array scan loops in a worker-enabled executable: the program can +// start a worker (it never does), so typed-array accesses check for shared +// storage. See typed-array-scan-kernels.ts. +import { Worker, isMainThread } from "node:worker_threads"; +import { run } from "./typed-array-scan-kernels.ts"; + +if (isMainThread) { + if (process.argv[3] === "spawn") new Worker(new URL(import.meta.url)); + run(Number(process.argv[2] ?? "1")); +} diff --git a/benchmarks/runtime/workloads/typed-array-scan.ts b/benchmarks/runtime/workloads/typed-array-scan.ts new file mode 100644 index 000000000..93ffdc6dd --- /dev/null +++ b/benchmarks/runtime/workloads/typed-array-scan.ts @@ -0,0 +1,5 @@ +// Typed-array scan loops in an ordinary executable. See +// typed-array-scan-kernels.ts. +import { run } from "./typed-array-scan-kernels.ts"; + +run(Number(process.argv[2] ?? "1")); diff --git a/docs/content/docs/how-it-works.mdx b/docs/content/docs/how-it-works.mdx index 39a9a7c4a..b093b3a4a 100644 --- a/docs/content/docs/how-it-works.mdx +++ b/docs/content/docs/how-it-works.mdx @@ -27,7 +27,7 @@ Runtime pack and linker -> executable 1. **Frontend:** TypeScript parses and checks the program against `es2025`, scriptc's ambient declarations, and available project declarations. `tsconfig.json` controls checker strictness. The compiler uses the checked types and narrowing information to lower supported operations; unsupported operations produce diagnostics. 2. **Typed IR:** A validated, serializable intermediate representation connects the frontend and backend. Types are concrete: generic functions are specialized, unions use tags, and closures have explicit captures. `--emit=ir` writes this representation as JSON. -3. **LLVM backend:** `--emit=llvm` writes textual LLVM IR. The matching LLVM 22 helper generates assembly or objects on supported macOS, Linux, and Windows hosts, including output targeting WASI. Executable and library builds use this backend. Large native executables are divided into partitions that are optimized and compiled in parallel; optimized builds summarize every partition first so small functions can still be inlined across partitions. The partition count depends only on the program, so output is the same on every host, and a rebuild reuses the compiled partitions that an edit does not affect. +3. **LLVM backend:** `--emit=llvm` writes textual LLVM IR. The matching LLVM 22 helper generates assembly or objects on supported macOS, Linux, and Windows hosts, including output targeting WASI. Executable and library builds use this backend. Large executables, native and WASI alike, are divided into partitions that are optimized and compiled in parallel; optimized builds summarize every partition first so small functions can still be inlined across partitions. The partition count depends only on the program, so output is the same on every host, and a rebuild reuses the compiled partitions that an edit does not affect. 4. **Linking:** Runtime packs contain the precompiled C runtime and vendored libraries. A hashed manifest selects the artifacts the program needs for its features, optimization mode, and (for libraries) state model. The target linker and SDK or sysroot produce the executable. Sanitizer builds use an instrumented C runtime. `comptime` calls run during the frontend stage in an isolated evaluator inside the compiler, based on QuickJS. Their results are embedded in the program as constants. diff --git a/docs/content/docs/limitations.mdx b/docs/content/docs/limitations.mdx index 320530d23..42d00b361 100644 --- a/docs/content/docs/limitations.mdx +++ b/docs/content/docs/limitations.mdx @@ -188,6 +188,10 @@ Every Buffer owns an ArrayBuffer of its own length. Node allocates small Buffers ../limitations/console-and-process-streams.mdx +### DNS lookup errors + +../limitations/dns-lookup-errors.mdx + ### Process arguments and errors ../limitations/process-arguments-and-errors.mdx diff --git a/docs/content/limitations/dns-lookup-errors.mdx b/docs/content/limitations/dns-lookup-errors.mdx new file mode 100644 index 000000000..e686ad47a --- /dev/null +++ b/docs/content/limitations/dns-lookup-errors.mdx @@ -0,0 +1 @@ +After a failed `dns.lookup` or `dns.lookupService`, Node calls the callback with the error alone, so the address and family parameters are `undefined`. A callback whose address parameter is typed `string` (as Node's own type declarations type it) receives `""` and `NaN` there, or an empty array for `all: true`, because a typed string or number cannot hold `undefined`. An untyped callback receives the error alone, as in Node. diff --git a/docs/src/generated/node-v24-compatibility-meta.json b/docs/src/generated/node-v24-compatibility-meta.json index a105476d0..95f04b58c 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": "1f294c83bdc97c884ed4", + "artifactVersion": "c7c956da1d50af685bf1", "rowCount": 3880 } diff --git a/docs/src/generated/node-v24-compatibility.json b/docs/src/generated/node-v24-compatibility.json index 74f141a32..7e4f9f668 100644 --- a/docs/src/generated/node-v24-compatibility.json +++ b/docs/src/generated/node-v24-compatibility.json @@ -494,9 +494,9 @@ "apiEntries": 119, "static": { "supported": 0, - "partial": 34, + "partial": 37, "refused": 0, - "not-implemented": 85, + "not-implemented": 82, "by-design": 0, "unreviewed": 0, "not-applicable": 0 @@ -32083,7 +32083,7 @@ }, "static": { "status": "partial", - "detail": "Implemented for the call shapes accepted by the compiler lowering.", + "detail": "Implemented for the documented scriptc module-loader subset.", "verification": "test-backed" } }, @@ -32198,7 +32198,7 @@ }, "static": { "status": "partial", - "detail": "Implemented for the call shapes accepted by the compiler lowering.", + "detail": "Implemented for the documented scriptc module-loader subset.", "verification": "test-backed" } }, @@ -52122,9 +52122,9 @@ "anchor": "netisipinput", "nodeStability": null, "static": { - "status": "not-implemented", - "detail": "No static compiler lowering is registered for this API yet.", - "verification": "registry-gap" + "status": "partial", + "detail": "Implemented for the call shapes accepted by the compiler lowering.", + "verification": "test-backed" } }, { @@ -52140,9 +52140,9 @@ "anchor": "netisipv4input", "nodeStability": null, "static": { - "status": "not-implemented", - "detail": "No static compiler lowering is registered for this API yet.", - "verification": "registry-gap" + "status": "partial", + "detail": "Implemented for the call shapes accepted by the compiler lowering.", + "verification": "test-backed" } }, { @@ -52158,9 +52158,9 @@ "anchor": "netisipv6input", "nodeStability": null, "static": { - "status": "not-implemented", - "detail": "No static compiler lowering is registered for this API yet.", - "verification": "registry-gap" + "status": "partial", + "detail": "Implemented for the call shapes accepted by the compiler lowering.", + "verification": "test-backed" } }, { diff --git a/internal/compatibility/generated/node-v24-backlog.json b/internal/compatibility/generated/node-v24-backlog.json index 182444995..2ae89c3b4 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": 45, - "verify-gap": 2365, - "audit-partial": 975, + "verify-gap": 2362, + "audit-partial": 978, "classify": 2, "implement": 85 } @@ -27453,8 +27453,7 @@ "confidence": "high", "source": "compiler-feature:fs.fs.mkdtemp", "tests": [ - "tests/corpus/5548-fs-callback-arguments.cjs", - "tests/corpus/5549-fs-callback-option-checks.cjs" + "tests/corpus/5545-fs-options-encoding.cjs" ] } } @@ -27671,8 +27670,7 @@ "confidence": "high", "source": "compiler-feature:fs.fs.readFile", "tests": [ - "tests/corpus/5548-fs-callback-arguments.cjs", - "tests/corpus/5549-fs-callback-option-checks.cjs" + "tests/corpus/5545-fs-options-encoding.cjs" ] } } @@ -28935,12 +28933,9 @@ "action": "audit-partial", "verification": "test-backed", "confidence": "high", - "source": "surface-manifest:node-builtin.fs.mkdtempSync", + "source": "compiler-feature:fs.fs.mkdtempSync", "tests": [ - "tests/corpus/1403-buffer-fs.ts", - "tests/corpus/2595-fs-arg-ladders.cjs", - "tests/corpus/2919-fs-function-values.ts", - "tests/corpus/2920-fs-promises-function-values.ts" + "tests/corpus/5545-fs-options-encoding.cjs" ] } } @@ -29077,13 +29072,9 @@ "action": "audit-partial", "verification": "test-backed", "confidence": "high", - "source": "surface-manifest:node-builtin.fs.readFileSync", + "source": "compiler-feature:fs.fs.readFileSync", "tests": [ - "tests/corpus/1403-buffer-fs.ts", - "tests/corpus/2595-fs-arg-ladders.cjs", - "tests/corpus/2919-fs-function-values.ts", - "tests/corpus/2920-fs-promises-function-values.ts", - "tests/corpus/fs-read-encodings.ts" + "tests/corpus/5545-fs-options-encoding.cjs" ] } } @@ -51307,12 +51298,15 @@ "priority": "normal", "tiers": { "static": { - "status": "not-implemented", - "action": "verify-gap", - "verification": "registry-gap", - "confidence": "low", - "source": "compiler-unmatched:net", - "tests": [] + "status": "partial", + "action": "audit-partial", + "verification": "test-backed", + "confidence": "high", + "source": "surface-manifest:node-builtin.net.isIP", + "tests": [ + "tests/corpus/2500-net-autosel-timeout.ts", + "tests/corpus/2596-net-arg-ladders.cjs" + ] } } }, @@ -51327,12 +51321,15 @@ "priority": "normal", "tiers": { "static": { - "status": "not-implemented", - "action": "verify-gap", - "verification": "registry-gap", - "confidence": "low", - "source": "compiler-unmatched:net", - "tests": [] + "status": "partial", + "action": "audit-partial", + "verification": "test-backed", + "confidence": "high", + "source": "surface-manifest:node-builtin.net.isIPv4", + "tests": [ + "tests/corpus/2500-net-autosel-timeout.ts", + "tests/corpus/2596-net-arg-ladders.cjs" + ] } } }, @@ -51347,12 +51344,15 @@ "priority": "normal", "tiers": { "static": { - "status": "not-implemented", - "action": "verify-gap", - "verification": "registry-gap", - "confidence": "low", - "source": "compiler-unmatched:net", - "tests": [] + "status": "partial", + "action": "audit-partial", + "verification": "test-backed", + "confidence": "high", + "source": "surface-manifest:node-builtin.net.isIPv6", + "tests": [ + "tests/corpus/2500-net-autosel-timeout.ts", + "tests/corpus/2596-net-arg-ladders.cjs" + ] } } }, diff --git a/internal/compatibility/generated/node-v24-internal.json b/internal/compatibility/generated/node-v24-internal.json index fd2223341..dc46d2363 100644 --- a/internal/compatibility/generated/node-v24-internal.json +++ b/internal/compatibility/generated/node-v24-internal.json @@ -521,9 +521,9 @@ "apiEntries": 119, "static": { "supported": 0, - "partial": 34, + "partial": 37, "refused": 0, - "not-implemented": 85, + "not-implemented": 82, "by-design": 0, "unreviewed": 0, "not-applicable": 0 @@ -43760,10 +43760,10 @@ "tests/corpus/5542-fs-promises-parity.cjs", "tests/corpus/5543-fs-promises-typed.ts", "tests/corpus/5544-fs-opendir.cjs", + "tests/corpus/5545-fs-options-encoding.cjs", "tests/corpus/5548-fs-callback-arguments.cjs", "tests/corpus/5549-fs-callback-option-checks.cjs", "tests/corpus/fs-native-realpath.ts", - "tests/corpus/fs-read-encodings.ts", "tests/corpus/node24-fs-links-callbacks.cjs", "tests/corpus/node24-fs-links-errors.cjs", "tests/corpus/node24-fs-links-options.cjs", @@ -46232,8 +46232,7 @@ "status": "partial", "evidence": "compiler-feature:fs.fs.mkdtemp", "tests": [ - "tests/corpus/5548-fs-callback-arguments.cjs", - "tests/corpus/5549-fs-callback-option-checks.cjs" + "tests/corpus/5545-fs-options-encoding.cjs" ] }, "anchorSource": "exact" @@ -46440,8 +46439,7 @@ "status": "partial", "evidence": "compiler-feature:fs.fs.readFile", "tests": [ - "tests/corpus/5548-fs-callback-arguments.cjs", - "tests/corpus/5549-fs-callback-option-checks.cjs" + "tests/corpus/5545-fs-options-encoding.cjs" ] }, "anchorSource": "exact" @@ -47813,12 +47811,9 @@ }, "static": { "status": "partial", - "evidence": "surface-manifest:node-builtin.fs.mkdtempSync", + "evidence": "compiler-feature:fs.fs.mkdtempSync", "tests": [ - "tests/corpus/1403-buffer-fs.ts", - "tests/corpus/2595-fs-arg-ladders.cjs", - "tests/corpus/2919-fs-function-values.ts", - "tests/corpus/2920-fs-promises-function-values.ts" + "tests/corpus/5545-fs-options-encoding.cjs" ] }, "anchorSource": "exact" @@ -47949,13 +47944,9 @@ }, "static": { "status": "partial", - "evidence": "surface-manifest:node-builtin.fs.readFileSync", + "evidence": "compiler-feature:fs.fs.readFileSync", "tests": [ - "tests/corpus/1403-buffer-fs.ts", - "tests/corpus/2595-fs-arg-ladders.cjs", - "tests/corpus/2919-fs-function-values.ts", - "tests/corpus/2920-fs-promises-function-values.ts", - "tests/corpus/fs-read-encodings.ts" + "tests/corpus/5545-fs-options-encoding.cjs" ] }, "anchorSource": "exact" @@ -73525,8 +73516,12 @@ "anchor": "netisipinput", "nodeStability": null, "static": { - "status": "not-implemented", - "evidence": "compiler-unmatched:net" + "status": "partial", + "evidence": "surface-manifest:node-builtin.net.isIP", + "tests": [ + "tests/corpus/2500-net-autosel-timeout.ts", + "tests/corpus/2596-net-arg-ladders.cjs" + ] }, "anchorSource": "exact" }, @@ -73543,8 +73538,12 @@ "anchor": "netisipv4input", "nodeStability": null, "static": { - "status": "not-implemented", - "evidence": "compiler-unmatched:net" + "status": "partial", + "evidence": "surface-manifest:node-builtin.net.isIPv4", + "tests": [ + "tests/corpus/2500-net-autosel-timeout.ts", + "tests/corpus/2596-net-arg-ladders.cjs" + ] }, "anchorSource": "exact" }, @@ -73561,8 +73560,12 @@ "anchor": "netisipv6input", "nodeStability": null, "static": { - "status": "not-implemented", - "evidence": "compiler-unmatched:net" + "status": "partial", + "evidence": "surface-manifest:node-builtin.net.isIPv6", + "tests": [ + "tests/corpus/2500-net-autosel-timeout.ts", + "tests/corpus/2596-net-arg-ladders.cjs" + ] }, "anchorSource": "exact" }, diff --git a/internal/compatibility/static-support.json b/internal/compatibility/static-support.json index ca3194744..9d5da0935 100644 --- a/internal/compatibility/static-support.json +++ b/internal/compatibility/static-support.json @@ -214,6 +214,7 @@ { "chapter": "perf_hooks", "parent": "Class: `PerformanceMeasure`", "symbols": ["detail"], "status": "partial", "evidence": ["tests/corpus/5212-perf-marks-measures.ts", "tests/corpus/5214-perf-entry-inspect.ts"] }, { "chapter": "process", "symbols": ["process.loadEnvFile"], "status": "partial", "evidence": ["tests/corpus/node24-env-files.ts", "tests/corpus/node24-env-files-errors.ts", "tests/corpus/node24-env-imports.cjs", "tests/fixtures/node-types/env-files.ts", "tests/harness/project-config.test.ts"] }, { "chapter": "fs", "symbols": ["fs.readv", "fs.writev", "fs.fdatasync", "fs.fchmod", "fs.ftruncate"], "status": "partial", "evidence": ["tests/corpus/3137-fs-vector-callbacks.cjs"] }, + { "chapter": "fs", "signatures": ["`fs.mkdtemp(prefix[, options], callback)`", "`fs.readFile(path[, options], callback)`", "`fs.mkdtempSync(prefix[, options])`", "`fs.readFileSync(path[, options])`"], "status": "partial", "evidence": ["tests/corpus/5545-fs-options-encoding.cjs"] }, { "chapter": "fs", "signatures": ["`fs.appendFile(path, data[, options], callback)`", "`fs.chmod(path, mode, callback)`", "`fs.copyFile(src, dest[, mode], callback)`", "`fs.fstat(fd[, options], callback)`", "`fs.fsync(fd, callback)`", "`fs.lstat(path[, options], callback)`", "`fs.mkdir(path[, options], callback)`", "`fs.mkdtemp(prefix[, options], callback)`", "`fs.open(path[, flags[, mode]], callback)`", "`fs.read(fd, buffer, offset, length, position, callback)`", "`fs.read(fd[, options], callback)`", "`fs.read(fd, buffer[, options], callback)`", "`fs.readdir(path[, options], callback)`", "`fs.readFile(path[, options], callback)`", "`fs.realpath(path[, options], callback)`", "`fs.rmdir(path[, options], callback)`", "`fs.stat(path[, options], callback)`", "`fs.unlink(path, callback)`", "`fs.write(fd, buffer, offset[, length[, position]], callback)`", "`fs.write(fd, buffer[, options], callback)`", "`fs.writeFile(file, data[, options], callback)`"], "status": "partial", "evidence": ["tests/corpus/5548-fs-callback-arguments.cjs", "tests/corpus/5549-fs-callback-option-checks.cjs"] }, { "chapter": "fs", "parent": "Class: `FileHandle`", "symbols": ["fd"], "status": "partial", "evidence": ["tests/corpus/2686-fs-file-handle.ts"] }, { "chapter": "fs", "symbols": ["FileHandle", "filehandle.fd", "filehandle.close", "filehandle.read", "filehandle.write", "filehandle.readFile", "filehandle.writeFile", "filehandle.appendFile", "filehandle.stat"], "status": "partial", "evidence": ["tests/corpus/2686-fs-file-handle.ts", "tests/corpus/3136-fs-file-handle-options.ts"] }, diff --git a/packages/compiler/ambient/scriptc-node-fallback.d.ts b/packages/compiler/ambient/scriptc-node-fallback.d.ts index d6d986c90..d37c4983e 100644 --- a/packages/compiler/ambient/scriptc-node-fallback.d.ts +++ b/packages/compiler/ambient/scriptc-node-fallback.d.ts @@ -3752,6 +3752,17 @@ declare module "net" { ref(): void; unref(): void; readonly remoteAddress: string | undefined; + /* The peer's fields are cached at first read, like remoteAddress; the + * local ones are undefined while connecting and after close. */ + readonly remotePort: number | undefined; + readonly remoteFamily: string | undefined; + readonly localAddress: string | undefined; + readonly localPort: number | undefined; + readonly localFamily: string | undefined; + readonly bytesRead: number; + readonly connecting: boolean; + readonly pending: boolean; + readonly readyState: "opening" | "open" | "readOnly" | "writeOnly" | "closed"; /* true once the fd is gone (destroy() or full close) — Node's flag. */ readonly destroyed: boolean; /* the write half is open: no end() yet, no FIN sent, fd alive. */ @@ -3761,8 +3772,11 @@ declare module "net" { /* every byte the write paths accepted (buffered included — Node * counts those too; plaintext on TLS sockets). */ readonly bytesWritten: number; - write(data: string | Uint8Array): void; - end(data?: string | Uint8Array): void; + /* write's callback runs once the data is flushed, with null; end's + * once the socket finishes. */ + write(data: string | Uint8Array, callback?: (err?: Error | null) => void): void; + end(callback?: () => void): void; + end(data: string | Uint8Array, callback?: () => void): void; destroy(): void; /* Flow control: pause holds reads off (kernel/TCP backpressure is * the buffer); resume flows — and discards without listeners, so @@ -3771,6 +3785,8 @@ declare module "net" { resume(): Socket; /* TCP_NODELAY on the live fd; missing means true. Chains. */ setNoDelay(noDelay?: boolean): Socket; + /* SO_KEEPALIVE with the initial delay in ms. Chains. */ + setKeepAlive(enable?: boolean, initialDelay?: number): Socket; /* end() now, destroy once the FIN actually flushed. */ destroySoon(): void; /* setEncoding('utf8'): 'data' delivers strings — the IncomingMessage @@ -3781,7 +3797,8 @@ declare module "net" { pipe( destination: Socket | import("http").ServerResponse | import("http2").Http2ServerResponse, ): void; - setTimeout(ms: number): void; + /* The callback is a once('timeout') listener (removed again by 0). */ + setTimeout(ms: number, callback?: () => void): void; /* The paused-mode demux surface: once('readable') + read(1) + * unshift — read answers exactly n buffered bytes or null. */ read(size?: number): Buffer | null; @@ -3792,25 +3809,34 @@ declare module "net" { * registration never fires — Node's exact split. 'session' fires * once with the serialized session (the received-ticket event). */ on(event: "data", listener: (chunk: Buffer) => void): void; + /* After setEncoding the chunk is a string. */ + on(event: "data", listener: (chunk: string) => void): void; on( - event: "end" | "close" | "connect" | "timeout" | "readable" | "finish" | "secureConnect", + event: "end" | "connect" | "ready" | "timeout" | "readable" | "finish" | "secureConnect", listener: () => void, ): void; + on(event: "close", listener: (hadError: boolean) => void): void; on(event: "error", listener: (err: Error) => void): void; on(event: "session", listener: (session: Buffer) => void): void; /* addListener IS on (Node aliases them) — the suite spells both. */ addListener(event: "data", listener: (chunk: Buffer) => void): void; + /* After setEncoding the chunk is a string. */ + addListener(event: "data", listener: (chunk: string) => void): void; addListener( - event: "end" | "close" | "connect" | "timeout" | "readable" | "finish" | "secureConnect", + event: "end" | "connect" | "ready" | "timeout" | "readable" | "finish" | "secureConnect", listener: () => void, ): void; + addListener(event: "close", listener: (hadError: boolean) => void): void; addListener(event: "error", listener: (err: Error) => void): void; addListener(event: "session", listener: (session: Buffer) => void): void; once(event: "data", listener: (chunk: Buffer) => void): void; + /* After setEncoding the chunk is a string. */ + once(event: "data", listener: (chunk: string) => void): void; once( - event: "end" | "close" | "connect" | "timeout" | "readable" | "finish" | "secureConnect", + event: "end" | "connect" | "ready" | "timeout" | "readable" | "finish" | "secureConnect", listener: () => void, ): void; + once(event: "close", listener: (hadError: boolean) => void): void; once(event: "error", listener: (err: Error) => void): void; once(event: "session", listener: (session: Buffer) => void): void; } @@ -3819,6 +3845,11 @@ declare module "net" { ref(): void; unref(): void; readonly listening: boolean; + /* Connections past this many are closed with a 'drop' event; + * undefined (the default) or null lifts the cap. */ + maxConnections: number | undefined; + /* cb(null, count) on the next tick. */ + getConnections(callback: (error: Error | null, count: number) => void): Server; /* Node answers the server itself (`return this` chaining). */ listen(port: number, callback?: () => void): Server; /* The positional bind address (the options form's host in argument @@ -3840,6 +3871,7 @@ declare module "net" { emit(event: "connection", socket: Socket): boolean; on(event: "connection" | "secureConnection", listener: (socket: Socket) => void): void; on(event: "close" | "listening", listener: () => void): void; + on(event: "drop", listener: (data?: DropArgument) => void): void; on(event: "timeout", listener: (socket: Socket) => void): void; on(event: "error", listener: (err: Error) => void): void; /* The WebSocket handover: fires INSTEAD of 'request' for @@ -3857,6 +3889,7 @@ declare module "net" { /* addListener IS on (Node aliases them) — the suite spells both. */ addListener(event: "connection" | "secureConnection", listener: (socket: Socket) => void): void; addListener(event: "close" | "listening", listener: () => void): void; + addListener(event: "drop", listener: (data?: DropArgument) => void): void; addListener(event: "timeout", listener: (socket: Socket) => void): void; addListener(event: "error", listener: (err: Error) => void): void; addListener( @@ -3869,6 +3902,7 @@ declare module "net" { ): void; once(event: "connection" | "secureConnection", listener: (socket: Socket) => void): void; once(event: "close" | "listening", listener: () => void): void; + once(event: "drop", listener: (data?: DropArgument) => void): void; once(event: "timeout", listener: (socket: Socket) => void): void; once(event: "error", listener: (err: Error) => void): void; once( @@ -3880,6 +3914,20 @@ declare module "net" { listener: (req: import("http").IncomingMessage, res: import("http").ServerResponse) => void, ): void; } + /* The 'drop' payload: the turned-away connection's addresses. */ + export interface DropArgument { + localAddress?: string; + localPort?: number; + localFamily?: string; + remoteAddress?: string; + remotePort?: number; + remoteFamily?: string; + } + export type AddressInfo = import("node:dgram").AddressInfo; + /* Node's IPv4Reg and IPv6Reg tests: 4, 6, or 0 for isIP. */ + export function isIP(input: string): number; + export function isIPv4(input: string): boolean; + export function isIPv6(input: string): boolean; /* The Socket VALUE — the `x instanceof net.Socket` narrowing target * (the h2 compat 'connect' listener's test). Constructing bare sockets * has no lowering; the construct signature exists for instanceof. */ @@ -5108,14 +5156,55 @@ declare module "node:dgram" { * ENOTFOUND ") as the callback's Error argument, with "" for * the address where Node passes undefined. */ declare module "dns" { + export interface LookupAddress { + address: string; + family: number; + } + export interface LookupOptions { + family?: number | "IPv4" | "IPv6"; + hints?: number; + all?: boolean; + order?: "ipv4first" | "ipv6first" | "verbatim"; + verbatim?: boolean; + } + export interface LookupOneOptions extends LookupOptions { + all?: false; + } + export interface LookupAllOptions extends LookupOptions { + all: true; + } + /* The callback comes with the other lookup deliveries (literal + * addresses on the next tick); the options follow lib/dns.js. */ export function lookup( hostname: string, - /* The options-record stance (see http.RequestOptions): family is the - * lowered option ({ family: 4 }); hints/all/order/verbatim fence by - * name; undocumented keys drop like Node. */ - options: { family: number; [option: string]: unknown }, - callback: (err: Error | null, address: string, family: number) => void, + callback: (err: NodeJS.ErrnoException | null, address: string, family: number) => void, ): void; + export function lookup( + hostname: string, + family: number, + callback: (err: NodeJS.ErrnoException | null, address: string, family: number) => void, + ): void; + export function lookup( + hostname: string, + options: LookupOneOptions, + callback: (err: NodeJS.ErrnoException | null, address: string, family: number) => void, + ): void; + export function lookup( + hostname: string, + options: LookupAllOptions, + callback: (err: NodeJS.ErrnoException | null, addresses: LookupAddress[]) => void, + ): void; + /* getnameinfo with NI_NAMEREQD: the host's name and the port's service + * name (the number when it has none). */ + export function lookupService( + address: string, + port: number, + callback: (err: NodeJS.ErrnoException | null, hostname: string, service: string) => void, + ): void; + /* The nameservers /etc/resolv.conf lists. */ + export function getServers(): string[]; + export function getDefaultResultOrder(): "ipv4first" | "ipv6first" | "verbatim"; + export function setDefaultResultOrder(order: "ipv4first" | "ipv6first" | "verbatim"): void; } declare module "node:dns" { export * from "dns"; diff --git a/packages/compiler/src/backend/llvm/emitter.ts b/packages/compiler/src/backend/llvm/emitter.ts index 506983dc4..5bfe250fd 100644 --- a/packages/compiler/src/backend/llvm/emitter.ts +++ b/packages/compiler/src/backend/llvm/emitter.ts @@ -4635,6 +4635,11 @@ export class LlEmitter { return slot; } + /** The amortized cancellation poll, emitted on the loop's back edge (the + * continue target), after the body. A poll in the header would be a loop + * exit ahead of the body; LLVM then cannot rotate the loop, so loads that + * the body repeats on every iteration (a typed array's length, for one) + * stay in the loop. */ private emitWorkerLoopCheck(slot: string | null): void { if (slot === null) return; const B = this.B; @@ -7275,15 +7280,15 @@ export class LlEmitter { const lc = B.newLabel("loop.c"); const lb = B.newLabel("loop.b"); const le = B.newLabel("loop.e"); + const lp = workerBudget !== null ? B.newLabel("loop.p") : lc; B.br(lc); B.startBlock(lc); - this.emitWorkerLoopCheck(workerBudget); B.condBr(this.emitCondition(s.cond), lb, le); B.startBlock(lb); this.jumpTargets.push({ kind: "loop", brkLabel: le, - contLabel: lc, + contLabel: lp, labels: s.labels, frameDepth: this.frames.length, scopeDepth: this.scopes.length, @@ -7291,7 +7296,12 @@ export class LlEmitter { }); this.emitBlock(s.body); this.jumpTargets.pop(); - B.br(lc); + B.br(lp); + if (lp !== lc) { + B.startBlock(lp); + this.emitWorkerLoopCheck(workerBudget); + B.br(lc); + } B.startBlock(le); break; } @@ -7303,7 +7313,6 @@ export class LlEmitter { const le = B.newLabel("loop.e"); B.br(lb); B.startBlock(lb); - this.emitWorkerLoopCheck(workerBudget); this.jumpTargets.push({ kind: "loop", brkLabel: le, @@ -7317,6 +7326,7 @@ export class LlEmitter { this.jumpTargets.pop(); B.br(lc); B.startBlock(lc); + this.emitWorkerLoopCheck(workerBudget); B.condBr(this.emitCondition(s.cond), lb, le); B.startBlock(le); break; @@ -7502,12 +7512,12 @@ export class LlEmitter { B.line(`store ptr ${fresh}, ptr ${slot}`); } }; - const lu = s.update || capturedLets.length > 0 ? B.newLabel("loop.u") : lc; - freshenBindings(); const workerBudget = this.workerLoopBudget(s); + const lu = + s.update || capturedLets.length > 0 || workerBudget !== null ? B.newLabel("loop.u") : lc; + freshenBindings(); B.br(lc); B.startBlock(lc); - this.emitWorkerLoopCheck(workerBudget); if (integerLoop && integerSlot) { const receiver = this.emitStableReceiver(integerLoop.limitReceiver, []); const lenPtr = B.tmp(); @@ -7549,6 +7559,7 @@ export class LlEmitter { B.br(lu); if (lu !== lc) { B.startBlock(lu); + this.emitWorkerLoopCheck(workerBudget); freshenBindings(); if (integerLoop && integerSlot) { const old = B.tmp(); @@ -7610,7 +7621,6 @@ export class LlEmitter { const workerBudget = this.workerLoopBudget(s); B.br(lc); B.startBlock(lc); - this.emitWorkerLoopCheck(workerBudget); const inBounds = B.tmp(); let cur: string; if (snapshot && cursor && scratch) { @@ -7694,6 +7704,7 @@ export class LlEmitter { this.jumpTargets.pop(); B.br(lu); B.startBlock(lu); + this.emitWorkerLoopCheck(workerBudget); if (idxSlot) { const i2 = B.tmp(), i3 = B.tmp(); diff --git a/packages/compiler/src/backend/llvm/expr-bytes.ts b/packages/compiler/src/backend/llvm/expr-bytes.ts index ce8919ae5..50c04617e 100644 --- a/packages/compiler/src/backend/llvm/expr-bytes.ts +++ b/packages/compiler/src/backend/llvm/expr-bytes.ts @@ -169,63 +169,13 @@ export function emitBytesLength( return { name: out, type: F64 }; } -export function emitBytesGet( - host: LlvmEmitterContext, - elem: IrBytesElem, - receiver: string, - index: LlValue, - expr?: IrExpr, - inBounds = false, - invalidNaN = false, -): LlValue { - const B = host.B; - if (host.mod.workers) { - const fn = invalidNaN ? "scr_bytes_get_or_nan" : "scr_bytes_get"; - host.declare(`declare double @${fn}(ptr, double)`); - const local = emitSharedBytesSplit(host, receiver); - const result = B.tmp(); - B.line(`${result} = call double @${fn}(ptr ${receiver}, double ${index.name})`); - // Integer elements read as integers in int32/uint32 range, or NaN for a - // missing one: ToUint32 is the integer, or 0. Keeping it lets the join - // carry the local path's integer hint. - let sharedU32: string | undefined; - if (elem !== "f32" && elem !== "f64") { - const missing = B.tmp(), - present = B.tmp(), - wide = B.tmp(); - sharedU32 = B.tmp(); - B.line(`${missing} = fcmp uno double ${result}, ${result}`); - B.line(`${present} = select i1 ${missing}, double 0.0, double ${result}`); - B.line(`${wide} = fptosi double ${present} to i64`); - B.line(`${sharedU32} = trunc i64 ${wide} to i32`); - } - const shared = B.newLabel("bytes.shared.done"); - B.br(shared); - B.startBlock(local); - const own = emitBytesGetLocal(host, elem, receiver, index, expr, inBounds, invalidNaN); - const owned = B.newLabel("bytes.local.done"); - B.br(owned); - B.startBlock(owned); - const join = B.newLabel("bytes.join"); - B.br(join); - B.startBlock(shared); - B.br(join); - B.startBlock(join); - const out = B.tmp(); - B.line(`${out} = phi double [ ${own.name}, %${owned} ], [ ${result}, %${shared} ]`); - if (own.uint32 === undefined || sharedU32 === undefined) return { name: out, type: F64 }; - const u32 = B.tmp(); - B.line(`${u32} = phi i32 [ ${own.uint32}, %${owned} ], [ ${sharedU32}, %${shared} ]`); - return { name: out, type: F64, uint32: u32 }; - } - return emitBytesGetLocal(host, elem, receiver, index, expr, inBounds, invalidNaN); -} - /** Worker programs: a view of a SharedArrayBuffer (`shared` is fixed before * script code can see the view) is read and written by the runtime under * its shared-memory lock; every other buffer belongs to this thread and - * takes the ordinary inline path. Branches to the returned label for a - * thread-owned buffer and continues in the shared block. */ + * takes the ordinary inline path. The test follows the index check: a + * view's length never changes, so both paths use the one checked integer + * index. Branches to the returned label for a thread-owned buffer and + * continues in the shared block. */ function emitSharedBytesSplit(host: LlvmEmitterContext, receiver: string): string { const B = host.B; host.declare(`declare i1 @llvm.expect.i1(i1, i1)`); @@ -244,14 +194,14 @@ function emitSharedBytesSplit(host: LlvmEmitterContext, receiver: string): strin return local; } -function emitBytesGetLocal( +export function emitBytesGet( host: LlvmEmitterContext, elem: IrBytesElem, receiver: string, index: LlValue, - expr: IrExpr | undefined, - inBounds: boolean, - invalidNaN: boolean, + expr?: IrExpr, + inBounds = false, + invalidNaN = false, ): LlValue { const B = host.B; // Any invalid index traps (the array runtime's discipline). @@ -304,8 +254,63 @@ function emitBytesLoad( inBounds: boolean, invalid: string | undefined, ): LlValue { - const B = host.B; const idx = emitBytesIndex(host, receiver, index, expr, invalid, inBounds); + if (host.mod.workers) return emitSharedOrLocalLoad(host, elem, receiver, idx); + return emitBytesLoadAt(host, elem, receiver, idx); +} + +/** Worker programs: the element at a checked index of a view that may be + * shared. An integer element joins as its 32-bit value and converts to a + * number after the join, as the local load does, so compares and + * ToInt32/ToUint32 of the result stay integer operations. The shared read + * is an ordinary call: another thread's write must stay visible to a loop + * that reads the same element again. */ +function emitSharedOrLocalLoad( + host: LlvmEmitterContext, + elem: IrBytesElem, + receiver: string, + idx: string, +): LlValue { + const B = host.B; + const floating = elem === "f32" || elem === "f64"; + const fn = floating ? "scr_bytes_shared_get_f64" : "scr_bytes_shared_get_int"; + const ret = floating ? "double" : "i32"; + host.declare(`declare ${ret} @${fn}(ptr, ${host.sizeType})`); + const local = emitSharedBytesSplit(host, receiver); + const shared = B.tmp(); + B.line(`${shared} = call ${ret} @${fn}(ptr ${receiver}, ${host.sizeType} ${idx}) cold`); + const sharedDone = B.newLabel("bytes.shared.done"); + B.br(sharedDone); + B.startBlock(local); + const own = emitBytesLoadAt(host, elem, receiver, idx); + const localDone = B.newLabel("bytes.local.done"); + B.br(localDone); + B.startBlock(localDone); + const join = B.newLabel("bytes.join"); + B.br(join); + B.startBlock(sharedDone); + B.br(join); + B.startBlock(join); + const out = B.tmp(); + if (floating) { + B.line(`${out} = phi double [ ${own.name}, %${localDone} ], [ ${shared}, %${sharedDone} ]`); + return { name: out, type: F64 }; + } + const u32 = B.tmp(); + B.line(`${u32} = phi i32 [ ${own.uint32!}, %${localDone} ], [ ${shared}, %${sharedDone} ]`); + const signed = elem === "i8" || elem === "i16" || elem === "i32"; + B.line(`${out} = ${signed ? "sitofp" : "uitofp"} i32 ${u32} to double`); + return { name: out, type: F64, uint32: u32 }; +} + +/** The element at a checked index of a thread-owned view. */ +function emitBytesLoadAt( + host: LlvmEmitterContext, + elem: IrBytesElem, + receiver: string, + idx: string, +): LlValue { + const B = host.B; const data = host.emitBytesData(receiver); const p = B.tmp(); if (BYTES_ELEMENT_SIZE[elem] < 4) { @@ -429,37 +434,20 @@ export function emitBytesSet( indexExpr?: IrExpr, expr?: IrExpr, inBounds = false, -): void { - const B = host.B; - if (host.mod.workers) { - host.declare(`declare void @scr_bytes_set(ptr, double, double)`); - const local = emitSharedBytesSplit(host, receiver); - B.line(`call void @scr_bytes_set(ptr ${receiver}, double ${index.name}, double ${value.name})`); - const join = B.newLabel("bytes.join"); - B.br(join); - B.startBlock(local); - emitBytesSetLocal(host, elem, receiver, index, value, indexExpr, expr, inBounds); - B.br(join); - B.startBlock(join); - return; - } - emitBytesSetLocal(host, elem, receiver, index, value, indexExpr, expr, inBounds); -} - -function emitBytesSetLocal( - host: LlvmEmitterContext, - elem: IrBytesElem, - receiver: string, - index: LlValue, - value: LlValue, - indexExpr: IrExpr | undefined, - expr: IrExpr | undefined, - inBounds: boolean, ): void { const B = host.B; // Invalid integer-indexed writes are ignored after evaluating the RHS. const done = B.newLabel("bytes.store.done"); const idx = emitBytesIndex(host, receiver, index, indexExpr, done, inBounds); + if (host.mod.workers) { + host.declare(`declare void @scr_bytes_shared_set(ptr, ${host.sizeType}, double)`); + const local = emitSharedBytesSplit(host, receiver); + B.line( + `call void @scr_bytes_shared_set(ptr ${receiver}, ${host.sizeType} ${idx}, double ${value.name})`, + ); + B.br(done); + B.startBlock(local); + } let stored: string | null; if (elem === "u8c") { host.declare(`declare double @scr_bytes_to_u8_clamp(double)`); diff --git a/packages/compiler/src/backend/llvm/lib-datagram.ts b/packages/compiler/src/backend/llvm/lib-datagram.ts index ab1478db5..3c68c3f00 100644 --- a/packages/compiler/src/backend/llvm/lib-datagram.ts +++ b/packages/compiler/src/backend/llvm/lib-datagram.ts @@ -216,6 +216,16 @@ export function emitDatagramLibCall(host: LlvmEmitterContext, e: LibCallExpr): L host.declare("declare void @scr_dns_thunk0(ptr, ptr, ptr, double)"); return finishRuntimeCall(host, e, "scr_dns_lookup", [...a, ptr("@" + symbol)]); } + case "dns.lookupDyn": + return finishRuntimeCall(host, e, "scr_dns_lookup_dyn", a); + case "dns.lookupService": + return finishRuntimeCall(host, e, "scr_dns_lookup_service", a); + case "dns.getServers": + return finishRuntimeCall(host, e, "scr_dns_get_servers", a); + case "dns.getDefaultResultOrder": + return finishRuntimeCall(host, e, "scr_dns_get_default_result_order", a); + case "dns.setDefaultResultOrder": + return finishRuntimeCall(host, e, "scr_dns_set_default_result_order", a); default: throw new InternalCompilerError(`unhandled datagram call ${e.fn}`); } diff --git a/packages/compiler/src/backend/llvm/lib-network.ts b/packages/compiler/src/backend/llvm/lib-network.ts index 19e9d1c13..5da0c4d3d 100644 --- a/packages/compiler/src/backend/llvm/lib-network.ts +++ b/packages/compiler/src/backend/llvm/lib-network.ts @@ -250,6 +250,7 @@ export function emitNetworkHttpLibCall(host: LlvmEmitterContext, e: LibCallExpr) e.fn === "net.sockOnConnect" || e.fn === "net.sockOnTimeout" || e.fn === "net.sockOnReadable" || + e.fn === "net.sockOnReady" || e.fn === "http.reqOnEnd" || e.fn === "http.reqOnClose" || e.fn === "http.reqOnAborted" || @@ -268,6 +269,7 @@ export function emitNetworkHttpLibCall(host: LlvmEmitterContext, e: LibCallExpr) "net.sockOnConnect": "scr_net_sock_on_connect", "net.sockOnTimeout": "scr_net_sock_on_timeout", "net.sockOnReadable": "scr_net_sock_on_readable", + "net.sockOnReady": "scr_net_sock_on_ready", "http.reqOnEnd": "scr_http_req_on_end", "http.reqOnClose": "scr_http_req_on_close", "http.reqOnAborted": "scr_http_req_on_aborted", @@ -640,5 +642,21 @@ export function emitNetworkHttpLibCall(host: LlvmEmitterContext, e: LibCallExpr) B.line(`call void @scr_net_sock_on_finish(ptr ${args[0]!.name}, ptr ${args[1]!.name})`); return { name: "", type: e.type }; } + if (e.fn === "net.sockOnWriteFlush") { + const args = e.args.map((a) => host.emitExpr(a)); + host.moveTemp(args[1]!); + host.declare(`declare void @scr_net_sock_on_write_flush(ptr, ptr)`); + B.line(`call void @scr_net_sock_on_write_flush(ptr ${args[0]!.name}, ptr ${args[1]!.name})`); + return { name: "", type: e.type }; + } + if (e.fn === "net.sockSetTimeoutCb") { + const args = e.args.map((a) => host.emitExpr(a)); + host.moveTemp(args[2]!); + host.declare("declare void @scr_net_sock_set_timeout_cb(ptr, double, ptr)"); + B.line( + `call void @scr_net_sock_set_timeout_cb(ptr ${args[0]!.name}, double ${args[1]!.name}, ptr ${args[2]!.name})`, + ); + return { name: "", type: e.type }; + } return host.emitGenericLibCall(e); } diff --git a/packages/compiler/src/backend/llvm/lib-shared.ts b/packages/compiler/src/backend/llvm/lib-shared.ts index ce44e2ce4..84f039f2c 100644 --- a/packages/compiler/src/backend/llvm/lib-shared.ts +++ b/packages/compiler/src/backend/llvm/lib-shared.ts @@ -973,13 +973,29 @@ export const LIB_FN_SYMS: Record = { "net.sockDestroyed": "scr_net_sock_destroyed", "net.sockWritable": "scr_net_sock_writable", "net.sockSetEncoding": "scr_net_sock_set_encoding", - "net.sockSetTimeout": "scr_net_sock_set_timeout", + "net.sockSetTimeout": "scr_net_sock_set_timeout_checked", "net.sockUnshift": "scr_net_sock_unshift_bytes", "net.sockPause": "scr_net_sock_pause", "net.sockResume": "scr_net_sock_resume", "net.sockSetNoDelay": "scr_net_sock_set_nodelay", "net.sockDestroySoon": "scr_net_sock_destroy_soon", "net.sockBytesWritten": "scr_net_sock_bytes_written", + "net.sockAddrField": "scr_net_sock_addr_field", + "net.sockBytesRead": "scr_net_sock_bytes_read", + "net.sockConnecting": "scr_net_sock_connecting", + "net.sockPending": "scr_net_sock_pending", + "net.sockReadyState": "scr_net_sock_ready_state", + "net.sockSetKeepAlive": "scr_net_sock_set_keepalive_ret", + "net.sockOnCloseDyn": "scr_net_sock_on_close_dyn", + "net.sockOnWriteFlushDyn": "scr_net_sock_on_write_flush_dyn", + "net.sockOnFinishDyn": "scr_net_sock_on_finish_dyn", + "net.isIP": "scr_net_is_ip", + "net.isIPv4": "scr_net_is_ipv4", + "net.isIPv6": "scr_net_is_ipv6", + "net.serverMaxConnectionsGet": "scr_net_server_max_connections_get", + "net.serverMaxConnectionsSet": "scr_net_server_max_connections_set", + "net.serverOnDrop": "scr_net_server_on_drop", + "net.serverGetConnections": "scr_net_server_get_connections", "net.sockReadable": "scr_net_sock_readable", "net.sockPipeRes": "scr_http_sock_pipe_res", "net.serverEmitConnection": "scr_net_server_emit_connection", diff --git a/packages/compiler/src/backend/native-codegen-core.ts b/packages/compiler/src/backend/native-codegen-core.ts index e9b11ff7e..5b9437cde 100644 --- a/packages/compiler/src/backend/native-codegen-core.ts +++ b/packages/compiler/src/backend/native-codegen-core.ts @@ -13,11 +13,10 @@ const MAX_PROGRAM_PARTITIONS = 64; /** Executable objects this large are divided into partitions that the helper * simplifies, optimizes and compiles concurrently, importing small functions * across partitions for inlining, and reuses individually across builds. The - * count is a power of two that depends only on the module, so artifacts never - * vary with the host and ordinary edits keep every function in its - * partition. */ -export function nativeProgramPartitions(target: NativeTargetSpec, llvmBytes: number): number { - if (target.platform === "wasi") return 1; + * count is a power of two that depends only on the module's size, on every + * target, so artifacts never vary with the host and ordinary edits keep every + * function in its partition. */ +export function nativeProgramPartitions(llvmBytes: number): number { const required = Math.ceil(llvmBytes / PROGRAM_PARTITION_BYTES); if (required < 2) return 1; return Math.min(MAX_PROGRAM_PARTITIONS, 2 ** Math.ceil(Math.log2(required))); diff --git a/packages/compiler/src/backend/native-codegen.test.ts b/packages/compiler/src/backend/native-codegen.test.ts index f6a8b721f..16704cbc8 100644 --- a/packages/compiler/src/backend/native-codegen.test.ts +++ b/packages/compiler/src/backend/native-codegen.test.ts @@ -346,16 +346,15 @@ test("sanitized native artifacts fail before helper resolution", async () => { ).rejects.toMatchObject({ diagnosticCode: "SC3002", detailCode: "sanitize_unsupported" }); }); -test("program partitions depend only on module size and target", () => { +test("program partitions depend only on module size", () => { const megabyte = 1024 * 1024; - expect(nativeProgramPartitions(LINUX_X64_GNU_TARGET, 4 * megabyte)).toBe(1); - expect(nativeProgramPartitions(LINUX_X64_GNU_TARGET, 4 * megabyte + 1)).toBe(2); + expect(nativeProgramPartitions(4 * megabyte)).toBe(1); + expect(nativeProgramPartitions(4 * megabyte + 1)).toBe(2); // Powers of two keep every function in its partition across small edits. - expect(nativeProgramPartitions(LINUX_X64_GNU_TARGET, 13 * megabyte)).toBe(4); - expect(nativeProgramPartitions(LINUX_X64_GNU_TARGET, 16 * megabyte)).toBe(4); - expect(nativeProgramPartitions(MACOS_ARM64_TARGET, 100 * megabyte)).toBe(32); - expect(nativeProgramPartitions(MACOS_ARM64_TARGET, 1024 * megabyte)).toBe(64); - expect(nativeProgramPartitions(WASM32_WASI_TARGET, 100 * megabyte)).toBe(1); + expect(nativeProgramPartitions(13 * megabyte)).toBe(4); + expect(nativeProgramPartitions(16 * megabyte)).toBe(4); + expect(nativeProgramPartitions(100 * megabyte)).toBe(32); + expect(nativeProgramPartitions(1024 * megabyte)).toBe(64); expect(nativePartitionPaths("/build.d/program.o", 3)).toEqual([ "/build.d/program.o", "/build.d/program.part1.o", diff --git a/packages/compiler/src/frontend/lowering/lower-dgram.ts b/packages/compiler/src/frontend/lowering/lower-dgram.ts index 98bd2eacd..a495c9ba0 100644 --- a/packages/compiler/src/frontend/lowering/lower-dgram.ts +++ b/packages/compiler/src/frontend/lowering/lower-dgram.ts @@ -25,7 +25,6 @@ import { STRING, VOID, } from "../../ir/ir.js"; -import { DNS_LOOKUP_DOCUMENTED_OPTIONS, fenceOrDropOptionKey } from "./surfaces.js"; import { resultIsDiscarded } from "./call-position.js"; import { lowerCallbackArg as lowerCallbackArgShared } from "./callback-arg.js"; @@ -66,17 +65,6 @@ function requireStatementPosition(lowerer: Lowerer, call: ts.CallExpression, wha * return, at most `maxParams` parameters, each parameter's IR kind * satisfying `paramOk` (indexed). The lower-server helper's shape, * re-stated here so the spoke stays self-contained. */ -/** True iff `t` is the `Error | null` union — dns.lookup's first callback - * parameter (NodeJS.ErrnoException maps to %Error in type-mapper.ts). */ -function isErrorOrNullUnion(lowerer: Lowerer, t: IrType): boolean { - if (t.kind !== "union") return false; - const def = lowerer.unions.get(t.unionId); - if (!def || def.arms.length !== 2) return false; - const hasNull = def.arms.some((a) => a.kind === "nullT"); - const hasError = def.arms.some((a) => a.kind === "object" && a.className === "%Error"); - return hasNull && hasError; -} - /** True iff `t` is a record of exactly the given (name-sorted) string/f64 * fields — the AddressInfo/RemoteInfo shape check. */ function isRecordOfFields( @@ -239,97 +227,118 @@ export function lowerDgramDnsModuleCall( ); } -/** dns.lookup(hostname, { family: 4 }, (err, address[, family]) => ...) — - * the ONE lowered dns member. getaddrinfo runs at call time; the callback - * defers to the next loop turn (SEMANTICS.md documents the split). */ +/** A dns callback the runtime calls through the checked-dynamic boundary: + * a dyn function as is, or a typed function boxed with per-argument + * checks. */ +function lowerDnsCallback(lowerer: Lowerer, node: ts.Expression, what: string): IrExpr { + const cb = lowerer.lowerExpr(node); + if (cb.type.kind === "dyn") return cb; + if ( + cb.type.kind === "func" && + canBoxFuncIntoDyn( + cb.type, + (id) => lowerer.shapes.get(id), + (id) => lowerer.unions.get(id), + ) + ) { + return { kind: "dynFrom", value: cb, type: DYN, loc: locOf(node) }; + } + lowerer.noLowering( + `${what} that cannot cross the checked-dynamic boundary`, + node, + "the callback's parameters must have checked-dynamic conversions", + ); +} + +/** dns module calls: lookup(hostname[, options], callback) with Node's + * option forms, lookupService(address, port, callback), getServers() and + * get/setDefaultResultOrder(). The runtime validates the arguments as + * lib/dns.js does; getaddrinfo runs at call time and the callback comes + * with the other lookup deliveries (literal addresses on the next tick). */ function lowerDnsModuleCall( lowerer: Lowerer, expr: ts.CallExpression, bi: { module: string; member: string }, loc: SrcLoc, ): IrExpr { - if (bi.member !== "lookup") { - lowerer.noLowering( - `dns.${bi.member}`, - expr, - "lookup is the lowered dns module function", - ts.isIdentifier(expr.expression) ? lowerer.resolveValueSymbol(expr.expression) : undefined, - ); - } - requireStatementPosition(lowerer, expr, "dns.lookup(...)"); const args = expr.arguments; - if (args.length !== 3) { - lowerer.noLowering( - `lookup with ${args.length} arguments`, - expr, - "the supported form is lookup(hostname, { family: 4 }, callback)", - ); - } - const hostname = lowerer.lowerExprExpecting(args[0]!, STRING); - // The options: an object literal whose one meaningful entry is the - // IPv4 pin — `{ family: 4 }`. Node's family-less and family-6 lookups - // have no lowering (the runtime resolves over getaddrinfo/AF_INET). - const opts = args[1]!; - if (!ts.isObjectLiteralExpression(opts)) { - lowerer.noLowering( - "lookup with a non-literal options argument", - opts, - "pass the options as an object literal: lookup(hostname, { family: 4 }, cb)", - ); - } - let family: IrExpr | null = null; - for (const prop of opts.properties) { - if (!ts.isPropertyAssignment(prop) || !ts.isIdentifier(prop.name)) { + const dyn = (node: ts.Expression): IrExpr => + lowerer.coerceToExpected(lowerer.lowerExpr(node), DYN); + // A callback whose second parameter is typed (string, LookupAddress[]) + // cannot take the undefined Node passes after an error. + const typedResult = (node: ts.Expression): IrExpr => { + const sig = lowerer.checker.getCallSignatures(lowerer.typeOf(node))[0]; + const second = sig?.getParameters()[1]; + const t = second ? lowerer.mapTypeOf(lowerer.checker.getTypeOfSymbol(second)) : null; + return boolLit(t !== null && t.kind !== "dyn", loc); + }; + if (bi.member === "lookup") { + requireStatementPosition(lowerer, expr, "dns.lookup(...)"); + if (args.length !== 2 && args.length !== 3) { lowerer.noLowering( - "lookup options with computed keys, spreads, or shorthand entries", - prop, - "each option must be a plain `name: value` entry with a literal key", + `lookup with ${args.length} arguments`, + expr, + "the supported forms are lookup(hostname, callback) and lookup(hostname, options, callback)", ); } - if (prop.name.text !== "family") { - // The options-record stance: hints/all/order/verbatim are - // documented dns.lookup knobs with no lowering — they fence by - // name; undocumented keys drop like Node drops them. - fenceOrDropOptionKey( - lowerer, - prop, - prop.name.text, - "lookup", - DNS_LOOKUP_DOCUMENTED_OPTIONS, - "family: 4 is the supported option", - { - all: "the all-addresses callback shape has no lowering — the lowered callback is (err, address, family) over one IPv4 answer", - }, - ); - continue; // an undocumented key, dropped like Node drops it - } - const t = lowerer.typeOf(prop.initializer); - if (!t.isNumberLiteralType() || t.value !== 4) { + const hostname = dyn(args[0]!); + const options = args.length === 3 ? dyn(args[1]!) : dynUndefinedExpr(loc); + const last = args[args.length - 1]!; + const typed = typedResult(last); + const cb = lowerDnsCallback(lowerer, last, "lookup callbacks"); + return { + kind: "libCall", + fn: "dns.lookupDyn", + args: [hostname, options, cb, typed], + type: VOID, + loc, + }; + } + if (bi.member === "lookupService") { + requireStatementPosition(lowerer, expr, "dns.lookupService(...)"); + if (args.length !== 3) { lowerer.noLowering( - "lookup with a family other than the literal 4", - prop.initializer, - "IPv4 lookups ({ family: 4 }) are the supported form", + `lookupService with ${args.length} arguments`, + expr, + "the form is lookupService(address, port, callback)", ); } - family = lowerer.lowerExprExpecting(prop.initializer, F64); + const typed = typedResult(args[2]!); + const cb = lowerDnsCallback(lowerer, args[2]!, "lookupService callbacks"); + return { + kind: "libCall", + fn: "dns.lookupService", + args: [dyn(args[0]!), dyn(args[1]!), cb, typed], + type: VOID, + loc, + }; } - if (!family) { - lowerer.noLowering( - "lookup options without a family", - opts, - "the supported form is lookup(hostname, { family: 4 }, cb)", - ); + if (bi.member === "getServers" && args.length === 0) { + const value: IrExpr = { kind: "libCall", fn: "dns.getServers", args: [], type: DYN, loc }; + const declared = lowerer.mapTypeOf(lowerer.typeOf(expr)); + return declared === null || declared.kind === "dyn" + ? value + : lowerer.coerceInto(expr, value, declared); } - const { cb } = lowerCallbackArg( - lowerer, - args[2]!, - "lookup callbacks", - 3, - (p, i) => - i === 0 ? isErrorOrNullUnion(lowerer, p) : i === 1 ? p.kind === "string" : p.kind === "f64", - "use (err, address) — err is Error | null, address a string", + if (bi.member === "getDefaultResultOrder" && args.length === 0) { + return { kind: "libCall", fn: "dns.getDefaultResultOrder", args: [], type: STRING, loc }; + } + if (bi.member === "setDefaultResultOrder" && args.length === 1) { + requireStatementPosition(lowerer, expr, "dns.setDefaultResultOrder(...)"); + return { + kind: "libCall", + fn: "dns.setDefaultResultOrder", + args: [dyn(args[0]!)], + type: VOID, + loc, + }; + } + lowerer.noLowering( + `dns.${bi.member}`, + expr, + "lookup, lookupService, getServers, getDefaultResultOrder, and setDefaultResultOrder are the lowered dns module functions", + ts.isIdentifier(expr.expression) ? lowerer.resolveValueSymbol(expr.expression) : undefined, ); - return { kind: "libCall", fn: "dns.lookup", args: [hostname, family, cb], type: VOID, loc }; } /** Method calls on dgram.Socket receivers — one entry in lower-calls.ts's diff --git a/packages/compiler/src/frontend/lowering/lower-server.ts b/packages/compiler/src/frontend/lowering/lower-server.ts index 050fe9be3..757020407 100644 --- a/packages/compiler/src/frontend/lowering/lower-server.ts +++ b/packages/compiler/src/frontend/lowering/lower-server.ts @@ -224,7 +224,7 @@ export function voidizedCallback(lowerer: Lowerer, cb: IrExpr, loc: SrcLoc): IrE body: [ { kind: "return", - value: { kind: "closure", fnName: impl, captures: ["f.0"], type: toT, loc }, + value: { kind: "closure", fnName: impl, captures: ["f.0"], type: toT, adapts: true, loc }, loc, }, ], @@ -234,6 +234,30 @@ export function voidizedCallback(lowerer: Lowerer, cb: IrExpr, loc: SrcLoc): IrE return { kind: "call", callee: name, args: [cb], type: toT, loc }; } +/** A socket or server listener with parameters that the runtime calls + * through the checked-dynamic boundary (close(hadError), write's + * cb(err), 'drop', getConnections): a dyn function value as is, or a + * typed function boxed with per-argument checks. */ +function lowerSocketDynListener(lowerer: Lowerer, node: ts.Expression, what: string): IrExpr { + const cb = lowerer.lowerExpr(node); + if (cb.type.kind === "dyn") return cb; + if ( + cb.type.kind === "func" && + canBoxFuncIntoDyn( + cb.type, + (id) => lowerer.shapes.get(id), + (id) => lowerer.unions.get(id), + ) + ) { + return { kind: "dynFrom", value: cb, type: DYN, loc: locOf(node) }; + } + lowerer.noLowering( + `${what} that cannot cross the checked-dynamic boundary`, + node, + "the callback's parameters and return value must have checked-dynamic conversions", + ); +} + /** A registration/side-effect libCall re-shaped to answer its RECEIVER — * Node's `return this` chaining (server.listen(...), res.writeHead(...)): * an interned helper runs the void call and returns its first argument. @@ -1283,6 +1307,33 @@ export function lowerHttpServerTimeoutAssignment( }; } +/** `server.maxConnections = n`: the value crosses as a dyn (undefined + * and null unset the cap). */ +export function lowerNetServerMaxConnectionsAssignment( + lowerer: Lowerer, + left: ts.Expression, + right: ts.Expression, + loc: SrcLoc, +): IrStmt | null { + if (!ts.isPropertyAccessExpression(left) || left.questionDotToken) return null; + if (left.name.text !== "maxConnections") return null; + if (lowerer.mapTypeOf(lowerer.typeOf(left.expression))?.kind !== "netServer") return null; + if (!lowerer.isStdlibMember(left)) return null; + const receiver = coerceToHandle(lowerer, left.expression, NETSERVER_T); + const value = lowerer.coerceToExpected(lowerer.lowerExpr(right), DYN); + return { + kind: "exprStmt", + expr: { + kind: "libCall", + fn: "net.serverMaxConnectionsSet", + args: [receiver, value], + type: VOID, + loc, + }, + loc, + }; +} + /** Method calls on net.Server receivers: listen(port[, cb]), close([cb]), * on/once("connection" | "error" | "close", cb). address() alone fences * toward the composed address().port read. Null for other receivers. */ @@ -1296,6 +1347,34 @@ function lowerNetServerMethodCall( const name = access.name.text; const loc = locOf(call); const args = call.arguments; + if (name === "getConnections") { + // cb(null, count) on the next tick; answers the server. + if (args.length !== 1) { + lowerer.noLowering( + `getConnections with ${args.length} arguments`, + call, + "the form is getConnections(callback)", + ); + } + const receiver = coerceToHandle(lowerer, access.expression, NETSERVER_T); + const cb = lowerSocketDynListener(lowerer, args[0]!, "getConnections callbacks"); + if (resultIsDiscarded(call)) { + return { + kind: "libCall", + fn: "net.serverGetConnections", + args: [receiver, cb], + type: VOID, + loc, + }; + } + return receiverReturningCall( + lowerer, + "net.serverGetConnections", + [receiver, cb], + NETSERVER_T, + loc, + ); + } if (name === "setTimeout") { if (args.length < 1 || args.length > 2) lowerer.noLowering( @@ -1744,6 +1823,17 @@ function lowerNetServerMethodCall( loc, }; } + if (event === "drop") { + // A connection past maxConnections, with its local and remote + // addresses. + return { + kind: "libCall", + fn: "net.serverOnDrop", + args: [receiver, lowerSocketDynListener(lowerer, args[1]!, "drop listeners"), once], + type: VOID, + loc, + }; + } if (event === "session") { // The h2c server's per-connection session event: (session). The // result is voidized; the handle passes by reference. @@ -1885,7 +1975,7 @@ function lowerNetServerMethodCall( lowerer.noLowering( `server.${name}(${event === null ? "non-literal event" : `"${event}"`}, ...)`, args[0]!, - '"connection", "request", "stream", "session", "upgrade", "connect", "error", "close", "listening", and "sessionError" are the supported server events (as literals)', + '"connection", "request", "drop", "stream", "session", "upgrade", "connect", "error", "close", "listening", and "sessionError" are the supported server events (as literals)', ); } if (name === "emit") { @@ -2019,6 +2109,85 @@ function lowerNetSocketMethodCall( } } } + // A trailing callback: write's runs once the data is flushed (with + // null for the error), end's once the socket finishes ('finish'). + const last = args.length > 0 ? args[args.length - 1]! : null; + if ( + last !== null && + args.length <= 2 && + (name === "end" || args.length === 2) && + lowerer.checker.getCallSignatures(lowerer.typeOf(last)).length > 0 + ) { + const sockLocal = lowerer.declareHiddenLocal("%sockCb", NETSOCKET_T); + const stmts: IrStmt[] = [ + { + kind: "varDecl", + localId: sockLocal.id, + init: coerceToHandle(lowerer, access.expression, NETSOCKET_T), + loc, + }, + ]; + const receiver = (): IrExpr => ({ + kind: "varRef", + localId: sockLocal.id, + type: NETSOCKET_T, + loc, + }); + const dataNode = args.length === 2 ? args[0]! : null; + let main: IrExpr = { + kind: "libCall", + fn: "net.sockEnd", + args: [receiver()], + type: VOID, + loc, + }; + if (dataNode !== null) { + const data = lowerer.lowerExpr(dataNode); + const fn: IrLibFn | null = + data.type.kind === "string" + ? name === "write" + ? "net.sockWrite" + : "net.sockEndStr" + : data.type.kind === "bytes" && data.type.elem === "u8" + ? name === "write" + ? "net.sockWriteBytes" + : "net.sockEndBytes" + : data.type.kind === "dyn" + ? name === "write" + ? "net.sockWriteDyn" + : "net.sockEndDyn" + : null; + if (fn === null) { + lowerer.noLowering( + `${name} of '${lowerer.fmt(data.type)}' data`, + dataNode, + NARROW_DATA_HINT, + ); + } + main = { kind: "libCall", fn, args: [receiver(), data], type: VOID, loc }; + } + const cb = lowerer.lowerExpr(last); + let register: IrExpr; + if (cb.type.kind === "func" && cb.type.params.length === 0 && cb.type.ret.kind === "void") { + register = { + kind: "libCall", + fn: name === "write" ? "net.sockOnWriteFlush" : "net.sockOnFinish", + args: [receiver(), cb], + type: VOID, + loc, + }; + } else { + register = { + kind: "libCall", + fn: name === "write" ? "net.sockOnWriteFlushDyn" : "net.sockOnFinishDyn", + args: [receiver(), lowerSocketDynListener(lowerer, last, `${name} callbacks`)], + type: VOID, + loc, + }; + } + stmts.push({ kind: "exprStmt", expr: main, loc }); + return { kind: "seqExpr", stmts, result: register, type: VOID, loc }; + } const maxArgs = name === "write" ? 1 : 1; const minArgs = name === "write" ? 1 : 0; if (args.length < minArgs || args.length > maxArgs) { @@ -2083,17 +2252,62 @@ function lowerNetSocketMethodCall( } if (name === "setTimeout") { requireStatementPosition(lowerer, call, "socket.setTimeout(...)"); - if (args.length !== 1) { + if (args.length !== 1 && args.length !== 2) { lowerer.noLowering( `setTimeout with ${args.length} arguments`, call, - "the supported form is setTimeout(ms) — register the callback separately: socket.once('timeout', ...)", + "the supported forms are setTimeout(ms) and setTimeout(ms, callback)", ); } const receiver = coerceToHandle(lowerer, access.expression, NETSOCKET_T); const ms = lowerer.lowerExprExpecting(args[0]!, F64); + if (args.length === 2) { + // Node adds the callback as a once('timeout') listener (removes it + // for 0); msecs is checked first either way. + const { cb } = lowerCallbackArg( + lowerer, + args[1]!, + "setTimeout callbacks", + 0, + () => false, + "use ()", + [], + ); + return { + kind: "libCall", + fn: "net.sockSetTimeoutCb", + args: [receiver, ms, cb], + type: VOID, + loc, + }; + } return { kind: "libCall", fn: "net.sockSetTimeout", args: [receiver, ms], type: VOID, loc }; } + if (name === "setKeepAlive") { + // SO_KEEPALIVE plus the initial delay (whole seconds, at least 1); + // Node's defaults are false and 0. Answers the socket. + if (args.length > 2) { + lowerer.noLowering( + `setKeepAlive with ${args.length} arguments`, + call, + "the form is setKeepAlive(enable?, initialDelay?)", + ); + } + const receiver = coerceToHandle(lowerer, access.expression, NETSOCKET_T); + const enable: IrExpr = + args.length >= 1 ? lowerer.lowerExprExpecting(args[0]!, BOOL) : boolLit(false, loc); + const delay: IrExpr = + args.length === 2 + ? lowerer.lowerExprExpecting(args[1]!, F64) + : { kind: "numLit", value: 0, type: F64, loc }; + return { + kind: "libCall", + fn: "net.sockSetKeepAlive", + args: [receiver, enable, delay], + type: NETSOCKET_T, + loc, + }; + } if (name === "pause" || name === "resume") { // Flow control (the struct's flag comments in scr_net.c): pause holds // reads off — kernel/TCP backpressure is the buffer; resume flows @@ -2212,6 +2426,31 @@ function lowerNetSocketMethodCall( const event = evT.isStringLiteralType() ? evT.value : null; const receiver = coerceToHandle(lowerer, access.expression, NETSOCKET_T); if (event === "data") { + const sigs = lowerer.checker.getCallSignatures(lowerer.typeOf(args[1]!)); + const firstParam = sigs.length === 1 ? sigs[0]!.getParameters()[0] : undefined; + const param = + firstParam !== undefined + ? lowerer.mapTypeOf(lowerer.checker.getTypeOfSymbol(firstParam)) + : null; + if ( + param !== null && + param.kind !== "dyn" && + !(param.kind === "bytes" && param.elem === "u8") + ) { + // A chunk typed string (or string | Buffer): the listener takes + // the chunk through a checked boundary, so a string arrives after + // setEncoding and a Buffer without it fails the check instead of + // being decoded. + const boxed = lowerSocketDynListener(lowerer, args[1]!, "data listeners"); + const cb: IrExpr = { kind: "dynCheck", value: boxed, type: funcOf([DYN], VOID), loc }; + return { + kind: "libCall", + fn: "net.sockOnData", + args: [receiver, cb, once], + type: VOID, + loc, + }; + } const { cb } = lowerCallbackArg( lowerer, args[1]!, @@ -2241,10 +2480,26 @@ function lowerNetSocketMethodCall( loc, }; } + if ( + event === "close" && + lowerer.checker + .getCallSignatures(lowerer.typeOf(args[1]!)) + .some((sig) => sig.getParameters().length > 0) + ) { + // close(hadError): true when the socket closed because of an error. + return { + kind: "libCall", + fn: "net.sockOnCloseDyn", + args: [receiver, lowerSocketDynListener(lowerer, args[1]!, "close listeners"), once], + type: VOID, + loc, + }; + } if ( event === "end" || event === "close" || event === "connect" || + event === "ready" || event === "timeout" || event === "readable" ) { @@ -2266,7 +2521,9 @@ function lowerNetSocketMethodCall( ? "net.sockOnTimeout" : event === "readable" ? "net.sockOnReadable" - : "net.sockOnConnect"; + : event === "ready" + ? "net.sockOnReady" + : "net.sockOnConnect"; return { kind: "libCall", fn, args: [receiver, cb, once], type: VOID, loc }; } if (event === "finish") { @@ -2328,13 +2585,13 @@ function lowerNetSocketMethodCall( lowerer.noLowering( `socket.${name}(${event === null ? "non-literal event" : `"${event}"`}, ...)`, args[0]!, - '"data", "end", "close", "error", "connect", "timeout", "readable", "secureConnect", and "session" are the supported socket events (as literals)', + '"data", "end", "close", "error", "connect", "ready", "timeout", "readable", "secureConnect", and "session" are the supported socket events (as literals)', ); } lowerer.noLowering( `Socket.${name}`, call, - "write, end, destroy, pipe, setTimeout, remoteAddress, read, unshift, and on/once of data/end/close/error/connect/timeout/readable are the supported Socket members", + "write, end, destroy, pipe, setTimeout, setKeepAlive, setNoDelay, the address fields, read, unshift, and on/once of data/end/close/error/connect/ready/timeout/readable are the supported Socket members", lowerer.checker.getSymbolAtLocation(access.name), ); } @@ -2978,6 +3235,25 @@ export function lowerServerProperty( const receiver = coerceToHandle(lowerer, expr.expression, NETSERVER_T); return { kind: "libCall", fn: "net.serverListening", args: [receiver], type: BOOL, loc }; } + if (expr.name.text === "maxConnections") { + const receiver = coerceToHandle(lowerer, expr.expression, NETSERVER_T); + const value: IrExpr = { + kind: "libCall", + fn: "net.serverMaxConnectionsGet", + args: [receiver], + type: DYN, + loc, + }; + // The property's declared type: an earlier `= undefined` narrows + // the read's own type to undefined. + const sym = lowerer.checker.getSymbolAtLocation(expr.name); + const declared = lowerer.mapTypeOf( + sym ? lowerer.checker.getTypeOfSymbol(sym) : lowerer.typeOf(expr), + ); + return declared === null || declared.kind === "dyn" + ? value + : lowerer.coerceInto(expr, value, declared); + } const field = httpServerTimeoutField(expr.name.text); if (field !== null) { const receiver = coerceToHandle(lowerer, expr.expression, NETSERVER_T); @@ -3205,6 +3481,46 @@ export function lowerServerProperty( loc, }; } + if (recvKind === "netSocket" && lowerer.isStdlibMember(expr)) { + const name = expr.name.text; + // The other address fields: the peer's cached like remoteAddress, the + // local ones read live (undefined while connecting and after close). + const field = [ + "remotePort", + "remoteFamily", + "localAddress", + "localPort", + "localFamily", + ].indexOf(name); + if (field >= 0) { + const receiver = coerceToHandle(lowerer, expr.expression, NETSOCKET_T); + const value: IrExpr = { + kind: "libCall", + fn: "net.sockAddrField", + args: [receiver, { kind: "numLit", value: field, type: F64, loc }], + type: DYN, + loc, + }; + const declared = lowerer.mapTypeOf(lowerer.typeOf(expr)); + return declared === null || declared.kind === "dyn" + ? value + : lowerer.coerceInto(expr, value, declared); + } + const scalar: { fn: IrLibFn; type: IrType } | null = + name === "bytesRead" + ? { fn: "net.sockBytesRead", type: F64 } + : name === "connecting" + ? { fn: "net.sockConnecting", type: BOOL } + : name === "pending" + ? { fn: "net.sockPending", type: BOOL } + : name === "readyState" + ? { fn: "net.sockReadyState", type: STRING } + : null; + if (scalar !== null) { + const receiver = coerceToHandle(lowerer, expr.expression, NETSOCKET_T); + return { kind: "libCall", fn: scalar.fn, args: [receiver], type: scalar.type, loc }; + } + } if ( recvKind === "httpClientReq" && lowerer.isStdlibMember(expr) && diff --git a/packages/compiler/src/frontend/lowering/lower-stmts.ts b/packages/compiler/src/frontend/lowering/lower-stmts.ts index 5d6305aa8..5ae5bef1a 100644 --- a/packages/compiler/src/frontend/lowering/lower-stmts.ts +++ b/packages/compiler/src/frontend/lowering/lower-stmts.ts @@ -126,6 +126,7 @@ import { lowerStreamUnderscoreAssign, streamClassAliasDecl } from "./lower-strea import { lowerHttpResPropertyAssignment, lowerHttpServerTimeoutAssignment, + lowerNetServerMaxConnectionsAssignment, lowerServerCloseOverrideAssignment, } from "./lower-server.js"; import { lowerUrlAssignment } from "./builtins/url.js"; @@ -6166,6 +6167,16 @@ export function lowerExprStatement(lowerer: Lowerer, expr: ts.Expression): IrStm ); if (timeoutProp) return timeoutProp; } + // server.maxConnections — net.Server's connection cap. + { + const capProp = lowerNetServerMaxConnectionsAssignment( + lowerer, + expr.left, + expr.right, + locOf(expr), + ); + if (capProp) return capProp; + } // `N.x = v` — a namespace-qualified write: exported `let` // members are module globals, so the qualified spelling assigns // exactly like the bare one inside the namespace body. diff --git a/packages/compiler/src/frontend/lowering/lowerer.ts b/packages/compiler/src/frontend/lowering/lowerer.ts index 99c6b4d46..80d5cbe83 100644 --- a/packages/compiler/src/frontend/lowering/lowerer.ts +++ b/packages/compiler/src/frontend/lowering/lowerer.ts @@ -1479,6 +1479,15 @@ export function jsFuncNameOf(node: ts.Node, lowerer?: Lowerer): string | null { } /** Answers `fallback` when a checker query panics inside the type checker. */ +/** The storage of a global whose binding type widened. A lexical binding's + * TDZ guard needs pointer, record or checked-value storage, so a guarded + * function binding that may now hold undefined is a checked value. */ +function tdzStorage(global: IrGlobal, type: IrType): IrType { + return global.tdz && type.kind !== "record" && type.kind !== "func" && type.kind !== "dyn" + ? DYN + : type; +} + function panicSafe(query: () => T, fallback: T): T { try { return query(); @@ -1840,7 +1849,10 @@ export class Lowerer { : this.checker.getSymbolAtLocation(node); if (!symbol) return fallback ?? null; const known = this.runtimeOptionalBindingTypes.get(symbol); - if (known) return known; + if (known) { + const global = this.globalsBySymbol.get(symbol); + return global ? tdzStorage(global, known) : known; + } // A module-scope function alias is collected as a global before the // optional-read fixed point settles. Derive its promoted function ABI // directly from the aliased declaration as a final read-side hook, so @@ -4667,8 +4679,18 @@ export class Lowerer { // Checker answers for the fixed point's repeated element-ABI queries. // Only a binding's widened storage can change between passes. const checkerTypes = new Map(); + // Mapping a type can lower a local class expression. When that class is + // unsupported, its diagnostic is already recorded and the element ABI + // is unknown here; the program fails with that diagnostic. const arrayKeysOf = (node: ts.Expression): readonly string[] | null => - panicSafe(() => arrayKeysUnsafe(node), null); + panicSafe(() => { + try { + return arrayKeysUnsafe(node); + } catch (e) { + if (e instanceof PoisonError) return null; + throw e; + } + }, null); const arrayKeysUnsafe = (node: ts.Expression): readonly string[] | null => { let checkerType = checkerTypes.get(node); if (checkerType === undefined) { @@ -5769,7 +5791,7 @@ export class Lowerer { ? this.globalOf(node.name) : this.globalsBySymbol.get(symbol); if (global) { - global.type = promoted; + global.type = tdzStorage(global, promoted); this.runtimeOptionalGlobals.add(global); } } else if (promoted.kind === "union" && this.armTag(promoted.unionId, UNDEFINED_T) >= 0) { diff --git a/packages/compiler/src/frontend/lowering/surfaces.ts b/packages/compiler/src/frontend/lowering/surfaces.ts index 20b8e5469..58882a552 100644 --- a/packages/compiler/src/frontend/lowering/surfaces.ts +++ b/packages/compiler/src/frontend/lowering/surfaces.ts @@ -1776,6 +1776,10 @@ export const BUILTIN_MODULE_FNS: Record< params: [F64], result: VOID, }, + // Node's IPv4Reg / IPv6Reg tests over String(input). + isIP: { fn: "net.isIP", params: [DYN], result: F64 }, + isIPv4: { fn: "net.isIPv4", params: [DYN], result: BOOL }, + isIPv6: { fn: "net.isIPv6", params: [DYN], result: BOOL }, }, http: {}, // node:tls and node:https ride the same spoke (tls.createServer's diff --git a/packages/compiler/src/frontend/pipeline.ts b/packages/compiler/src/frontend/pipeline.ts index f2fc69228..db2b57af8 100644 --- a/packages/compiler/src/frontend/pipeline.ts +++ b/packages/compiler/src/frontend/pipeline.ts @@ -97,7 +97,7 @@ function detectAutoPackages( // or a package visible only to a nested package.json realm (a pnpm // monorepo's packages/*/node_modules, unreachable from the entry's own // walk-up) answers "no runtime JS" for perfectly ordinary installs. - const seen = new Map(); + const seen = new Map(); for (const sf of [...load.moduleOrder, load.entry]) { if (mode === "auto" && sf.fileName.includes("/node_modules/")) continue; const edges: { spec: string; loc: SrcLoc }[] = []; @@ -152,15 +152,18 @@ function detectAutoPackages( } if (judged?.has(npm.packageName)) continue; if (!seen.has(npm.packageName)) { - seen.set(npm.packageName, { typesFile: npm.typesFile, fromFile: sf.fileName }); + seen.set(npm.packageName, { typesFile: npm.typesFile, fromFile: sf.fileName, spec }); sites?.set(npm.packageName, loc); } } } const chosen: string[] = []; - for (const [pkg, { typesFile, fromFile }] of seen) { + for (const [pkg, { typesFile, fromFile, spec }] of seen) { judged?.add(pkg); - const jsEntry = resolveBareModule(fromFile, pkg, "js-only"); + // A package without a root entry (no "main", "exports" or index file) + // can still serve the subpath the program imports. + const jsEntry = + resolveBareModule(fromFile, pkg, "js-only") ?? resolveBareModule(fromFile, spec, "js-only"); const reason = npmStaticIneligibleReason( pkg, typesFile, diff --git a/packages/compiler/src/frontend/program.ts b/packages/compiler/src/frontend/program.ts index b63212982..2dca21ce9 100644 --- a/packages/compiler/src/frontend/program.ts +++ b/packages/compiler/src/frontend/program.ts @@ -55,6 +55,7 @@ import { nearestInvalidPackageJsonPath, nearestPackageType, nearestPkgJsonPath, + packageImportTarget, projectDtsRuntimeSibling, resolveBareModule, resolveProjectModule, @@ -3881,6 +3882,17 @@ function preflight7(load: LoadResult): { continue; } if (spec.startsWith("#")) { + const target = packageImportTarget(sf.fileName, spec); + if (target !== null && !target.startsWith("./") && !target.startsWith("../")) { + diags.push( + unsupportedDiag( + "SC1010", + locOf7(stmt), + `package.json imports entries that map to other packages (the '${spec}' import maps to '${target}')`, + ), + ); + continue; + } const pkgJson = nearestPkgJsonPath(sf.fileName); refuse( `Package import specifier "${spec}" is not defined${pkgJson !== null ? ` in package ${pkgJson}` : ""} imported from ${sf.fileName}`, diff --git a/packages/compiler/src/frontend/resolve.ts b/packages/compiler/src/frontend/resolve.ts index 14ed80c0f..db440c199 100644 --- a/packages/compiler/src/frontend/resolve.ts +++ b/packages/compiler/src/frontend/resolve.ts @@ -556,6 +556,21 @@ export function nearestPkgJsonPath(fromFile: string): string | null { return dir === null ? null : join(dir, "package.json"); } +/** The target the nearest package scope's "imports" field maps a '#' + * specifier to under Node's ESM runtime conditions: a './' path inside the + * package or another package's specifier. Null when no entry maps it. */ +export function packageImportTarget(fromFile: string, specifier: string): string | null { + const dir = nearestPkgDir(dirname(resolve(fromFile))); + if (dir === null) return null; + const imports = pkgJsonOf(dir)?.imports; + if (!imports || typeof imports !== "object" || Array.isArray(imports)) return null; + // The "imports" map matches like a subpath "exports" map keyed by '#'. + const asExports = Object.fromEntries( + Object.entries(imports).map(([key, value]) => ["." + key, value]), + ); + return resolveExports(asExports, "." + specifier, RUNTIME_IMPORT_CONDITIONS); +} + /** The explicit module format of the nearest package scope. A package.json * without a recognized `type` keeps the file ambiguous, so callers may * apply Node's syntax detector; an explicit commonjs/module answer must win diff --git a/packages/compiler/src/frontend/type-mapper.ts b/packages/compiler/src/frontend/type-mapper.ts index 933607de4..3aa544ed9 100644 --- a/packages/compiler/src/frontend/type-mapper.ts +++ b/packages/compiler/src/frontend/type-mapper.ts @@ -2994,6 +2994,21 @@ function mapTypeInner(type: ts.Type, ctx: TypeMapperCtx): IrType | null { // PromiseFulfilledResult precedent). RemoteInfo's 'IPv4' | 'IPv6' // family widens to string here; the runtime only ever answers "IPv4" // (udp4 is the one lowered socket type). + // dns.lookup's all: true entries: { address, family }. + if (isStdlibInterface("LookupAddress")) { + return { + kind: "record", + shapeId: ctx.shapes.intern( + [ + { name: "address", type: STRING }, + { name: "family", type: F64 }, + ], + false, + undefined, + ["address", "family"], + ), + }; + } // child_process.ExecFileSyncOptionsWithStringEncoding: the exec-options // interface interned explicitly on the AddressInfo pattern, so a TYPED // options value (`const commandOptions: ExecFileSyncOptions... = {...}`, @@ -3049,6 +3064,28 @@ function mapTypeInner(type: ts.Type, ctx: TypeMapperCtx): IrType | null { ), }; } + // net's 'drop' payload: the record the runtime's address object checks + // into, each optional field with its declared type. + if (isStdlibInterface("DropArgument")) { + const known = [ + "localAddress", + "localPort", + "localFamily", + "remoteAddress", + "remotePort", + "remoteFamily", + ]; + const fields: { name: string; type: IrType }[] = []; + for (const property of checker.getPropertiesOfType(widened)) { + if (!known.includes(property.name)) return null; + const type = mapType(checker.getTypeOfSymbol(property), ctx); + if (!type) return null; + fields.push({ name: property.name, type }); + } + const order = fields.map((field) => field.name); + fields.sort((a, b) => (a.name < b.name ? -1 : a.name > b.name ? 1 : 0)); + return { kind: "record", shapeId: ctx.shapes.intern(fields, false, undefined, order) }; + } // Filesystem options are data records. Keep their declared optional fields // instead of allowing arbitrary standard-library interfaces into records. if ( diff --git a/packages/compiler/src/index.ts b/packages/compiler/src/index.ts index ab1131ae6..d9c3521b8 100644 --- a/packages/compiler/src/index.ts +++ b/packages/compiler/src/index.ts @@ -570,7 +570,6 @@ async function emitNativeProgramObject( opts.nativeProgramObject === true || target === null ? 1 : nativeProgramPartitions( - target, (typeof llvm === "string" ? [llvm] : llvm).reduce( (bytes, part) => bytes + Buffer.byteLength(part), 0, diff --git a/packages/compiler/src/ir/builtin-effects.ts b/packages/compiler/src/ir/builtin-effects.ts index b6b15b03c..01d9cad07 100644 --- a/packages/compiler/src/ir/builtin-effects.ts +++ b/packages/compiler/src/ir/builtin-effects.ts @@ -182,6 +182,9 @@ export const MAY_THROW_LIB_FNS: ReadonlySet = new Set([ "readable.nextChunkDyn", // The checked-dynamic chunk forms throw Node's ERR_INVALID_ARG_TYPE // chunk TypeError on a non-string/Buffer dyn kind. + // socket.setTimeout's msecs check (ERR_OUT_OF_RANGE). + "net.sockSetTimeout", + "net.sockSetTimeoutCb", "net.sockWriteDyn", "net.sockEndDyn", "http.resWriteDyn", @@ -661,6 +664,10 @@ export const MAY_THROW_LIB_FNS: ReadonlySet = new Set([ // dns.lookup and net.connect throw ERR_INVALID_ARG_VALUE for a host with // null bytes. "dns.lookup", + // Node's argument checks (ERR_INVALID_ARG_TYPE/VALUE, ERR_SOCKET_BAD_PORT). + "dns.lookupDyn", + "dns.lookupService", + "dns.setDefaultResultOrder", "net.connect", "net.connectCb", // node:dgram state errors (Node throws synchronously): bind on a bound diff --git a/packages/compiler/src/ir/builtin-signatures.ts b/packages/compiler/src/ir/builtin-signatures.ts index b225d46ec..017f1752a 100644 --- a/packages/compiler/src/ir/builtin-signatures.ts +++ b/packages/compiler/src/ir/builtin-signatures.ts @@ -1341,6 +1341,13 @@ export const LIB_FN_SIGS = defineLibFnSignatures({ result: VOID, }, "dns.lookup": { argTypes: [STRING, F64, null], result: VOID }, + /** Node's argument forms with dyn callbacks: lookup(hostname, options, + * callback) and lookupService(address, port, callback). */ + "dns.lookupDyn": { argTypes: [DYN, DYN, DYN, BOOL], result: VOID }, + "dns.lookupService": { argTypes: [DYN, DYN, DYN, BOOL], result: VOID }, + "dns.getServers": { argTypes: [], result: DYN }, + "dns.getDefaultResultOrder": { argTypes: [], result: STRING }, + "dns.setDefaultResultOrder": { argTypes: [DYN], result: VOID }, // node:test (scr_test.c). Bodies are program-dependent closures (0 or // 1 testCtx param, void or Promise result — the spoke pinned the // shape): null slots. sub's result is the settled Promise the @@ -1686,6 +1693,38 @@ export const LIB_FN_SIGS = defineLibFnSignatures({ "net.sockSetNoDelay": { argTypes: [NETSOCKET_T, BOOL], result: NETSOCKET_T }, "net.sockDestroySoon": { argTypes: [NETSOCKET_T], result: VOID }, "net.sockBytesWritten": { argTypes: [NETSOCKET_T], result: F64 }, + /** remotePort (0), remoteFamily (1), localAddress (2), localPort (3), + * localFamily (4): dyn values, undefined when unknown. */ + "net.sockAddrField": { argTypes: [NETSOCKET_T, F64], result: DYN }, + "net.sockBytesRead": { argTypes: [NETSOCKET_T], result: F64 }, + "net.sockConnecting": { argTypes: [NETSOCKET_T], result: BOOL }, + "net.sockPending": { argTypes: [NETSOCKET_T], result: BOOL }, + "net.sockReadyState": { argTypes: [NETSOCKET_T], result: STRING }, + "net.sockSetKeepAlive": { argTypes: [NETSOCKET_T, BOOL, F64], result: NETSOCKET_T }, + "net.sockOnReady": { + argTypes: [NETSOCKET_T, { kind: "func", params: [], ret: VOID }, BOOL], + result: VOID, + }, + /** Listeners with a parameter, as dyn functions: close(hadError), + * write's cb(err) and end's cb. */ + "net.sockOnCloseDyn": { argTypes: [NETSOCKET_T, DYN, BOOL], result: VOID }, + "net.sockOnWriteFlush": { + argTypes: [NETSOCKET_T, { kind: "func", params: [], ret: VOID }], + result: VOID, + }, + "net.sockOnWriteFlushDyn": { argTypes: [NETSOCKET_T, DYN], result: VOID }, + "net.sockOnFinishDyn": { argTypes: [NETSOCKET_T, DYN], result: VOID }, + "net.sockSetTimeoutCb": { + argTypes: [NETSOCKET_T, F64, { kind: "func", params: [], ret: VOID }], + result: VOID, + }, + "net.isIP": { argTypes: [DYN], result: F64 }, + "net.isIPv4": { argTypes: [DYN], result: BOOL }, + "net.isIPv6": { argTypes: [DYN], result: BOOL }, + "net.serverMaxConnectionsGet": { argTypes: [NETSERVER_T], result: DYN }, + "net.serverMaxConnectionsSet": { argTypes: [NETSERVER_T, DYN], result: VOID }, + "net.serverOnDrop": { argTypes: [NETSERVER_T, DYN, BOOL], result: VOID }, + "net.serverGetConnections": { argTypes: [NETSERVER_T, DYN], result: VOID }, "net.sockReadable": { argTypes: [NETSOCKET_T], result: BOOL }, /** socket.on('finish', cb) / end(cb): fires once when the FIN goes out * (sweep-deferred, never the registering stack). */ diff --git a/packages/compiler/src/native/compiler.ts b/packages/compiler/src/native/compiler.ts index 57d2ab4d6..1ce842d26 100644 --- a/packages/compiler/src/native/compiler.ts +++ b/packages/compiler/src/native/compiler.ts @@ -237,9 +237,7 @@ export class NativeCompiler { : null; const split = llvm === null ? null : splitLlvmLibraryProgram(llvm); if (split === null) { - const partitions = library - ? 1 - : nativeProgramPartitions(this.toolchain.target, statSync(input).size); + const partitions = library ? 1 : nativeProgramPartitions(statSync(input).size); return this.emitObject(input, output, source, optimization, "obj", partitions); } const objects: string[] = []; diff --git a/packages/compiler/surface-manifest.json b/packages/compiler/surface-manifest.json index 025cbb880..575ace774 100644 --- a/packages/compiler/surface-manifest.json +++ b/packages/compiler/surface-manifest.json @@ -1394,6 +1394,24 @@ "name": "net.getDefaultAutoSelectFamilyAttemptTimeout", "status": "static" }, + { + "id": "node-builtin.net.isIP", + "kind": "node-builtin", + "name": "net.isIP", + "status": "static" + }, + { + "id": "node-builtin.net.isIPv4", + "kind": "node-builtin", + "name": "net.isIPv4", + "status": "static" + }, + { + "id": "node-builtin.net.isIPv6", + "kind": "node-builtin", + "name": "net.isIPv6", + "status": "static" + }, { "id": "node-builtin.net.setDefaultAutoSelectFamilyAttemptTimeout", "kind": "node-builtin", diff --git a/packages/compiler/test/bytes-element-emission.test.ts b/packages/compiler/test/bytes-element-emission.test.ts index c8802f2d4..d3faa83c9 100644 --- a/packages/compiler/test/bytes-element-emission.test.ts +++ b/packages/compiler/test/bytes-element-emission.test.ts @@ -332,6 +332,33 @@ test("a body mutation keeps the byte loop on the general f64 path", () => { expect(ll).toContain("fptosi double"); }); +test("worker programs read and write shared views at the checked integer index", () => { + const ll = emitLlvmModule({ ...fixture(), workers: true }); + // The shared path receives the index the inline path checked, as an integer, + // and integer elements come back as 32-bit values. + expect(ll).toMatch(/call i32 @scr_bytes_shared_get_int\(ptr %\w+, i64 %?\w+\) cold/); + expect(ll).toMatch(/call double @scr_bytes_shared_get_f64\(ptr %\w+, i64 %?\w+\) cold/); + expect(ll).toMatch(/call void @scr_bytes_shared_set\(ptr %\w+, i64 %?\w+, double %?[\w.]+\)/); + expect(ll).not.toContain("@scr_bytes_get_or_nan"); + expect(ll).not.toContain("@scr_bytes_set("); + // Both paths join as one integer, which converts to a number after the join. + expect(ll).toMatch( + /= phi i32 \[ %\w+, %bytes\.local\.done\d+ \], \[ %\w+, %bytes\.shared\.done\d+ \]/, + ); +}); + +test("worker loops poll for cancellation on the back edge", () => { + const mod = integerLoopFixture(true); + const ll = emitLlvmModule({ ...mod, workers: true }); + const blocks = ll.split(/\n(?=[\w.]+:)/); + const update = blocks.find((block) => block.startsWith("loop.u")); + // The poll budget is decremented in the continue target, after the body; + // the condition block is a single exit, so LLVM can rotate the loop. + expect(update).toContain("sub i32"); + const cond = blocks.find((block) => block.startsWith("loop.c")); + expect(cond).not.toContain("sub i32"); +}); + test("large record clones stay outlined while small clones remain inlineable", () => { const record = (id: string, count: number): IrModule => { const fields = Array.from({ length: count }, (_, i) => ({ name: `f${i}`, type: F64 })); diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5337-dc-subscriber-throw-next-tick.cjs.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5337-dc-subscriber-throw-next-tick.cjs.json new file mode 100644 index 000000000..dec6c877d --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5337-dc-subscriber-throw-next-tick.cjs.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/5337-dc-subscriber-throw-next-tick.cjs" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5338-js-tdz-function-element.js.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5338-js-tdz-function-element.js.json new file mode 100644 index 000000000..b77c33e33 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5338-js-tdz-function-element.js.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/5338-js-tdz-function-element.js" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5339-array-buffer-constructor.js.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5339-array-buffer-constructor.js.json new file mode 100644 index 000000000..52822ac69 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5339-array-buffer-constructor.js.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/5339-array-buffer-constructor.js" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5545-fs-options-encoding.cjs.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5545-fs-options-encoding.cjs.json new file mode 100644 index 000000000..5da697340 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5545-fs-options-encoding.cjs.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/5545-fs-options-encoding.cjs" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5644-net-socket-addresses.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5644-net-socket-addresses.ts.json new file mode 100644 index 000000000..0f0c242e9 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5644-net-socket-addresses.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/5644-net-socket-addresses.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5645-net-socket-callbacks.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5645-net-socket-callbacks.ts.json new file mode 100644 index 000000000..b4277d87a --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5645-net-socket-callbacks.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/5645-net-socket-callbacks.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5646-net-server-connections.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5646-net-server-connections.ts.json new file mode 100644 index 000000000..f7618cd63 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5646-net-server-connections.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/5646-net-server-connections.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5647-net-is-ip.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5647-net-is-ip.ts.json new file mode 100644 index 000000000..2664352c1 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5647-net-is-ip.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/5647-net-is-ip.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5648-dns-lookup-forms.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5648-dns-lookup-forms.ts.json new file mode 100644 index 000000000..03ffbb958 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5648-dns-lookup-forms.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/5648-dns-lookup-forms.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5649-dns-service-servers.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5649-dns-service-servers.ts.json new file mode 100644 index 000000000..67f6ddc76 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5649-dns-service-servers.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/5649-dns-service-servers.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5650-dns-lookup-errors-untyped.cjs.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5650-dns-lookup-errors-untyped.cjs.json new file mode 100644 index 000000000..5ab7a0ecf --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5650-dns-lookup-errors-untyped.cjs.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/5650-dns-lookup-errors-untyped.cjs" + ], + "diags": [] +} diff --git a/packages/runtime/src/scr_bytes.c b/packages/runtime/src/scr_bytes.c index 8cb2e3fb8..ed8642e19 100644 --- a/packages/runtime/src/scr_bytes.c +++ b/packages/runtime/src/scr_bytes.c @@ -272,9 +272,8 @@ static uint32_t scr_bytes_to_u32(double v) { return scr_numeric_to_u32(v); } -double scr_bytes_get(const ScrBytes *b, double i) { - SCR_SHARED_GUARD(b, NULL); - size_t idx = scr_bytes_check_index(b, i); +/* The element at a valid index, as a number. */ +static double scr_bytes_load(const ScrBytes *b, size_t idx) { switch (b->elem) { case SCR_BYTES_U8: case SCR_BYTES_U8C: return (double)b->data[idx]; @@ -317,6 +316,11 @@ double scr_bytes_get(const ScrBytes *b, double i) { return 0; /* unreachable */ } +double scr_bytes_get(const ScrBytes *b, double i) { + SCR_SHARED_GUARD(b, NULL); + return scr_bytes_load(b, scr_bytes_check_index(b, i)); +} + /* The numeric view of an ordinary typed-array read: Node answers undefined * for an index that is not an integer in [0, length), which ToNumber turns * into NaN. Never touches storage out of bounds. */ @@ -325,10 +329,8 @@ double scr_bytes_get_or_nan(const ScrBytes *b, double i) { return scr_bytes_get(b, i); } -void scr_bytes_set(ScrBytes *b, double i, double v) { - SCR_SHARED_GUARD(b, NULL); - if (!(i >= 0) || i != trunc(i) || i >= (double)b->len) return; - size_t idx = (size_t)i; +/* Store v at a valid index with the element type's JS conversion. */ +static void scr_bytes_store(ScrBytes *b, size_t idx, double v) { switch (b->elem) { case SCR_BYTES_U8: case SCR_BYTES_I8: b->data[idx] = (uint8_t)scr_bytes_to_u32(v); @@ -365,6 +367,56 @@ void scr_bytes_set(ScrBytes *b, double i, double v) { } } +void scr_bytes_set(ScrBytes *b, double i, double v) { + SCR_SHARED_GUARD(b, NULL); + if (!(i >= 0) || i != trunc(i) || i >= (double)b->len) return; + scr_bytes_store(b, (size_t)i, v); +} + +/* Compiled code validates the index against the view's length, which never + * changes, before it takes these shared-view paths; they only add the + * shared-memory lock around the element access. */ +int32_t scr_bytes_shared_get_int(const ScrBytes *b, size_t i) { + SCR_SHARED_GUARD(b, NULL); + switch (b->elem) { + case SCR_BYTES_U8: case SCR_BYTES_U8C: + return b->data[i]; + case SCR_BYTES_I8: { + int8_t v; + memcpy(&v, b->data + i, 1); + return v; + } + case SCR_BYTES_U16: { + uint16_t v; + memcpy(&v, b->data + i * 2, 2); + return v; + } + case SCR_BYTES_I16: { + int16_t v; + memcpy(&v, b->data + i * 2, 2); + return v; + } + case SCR_BYTES_U32: case SCR_BYTES_I32: { + int32_t v; + memcpy(&v, b->data + i * 4, 4); + return v; + } + case SCR_BYTES_F32: case SCR_BYTES_F64: + break; + } + scr_trap("scriptc: integer read of a float typed array\n"); +} + +double scr_bytes_shared_get_f64(const ScrBytes *b, size_t i) { + SCR_SHARED_GUARD(b, NULL); + return scr_bytes_load(b, i); +} + +void scr_bytes_shared_set(ScrBytes *b, size_t i, double v) { + SCR_SHARED_GUARD(b, NULL); + scr_bytes_store(b, i, v); +} + /* Dispatch once per bounded block, not once per element. Loads and stores * use memcpy so views and external storage need no stronger alignment or * aliasing promise. A bounded scratch block avoids a kind-pair code matrix. */ diff --git a/packages/runtime/src/scr_bytes.h b/packages/runtime/src/scr_bytes.h index 4634a2994..68e7c21e7 100644 --- a/packages/runtime/src/scr_bytes.h +++ b/packages/runtime/src/scr_bytes.h @@ -221,6 +221,15 @@ void scr_dataview_set(ScrBytes *b, double byte_off, double value, ScrDataViewGet double scr_bytes_get(const ScrBytes *b, double i); double scr_bytes_get_or_nan(const ScrBytes *b, double i); /* NaN when the index is invalid */ void scr_bytes_set(ScrBytes *b, double i, double v); +/* Shared-view element access at an index the caller has checked against + * the length (0 <= i < len). Each takes the shared-memory lock around the + * access. Integer elements read as their value in 32 bits, sign- or + * zero-extended, with Uint32 elements as their bit pattern; float elements + * read as a double. Writes coerce like scr_bytes_set. Compiled code calls + * these only from a view's shared branch. */ +int32_t scr_bytes_shared_get_int(const ScrBytes *b, size_t i); +double scr_bytes_shared_get_f64(const ScrBytes *b, size_t i); +void scr_bytes_shared_set(ScrBytes *b, size_t i, double v); /* Replace a live typed-array capsule's fixed-size payload from its dyn * snapshot. Source and target have the same compiler-checked bytes type. */ void scr_bytes_copy_contents(ScrBytes *dst, const ScrBytes *src); diff --git a/packages/runtime/src/scr_bytes_io.c b/packages/runtime/src/scr_bytes_io.c index 33fff8e79..a9a77068c 100644 --- a/packages/runtime/src/scr_bytes_io.c +++ b/packages/runtime/src/scr_bytes_io.c @@ -65,10 +65,13 @@ ScrStr *scr_fs_read_fd_encoded(double fd, ScrStr *encoding) { /* ── fs (the Buffer forms of scr_lib.c's utf8 pair) ────────────────────── */ +static bool scr_fs_str_is(const ScrStr *s, const char *lit); + /* readFileSync's runtime-encoding form (a JS helper's untyped `enc` * parameter — test/common fixtures.js): undefined/null answer a Buffer, * a supported encoding answers a string, invalid names throw - * ERR_INVALID_ARG_VALUE before reading, and an + * ERR_INVALID_ARG_VALUE before reading, 'buffer' reads and then throws + * ERR_UNKNOWN_ENCODING, and an * options object dispatches on its `encoding` member (Node's form). +1 * dyn value, or NULL with the exception pending. */ ScrDyn *scr_fs_read_file_sync_dyn(ScrStr *path, const ScrDyn *enc) { @@ -85,7 +88,16 @@ ScrDyn *scr_fs_read_file_sync_dyn(ScrStr *path, const ScrDyn *enc) { return d; } if (enc->kind == SCR_DYN_STR) { - ScrStr *text = scr_fs_read_file_encoded(path, enc->v.str); + ScrStr *text; + if (scr_fs_str_is(enc->v.str, "buffer")) { + /* getOptions lets 'buffer' through; Node reads the file, then + * buffer.toString('buffer') throws ERR_UNKNOWN_ENCODING (an empty + * file answers ""). */ + ScrBytes *b = scr_fs_read_file_bytes(path); + if (!b) return NULL; + text = scr_bytes_to_str_checked(b, enc->v.str); + scr_bytes_release(b); + } else text = scr_fs_read_file_encoded(path, enc->v.str); if (!text) return NULL; ScrDyn *value = scr_dyn_new_str(text); scr_str_release(text); @@ -497,6 +509,31 @@ static ScrDyn *scr_fs_cb_string(ScrStr *text) { return value; } +/* A path result (mkdtemp) in getOptions' encoding: a string for utf8 and + * the default, a Buffer for 'buffer', and the UTF-8 bytes re-encoded + * otherwise, as the binding's StringBytes::Encode does. Consumes +1 text. */ +static ScrDyn *scr_fs_cb_encoded_path(ScrStr *text, const ScrDyn *options) { + if (!text) return NULL; + ScrDyn *encoding = options->kind == SCR_DYN_STR ? scr_dyn_retain((ScrDyn *)options) : scr_fs_cb_option(options, "encoding"); + ScrDyn *value = NULL; + if (encoding && encoding->kind == SCR_DYN_STR && encoding->v.str->len > 0 && !scr_fs_str_is(encoding->v.str, "utf8") && + !scr_fs_str_is(encoding->v.str, "utf-8")) { + ScrBytes *bytes = scr_bytes_from_data((const uint8_t *)text->data, text->len); + if (scr_fs_str_is(encoding->v.str, "buffer")) value = scr_dyn_new_buffer(bytes); + else { + ScrStr *encoded = scr_bytes_to_str_checked(bytes, encoding->v.str); + if (encoded) { + value = scr_dyn_new_str(encoded); + scr_str_release(encoded); + } + } + scr_bytes_release(bytes); + } else if (encoding) value = scr_dyn_new_str(text); + scr_dyn_release(encoding); + scr_str_release(text); + return value; +} + static ScrStr *scr_fs_cb_join(const ScrStr *directory, const char *name) { ScrStr *suffix = scr_str_new("/", 1); ScrStr *base = scr_str_concat((ScrStr *)directory, suffix); @@ -814,6 +851,7 @@ static ScrDyn *scr_fs_cb_invoke(ScrStr *member, ScrDyn *const *args, size_t argc double mkdir_mode = 0777; bool mkdir_recursive = false; if (!strcmp(op, "mkdir") && (!scr_fs_cb_mkdir_options(ARG(1), &mkdir_mode, &mkdir_recursive) || !scr_fs_cb_chk(callback, "cb"))) return NULL; + if (!strcmp(op, "mkdtemp") && !scr_fs_encoding_chk(ARG(1))) return NULL; if (!strcmp(op, "readFile") && (!scr_fs_encoding_chk(ARG(1)) || (ARG(1)->kind != SCR_DYN_STR && !scr_fs_cb_read_buffer_option(ARG(1))))) return NULL; /* read checks its fd and the buffer of the full form before the @@ -885,7 +923,15 @@ static ScrDyn *scr_fs_cb_invoke(ScrStr *member, ScrDyn *const *args, size_t argc 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) { + if (text && scr_fs_str_is(encoding->v.str, "buffer")) { + ScrBytes *bytes = scr_fs_read_fd_bytes(opened); + ScrStr *content = bytes ? scr_bytes_to_str_checked(bytes, encoding->v.str) : NULL; + scr_bytes_release(bytes); + if (content) { + result = scr_dyn_new_str(content); + scr_str_release(content); + } + } else if (text) { ScrStr *content = scr_fs_read_fd_encoded(opened, encoding->v.str); if (content) { result = scr_dyn_new_str(content); @@ -940,7 +986,7 @@ static ScrDyn *scr_fs_cb_invoke(ScrStr *member, ScrDyn *const *args, size_t argc if (first && !scr_exc_pending()) result = scr_fs_cb_string(first); else if (first) scr_str_release(first); } else scr_fs_mkdir_mode(path, mkdir_mode); - } else if (!strcmp(op, "mkdtemp")) result = scr_fs_cb_string(scr_fs_mkdtemp_async(path)); + } else if (!strcmp(op, "mkdtemp")) result = scr_fs_cb_encoded_path(scr_fs_mkdtemp_async(path), ARG(1)); 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); else if (!strcmp(op, "unlink")) scr_fs_unlink(path); @@ -1384,9 +1430,18 @@ static bool scr_fs_path_chk(const ScrDyn *p, const char *name) { return false; } -/* assertEncoding over an options slot (a bare encoding string or an - * options record's `encoding` member): Node throws ERR_INVALID_ARG_VALUE - * for any truthy value Buffer.isEncoding rejects. */ +/* An encoding that leaves getOptions' utf8 default: absent, falsy, or a + * utf8 spelling. */ +static bool scr_fs_utf8_encoding(const ScrDyn *enc) { + if (scr_fs_dyn_absent(enc) || !scr_dyn_truthy(enc)) return true; + return enc->kind == SCR_DYN_STR && (scr_fs_str_is(enc->v.str, "utf8") || scr_fs_str_is(enc->v.str, "utf-8")); +} + +/* getOptions: null, undefined and functions take the defaults; a string + * is the encoding; any other non-object throws ERR_INVALID_ARG_TYPE. The + * encoding (the string, or an object's `encoding` member) then goes + * through assertEncoding unless it is 'buffer': Node throws + * ERR_INVALID_ARG_VALUE for any truthy value Buffer.isEncoding rejects. */ static bool scr_fs_encoding_chk(const ScrDyn *opts) { if (opts->kind == SCR_DYN_TYPED_REF) { ScrDyn *view = scr_dyn_typed_ref_materialize(opts); @@ -1394,18 +1449,24 @@ static bool scr_fs_encoding_chk(const ScrDyn *opts) { scr_dyn_release(view); return valid; } + if (scr_fs_dyn_absent(opts) || opts->kind == SCR_DYN_FUNC) return true; + if (opts->kind == SCR_DYN_NUM || opts->kind == SCR_DYN_BOOL || opts->kind == SCR_DYN_BIGINT || + opts->kind == SCR_DYN_SYMBOL) { + scr_dyn_arg_type_fail("options", "one of type string or object", opts); + return false; + } const ScrDyn *enc = opts; - if (opts->kind == SCR_DYN_OBJ) { - enc = scr_dyn_obj_get(opts, "encoding", 8); + if (opts->kind != SCR_DYN_STR) { + enc = opts->kind == SCR_DYN_OBJ ? scr_dyn_obj_get(opts, "encoding", 8) : NULL; if (enc == NULL) return true; } if (scr_fs_dyn_absent(enc)) return true; if (enc->kind == SCR_DYN_STR) { if (enc->v.str->len == 0) return true; /* falsy: assertEncoding's `encoding &&` gate */ - if (scr_bytes_is_encoding(enc->v.str)) return true; + if (scr_bytes_is_encoding(enc->v.str) || scr_fs_str_is(enc->v.str, "buffer")) return true; } if (enc->kind == SCR_DYN_BOOL && !enc->v.b) return true; - if (enc->kind == SCR_DYN_NUM && enc->v.num == 0) return true; + if (enc->kind == SCR_DYN_NUM && (enc->v.num == 0 || enc->v.num != enc->v.num)) return true; /* 0, NaN */ scr_dyn_arg_value_fail("encoding", "is invalid encoding", enc); return false; } @@ -1531,19 +1592,13 @@ ScrStr *scr_fs_mkdtemp_sync_chk(const ScrDyn *prefix, const ScrDyn *opts, const const ScrDynEntry *e = &opts->v.obj.entries[i]; if (scr_fs_dyn_absent(e->value)) continue; if (strcmp(e->key, "encoding") == 0) { - const ScrDyn *enc = e->value; - if (!(enc->kind == SCR_DYN_STR && - (scr_fs_str_is(enc->v.str, "utf8") || scr_fs_str_is(enc->v.str, "utf-8")))) { - utf8 = false; - } + if (!scr_fs_utf8_encoding(e->value)) utf8 = false; continue; } utf8 = false; /* an unmodeled effectful option */ } - } else if (opts->kind == SCR_DYN_STR) { - utf8 = scr_fs_str_is(opts->v.str, "utf8") || scr_fs_str_is(opts->v.str, "utf-8"); - } else if (!scr_fs_dyn_absent(opts)) { - utf8 = false; + } else if (opts->kind != SCR_DYN_FUNC) { + utf8 = scr_fs_utf8_encoding(opts); } if (!utf8 || prefix->kind != SCR_DYN_STR) { scr_throw_lowering_fence(fence); diff --git a/packages/runtime/src/scr_dc.c b/packages/runtime/src/scr_dc.c index c962d3b67..053ba7072 100644 --- a/packages/runtime/src/scr_dc.c +++ b/packages/runtime/src/scr_dc.c @@ -14,8 +14,9 @@ * - publish iterates a SNAPSHOT of the subscriber list: a subscriber * unsubscribing itself mid-publish still lets its siblings fire (the * test suite's sync-unsubscribe shape, Node's behavior). A subscriber's - * throw PROPAGATES out of publish — catchable there — where Node routes - * it to triggerUncaughtException (SEMANTICS.md divergence). + * throw does not stop the dispatch: like Node's + * process.nextTick(triggerUncaughtException), it surfaces as an uncaught + * exception on the next tick. * - The atexit teardown releases names and subscribers before the RC * audit runs (atexit is LIFO; scr_init registered the audit first), so * registry-held values never count as leaks. @@ -46,7 +47,40 @@ static SCR_TL ScrDcChannel *dc_channels = NULL; static SCR_TL size_t dc_nchannels = 0, dc_cap = 0; static SCR_TL bool dc_teardown_registered = false; +/* Subscriber and transform throws waiting for their next-tick report, + * oldest first. */ +static SCR_TL ScrCaught **dc_deferred = NULL; +static SCR_TL size_t dc_ndeferred = 0, dc_deferred_cap = 0; + +static void dc_report_deferred(void) { + if (dc_ndeferred == 0) return; + ScrCaught *caught = dc_deferred[0]; + memmove(dc_deferred, dc_deferred + 1, --dc_ndeferred * sizeof *dc_deferred); + scr_rethrow(caught); + scr_caught_release(caught); +} + +/* Move the pending exception into the deferred queue and report it from a + * next tick of its own (Node: process.nextTick(() => + * triggerUncaughtException(err, false))). */ +static void dc_defer_pending_throw(void) { + if (dc_ndeferred == dc_deferred_cap) { + dc_deferred_cap = dc_deferred_cap ? dc_deferred_cap * 2 : 4; + dc_deferred = realloc(dc_deferred, dc_deferred_cap * sizeof *dc_deferred); + if (!dc_deferred) { + fputs("scriptc: out of memory\n", stderr); + abort(); + } + } + dc_deferred[dc_ndeferred++] = scr_exc_take(); + scr_next_tick_raw(dc_report_deferred); +} + static void scr_dc_teardown(void) { + for (size_t i = 0; i < dc_ndeferred; i++) scr_caught_release(dc_deferred[i]); + free(dc_deferred); + dc_deferred = NULL; + dc_ndeferred = dc_deferred_cap = 0; for (size_t i = 0; i < dc_nchannels; i++) { ScrDcChannel *ch = &dc_channels[i]; scr_str_release(ch->name); @@ -240,8 +274,8 @@ bool scr_dc_has_subscribers(ScrStr *name) { * (identity without a transform), publishes INSIDE the entered contexts * (subscribers observe getStore(), Node's shape), runs fn with thisArg * bound and the arguments forwarded, and restores in reverse — the - * finally, so a throw still restores. A transform throw propagates - * (Node defers it to reportError and skips the store — SEMANTICS.md). */ + * finally, so a throw still restores. A transform throw skips its store + * and surfaces as an uncaught exception on the next tick, as in Node. */ void scr_dc_chan_bind_store(double handle, double als_id, ScrDyn *transform) { ScrDcChannel *ch = dc_of(handle); @@ -282,9 +316,8 @@ bool scr_dc_chan_unbind_store(double handle, double als_id) { } /* Enter every bound store (transform(data) or data itself); the returned - * malloc'd array of PREVIOUS snapshots restores in reverse. On a - * transform throw the already-entered stores restore here and the - * exception stays pending (*n_out counts the entered prefix). */ + * malloc'd array of PREVIOUS snapshots (*n_out entries) restores in + * reverse. A store whose transform throws is not entered. */ static ScrAlsCtx **dc_stores_enter(ScrDcChannel *ch, ScrDyn *data, size_t *n_out) { *n_out = 0; if (ch->nbinds == 0) return NULL; @@ -298,11 +331,14 @@ static ScrAlsCtx **dc_stores_enter(ScrDcChannel *ch, ScrDyn *data, size_t *n_out if (ch->binds[i].transform != NULL) { ScrDyn *args[1] = {data}; value = scr_dyn_call(ch->binds[i].transform, args, 1, "transform"); - if (value == NULL) return prevs; /* pending; entered prefix restores */ + if (value == NULL) { + dc_defer_pending_throw(); + continue; + } } else { value = scr_dyn_retain(data); } - prevs[i] = scr_als_enter(ch->binds[i].als_id, value); + prevs[*n_out] = scr_als_enter(ch->binds[i].als_id, value); scr_dyn_release(value); (*n_out)++; } @@ -320,10 +356,6 @@ ScrDyn *scr_dc_chan_run_stores(double handle, ScrDyn *data, ScrDyn *fn, ScrDyn * ScrDcChannel *ch = dc_of(handle); size_t entered = 0; ScrAlsCtx **prevs = dc_stores_enter(ch, data, &entered); - if (scr_exc_pending()) { - dc_stores_restore(prevs, entered); - return NULL; - } scr_dc_publish(handle, data); ScrDyn *r = NULL; if (!scr_exc_pending()) { @@ -794,15 +826,11 @@ void scr_dc_publish(double handle, ScrDyn *message) { } for (size_t i = 0; i < n; i++) snap[i] = scr_dyn_retain(ch->subs[i]); ScrDyn *namev = scr_dyn_new_str(ch->name); - bool threw = false; - for (size_t i = 0; i < n && !threw; i++) { + for (size_t i = 0; i < n; i++) { ScrDyn *args[2] = {message, namev}; ScrDyn *r = scr_dyn_call(snap[i], args, 2, "onMessage"); - if (r == NULL) { - threw = true; /* pending exception propagates out of publish */ - } else { - scr_dyn_release(r); - } + if (r == NULL) dc_defer_pending_throw(); + else scr_dyn_release(r); } scr_dyn_release(namev); for (size_t i = 0; i < n; i++) scr_dyn_release(snap[i]); diff --git a/packages/runtime/src/scr_dgram.c b/packages/runtime/src/scr_dgram.c index eb2a7ca30..4e04354f0 100644 --- a/packages/runtime/src/scr_dgram.c +++ b/packages/runtime/src/scr_dgram.c @@ -72,6 +72,7 @@ #ifdef _WIN32 #include #include /* getaddrinfo/EAI_*, inet_pton/ntop, socklen_t */ +#include /* _getpid */ #else #include #include @@ -356,7 +357,8 @@ static SCR_TL ScrPoller *scr_dgram_poller = NULL; /* Pending dns.lookup deliveries (already resolved — getaddrinfo ran at * the call; the sweep fires them FIFO). */ typedef struct ScrDnsPending { - ScrClosure *cb; /* +1 */ + ScrClosure *cb; /* +1, or NULL for a dyn call */ + ScrDyn *call; /* a dyn [callback, ...args] array (+1), or NULL */ ScrDnsLookupFn fn; ScrStr *errmsg; /* +1 or NULL (success) */ ScrStr *addr; /* +1 ("" on failure) */ @@ -869,6 +871,457 @@ void scr_dns_lookup(ScrStr *hostname, double family, ScrClosure *cb, ScrDnsLooku *link = p; } +/* ── dns with Node's argument forms (dyn callbacks) ──────────────────── */ + +/* Calls a dyn [callback, ...args] array. */ +static void scr_dns_dyn_call(const ScrDyn *call) { + size_t argc = call->v.arr.len - 1; + scr_dyn_release(scr_dyn_call(call->v.arr.items[0], call->v.arr.items + 1, argc, "callback")); +} + +static void scr_dns_tick_fire(ScrClosure *self) { + ScrDyn *call = (ScrDyn *)scr_box_get_ref(self->caps[0]); + scr_dns_dyn_call(call); + scr_dyn_release(call); +} + +/* Queues callback(args...) (the args move): on the next tick, as Node's + * process.nextTick answers for literal addresses, or with the lookup + * deliveries. */ +static void scr_dns_call_later(const ScrDyn *callback, ScrDyn **args, size_t argc, bool tick) { + ScrDyn *call = scr_dyn_new_arr(); + scr_dyn_arr_push(call, scr_dyn_retain((ScrDyn *)callback)); + for (size_t i = 0; i < argc; i++) scr_dyn_arr_push(call, args[i]); + if (tick) { + ScrClosure *clo = scr_closure_new((void *)&scr_dns_tick_fire, 1); + ScrBox *box = scr_box_new_obj(&scr_dyn_retain_v, &scr_dyn_release_v, NULL); + scr_box_set_ref(box, call); + clo->caps[0] = box; + scr_next_tick(clo); + return; + } + ScrDnsPending *p = calloc(1, sizeof *p); + if (!p) scr_dgram_oom(); + p->call = call; + ScrDnsPending **link = &scr_dns_pending; + while (*link) link = &(*link)->next; + *link = p; +} + +/* The default result order (dns.setDefaultResultOrder): 0 verbatim, + * 1 ipv4first, 2 ipv6first. */ +static SCR_TL int scr_dns_default_order = 0; +static const char *const scr_dns_orders[] = { "verbatim", "ipv4first", "ipv6first" }; + +static int scr_dns_order_of(const ScrDyn *v) { + if (v->kind != SCR_DYN_STR) return -1; + for (int i = 0; i < 3; i++) { + size_t n = strlen(scr_dns_orders[i]); + if (v->v.str->len == n && memcmp(v->v.str->data, scr_dns_orders[i], n) == 0) return i; + } + return -1; +} + +ScrStr *scr_dns_get_default_result_order(void) { + const char *o = scr_dns_orders[scr_dns_default_order]; + return scr_str_new(o, strlen(o)); +} + +void scr_dns_set_default_result_order(const ScrDyn *order) { + int o = scr_dns_order_of(order); + if (o < 0) { + scr_dyn_arg_value_fail("dnsOrder", "must be one of: 'verbatim', 'ipv4first', 'ipv6first'", order); + return; + } + scr_dns_default_order = o; +} + +/* Node's DNSException: " " with errno, code, + * syscall and hostname. +1 dyn. */ +static ScrDyn *scr_dns_exception(int rc, const char *syscall, const ScrStr *host) { + bool notfound = rc == EAI_NONAME +#ifdef EAI_NODATA + || rc == EAI_NODATA +#endif + ; + const char *code = notfound ? "ENOTFOUND" : rc == EAI_AGAIN ? "EAI_AGAIN" : "EAI_FAIL"; + double errnum = rc == EAI_NONAME ? -3008 +#ifdef EAI_NODATA + : rc == EAI_NODATA ? -3007 +#endif + : rc == EAI_AGAIN ? -3001 + : -3002; + char msg[320]; + int len = snprintf(msg, sizeof msg, "%s %s %.*s", syscall, code, (int)(host->len < 255 ? host->len : 255), + host->data); + ScrStr *m = scr_str_new(msg, (size_t)len); + ScrError *e = scr_error_new(SCR_ERR_ERROR, m); + scr_str_release(m); + scr_error_set_code(e, code); + e->system_call = scr_str_new(syscall, strlen(syscall)); + e->system_errno = errnum; + ScrDyn *d = scr_dyn_from_error(e); + scr_error_release(e); + scr_dyn_obj_set(d, "hostname", 8, scr_dyn_new_str((ScrStr *)host)); + return d; +} + +static double scr_dns_literal_family(const ScrStr *h) { + unsigned char buf[16]; + char tmp[64]; + if (h->len == 0 || h->len >= sizeof tmp) return 0; + memcpy(tmp, h->data, h->len); + tmp[h->len] = '\0'; + if (inet_pton(AF_INET, tmp, buf) == 1) return 4; + if (strchr(tmp, ':') != NULL) { + char *pct = strchr(tmp, '%'); + if (pct) *pct = '\0'; + if (inet_pton(AF_INET6, tmp, buf) == 1) return 6; + } + return 0; +} + +static SCR_TL bool scr_dns_hostname_warned = false; + +/* DEP0118 on the next tick, as process.emitWarning reports it: through + * the events unit's warning machinery when it is linked, otherwise the + * default stderr report. */ +static void scr_dns_warn_tick(ScrClosure *self) { + ScrStr *message = (ScrStr *)scr_box_get_ref(self->caps[0]); + if (scr_warning_emit_hook) { + scr_warning_emit_hook("DeprecationWarning", "DEP0118", message); + } else { + fflush(stdout); +#ifdef _WIN32 + long pid = (long)_getpid(); +#else + long pid = (long)getpid(); +#endif + fprintf(stderr, "(node:%ld) [DEP0118] DeprecationWarning: %.*s\n", pid, (int)message->len, + message->data); + fputs("(Use `node --trace-deprecation ...` to show where the warning was created)\n", stderr); + } + scr_str_release(message); +} + +/* dns.lookup(hostname[, options], callback) as Node's lib/dns.js parses + * it: options is a family number or { family, all, order, verbatim, + * hints }, validated in Node's order. Results come back with the other + * lookups; literal addresses and an empty hostname answer on the next + * tick. */ +static void scr_dns_lookup_plain(const ScrDyn *hostname, const ScrDyn *options, const ScrDyn *callback, + bool typed); + +/* `typed`: the callback declares a string address (or a LookupAddress + * array), which cannot hold the undefined Node passes after an error; + * it receives "" and NaN (or []) there instead. */ +void scr_dns_lookup_dyn(const ScrDyn *hostname, const ScrDyn *options, const ScrDyn *callback, + bool typed) { + /* a typed options record arrives by reference */ + if (options->kind == SCR_DYN_TYPED_REF) { + ScrDyn *view = scr_dyn_typed_ref_materialize(options); + if (!scr_exc_pending()) scr_dns_lookup_plain(hostname, view, callback, typed); + scr_dyn_release(view); + return; + } + scr_dns_lookup_plain(hostname, options, callback, typed); +} + +/* A lookup failure's callback arguments. */ +static void scr_dns_fail(const ScrDyn *callback, int rc, const ScrStr *host, bool all, bool typed) { + ScrDyn *err = scr_dns_exception(rc, "getaddrinfo", host); + if (!typed) { + ScrDyn *args[1] = { err }; + scr_dns_call_later(callback, args, 1, false); + } else if (all) { + ScrDyn *args[2] = { err, scr_dyn_new_arr() }; + scr_dns_call_later(callback, args, 2, false); + } else { + ScrStr *empty = scr_str_new("", 0); + ScrDyn *args[3] = { err, scr_dyn_new_str(empty), scr_dyn_new_num(NAN) }; + scr_str_release(empty); + scr_dns_call_later(callback, args, 3, false); + } +} + +static void scr_dns_lookup_plain(const ScrDyn *hostname, const ScrDyn *options, const ScrDyn *callback, + bool typed) { + bool host_given = scr_dyn_truthy(hostname); + if (host_given && hostname->kind != SCR_DYN_STR) { + scr_dyn_arg_type_fail("hostname", "of type string", hostname); + return; + } + double family = 0; + bool all = false; + int order = scr_dns_default_order; + if (options->kind == SCR_DYN_NUM) { + double f = options->v.num; + if (f != 0 && f != 4 && f != 6) { + scr_dyn_arg_value_fail("family", "must be one of: 0, 4, 6", options); + return; + } + family = f; + } else if (options->kind != SCR_DYN_UNDEF && options->kind != SCR_DYN_OBJ && + options->kind != SCR_DYN_NULL) { + scr_dyn_arg_type_fail("options", "of type integer or an instance of Object", options); + return; + } else if (options->kind == SCR_DYN_OBJ) { + const ScrDyn *v = scr_dyn_obj_get(options, "hints", 5); + if (v != NULL && v->kind != SCR_DYN_UNDEF && v->kind != SCR_DYN_NULL) { + if (v->kind != SCR_DYN_NUM) { + scr_dyn_prop_type_fail("options.hints", "of type number", v); + return; + } + if (v->v.num != 0) { + scr_throw_error_msg_code(SCR_ERR_ERROR, "dns.lookup hints are not supported yet", 38, "SC2020"); + return; + } + } + v = scr_dyn_obj_get(options, "family", 6); + if (v != NULL && v->kind != SCR_DYN_UNDEF && v->kind != SCR_DYN_NULL) { + if (v->kind == SCR_DYN_STR && v->v.str->len == 4 && memcmp(v->v.str->data, "IPv4", 4) == 0) family = 4; + else if (v->kind == SCR_DYN_STR && v->v.str->len == 4 && memcmp(v->v.str->data, "IPv6", 4) == 0) family = 6; + else if (v->kind == SCR_DYN_NUM && (v->v.num == 0 || v->v.num == 4 || v->v.num == 6)) family = v->v.num; + else { + scr_dyn_arg_value_fail("options.family", "must be one of: 0, 4, 6", v); + return; + } + } + v = scr_dyn_obj_get(options, "all", 3); + if (v != NULL && v->kind != SCR_DYN_UNDEF && v->kind != SCR_DYN_NULL) { + if (v->kind != SCR_DYN_BOOL) { + scr_dyn_prop_type_fail("options.all", "of type boolean", v); + return; + } + all = v->v.b; + } + v = scr_dyn_obj_get(options, "verbatim", 8); + if (v != NULL && v->kind != SCR_DYN_UNDEF && v->kind != SCR_DYN_NULL) { + if (v->kind != SCR_DYN_BOOL) { + scr_dyn_prop_type_fail("options.verbatim", "of type boolean", v); + return; + } + order = v->v.b ? 0 : 1; + } + v = scr_dyn_obj_get(options, "order", 5); + if (v != NULL && v->kind != SCR_DYN_UNDEF && v->kind != SCR_DYN_NULL) { + int o = scr_dns_order_of(v); + if (o < 0) { + scr_dyn_arg_value_fail("options.order", "must be one of: 'verbatim', 'ipv4first', 'ipv6first'", v); + return; + } + order = o; + } + } + if (callback->kind != SCR_DYN_FUNC) { + scr_dyn_arg_type_fail("callback", "of type function", callback); + return; + } + if (!host_given) { + if (!scr_dns_hostname_warned) { + scr_dns_hostname_warned = true; + ScrStr *shown = scr_dyn_string_coerce_js(hostname); + ScrJsonBuf b; + scr_jb_init(&b); + scr_jb_puts(&b, "The provided hostname \""); + if (shown) scr_jb_write(&b, shown->data, shown->len); + scr_jb_puts(&b, "\" is not a valid hostname, and is supported in the dns module solely for compatibility."); + scr_str_release(shown); + ScrStr *message = scr_jb_finish(&b); + ScrClosure *clo = scr_closure_new((void *)&scr_dns_warn_tick, 1); + ScrBox *box = scr_box_new(SCR_BOX_STR); + scr_box_set_ref(box, message); + clo->caps[0] = box; + scr_next_tick(clo); + } + if (all) { + ScrDyn *args[2] = { scr_dyn_new_null(), scr_dyn_new_arr() }; + scr_dns_call_later(callback, args, 2, true); + } else { + ScrDyn *args[3] = { scr_dyn_new_null(), scr_dyn_new_null(), scr_dyn_new_num(family == 6 ? 6 : 4) }; + scr_dns_call_later(callback, args, 3, true); + } + return; + } + const ScrStr *host = hostname->v.str; + double literal = scr_dns_literal_family(host); + if (literal != 0) { + if (all) { + ScrDyn *entry = scr_dyn_new_obj(); + scr_dyn_obj_set(entry, "address", 7, scr_dyn_new_str((ScrStr *)host)); + scr_dyn_obj_set(entry, "family", 6, scr_dyn_new_num(literal)); + ScrDyn *list = scr_dyn_new_arr(); + scr_dyn_arr_push(list, entry); + ScrDyn *args[2] = { scr_dyn_new_null(), list }; + scr_dns_call_later(callback, args, 2, true); + } else { + ScrDyn *args[3] = { scr_dyn_new_null(), scr_dyn_new_str((ScrStr *)host), scr_dyn_new_num(literal) }; + scr_dns_call_later(callback, args, 3, true); + } + return; + } + if (memchr(host->data, '\0', host->len) != NULL) { + ScrStr *shown = scr_str_inspect_quoted((ScrStr *)host, true); + ScrJsonBuf msg; + scr_jb_init(&msg); + scr_jb_puts(&msg, "The argument 'hostname' must be a string without null bytes. Received "); + scr_jb_put_str(&msg, shown); + scr_str_release(shown); + ScrStr *text = scr_jb_finish(&msg); + scr_throw_error_msg_code(SCR_ERR_TYPE, text->data, text->len, "ERR_INVALID_ARG_VALUE"); + scr_str_release(text); + return; + } +#ifdef _WIN32 + (void)scr_dgram_poller_init(); +#endif + struct addrinfo hints; + memset(&hints, 0, sizeof hints); + hints.ai_family = family == 4 ? AF_INET : family == 6 ? AF_INET6 : AF_UNSPEC; + hints.ai_socktype = SOCK_STREAM; + struct addrinfo *res = NULL; + int rc = getaddrinfo(host->data, NULL, &hints, &res); + if (rc != 0 || res == NULL) { + if (res) freeaddrinfo(res); + scr_dns_fail(callback, rc != 0 ? rc : EAI_NONAME, host, all, typed); + return; + } + /* the answers in order: verbatim, or one family first (stable) */ + ScrDyn *list = scr_dyn_new_arr(); + for (int pass = 0; pass < 2; pass++) { + for (struct addrinfo *ai = res; ai; ai = ai->ai_next) { + int fam = ai->ai_family == AF_INET6 ? 6 : ai->ai_family == AF_INET ? 4 : 0; + if (fam == 0) continue; + bool first = order == 0 || (order == 1 ? fam == 4 : fam == 6); + if (order == 0 ? pass == 1 : first != (pass == 0)) continue; + char ip[INET6_ADDRSTRLEN]; + const void *src = fam == 4 ? (const void *)&((struct sockaddr_in *)ai->ai_addr)->sin_addr + : (const void *)&((struct sockaddr_in6 *)ai->ai_addr)->sin6_addr; + if (!inet_ntop(fam == 4 ? AF_INET : AF_INET6, src, ip, sizeof ip)) continue; + ScrDyn *entry = scr_dyn_new_obj(); + ScrStr *a = scr_str_new(ip, strlen(ip)); + scr_dyn_obj_set(entry, "address", 7, scr_dyn_new_str(a)); + scr_str_release(a); + scr_dyn_obj_set(entry, "family", 6, scr_dyn_new_num(fam)); + scr_dyn_arr_push(list, entry); + } + } + freeaddrinfo(res); + if (all) { + ScrDyn *args[2] = { scr_dyn_new_null(), list }; + scr_dns_call_later(callback, args, 2, false); + return; + } + if (list->v.arr.len == 0) { + scr_dyn_release(list); + scr_dns_fail(callback, EAI_NONAME, host, false, typed); + return; + } + const ScrDyn *first = list->v.arr.items[0]; + ScrDyn *args[3] = { scr_dyn_new_null(), scr_dyn_retain((ScrDyn *)scr_dyn_obj_get(first, "address", 7)), + scr_dyn_retain((ScrDyn *)scr_dyn_obj_get(first, "family", 6)) }; + scr_dyn_release(list); + scr_dns_call_later(callback, args, 3, false); +} + +/* dns.lookupService(address, port, callback): Node's checks (an IP + * address, a valid port), then getnameinfo with NI_NAMEREQD. */ +void scr_dns_lookup_service(const ScrDyn *address, const ScrDyn *port, const ScrDyn *callback, + bool typed) { + if (address->kind != SCR_DYN_STR) { + scr_dyn_arg_type_fail("address", "of type string", address); + return; + } + double fam = scr_dns_literal_family(address->v.str); + if (fam == 0) { + scr_dyn_arg_value_fail("address", "is invalid", address); + return; + } + double p = NAN; + bool numeric = (port->kind == SCR_DYN_NUM || port->kind == SCR_DYN_STR) && scr_dyn_number_coerce_js(port, &p); + if (port->kind == SCR_DYN_STR && port->v.str->len == 0) numeric = false; + if (!numeric || !(p >= 0 && p < 65536) || trunc(p) != p) { + char detail[256]; + const char *d = scr_dyn_specific_type(port, detail, sizeof detail); + char msg[320]; + int len = snprintf(msg, sizeof msg, "Port should be >= 0 and < 65536. Received %s.", d); + scr_throw_error_msg_code(SCR_ERR_RANGE, msg, (size_t)len, "ERR_SOCKET_BAD_PORT"); + return; + } + if (callback->kind != SCR_DYN_FUNC) { + scr_dyn_arg_type_fail("callback", "of type function", callback); + return; + } + struct sockaddr_storage ss; + memset(&ss, 0, sizeof ss); + socklen_t len; + char tmp[64]; + memcpy(tmp, address->v.str->data, address->v.str->len); + tmp[address->v.str->len] = '\0'; + if (fam == 4) { + struct sockaddr_in *in = (struct sockaddr_in *)&ss; + in->sin_family = AF_INET; + in->sin_port = htons((unsigned short)p); + inet_pton(AF_INET, tmp, &in->sin_addr); + len = sizeof *in; + } else { + char *pct = strchr(tmp, '%'); + if (pct) *pct = '\0'; + struct sockaddr_in6 *in6 = (struct sockaddr_in6 *)&ss; + in6->sin6_family = AF_INET6; + in6->sin6_port = htons((unsigned short)p); + inet_pton(AF_INET6, tmp, &in6->sin6_addr); + len = sizeof *in6; + } + char host[1025], serv[64]; + int rc = getnameinfo((struct sockaddr *)&ss, len, host, sizeof host, serv, sizeof serv, NI_NAMEREQD); + if (rc != 0) { + ScrDyn *err = scr_dns_exception(rc, "getnameinfo", address->v.str); + if (typed) { + ScrStr *empty = scr_str_new("", 0); + ScrDyn *args[3] = { err, scr_dyn_new_str(empty), scr_dyn_new_str(empty) }; + scr_str_release(empty); + scr_dns_call_later(callback, args, 3, false); + } else { + ScrDyn *args[1] = { err }; + scr_dns_call_later(callback, args, 1, false); + } + return; + } + ScrStr *h = scr_str_new(host, strlen(host)); + ScrStr *sv = scr_str_new(serv, strlen(serv)); + ScrDyn *args[3] = { scr_dyn_new_null(), scr_dyn_new_str(h), scr_dyn_new_str(sv) }; + scr_str_release(h); + scr_str_release(sv); + scr_dns_call_later(callback, args, 3, false); +} + +/* dns.getServers(): the nameserver lines of /etc/resolv.conf, as c-ares + * reads them ("[addr]:port" only for a port other than 53). +1. */ +ScrDyn *scr_dns_get_servers(void) { + ScrDyn *out = scr_dyn_new_arr(); +#ifndef _WIN32 + FILE *f = fopen("/etc/resolv.conf", "r"); + if (!f) return out; + char line[512]; + while (fgets(line, sizeof line, f)) { + char *p = line; + while (*p == ' ' || *p == '\t') p++; + if (strncmp(p, "nameserver", 10) != 0 || (p[10] != ' ' && p[10] != '\t')) continue; + p += 10; + while (*p == ' ' || *p == '\t') p++; + char *end = p; + while (*end && *end != ' ' && *end != '\t' && *end != '\n' && *end != '\r' && *end != '#' && *end != ';') end++; + if (end == p) continue; + ScrStr *s = scr_str_new(p, (size_t)(end - p)); + scr_dyn_arr_push(out, scr_dyn_new_str(s)); + scr_str_release(s); + } + fclose(f); +#endif + return out; +} + void scr_dns_thunk0(ScrClosure *cb, ScrStr *errmsg, ScrStr *addr, double family) { (void)errmsg; (void)addr; (void)family; ((void (*)(ScrClosure *))cb->fn)(cb); @@ -957,7 +1410,11 @@ static void scr_dgram_sweep(void) { while (scr_dns_pending) { ScrDnsPending *p = scr_dns_pending; scr_dns_pending = p->next; - if (!scr_exc_pending()) p->fn(p->cb, p->errmsg, p->addr, p->family); + if (!scr_exc_pending()) { + if (p->call != NULL) scr_dns_dyn_call(p->call); + else p->fn(p->cb, p->errmsg, p->addr, p->family); + } + scr_dyn_release(p->call); scr_closure_release(p->cb); scr_str_release(p->errmsg); scr_str_release(p->addr); @@ -1022,6 +1479,7 @@ static void scr_dgram_cleanup_atexit(void) { while (scr_dns_pending) { ScrDnsPending *p = scr_dns_pending; scr_dns_pending = p->next; + scr_dyn_release(p->call); scr_closure_release(p->cb); scr_str_release(p->errmsg); scr_str_release(p->addr); diff --git a/packages/runtime/src/scr_dgram.h b/packages/runtime/src/scr_dgram.h index 2f2d7e642..56a3021f3 100644 --- a/packages/runtime/src/scr_dgram.h +++ b/packages/runtime/src/scr_dgram.h @@ -60,6 +60,14 @@ void scr_dgram_msg_thunk1(ScrClosure *cb, ScrBytes *msg, ScrStr *addr, ScrStr *f * frontend), the callback moves. getaddrinfo runs NOW (blocking — * SEMANTICS.md); delivery defers to the next loop turn. */ void scr_dns_lookup(ScrStr *hostname /*borrowed*/, double family, ScrClosure *cb /*moves*/, ScrDnsLookupFn fn); +/* Node's dns argument forms, with dyn callbacks (all borrowed). */ +void scr_dns_lookup_dyn(const ScrDyn *hostname, const ScrDyn *options, const ScrDyn *callback, + bool typed); +void scr_dns_lookup_service(const ScrDyn *address, const ScrDyn *port, const ScrDyn *callback, + bool typed); +ScrDyn *scr_dns_get_servers(void); /* +1 */ +ScrStr *scr_dns_get_default_result_order(void); /* +1 */ +void scr_dns_set_default_result_order(const ScrDyn *order); void scr_dns_thunk0(ScrClosure *cb, ScrStr *errmsg, ScrStr *addr, double family); void scr_dgram_install(void); #ifdef SCR_RC_AUDIT diff --git a/packages/runtime/src/scr_exception.c b/packages/runtime/src/scr_exception.c index f3e235cd9..fb8ba013f 100644 --- a/packages/runtime/src/scr_exception.c +++ b/packages/runtime/src/scr_exception.c @@ -604,6 +604,17 @@ static bool scr_uncaught_print_dyn_primitive(const ScrDyn *d) { } } +/* An SC diagnostic code ("SC" and four digits) that `text` does not + * already name as "[SCnnnn". */ +static bool scr_uncaught_unnamed_sc_code(const ScrStr *code, const ScrStr *text) { + if (code == NULL || code->len != 6 || code->data[0] != 'S' || code->data[1] != 'C') return false; + for (size_t i = 2; i < 6; i++) + if (code->data[i] < '0' || code->data[i] > '9') return false; + for (size_t i = 0; i + 7 <= text->len; i++) + if (text->data[i] == '[' && memcmp(text->data + i + 1, code->data, 6) == 0) return false; + return true; +} + void scr_exc_print_uncaught(void) { #ifdef SCR_WORKERS if (scr_context_checkpoint()) { scr_exc_clear(); return; } @@ -638,8 +649,14 @@ void scr_exc_print_uncaught(void) { * line: "name: message" (toString rules for the empty halves). Node * appends a stack trace; scriptc has none (documented divergence). */ if (scr_error_is(scr_exc_payload)) { + const ScrError *error = (const ScrError *)scr_exc_payload; ScrStr *s = scr_error_to_string((ScrError *)scr_exc_payload); fwrite(s->data, 1, s->len, stderr); + /* A runtime refusal carries its scriptc diagnostic code on `code`; + * name it unless the message already does, as deferred compile-time + * refusals' "[SC1090 at file:line]" suffix does. */ + if (scr_uncaught_unnamed_sc_code(error->code, s)) + fprintf(stderr, " [%.*s]", (int)error->code->len, error->code->data); scr_str_release(s); break; } diff --git a/packages/runtime/src/scr_json.c b/packages/runtime/src/scr_json.c index 20eb679a0..82c14b7c4 100644 --- a/packages/runtime/src/scr_json.c +++ b/packages/runtime/src/scr_json.c @@ -9949,6 +9949,8 @@ static ScrDyn *scr_array_buffer_get(void *h, const char *key, size_t len) { if (len == 8 && !memcmp(key, "growable", len) && ((ScrBytes *)h)->shared) return scr_dyn_new_bool(false); if (!((ScrBytes *)h)->shared && ((len == 9 && memcmp(key, "resizable", len) == 0) || (len == 8 && memcmp(key, "detached", len) == 0))) return scr_dyn_new_bool(false); + if (len == 11 && !memcmp(key, "constructor", len) && !((ScrBytes *)h)->shared) + return scr_array_buffer_constructor(); return NULL; } diff --git a/packages/runtime/src/scr_net.c b/packages/runtime/src/scr_net.c index 461acbedc..8293759ee 100644 --- a/packages/runtime/src/scr_net.c +++ b/packages/runtime/src/scr_net.c @@ -4,6 +4,7 @@ #include #include +#include #include #include #include @@ -330,6 +331,62 @@ void scr_net_fire0_this(ScrNetLs *l, void *self, ScrDynHandleTag tag) { void scr_net_fire0(ScrNetLs *l) { scr_net_fire0_this(l, NULL, 0); } +/* A list mixing zero-argument closures (fn NULL) with dyn listeners + * (fn = a ScrNetArgFn) — the close(hadError) and write(chunk, cb(err)) + * shapes: the dyn ones receive `arg`. */ +typedef void (*ScrNetArgFn)(ScrClosure *cb, const ScrDyn *arg); + +static void scr_net_fire_arg_this(ScrNetLs *l, const ScrDyn *arg, void *self, ScrDynHandleTag tag) { + ScrNetL *snap; + size_t n = scr_net_ls_snapshot(l, &snap); + scr_dyn_this_push(self, tag); + for (size_t i = 0; i < n; i++) { + if (!scr_exc_pending()) { + if (snap[i].fn != NULL) ((ScrNetArgFn)snap[i].fn)(snap[i].cb, arg); + else ((void (*)(ScrClosure *))snap[i].cb->fn)(snap[i].cb); + } + scr_closure_release(snap[i].cb); + } + scr_dyn_this_pop(); + free(snap); +} + +/* The dyn listener's call with one argument (none for NULL). */ +static void scr_net_dyn_fire_arg(ScrClosure *cb, const ScrDyn *arg) { + ScrDyn *fn = scr_dyn_listener_fn(cb); + ScrDyn *args[1] = { (ScrDyn *)arg }; + scr_dyn_release(scr_dyn_call(fn, args, arg != NULL ? 1 : 0, "listener")); + scr_dyn_release(fn); +} + +/* The JS function a listener entry stands for: the boxed function of a + * dyn listener closure, otherwise the closure's own identity. */ +static ScrClosure *scr_net_listener_root(const ScrNetL *e) { + ScrClosure *c = e->cb; + if (e->fn == (void *)&scr_net_dyn_fire_arg || c->fn == (void *)&scr_dyn_listener_fire0) { + ScrDyn *boxed = scr_dyn_listener_fn(c); + ScrClosure *inner = boxed->kind == SCR_DYN_FUNC ? boxed->v.fn.clo : NULL; + scr_dyn_release(boxed); + if (inner != NULL) return inner->identity ? inner->identity : inner; + } + return c->identity ? c->identity : c; +} + +/* The listener standing for the same function as `root` (through any + * adapter) leaves the list: removeListener's first match. */ +static void scr_net_ls_remove_fn(ScrNetLs *l, ScrClosure *root) { + root = root->identity ? root->identity : root; + for (size_t i = 0; i < l->n; i++) { + ScrClosure *c = l->ls[i].cb; + if (scr_net_listener_root(&l->ls[i]) != root) continue; + scr_closure_release(c); + scr_net_once_release(l->ls[i].shared_once); + memmove(&l->ls[i], &l->ls[i + 1], (l->n - i - 1) * sizeof *l->ls); + l->n--; + return; + } +} + /* Fire an error list through the ScrChildErrFn adapters (msg borrowed), * the emitting handle bound as the ambient receiver. An 'error' with NO * listener is fatal, like Node's unhandled 'error' EventEmitter throw: @@ -387,6 +444,10 @@ struct ScrNetServer { ScrStr *pending_err; /* deferred listen failure */ size_t nconns; ScrNetLs conn_ls, err_ls, close_ls, listening_cbs, timeout_ls; + /* server.maxConnections (unset = undefined) and its 'drop' listeners */ + bool max_conns_set; + double max_conns; + ScrNetLs drop_ls; /* protocol layer (scr_http.c): a C-level connection consumer + its * context, freed with the handle */ ScrNetNativeConnFn native_conn; @@ -448,6 +509,8 @@ struct ScrNetSocket { * value read while connected survives destroy; never-read sockets * answer undefined after close */ ScrStr *remote_cache; + int remote_port_cache; + bool remote_v6_cache; ScrStr *pending_err; /* deferred connect failure */ /* the caller-lookup dial (net.connect with a lookup option): true while * the lookup decides; dial_ips holds its answered addresses (+1 each), @@ -483,6 +546,8 @@ struct ScrNetSocket { * buffer to drain (this surface's flush moment). */ ScrNetLs finish_ls, wcb_ls; bool finish_pending; /* wr_done seen: fire finish_ls at the next sweep */ + ScrNetLs ready_ls; /* 'ready': right after the client's 'connect' listeners */ + size_t bytes_read; /* socket.bytesRead: every byte delivered from the peer */ /* a server whose 'connection' waits for this socket's handshake — the * TLS defer target (+1; the socket's own `server` backref stays the * ACCEPTING server, which may be the demux wrapper) */ @@ -549,6 +614,7 @@ void scr_net_server_release(ScrNetServer *s) { scr_net_ls_drop(&s->close_ls); scr_net_ls_drop(&s->listening_cbs); scr_net_ls_drop(&s->timeout_ls); + scr_net_ls_drop(&s->drop_ls); scr_closure_release(s->close_override); for (size_t i = 0; i < 5; i++) scr_dyn_release(s->http_timeout_dyn[i]); scr_str_release(s->bound_host); @@ -583,6 +649,7 @@ void scr_net_sock_release(ScrNetSocket *s) { scr_net_ls_drop(&s->readable_ls); scr_net_ls_drop(&s->finish_ls); scr_net_ls_drop(&s->wcb_ls); + scr_net_ls_drop(&s->ready_ls); scr_str_release(s->pending_err); scr_str_release(s->remote_cache); for (size_t i = 0; i < s->dial_n; i++) scr_str_release(s->dial_ips[i]); @@ -1164,6 +1231,7 @@ static void scr_net_sock_read(ScrNetSocket *s) { return; } scr_net_sock_touch(s); + s->bytes_read += (size_t)n; if (flowing) { if (!scr_net_sock_deliver(s, buf, (size_t)n)) return; } else if (s->readable_ls.n == 0) { @@ -1260,6 +1328,8 @@ static void scr_net_sock_connect_done(ScrNetSocket *s) { } scr_net_fire0_this(&s->conn_ls, s, SCR_DYNH_NET_SOCKET); if (scr_exc_pending()) return; + scr_net_fire0_this(&s->ready_ls, s, SCR_DYNH_NET_SOCKET); + if (scr_exc_pending()) return; scr_net_sock_flush(s); } @@ -1312,6 +1382,8 @@ static void scr_net_sock_transport_pump(ScrNetSocket *s) { } scr_net_fire0_this(&s->conn_ls, s, SCR_DYNH_NET_SOCKET); if (scr_exc_pending()) return; + scr_net_fire0_this(&s->ready_ls, s, SCR_DYNH_NET_SOCKET); + if (scr_exc_pending()) return; scr_net_sock_flush(s); scr_net_sock_update_read(s); scr_net_sock_update_write(s); @@ -1679,6 +1751,51 @@ void scr_net_server_close_idle_connections(ScrNetServer *s) { scr_net_server_close_http_connections(s, true); } +/* server.maxConnections: undefined until set; a set number turns away + * connections once that many are open ('drop'). Setting undefined or + * null unsets it. +1 for the getter. */ +ScrDyn *scr_net_server_max_connections_get(ScrNetServer *s) { + return s->max_conns_set ? scr_dyn_new_num(s->max_conns) : scr_dyn_retain(scr_dyn_undefined()); +} + +void scr_net_server_max_connections_set(ScrNetServer *s, const ScrDyn *value /*borrowed*/) { + if (value->kind == SCR_DYN_UNDEF || value->kind == SCR_DYN_NULL) { + s->max_conns_set = false; + return; + } + double n = NAN; + scr_dyn_number_coerce_js(value, &n); + s->max_conns_set = true; + s->max_conns = n; +} + +void scr_net_server_on_drop(ScrNetServer *s, const ScrDyn *listener /*borrowed*/, bool once) { + if (s->close_emitted) return; + scr_net_ls_add(&s->drop_ls, scr_dyn_listener_closure_fn(listener, (void *)&scr_net_dyn_fire_arg), + (void *)&scr_net_dyn_fire_arg, once); +} + +/* server.getConnections(cb): cb(null, count) on the next tick, with the + * count at the call, as Node's process.nextTick(end, null, count). */ +static void scr_net_get_connections_fire(ScrClosure *self) { + ScrDyn *fn = (ScrDyn *)scr_box_get_ref(self->caps[0]); + ScrDyn *args[2] = { scr_dyn_new_null(), scr_dyn_new_num(scr_box_get_f64(self->caps[1])) }; + scr_dyn_release(scr_dyn_call(fn, args, 2, "callback")); + scr_dyn_release(args[0]); + scr_dyn_release(args[1]); + scr_dyn_release(fn); +} + +void scr_net_server_get_connections(ScrNetServer *s, const ScrDyn *callback /*borrowed*/) { + ScrClosure *clo = scr_dyn_listener_closure_fn(callback, (void *)&scr_net_get_connections_fire); + ScrClosure *job = scr_closure_new((void *)&scr_net_get_connections_fire, 2); + job->caps[0] = scr_box_retain(clo->caps[0]); + job->caps[1] = scr_box_new(SCR_BOX_F64); + scr_box_set_f64(job->caps[1], (double)s->nconns); + scr_closure_release(clo); + scr_next_tick(job); +} + /* wrapper.close = fn: the override MOVES in (a compiler-emitted zero-arg * wrapper — see the struct comment); reassignment releases the old one, * matching JS's last-write-wins. */ @@ -1748,6 +1865,48 @@ void scr_net_server_on_timeout(ScrNetServer *s, ScrClosure *cb /*moves*/, ScrNet scr_net_ls_add(&s->timeout_ls, cb, (void *)fn, once); } +/* One end of a dropped connection as Node's 'drop' data reports it. */ +static void scr_net_drop_side(ScrDyn *data, const char *prefix, const struct sockaddr_storage *ss) { + char addr[INET6_ADDRSTRLEN] = ""; + int port = 0; + bool v6 = ss->ss_family == AF_INET6; + if (v6) { + inet_ntop(AF_INET6, &((const struct sockaddr_in6 *)ss)->sin6_addr, addr, sizeof addr); + port = ntohs(((const struct sockaddr_in6 *)ss)->sin6_port); + } else { + inet_ntop(AF_INET, &((const struct sockaddr_in *)ss)->sin_addr, addr, sizeof addr); + port = ntohs(((const struct sockaddr_in *)ss)->sin_port); + } + char key[24]; + int n = snprintf(key, sizeof key, "%sAddress", prefix); + ScrStr *a = scr_str_new(addr, strlen(addr)); + scr_dyn_obj_set(data, key, (size_t)n, scr_dyn_new_str(a)); + scr_str_release(a); + n = snprintf(key, sizeof key, "%sPort", prefix); + scr_dyn_obj_set(data, key, (size_t)n, scr_dyn_new_num(port)); + n = snprintf(key, sizeof key, "%sFamily", prefix); + ScrStr *f = scr_str_new(v6 ? "IPv6" : "IPv4", 4); + scr_dyn_obj_set(data, key, (size_t)n, scr_dyn_new_str(f)); + scr_str_release(f); +} + +/* A connection past server.maxConnections: 'drop' with the local and + * remote addresses (a null-prototype object in Node), then the close. */ +static void scr_net_server_drop_conn(ScrNetServer *srv, int fd) { + if (srv->drop_ls.n > 0) { + ScrDyn *data = scr_dyn_new_obj(); + data->null_proto = true; + struct sockaddr_storage ss; + socklen_t len = sizeof ss; + if (getsockname(fd, (struct sockaddr *)&ss, &len) == 0) scr_net_drop_side(data, "local", &ss); + len = sizeof ss; + if (getpeername(fd, (struct sockaddr *)&ss, &len) == 0) scr_net_drop_side(data, "remote", &ss); + scr_net_fire_arg_this(&srv->drop_ls, data, srv, SCR_DYNH_NET_SERVER); + scr_dyn_release(data); + } + scr_net_close_fd_raw(fd); +} + /* Accept until EAGAIN; each connection fires the 'connection' listeners * directly (the accepted socket passes +1 per listener via the adapter). */ static void scr_net_server_accept(ScrNetServer *srv) { @@ -1761,6 +1920,13 @@ static void scr_net_server_accept(ScrNetServer *srv) { scr_net_nonblock(fd); int one = 1; setsockopt(fd, IPPROTO_TCP, TCP_NODELAY, &one, sizeof one); + if (srv->max_conns_set && (double)srv->nconns >= srv->max_conns) { + /* server.maxConnections: Node closes the connection and emits + * 'drop' with the addresses instead of 'connection' */ + scr_net_server_drop_conn(srv, fd); + if (scr_exc_pending()) return; + continue; + } ScrNetSocket *sock = scr_net_sock_new(); sock->fd = fd; sock->server = scr_net_server_retain(srv); @@ -2307,6 +2473,19 @@ void scr_net_sock_set_keepalive(ScrNetSocket *s, bool enable, double delay_ms) { #endif } +/* socket.setKeepAlive(enable, initialDelay) as the program calls it: + * answers the socket (+1), Node's chaining. */ +ScrNetSocket *scr_net_sock_set_keepalive_ret(ScrNetSocket *s, bool enable, double delay_ms) { + scr_net_sock_set_keepalive(s, enable, delay_ms); + return scr_net_sock_retain(s); +} + +/* end(callback) with a dyn callback: once 'finish', called without + * arguments. */ +void scr_net_sock_on_finish_dyn(ScrNetSocket *s, const ScrDyn *listener /*borrowed*/) { + scr_net_sock_on_finish(s, scr_dyn_listener_closure0(listener)); +} + /* socket.destroySoon(): end the write half now, destroy once the FIN is * actually out (buffered bytes flush first — Node's 'finish'-then-destroy). */ void scr_net_sock_destroy_soon(ScrNetSocket *s) { @@ -2377,7 +2556,8 @@ bool scr_net_sock_encrypted(ScrNetSocket *s) { return s->tops != NULL; } ScrStr *scr_net_sock_remote_address(ScrNetSocket *s) { if (s->remote_cache) return scr_str_retain(s->remote_cache); - if (s->fd < 0) return NULL; + /* Node's _getpeername answers {} while connecting */ + if (s->fd < 0 || scr_net_sock_connecting(s)) return NULL; struct sockaddr_storage ss; socklen_t len = sizeof ss; if (getpeername(s->fd, (struct sockaddr *)&ss, &len) != 0) return NULL; @@ -2390,9 +2570,251 @@ ScrStr *scr_net_sock_remote_address(ScrNetSocket *s) { } if (!out) return NULL; s->remote_cache = scr_str_new(out, strlen(out)); + s->remote_v6_cache = ss.ss_family == AF_INET6; + s->remote_port_cache = ntohs(ss.ss_family == AF_INET6 ? ((struct sockaddr_in6 *)&ss)->sin6_port + : ((struct sockaddr_in *)&ss)->sin_port); return scr_str_retain(s->remote_cache); } +/* socket.remotePort / remoteFamily: the same cached peer read as + * remoteAddress; NaN / NULL answer undefined. */ +double scr_net_sock_remote_port(ScrNetSocket *s) { + scr_str_release(scr_net_sock_remote_address(s)); + return s->remote_cache ? (double)s->remote_port_cache : NAN; +} + +ScrStr *scr_net_sock_remote_family(ScrNetSocket *s) { + scr_str_release(scr_net_sock_remote_address(s)); + if (!s->remote_cache) return NULL; + return s->remote_v6_cache ? scr_str_new("IPv6", 4) : scr_str_new("IPv4", 4); +} + +/* socket.localAddress / localPort / localFamily: read from the open + * socket (undefined while connecting and after close, as in Node). */ +static bool scr_net_sock_local(ScrNetSocket *s, char *addr, size_t cap, int *port, bool *v6) { + if (s->fd < 0 || s->connecting || s->dial_deferred || s->lookup_wait) return false; + struct sockaddr_storage ss; + socklen_t len = sizeof ss; + if (getsockname(s->fd, (struct sockaddr *)&ss, &len) != 0) return false; + if (ss.ss_family == AF_INET) { + if (!inet_ntop(AF_INET, &((struct sockaddr_in *)&ss)->sin_addr, addr, (socklen_t)cap)) return false; + *port = ntohs(((struct sockaddr_in *)&ss)->sin_port); + *v6 = false; + return true; + } + if (ss.ss_family == AF_INET6) { + if (!inet_ntop(AF_INET6, &((struct sockaddr_in6 *)&ss)->sin6_addr, addr, (socklen_t)cap)) return false; + *port = ntohs(((struct sockaddr_in6 *)&ss)->sin6_port); + *v6 = true; + return true; + } + return false; +} + +ScrStr *scr_net_sock_local_address(ScrNetSocket *s) { + char addr[INET6_ADDRSTRLEN]; + int port; + bool v6; + return scr_net_sock_local(s, addr, sizeof addr, &port, &v6) ? scr_str_new(addr, strlen(addr)) : NULL; +} + +double scr_net_sock_local_port(ScrNetSocket *s) { + char addr[INET6_ADDRSTRLEN]; + int port; + bool v6; + return scr_net_sock_local(s, addr, sizeof addr, &port, &v6) ? (double)port : NAN; +} + +ScrStr *scr_net_sock_local_family(ScrNetSocket *s) { + char addr[INET6_ADDRSTRLEN]; + int port; + bool v6; + if (!scr_net_sock_local(s, addr, sizeof addr, &port, &v6)) return NULL; + return v6 ? scr_str_new("IPv6", 4) : scr_str_new("IPv4", 4); +} + +/* The address fields as dyn values (+1): remotePort (0), remoteFamily + * (1), localAddress (2), localPort (3) and localFamily (4), each + * undefined when unknown. */ +ScrDyn *scr_net_sock_addr_field(ScrNetSocket *s, double field) { + int f = (int)field; + if (f == 0 || f == 3) { + double port = f == 0 ? scr_net_sock_remote_port(s) : scr_net_sock_local_port(s); + return isnan(port) ? scr_dyn_retain(scr_dyn_undefined()) : scr_dyn_new_num(port); + } + ScrStr *v = f == 1 ? scr_net_sock_remote_family(s) + : f == 2 ? scr_net_sock_local_address(s) + : scr_net_sock_local_family(s); + if (v == NULL) return scr_dyn_retain(scr_dyn_undefined()); + ScrDyn *d = scr_dyn_new_str(v); + scr_str_release(v); + return d; +} + +double scr_net_sock_bytes_read(ScrNetSocket *s) { return (double)s->bytes_read; } + +/* socket.connecting: a dial in progress (deferred, looking up, or + * awaiting the TCP handshake). */ +bool scr_net_sock_connecting(ScrNetSocket *s) { + return s->fd >= 0 ? s->connecting : (s->dial_deferred || s->lookup_wait); +} + +/* socket.pending: no open handle yet (or anymore), or still connecting. */ +bool scr_net_sock_pending(ScrNetSocket *s) { return s->fd < 0 || scr_net_sock_connecting(s); } + +/* socket.readyState, Node's getter: opening while connecting, then by + * which halves are still readable and writable. +1. */ +ScrStr *scr_net_sock_ready_state(ScrNetSocket *s) { + const char *state; + if (scr_net_sock_connecting(s)) state = "opening"; + else { + bool r = scr_net_sock_readable(s), w = scr_net_sock_writable(s); + state = r && w ? "open" : r ? "readOnly" : w ? "writeOnly" : "closed"; + } + return scr_str_new(state, strlen(state)); +} + +void scr_net_sock_on_ready(ScrNetSocket *s, ScrClosure *cb /*moves*/, bool once) { + if (s->close_emitted) { + scr_closure_release(cb); + return; + } + scr_net_ls_add(&s->ready_ls, cb, NULL, once); +} + +/* close(hadError) and write(chunk, cb(err)) listeners as dyn functions, + * called with the hadError boolean and null. */ +void scr_net_sock_on_close_dyn(ScrNetSocket *s, const ScrDyn *listener /*borrowed*/, bool once) { + if (s->close_emitted) return; + scr_net_ls_add(&s->close_ls, scr_dyn_listener_closure_fn(listener, (void *)&scr_net_dyn_fire_arg), + (void *)&scr_net_dyn_fire_arg, once); +} + +void scr_net_sock_on_write_flush_dyn(ScrNetSocket *s, const ScrDyn *listener /*borrowed*/) { + if (s->close_emitted) return; + scr_net_ls_add(&s->wcb_ls, scr_dyn_listener_closure_fn(listener, (void *)&scr_net_dyn_fire_arg), + (void *)&scr_net_dyn_fire_arg, true); +} + +/* socket.setTimeout(ms[, callback]) as the program calls it: Node's + * msecs check (a non-negative finite number), then the timer; a callback + * becomes a once('timeout') listener, and with 0 it is removed. */ +static bool scr_net_timeout_msecs_valid(double ms) { + if (isfinite(ms) && ms >= 0) return true; + char recv[48], msg[160]; + scr_num_received(ms, recv); + int len = snprintf(msg, sizeof msg, + "The value of \"msecs\" is out of range. It must be a non-negative finite number. " + "Received %s", + recv); + scr_throw_error_msg_code(SCR_ERR_RANGE, msg, (size_t)len, "ERR_OUT_OF_RANGE"); + return false; +} + +void scr_net_sock_set_timeout_checked(ScrNetSocket *s, double ms) { + if (!scr_net_timeout_msecs_valid(ms)) return; + scr_net_sock_set_timeout(s, ms); +} + +void scr_net_sock_set_timeout_cb(ScrNetSocket *s, double ms, ScrClosure *cb /*moves*/) { + if (!scr_net_timeout_msecs_valid(ms)) { + scr_closure_release(cb); + return; + } + scr_net_sock_set_timeout(s, ms); + if (ms == 0) { + scr_net_ls_remove_fn(&s->timeout_ls, cb); + scr_closure_release(cb); + return; + } + scr_net_sock_on_timeout(s, cb, true); +} + +/* net.isIP / isIPv4 / isIPv6, as Node's IPv4Reg and IPv6Reg match: dotted + * decimal without leading zeros, and the RFC 4291 text forms (one "::" + * standing for at least one group, an IPv4 tail as two groups) with an + * optional %zone. */ +static bool scr_net_ipv4_text(const char *p, size_t n) { + size_t i = 0; + for (int part = 0; part < 4; part++) { + if (part > 0) { + if (i >= n || p[i] != '.') return false; + i++; + } + size_t start = i; + int value = 0; + while (i < n && p[i] >= '0' && p[i] <= '9' && i - start < 3) value = value * 10 + (p[i++] - '0'); + size_t digits = i - start; + if (digits == 0 || (digits > 1 && p[start] == '0') || value > 255) return false; + } + return i == n; +} + +static bool scr_net_ipv6_text(const char *p, size_t n) { + /* an optional zone: '%' and at least one of [0-9a-zA-Z-.:] */ + for (size_t i = 0; i < n; i++) { + if (p[i] != '%') continue; + if (i + 1 == n) return false; + for (size_t j = i + 1; j < n; j++) { + char c = p[j]; + if (!(isalnum((unsigned char)c) || c == '-' || c == '.' || c == ':')) return false; + } + n = i; + break; + } + int groups = 0; + bool compressed = false; + size_t i = 0; + if (n >= 2 && p[0] == ':' && p[1] == ':') { + compressed = true; + i = 2; + if (i == n) return true; + } else if (n >= 1 && p[0] == ':') { + return false; + } + while (i < n) { + size_t start = i; + while (i < n && isxdigit((unsigned char)p[i]) && i - start < 5) i++; + size_t digits = i - start; + if (i < n && p[i] == '.') { + /* the IPv4 tail ends the address */ + if (!scr_net_ipv4_text(p + start, n - start)) return false; + groups += 2; + i = n; + break; + } + if (digits == 0 || digits > 4) return false; + groups++; + if (i == n) break; + if (p[i] != ':') return false; + i++; + if (i < n && p[i] == ':') { + if (compressed) return false; + compressed = true; + i++; + if (i == n) break; + } else if (i == n) { + return false; /* a single trailing ':' */ + } + } + return compressed ? groups <= 7 : groups == 8; +} + +static ScrStr *scr_net_ip_input(const ScrDyn *input) { + return input->kind == SCR_DYN_STR ? scr_str_retain(input->v.str) : scr_dyn_string_coerce_js(input); +} + +double scr_net_is_ip(const ScrDyn *input) { + ScrStr *t = scr_net_ip_input(input); + if (!t) return 0; + double r = scr_net_ipv4_text(t->data, t->len) ? 4 : scr_net_ipv6_text(t->data, t->len) ? 6 : 0; + scr_str_release(t); + return r; +} + +bool scr_net_is_ipv4(const ScrDyn *input) { return scr_net_is_ip(input) == 4; } +bool scr_net_is_ipv6(const ScrDyn *input) { return scr_net_is_ip(input) == 6; } + void scr_net_sock_destroy(ScrNetSocket *s) { if (s->fd < 0 && !s->in_registry) return; s->rd_eof = true; @@ -2898,7 +3320,9 @@ static void scr_net_sweep(void) { * stack. A dead socket fires them too (the buffered bytes are * gone either way; Node errors them — documented divergence, * the no-backpressure stance). */ - scr_net_fire0_this(&sock->wcb_ls, sock, SCR_DYNH_NET_SOCKET); + ScrDyn *no_error = scr_dyn_new_null(); + scr_net_fire_arg_this(&sock->wcb_ls, no_error, sock, SCR_DYNH_NET_SOCKET); + scr_dyn_release(no_error); if (scr_exc_pending()) { scr_net_sock_release(sock); return; @@ -2954,7 +3378,11 @@ static void scr_net_sweep(void) { return; } } - scr_net_fire0_this(&sock->close_ls, sock, SCR_DYNH_NET_SOCKET); + { + ScrDyn *had_error = scr_dyn_new_bool(sock->had_error); + scr_net_fire_arg_this(&sock->close_ls, had_error, sock, SCR_DYNH_NET_SOCKET); + scr_dyn_release(had_error); + } /* settle: listeners drop (cycle story) */ scr_net_ls_drop(&sock->data_ls); scr_net_ls_drop(&sock->end_ls); @@ -2965,6 +3393,7 @@ static void scr_net_sweep(void) { scr_net_ls_drop(&sock->readable_ls); scr_net_ls_drop(&sock->finish_ls); scr_net_ls_drop(&sock->wcb_ls); + scr_net_ls_drop(&sock->ready_ls); if (sock->pipe_dst) { scr_net_sock_release(sock->pipe_dst); sock->pipe_dst = NULL; @@ -3359,13 +3788,23 @@ static ScrDyn *scr_net_dynh_sock_invoke(void *h, ScrDyn *self, const char *metho scr_dyn_arg_type_fail("msecs", "of type number", ms); return NULL; } - scr_net_sock_set_timeout(s, ms->v.num); - if (argc > 1 && args[1]->kind == SCR_DYN_FUNC) { - scr_net_sock_on_timeout(s, scr_dyn_listener_closure0(args[1]), true); - } else if (argc > 1 && args[1]->kind != SCR_DYN_UNDEF) { + if (argc > 1 && args[1]->kind != SCR_DYN_UNDEF && args[1]->kind != SCR_DYN_FUNC) { scr_dyn_check_listener(args[1], "callback"); return NULL; } + if (!scr_net_timeout_msecs_valid(ms->v.num)) return NULL; + scr_net_sock_set_timeout(s, ms->v.num); + if (argc > 1 && args[1]->kind == SCR_DYN_FUNC) { + if (ms->v.num == 0) scr_net_ls_remove_fn(&s->timeout_ls, args[1]->v.fn.clo); + else scr_net_sock_on_timeout(s, scr_dyn_listener_closure0(args[1]), true); + } + return scr_dyn_retain(self); + } + if (strcmp(method, "setKeepAlive") == 0) { + bool enable = argc > 0 && scr_dyn_truthy(args[0]); + double delay = 0; + if (argc > 1 && args[1]->kind != SCR_DYN_UNDEF) scr_dyn_number_coerce_js(args[1], &delay); + scr_net_sock_set_keepalive(s, enable, delay); return scr_dyn_retain(self); } if (strcmp(method, "setEncoding") == 0) { @@ -3410,7 +3849,7 @@ static ScrDyn *scr_net_dynh_sock_invoke(void *h, ScrDyn *self, const char *metho if (scr_net_dynh_tls->invoke(s, method, args, argc, &out)) return out; } { - static const char *const known[] = { "connect", "setKeepAlive", + static const char *const known[] = { "connect", "address", "cork", "uncork", "read", "unpipe", "off", "removeListener", "removeAllListeners", "emit", "prependListener", "prependOnceListener", "listenerCount", "listeners", "resetAndDestroy", NULL }; @@ -3447,6 +3886,28 @@ static ScrDyn *scr_net_dynh_sock_get(void *h, const char *key, size_t key_len) { return scr_net_sock_encrypted(s) ? scr_dyn_new_bool(true) : NULL; } if (strcmp(key, "bytesWritten") == 0) return scr_dyn_new_num(scr_net_sock_bytes_written(s)); + if (strcmp(key, "bytesRead") == 0) return scr_dyn_new_num(scr_net_sock_bytes_read(s)); + if (strcmp(key, "connecting") == 0) return scr_dyn_new_bool(scr_net_sock_connecting(s)); + if (strcmp(key, "pending") == 0) return scr_dyn_new_bool(scr_net_sock_pending(s)); + if (strcmp(key, "readyState") == 0) { + ScrStr *state = scr_net_sock_ready_state(s); + ScrDyn *d = scr_dyn_new_str(state); + scr_str_release(state); + return d; + } + { + static const char *const fields[] = { "remotePort", "remoteFamily", "localAddress", "localPort", + "localFamily", NULL }; + for (size_t i = 0; fields[i]; i++) { + if (strcmp(key, fields[i]) != 0) continue; + ScrDyn *d = scr_net_sock_addr_field(s, (double)i); + if (d->kind == SCR_DYN_UNDEF) { + scr_dyn_release(d); + return NULL; + } + return d; + } + } if (strcmp(key, "readable") == 0) return scr_dyn_new_bool(scr_net_sock_readable(s)); if (strcmp(key, "writableHighWaterMark") == 0 || strcmp(key, "readableHighWaterMark") == 0) { /* Node's default stream highWaterMark — a constant here (backpressure @@ -3458,9 +3919,7 @@ static ScrDyn *scr_net_dynh_sock_get(void *h, const char *key, size_t key_len) { if (scr_net_dynh_tls->get(s, key, &out)) return out; } { - static const char *const known[] = { "remotePort", "remoteFamily", "localAddress", - "localPort", "localFamily", "bytesRead", "connecting", "pending", - "readyState", "bufferSize", "timeout", "closed", "errored", NULL }; + static const char *const known[] = { "bufferSize", "timeout", "closed", "errored", NULL }; for (size_t i = 0; known[i]; i++) { if (strcmp(key, known[i]) == 0) { scr_net_dynh_unsupported(key, NULL); @@ -3643,6 +4102,15 @@ static ScrDyn *scr_net_dynh_srv_invoke(void *h, ScrDyn *self, const char *method scr_net_server_set_ref((ScrNetServer *)h, method[0] == 'r'); return scr_dyn_retain(self); } + if (strcmp(method, "getConnections") == 0) { + const ScrDyn *cb = argc > 0 ? args[0] : scr_dyn_undefined(); + if (cb->kind != SCR_DYN_FUNC) { + scr_dyn_check_listener(cb, "callback"); + return NULL; + } + scr_net_server_get_connections((ScrNetServer *)h, cb); + return scr_dyn_retain(self); + } if (strcmp(method, "toString") == 0) { ScrStr *str = scr_str_new("[object Object]", 15); ScrDyn *d = scr_dyn_new_str(str); @@ -3650,7 +4118,7 @@ static ScrDyn *scr_net_dynh_srv_invoke(void *h, ScrDyn *self, const char *method return d; } { - static const char *const known[] = { "setTimeout", "getConnections", + static const char *const known[] = { "setTimeout", "off", "removeListener", "removeAllListeners", "emit", "prependListener", "prependOnceListener", "listenerCount", "listeners", "closeAllConnections", "closeIdleConnections", NULL }; @@ -3690,7 +4158,8 @@ static ScrDyn *scr_net_dynh_srv_get(void *h, const char *key, size_t key_len) { return scr_dyn_new_num(scr_net_server_timeout_get(s, (double)timeout_field)); } { - static const char *const known[] = { "maxConnections", "connections", "maxHeadersCount", NULL }; + if (strcmp(key, "maxConnections") == 0) return s->max_conns_set ? scr_dyn_new_num(s->max_conns) : NULL; + static const char *const known[] = { "connections", "maxHeadersCount", NULL }; for (size_t i = 0; known[i]; i++) { if (strcmp(key, known[i]) == 0) { scr_net_dynh_srv_unsupported(key, NULL); @@ -3714,6 +4183,10 @@ static bool scr_net_dynh_srv_set(void *h, const char *key, size_t key_len, const } return true; } + if (strcmp(key, "maxConnections") == 0) { + scr_net_server_max_connections_set(s, value); + return true; + } return false; } diff --git a/packages/runtime/src/scr_net.h b/packages/runtime/src/scr_net.h index 117a242c7..ad81ac200 100644 --- a/packages/runtime/src/scr_net.h +++ b/packages/runtime/src/scr_net.h @@ -162,6 +162,10 @@ ScrStr *scr_net_server_addr_family(ScrNetServer *s); /* +1 — address().family void scr_net_server_close(ScrNetServer *s, ScrClosure *cb /*moves, nullable*/); void scr_net_server_close_all_connections(ScrNetServer *s); void scr_net_server_close_idle_connections(ScrNetServer *s); +ScrDyn *scr_net_server_max_connections_get(ScrNetServer *s); /* +1 */ +void scr_net_server_max_connections_set(ScrNetServer *s, const ScrDyn *value /*borrowed*/); +void scr_net_server_on_drop(ScrNetServer *s, const ScrDyn *listener /*borrowed*/, bool once); +void scr_net_server_get_connections(ScrNetServer *s, const ScrDyn *callback /*borrowed*/); /* The REAL close behind `wrapper.close.bind(wrapper)` — never consults * the override (the proxy-through idiom cannot recurse). */ void scr_net_server_close_direct(ScrNetServer *s, ScrClosure *cb /*moves, nullable*/); @@ -233,6 +237,26 @@ void scr_net_sock_dial_start(ScrNetSocket *s); void scr_net_sock_set_timeout(ScrNetSocket *s, double ms); void scr_net_sock_on_timeout(ScrNetSocket *s, ScrClosure *cb /*moves*/, bool once); ScrStr *scr_net_sock_remote_address(ScrNetSocket *s); /* +1 or NULL (undefined arm) */ +double scr_net_sock_remote_port(ScrNetSocket *s); /* NaN = undefined */ +ScrStr *scr_net_sock_remote_family(ScrNetSocket *s); /* +1 or NULL */ +ScrStr *scr_net_sock_local_address(ScrNetSocket *s); /* +1 or NULL */ +double scr_net_sock_local_port(ScrNetSocket *s); /* NaN = undefined */ +ScrStr *scr_net_sock_local_family(ScrNetSocket *s); /* +1 or NULL */ +ScrDyn *scr_net_sock_addr_field(ScrNetSocket *s, double field); /* +1 */ +double scr_net_sock_bytes_read(ScrNetSocket *s); +bool scr_net_sock_connecting(ScrNetSocket *s); +bool scr_net_sock_pending(ScrNetSocket *s); +ScrStr *scr_net_sock_ready_state(ScrNetSocket *s); /* +1 */ +void scr_net_sock_on_ready(ScrNetSocket *s, ScrClosure *cb /*moves*/, bool once); +void scr_net_sock_on_close_dyn(ScrNetSocket *s, const ScrDyn *listener /*borrowed*/, bool once); +void scr_net_sock_on_write_flush_dyn(ScrNetSocket *s, const ScrDyn *listener /*borrowed*/); +void scr_net_sock_on_finish_dyn(ScrNetSocket *s, const ScrDyn *listener /*borrowed*/); +ScrNetSocket *scr_net_sock_set_keepalive_ret(ScrNetSocket *s, bool enable, double delay_ms); /* +1 */ +void scr_net_sock_set_timeout_checked(ScrNetSocket *s, double ms); +void scr_net_sock_set_timeout_cb(ScrNetSocket *s, double ms, ScrClosure *cb /*moves*/); +double scr_net_is_ip(const ScrDyn *input); +bool scr_net_is_ipv4(const ScrDyn *input); +bool scr_net_is_ipv6(const ScrDyn *input); bool scr_net_sock_encrypted(ScrNetSocket *s); /* true = TLS transport (false maps to the undefined arm) */ void scr_net_sock_pipe(ScrNetSocket *src, ScrNetSocket *dst); void scr_net_sock_on_data(ScrNetSocket *s, ScrClosure *cb /*moves*/, ScrNetDataFn fn, bool once); diff --git a/scripts/ci-test-plan.mjs b/scripts/ci-test-plan.mjs index 9162006b1..2e3a3f6b7 100644 --- a/scripts/ci-test-plan.mjs +++ b/scripts/ci-test-plan.mjs @@ -3,6 +3,7 @@ export const effectFiles = ["tests/harness/effect.test.ts", "tests/harness/effec export const corpusFiles = [ "tests/harness/differential.test.ts", "tests/harness/llvm-differential.test.ts", + "tests/harness/npm.test.ts", "tests/harness/server.test.ts", "tests/harness/test262.test.ts", "tests/harness/three.test.ts", diff --git a/tests/corpus/5337-dc-subscriber-throw-next-tick.cjs b/tests/corpus/5337-dc-subscriber-throw-next-tick.cjs new file mode 100644 index 000000000..a5b6792b4 --- /dev/null +++ b/tests/corpus/5337-dc-subscriber-throw-next-tick.cjs @@ -0,0 +1,29 @@ +// A throwing subscriber or bindStore transform does not stop the dispatch: +// later subscribers run, publish and runStores return, and each error +// reaches 'uncaughtException' on a next tick of its own. +const dc = require("node:diagnostics_channel"); +const { AsyncLocalStorage } = require("node:async_hooks"); + +process.on("uncaughtException", (error) => console.log("uncaught", error.message)); + +const channel = dc.channel("subscriber-throw"); +channel.subscribe(() => { + console.log("first"); + throw new Error("one"); +}); +channel.subscribe(() => { + console.log("second"); + throw new Error("two"); +}); +channel.subscribe(() => console.log("third")); +channel.publish({}); +console.log("published"); +process.nextTick(() => console.log("user tick")); + +const store = new AsyncLocalStorage(); +const stores = dc.channel("transform-throw"); +stores.bindStore(store, () => { + throw new Error("transform"); +}); +stores.subscribe(() => console.log("subscriber store", store.getStore())); +console.log(stores.runStores({ v: 1 }, () => "body " + store.getStore())); diff --git a/tests/corpus/5338-js-tdz-function-element.js b/tests/corpus/5338-js-tdz-function-element.js new file mode 100644 index 000000000..d8122ebd6 --- /dev/null +++ b/tests/corpus/5338-js-tdz-function-element.js @@ -0,0 +1,17 @@ +// A lexical module binding initialized from an unchecked array element that +// holds a function keeps its TDZ guard: an earlier call through a hoisted +// function throws ReferenceError, and later reads see the function or undefined. +function early() { + try { + return f(1); + } catch (error) { + return error.name; + } +} +console.log(early()); +function id(v) { + return v; +} +const f = [id][0]; +let g = [id][3]; +console.log(f(2), typeof f, typeof g); diff --git a/tests/corpus/5339-array-buffer-constructor.js b/tests/corpus/5339-array-buffer-constructor.js new file mode 100644 index 000000000..bb54749cb --- /dev/null +++ b/tests/corpus/5339-array-buffer-constructor.js @@ -0,0 +1,6 @@ +// A dynamically reached ArrayBuffer answers constructor with the global +// ArrayBuffer, as typed arrays answer theirs. +const items = [new Uint8Array(2), new ArrayBuffer(3), new Float32Array(1)]; +for (const item of items) console.log(item.constructor.name, item.constructor === ArrayBuffer); +const b = new ArrayBuffer(4); +console.log(b.constructor.name, b.byteLength); diff --git a/tests/corpus/5545-fs-options-encoding.cjs b/tests/corpus/5545-fs-options-encoding.cjs new file mode 100644 index 000000000..53a32f9e3 --- /dev/null +++ b/tests/corpus/5545-fs-options-encoding.cjs @@ -0,0 +1,99 @@ +// fs's getOptions: null, undefined and functions take the defaults, a +// string is the encoding, other non-objects throw ERR_INVALID_ARG_TYPE, and +// assertEncoding lets 'buffer' through. readFile and readFileSync then read +// before buffer.toString('buffer') throws ERR_UNKNOWN_ENCODING (an empty +// file answers ""), and mkdtemp's callback answers its path in the +// requested encoding: a Buffer for 'buffer', re-encoded text otherwise. +const fs = require("node:fs"); +const os = require("node:os"); +const path = require("node:path"); + +const dir = fs.mkdtempSync(path.join(os.tmpdir(), "options-encoding-")); +const file = path.join(dir, "f.txt"); +const empty = path.join(dir, "empty.txt"); +fs.writeFileSync(file, "hi"); +fs.writeFileSync(empty, ""); +const prefix = path.join(dir, "t-"); +const clean = (text) => String(text).split(dir).join(""); +const describe = (value) => + Buffer.isBuffer(value) ? `Buffer ${clean(value.toString())}` : `${typeof value} ${clean(value)}`; +const show = (label, fn) => { + try { + console.log(label, "->", describe(fn())); + } catch (error) { + console.log(label, "throws", error.name, error.code, clean(error.message)); + } +}; + +for (const options of [5, 0, true, false, 10n, Symbol("s")]) { + show(`readFile ${typeof options}`, () => fs.readFile(file, options, () => {})); + show(`mkdtemp ${typeof options}`, () => fs.mkdtemp(prefix, options, () => {})); + show(`mkdtempSync ${typeof options}`, () => fs.mkdtempSync(prefix, options)); +} +show("mkdtemp bad encoding", () => fs.mkdtemp(prefix, { encoding: "nope" }, () => {})); +show("mkdtemp bad string", () => fs.mkdtemp(prefix, "nope", () => {})); +show("mkdtempSync empty string", () => fs.statSync(fs.mkdtempSync(prefix, "")).isDirectory()); +show("mkdtempSync NaN encoding", () => fs.statSync(fs.mkdtempSync(prefix, { encoding: NaN })).isDirectory()); +show("readFileSync buffer", () => fs.readFileSync(file, "buffer")); +show("readFileSync buffer object", () => fs.readFileSync(file, { encoding: "buffer" })); +show("readFileSync buffer empty", () => fs.readFileSync(empty, "buffer")); +show("readFileSync buffer missing", () => fs.readFileSync(path.join(dir, "missing"), "buffer")); + +const steps = [ + (next) => fs.readFile(file, { encoding: "buffer" }, (error, data) => { + console.log("readFile buffer", error && `${error.name} ${error.code} ${error.message}`, data); + next(); + }), + (next) => fs.readFile(file, "buffer", (error, data) => { + console.log("readFile buffer string", error && error.code, data); + next(); + }), + (next) => fs.readFile(empty, "buffer", (error, data) => { + console.log("readFile buffer empty", error, JSON.stringify(data)); + next(); + }), + (next) => fs.readFile(path.join(dir, "missing"), { encoding: "buffer" }, (error, data) => { + console.log("readFile buffer missing", error && error.code, data); + next(); + }), + (next) => fs.readFile(file, { encoding: "buffer", flag: "r+" }, (error, data) => { + console.log("readFile buffer flag", error && error.code, data); + next(); + }), + (next) => fs.mkdtemp(prefix, { encoding: "buffer" }, (error, made) => { + console.log("mkdtemp buffer", error, describe(made).startsWith(`Buffer ${clean(prefix)}`), fs.existsSync(made.toString())); + next(); + }), + (next) => fs.mkdtemp(prefix, "buffer", (error, made) => { + console.log("mkdtemp buffer string", error, Buffer.isBuffer(made), made.length === prefix.length + 6); + next(); + }), + (next) => fs.mkdtemp(prefix, "hex", (error, made) => { + const decoded = Buffer.from(made, "hex").toString(); + console.log("mkdtemp hex", error, typeof made, decoded.startsWith(prefix), fs.existsSync(decoded)); + next(); + }), + (next) => fs.mkdtemp(prefix, { encoding: "base64" }, (error, made) => { + const decoded = Buffer.from(made, "base64").toString(); + console.log("mkdtemp base64", error, decoded.startsWith(prefix), fs.existsSync(decoded)); + next(); + }), + (next) => fs.mkdtemp(prefix, { encoding: "latin1" }, (error, made) => { + console.log("mkdtemp latin1", error, made.startsWith(prefix)); + next(); + }), + (next) => fs.mkdtemp(prefix, { encoding: "utf8" }, (error, made) => { + console.log("mkdtemp utf8", error, typeof made, made.startsWith(prefix)); + next(); + }), + (next) => fs.mkdtemp(path.join(dir, "missing", "t-"), { encoding: "buffer" }, (error, made) => { + console.log("mkdtemp buffer missing", error && error.code, made); + next(); + }), +]; +const run = (index) => { + if (index < steps.length) return steps[index](() => run(index + 1)); + fs.rmSync(dir, { recursive: true }); + console.log("removed", fs.existsSync(dir)); +}; +run(0); diff --git a/tests/corpus/5644-net-socket-addresses.ts b/tests/corpus/5644-net-socket-addresses.ts new file mode 100644 index 000000000..fff2f7aee --- /dev/null +++ b/tests/corpus/5644-net-socket-addresses.ts @@ -0,0 +1,70 @@ +// A socket's address fields, byte counts and state: the remote fields +// are cached at first read and stay readable after close, the local ones +// are undefined while connecting and after close, and readyState follows +// the open halves. +import * as net from "node:net"; + +// The server's 'connection' and the client's 'connect' race (either can +// run first), so each side records its line and both print once both +// have run. +let serverLine: string | null = null; +let clientLine: string | null = null; +let startClient: (() => void) | null = null; +function bothConnected(): void { + if (serverLine === null || clientLine === null || startClient === null) return; + console.log(serverLine); + console.log(clientLine); + startClient(); +} + +const server = net.createServer((sock) => { + const port = (server.address() as net.AddressInfo).port; + serverLine = + `server side ${sock.remoteAddress} ${sock.remoteFamily} ${typeof sock.remotePort} ` + + `${sock.localAddress} ${sock.localPort === port} ${sock.localFamily}`; + bothConnected(); + sock.on("data", (d: Buffer) => { + console.log("server got", d.toString(), sock.bytesRead); + sock.end("bye"); + }); +}); + +server.listen(0, "127.0.0.1", () => { + const port = (server.address() as net.AddressInfo).port; + const c = net.connect(port, "127.0.0.1"); + console.log( + "before connect", + c.readyState, + c.connecting, + c.pending, + c.remoteAddress, + c.remotePort, + c.localAddress, + c.localPort, + c.bytesRead, + ); + c.on("connect", () => { + clientLine = + `connected ${c.readyState} ${c.connecting} ${c.pending} ${c.remotePort === port} ` + + `${c.remoteFamily} ${c.localAddress} ${typeof c.localPort} ${c.localFamily}`; + startClient = () => c.write("hello"); + bothConnected(); + }); + c.on("data", (d: Buffer) => console.log("client got", d.toString(), c.bytesRead)); + c.on("end", () => console.log("end", c.readyState, c.bytesRead, c.bytesWritten)); + c.on("close", () => { + console.log( + "closed", + c.readyState, + c.pending, + c.remoteAddress, + c.remotePort === port, + c.localAddress, + c.localPort, + ); + const d = net.connect(port, "127.0.0.1"); + d.destroy(); + console.log("destroyed before connect", d.readyState, d.connecting, d.pending, d.remoteAddress); + d.on("close", () => server.close()); + }); +}); diff --git a/tests/corpus/5645-net-socket-callbacks.ts b/tests/corpus/5645-net-socket-callbacks.ts new file mode 100644 index 000000000..c80704828 --- /dev/null +++ b/tests/corpus/5645-net-socket-callbacks.ts @@ -0,0 +1,72 @@ +// write and end callbacks, close listeners with hadError, the 'ready' +// event, string 'data' chunks after setEncoding, setTimeout with a callback (and its msecs checks) and +// setKeepAlive's chaining. +import * as net from "node:net"; + +// Each side keeps its own list: the client's callbacks and the server's +// events can interleave either way, so the lists print one after the +// other at the end. +const events: string[] = []; +const serverEvents: string[] = []; +let serverSockets = 0; +const server = net.createServer((sock) => { + sock.setEncoding("utf8"); + sock.on("data", (d: string) => { + serverEvents.push(`server data ${typeof d} ${d}`); + sock.end("pong", () => serverEvents.push("server end callback")); + }); + // the first connection's server side (the timeout connection's close + // order against the client's depends on the event loop phase) + if (serverSockets++ === 0) { + sock.on("close", (hadError: boolean) => serverEvents.push(`server close ${hadError}`)); + } +}); + +function attempt(label: string, f: () => void): void { + try { + f(); + events.push(`${label} ok`); + } catch (err) { + const e = err as NodeJS.ErrnoException; + events.push(`${label} ${e.name} ${e.code} ${e.message}`); + } +} + +server.listen(0, "127.0.0.1", () => { + const port = (server.address() as net.AddressInfo).port; + const c = net.connect(port, "127.0.0.1"); + c.on("connect", () => { + events.push("connect"); + c.write("ping", (err?: Error | null) => events.push(`write callback ${err === null}`)); + events.push(`setKeepAlive chains ${c.setKeepAlive(true, 1000) === c}`); + }); + c.on("ready", () => events.push("ready")); + c.on("data", (d: Buffer) => events.push(`client data ${d.toString()}`)); + c.on("end", () => { + events.push("client end"); + c.end(() => events.push("client end callback")); + }); + c.on("close", (hadError: boolean) => { + events.push(`client close ${hadError}`); + const t = net.connect(port, "127.0.0.1", () => { + attempt("negative", () => t.setTimeout(-1)); + attempt("NaN", () => t.setTimeout(NaN)); + attempt("Infinity", () => t.setTimeout(Infinity)); + const never = () => events.push("removed callback ran"); + t.setTimeout(20, never); + t.setTimeout(0, never); + t.setTimeout(30, () => { + events.push("timeout callback"); + t.destroy(); + }); + }); + t.on("timeout", () => events.push("timeout event")); + t.on("close", (hadError: boolean) => { + events.push(`t close ${hadError}`); + server.close(() => { + console.log(serverEvents.join("\n")); + console.log(events.join("\n")); + }); + }); + }); +}); diff --git a/tests/corpus/5646-net-server-connections.ts b/tests/corpus/5646-net-server-connections.ts new file mode 100644 index 000000000..9f2e01aec --- /dev/null +++ b/tests/corpus/5646-net-server-connections.ts @@ -0,0 +1,44 @@ +// server.maxConnections turns away connections past the cap with a +// 'drop' event carrying their addresses, and getConnections reports the +// open count on the next tick. +import * as net from "node:net"; + +const server = net.createServer((sock) => { + sock.on("data", () => {}); +}); +console.log("default", server.maxConnections); +server.maxConnections = 1; +console.log("set", server.maxConnections); +server.on("drop", (data?: net.DropArgument) => { + console.log( + "drop", + data?.localAddress, + typeof data?.localPort, + data?.localFamily, + data?.remoteAddress, + typeof data?.remotePort, + data?.remoteFamily, + ); +}); + +server.listen(0, "127.0.0.1", () => { + const port = (server.address() as net.AddressInfo).port; + const first = net.connect(port, "127.0.0.1", () => { + const second = net.connect(port, "127.0.0.1"); + second.resume(); + second.on("close", () => { + console.log("second closed"); + process.nextTick(() => console.log("tick before")); + const same = server.getConnections((err: Error | null, count: number) => { + console.log("connections", err, count); + }); + console.log("getConnections chains", same === server); + process.nextTick(() => console.log("tick after")); + Promise.resolve().then(() => console.log("microtask")); + server.maxConnections = undefined; + console.log("unset", server.maxConnections); + first.destroy(); + server.close(() => console.log("server closed")); + }); + }); +}); diff --git a/tests/corpus/5647-net-is-ip.ts b/tests/corpus/5647-net-is-ip.ts new file mode 100644 index 000000000..4db2cc0cc --- /dev/null +++ b/tests/corpus/5647-net-is-ip.ts @@ -0,0 +1,34 @@ +// net.isIP, isIPv4 and isIPv6 follow Node's address patterns: dotted +// decimal without leading zeros, and IPv6 text forms with one "::", an +// IPv4 tail and an optional %zone. +import * as net from "node:net"; + +const inputs = [ + "1.2.3.4", + "255.255.255.255", + "01.2.3.4", + "256.1.1.1", + "1.2.3", + "1.2.3.4.5", + "::", + "::1", + "fe80::1%eth0", + "fe80::1%", + "1:2:3:4:5:6:7:8", + "1:2:3:4:5:6:7::", + "::1:2:3:4:5:6:7", + "1:2:3:4:5:6:7:8:9", + "1::2::3", + ":1::", + "1:", + "::ffff:1.2.3.4", + "1:2:3:4:5:6:1.2.3.4", + "1:2:3:4:5:6:7:1.2.3.4", + "12345::1", + "abcd:EF01::", + "", + "localhost", +]; +for (const input of inputs) { + console.log(JSON.stringify(input), net.isIP(input), net.isIPv4(input), net.isIPv6(input)); +} diff --git a/tests/corpus/5648-dns-lookup-forms.ts b/tests/corpus/5648-dns-lookup-forms.ts new file mode 100644 index 000000000..edd9ebc82 --- /dev/null +++ b/tests/corpus/5648-dns-lookup-forms.ts @@ -0,0 +1,59 @@ +// dns.lookup with Node's argument forms: a callback alone, a family +// number, and options with family, all and order. Literal addresses +// answer on the next tick, a missing name fails with ENOTFOUND, and bad +// arguments throw Node's errors. +import * as dns from "node:dns"; + +function attempt(label: string, f: () => void): void { + try { + f(); + console.log(label, "ok"); + } catch (err) { + const e = err as NodeJS.ErrnoException; + console.log(label, e.name, e.code, e.message); + } +} + +attempt("family 5", () => dns.lookup("localhost", 5, () => {})); +attempt("options.family", () => dns.lookup("localhost", { family: 3 }, () => {})); +attempt("options.order", () => + dns.lookup("localhost", { order: "first" as "verbatim" }, () => {}), +); +attempt("hostname", () => dns.lookup(42 as unknown as string, () => {})); + +dns.lookup("127.0.0.1", (err, address, family) => console.log("literal", err, address, family)); +process.nextTick(() => console.log("tick after the literal lookup")); + +// one lookup at a time: concurrent ones complete in any order +const steps: ((next: () => void) => void)[] = [ + (next) => + dns.lookup("::1", { all: true }, (err, addresses) => { + console.log("literal all", err, JSON.stringify(addresses)); + next(); + }), + (next) => + dns.lookup("localhost", 4, (err, address, family) => { + console.log("family 4", err, address, family); + next(); + }), + (next) => + dns.lookup("localhost", { family: 4, all: true }, (err, addresses) => { + console.log("all", err, JSON.stringify(addresses)); + next(); + }), + (next) => + dns.lookup("localhost", { family: "IPv4", order: "ipv6first" }, (err, address, family) => { + console.log("order", err, address, family); + next(); + }), + (next) => + dns.lookup("no-such-host.invalid", (err) => { + console.log("missing", err?.name, err?.code, err?.syscall, err?.errno, err?.message); + next(); + }), +]; +let step = 0; +const run = (): void => { + if (step < steps.length) steps[step++]!(run); +}; +run(); diff --git a/tests/corpus/5649-dns-service-servers.ts b/tests/corpus/5649-dns-service-servers.ts new file mode 100644 index 000000000..ca631299c --- /dev/null +++ b/tests/corpus/5649-dns-service-servers.ts @@ -0,0 +1,31 @@ +// dns.lookupService checks its address and port as Node does, +// getServers lists the configured nameservers, and the default result +// order is 'verbatim' until setDefaultResultOrder changes it. +import * as dns from "node:dns"; + +function attempt(label: string, f: () => void): void { + try { + f(); + console.log(label, "ok"); + } catch (err) { + const e = err as NodeJS.ErrnoException; + console.log(label, e.name, e.code, e.message); + } +} + +dns.lookupService("127.0.0.1", 65000, (err, hostname, service) => { + console.log("service", err, typeof hostname, service); +}); +attempt("address", () => dns.lookupService("not-an-ip", 80, () => {})); +attempt("port", () => dns.lookupService("127.0.0.1", 70000, () => {})); +attempt("negative port", () => dns.lookupService("127.0.0.1", -1, () => {})); + +const servers = dns.getServers(); +console.log("servers", Array.isArray(servers), servers.every((s) => typeof s === "string")); + +console.log("order", dns.getDefaultResultOrder()); +dns.setDefaultResultOrder("ipv4first"); +console.log("order", dns.getDefaultResultOrder()); +attempt("bad order", () => dns.setDefaultResultOrder("first" as "verbatim")); +console.log("order", dns.getDefaultResultOrder()); +dns.setDefaultResultOrder("verbatim"); diff --git a/tests/corpus/5650-dns-lookup-errors-untyped.cjs b/tests/corpus/5650-dns-lookup-errors-untyped.cjs new file mode 100644 index 000000000..1e5af0f7c --- /dev/null +++ b/tests/corpus/5650-dns-lookup-errors-untyped.cjs @@ -0,0 +1,14 @@ +// An untyped lookup callback receives only the error after a failure, +// as Node calls it, with the error's code, errno, syscall and hostname. +const dns = require("node:dns"); + +dns.lookup("no-such-host.invalid", (...args) => { + const [err] = args; + console.log(args.length, err.code, err.errno, err.syscall, err.hostname); + dns.lookup("no-such-host.invalid", { all: true }, (...args2) => { + console.log(args2.length, args2[0].message); + dns.lookupService("127.0.0.1", 65000, (...args3) => { + console.log(args3.length, args3[0], typeof args3[1], args3[2]); + }); + }); +}); diff --git a/tests/diagnostics/options-record.ts b/tests/diagnostics/options-record.ts index 67ae46a47..f7079a37e 100644 --- a/tests/diagnostics/options-record.ts +++ b/tests/diagnostics/options-record.ts @@ -7,7 +7,6 @@ import * as http from "node:http"; import * as tls from "node:tls"; import * as fs from "node:fs"; -import * as dns from "node:dns"; import * as readline from "node:readline"; // The Agent lowers to a checked-dynamic handle now — but only from a @@ -42,9 +41,6 @@ tls.createServer({ cert: "c", key: "k", ticketKeys: new Uint8Array(48) }, () => fs.watch("/tmp", { recursive: true }, () => {}); fs.watch("/tmp", { persistent: false }, () => {}); -// dns.lookup: hints/all are documented knobs with no lowering. -dns.lookup("localhost", { family: 4, hints: 32 }, () => {}); -dns.lookup("localhost", { family: 4, all: true }, () => {}); // readline: history only exists for terminal interfaces; completer // needs an interactive terminal. diff --git a/tests/harness/__snapshots__/options-record.ts.txt b/tests/harness/__snapshots__/options-record.ts.txt index a4c74104a..571097061 100644 --- a/tests/harness/__snapshots__/options-record.ts.txt +++ b/tests/harness/__snapshots__/options-record.ts.txt @@ -1,143 +1,125 @@ -options-record.ts:15:7 - error SC2020: 'AgentOptions' is part of the standard library types but has no scriptc lowering yet +options-record.ts:14:7 - error SC2020: 'AgentOptions' is part of the standard library types but has no scriptc lowering yet - 14 | // LITERAL options object; a runtime options value cannot be walked. - 15 | const agentOpts: http.AgentOptions = { maxSockets: 1 }; + 13 | // LITERAL options object; a runtime options value cannot be walked. + 14 | const agentOpts: http.AgentOptions = { maxSockets: 1 }; | ^~~~~~~~~ - 16 | const agent = new http.Agent(agentOpts); + 15 | const agent = new http.Agent(agentOpts); hint: the type checker sees the full standard library, but only the supported surface compiles (https://scriptc.dev/limitations) -options-record.ts:16:30 - error SC2020: 'new http.Agent with a non-literal options value' is part of the standard library types but has no scriptc lowering yet +options-record.ts:15:30 - error SC2020: 'new http.Agent with a non-literal options value' is part of the standard library types but has no scriptc lowering yet - 15 | const agentOpts: http.AgentOptions = { maxSockets: 1 }; - 16 | const agent = new http.Agent(agentOpts); + 14 | const agentOpts: http.AgentOptions = { maxSockets: 1 }; + 15 | const agent = new http.Agent(agentOpts); | ^~~~~~~~~ - 17 | http.get({ port: 80, agent }, () => {}); + 16 | http.get({ port: 80, agent }, () => {}); hint: spell the options as an object literal at the construction site -options-record.ts:22:22 - error SC2020: 'get with agent: false and a non-literal headers record' is part of the standard library types but has no scriptc lowering yet +options-record.ts:21:22 - error SC2020: 'get with agent: false and a non-literal headers record' is part of the standard library types but has no scriptc lowering yet - 21 | const dynHeaders: Record = { "x-a": "1" }; - 22 | http.get({ port: 80, agent: false, headers: dynHeaders }, () => {}); + 20 | const dynHeaders: Record = { "x-a": "1" }; + 21 | http.get({ port: 80, agent: false, headers: dynHeaders }, () => {}); | ^~~~~~~~~~~~ - 23 | + 22 | hint: agent: false lowers by injecting Connection: close into a LITERAL headers object — add connection: "close" to the record instead, or write the headers inline -options-record.ts:25:22 - error SC2020: 'get option 'socketPath'' is part of the standard library types but has no scriptc lowering yet +options-record.ts:24:22 - error SC2020: 'get option 'socketPath'' is part of the standard library types but has no scriptc lowering yet - 24 | // Documented client options with no lowering fence by name. - 25 | http.get({ port: 80, socketPath: "/tmp/sock.s" }, () => {}); + 23 | // Documented client options with no lowering fence by name. + 24 | http.get({ port: 80, socketPath: "/tmp/sock.s" }, () => {}); | ^~~~~~~~~~~~~~~~~~~~~~~~~ - 26 | http.get({ port: 80, localAddress: "127.0.0.1" }, () => {}); + 25 | http.get({ port: 80, localAddress: "127.0.0.1" }, () => {}); hint: hostname/host, port, path, method, timeout, headers, and agent are the supported options -options-record.ts:26:22 - error SC2020: 'get option 'localAddress'' is part of the standard library types but has no scriptc lowering yet +options-record.ts:25:22 - error SC2020: 'get option 'localAddress'' is part of the standard library types but has no scriptc lowering yet - 25 | http.get({ port: 80, socketPath: "/tmp/sock.s" }, () => {}); - 26 | http.get({ port: 80, localAddress: "127.0.0.1" }, () => {}); + 24 | http.get({ port: 80, socketPath: "/tmp/sock.s" }, () => {}); + 25 | http.get({ port: 80, localAddress: "127.0.0.1" }, () => {}); | ^~~~~~~~~~~~~~~~~~~~~~~~~ - 27 | + 26 | hint: hostname/host, port, path, method, timeout, headers, and agent are the supported options -options-record.ts:34:22 - error SC2020: 'the undocumented get option 'zorp' with an effectful value' is part of the standard library types but has no scriptc lowering yet +options-record.ts:33:22 - error SC2020: 'the undocumented get option 'zorp' with an effectful value' is part of the standard library types but has no scriptc lowering yet - 33 | } - 34 | http.get({ port: 80, zorp: effect() }, () => {}); + 32 | } + 33 | http.get({ port: 80, zorp: effect() }, () => {}); | ^~~~~~~~~~~~~~ - 35 | + 34 | hint: Node ignores undocumented option keys and so does this compiler, but Node still evaluates the value — hoist it (const v = ...) or drop the entry -options-record.ts:37:41 - error SC2020: 'tls.createServer option 'ciphers'' is part of the standard library types but has no scriptc lowering yet +options-record.ts:36:41 - error SC2020: 'tls.createServer option 'ciphers'' is part of the standard library types but has no scriptc lowering yet - 36 | // Documented TLS server options with no lowering fence by name. - 37 | tls.createServer({ cert: "c", key: "k", ciphers: "TLS_AES_128_GCM_SHA256" }, () => {}); + 35 | // Documented TLS server options with no lowering fence by name. + 36 | tls.createServer({ cert: "c", key: "k", ciphers: "TLS_AES_128_GCM_SHA256" }, () => {}); | ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - 38 | tls.createServer({ cert: "c", key: "k", ticketKeys: new Uint8Array(48) }, () => {}); + 37 | tls.createServer({ cert: "c", key: "k", ticketKeys: new Uint8Array(48) }, () => {}); hint: cert and key (PEM strings or Buffers) are the supported options -options-record.ts:38:41 - error SC2020: 'tls.createServer option 'ticketKeys'' is part of the standard library types but has no scriptc lowering yet +options-record.ts:37:41 - error SC2020: 'tls.createServer option 'ticketKeys'' is part of the standard library types but has no scriptc lowering yet - 37 | tls.createServer({ cert: "c", key: "k", ciphers: "TLS_AES_128_GCM_SHA256" }, () => {}); - 38 | tls.createServer({ cert: "c", key: "k", ticketKeys: new Uint8Array(48) }, () => {}); + 36 | tls.createServer({ cert: "c", key: "k", ciphers: "TLS_AES_128_GCM_SHA256" }, () => {}); + 37 | tls.createServer({ cert: "c", key: "k", ticketKeys: new Uint8Array(48) }, () => {}); | ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - 39 | + 38 | hint: cert and key (PEM strings or Buffers) are the supported options -options-record.ts:42:20 - error SC2020: 'fs.watch with the recursive option' is part of the standard library types but has no scriptc lowering yet +options-record.ts:41:20 - error SC2020: 'fs.watch with the recursive option' is part of the standard library types but has no scriptc lowering yet - 41 | // stated defaults are accepted (corpus 1751). - 42 | fs.watch("/tmp", { recursive: true }, () => {}); + 40 | // stated defaults are accepted (corpus 1751). + 41 | fs.watch("/tmp", { recursive: true }, () => {}); | ^~~~~~~~~~~~~~~ - 43 | fs.watch("/tmp", { persistent: false }, () => {}); + 42 | fs.watch("/tmp", { persistent: false }, () => {}); hint: recursive watching has no lowering yet — kqueue watches the one opened path; watch each path -options-record.ts:43:20 - error SC2020: 'fs.watch with persistent disabled' is part of the standard library types but has no scriptc lowering yet +options-record.ts:42:20 - error SC2020: 'fs.watch with persistent disabled' is part of the standard library types but has no scriptc lowering yet - 42 | fs.watch("/tmp", { recursive: true }, () => {}); - 43 | fs.watch("/tmp", { persistent: false }, () => {}); + 41 | fs.watch("/tmp", { recursive: true }, () => {}); + 42 | fs.watch("/tmp", { persistent: false }, () => {}); | ^~~~~~~~~~~~~~~~~ - 44 | + 43 | hint: an open watcher keeps the loop alive until close() — that IS the lowering; persistent: false (a watcher the process does not wait for) has no lowering -options-record.ts:46:38 - error SC2020: 'lookup option 'hints'' is part of the standard library types but has no scriptc lowering yet +options-record.ts:47:74 - error SC2020: 'createInterface option 'historySize'' is part of the standard library types but has no scriptc lowering yet - 45 | // dns.lookup: hints/all are documented knobs with no lowering. - 46 | dns.lookup("localhost", { family: 4, hints: 32 }, () => {}); - | ^~~~~~~~~ - 47 | dns.lookup("localhost", { family: 4, all: true }, () => {}); - - hint: family: 4 is the supported option - -options-record.ts:47:38 - error SC2020: 'lookup option 'all'' is part of the standard library types but has no scriptc lowering yet - - 46 | dns.lookup("localhost", { family: 4, hints: 32 }, () => {}); - 47 | dns.lookup("localhost", { family: 4, all: true }, () => {}); - | ^~~~~~~~~ - 48 | - - hint: the all-addresses callback shape has no lowering — the lowered callback is (err, address, family) over one IPv4 answer - -options-record.ts:51:74 - error SC2020: 'createInterface option 'historySize'' is part of the standard library types but has no scriptc lowering yet - - 50 | // needs an interactive terminal. - 51 | readline.createInterface({ input: process.stdin, output: process.stdout, historySize: 10 }); + 46 | // needs an interactive terminal. + 47 | readline.createInterface({ input: process.stdin, output: process.stdout, historySize: 10 }); | ^~~~~~~~~~~~~~~ - 52 | readline.createInterface({ input: process.stdin, output: process.stdout, completer: (l: string) => [[l], l] }); + 48 | readline.createInterface({ input: process.stdin, output: process.stdout, completer: (l: string) => [[l], l] }); hint: input, output, terminal: false, crlfDelay, prompt and signal are the supported options -options-record.ts:52:74 - error SC2020: 'createInterface with a completer' is part of the standard library types but has no scriptc lowering yet +options-record.ts:48:74 - error SC2020: 'createInterface with a completer' is part of the standard library types but has no scriptc lowering yet - 51 | readline.createInterface({ input: process.stdin, output: process.stdout, historySize: 10 }); - 52 | readline.createInterface({ input: process.stdin, output: process.stdout, completer: (l: string) => [[l], l] }); + 47 | readline.createInterface({ input: process.stdin, output: process.stdout, historySize: 10 }); + 48 | readline.createInterface({ input: process.stdin, output: process.stdout, completer: (l: string) => [[l], l] }); | ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - 53 | + 49 | hint: tab completion needs an interactive terminal — the lowered interface reads piped lines (terminal: false) -options-record.ts:57:44 - error SC2020: 'writeFileSync option '__proto__'' is part of the standard library types but has no scriptc lowering yet +options-record.ts:53:44 - error SC2020: 'writeFileSync option '__proto__'' is part of the standard library types but has no scriptc lowering yet - 56 | // These must fence instead of dropping an inherited flag or mode without a diagnostic. - 57 | fs.writeFileSync("/tmp/proto-sync", "x", { __proto__: { flag: "a" } }); + 52 | // These must fence instead of dropping an inherited flag or mode without a diagnostic. + 53 | fs.writeFileSync("/tmp/proto-sync", "x", { __proto__: { flag: "a" } }); | ^~~~~~~~~~~~~~~~~~~~~~~~ - 58 | void fs.promises.writeFile("/tmp/proto-promise", "x", { __proto__: { mode: 0o600 } }); + 54 | void fs.promises.writeFile("/tmp/proto-promise", "x", { __proto__: { mode: 0o600 } }); hint: a bare __proto__: value entry changes the options object's prototype, so inherited options cannot be lowered -options-record.ts:58:57 - error SC2020: 'fs.promises.writeFile option '__proto__'' is part of the standard library types but has no scriptc lowering yet +options-record.ts:54:57 - error SC2020: 'fs.promises.writeFile option '__proto__'' is part of the standard library types but has no scriptc lowering yet - 57 | fs.writeFileSync("/tmp/proto-sync", "x", { __proto__: { flag: "a" } }); - 58 | void fs.promises.writeFile("/tmp/proto-promise", "x", { __proto__: { mode: 0o600 } }); + 53 | fs.writeFileSync("/tmp/proto-sync", "x", { __proto__: { flag: "a" } }); + 54 | void fs.promises.writeFile("/tmp/proto-promise", "x", { __proto__: { mode: 0o600 } }); | ^~~~~~~~~~~~~~~~~~~~~~~~~~ - 59 | // Both calls must terminate in the explicit prototype-options fence. + 55 | // Both calls must terminate in the explicit prototype-options fence. hint: a bare __proto__: value entry changes the options object's prototype, so inherited options cannot be lowered \ No newline at end of file diff --git a/tests/harness/npm.test.ts b/tests/harness/npm.test.ts new file mode 100644 index 000000000..51cff197c --- /dev/null +++ b/tests/harness/npm.test.ts @@ -0,0 +1,246 @@ +/* npm packages compiled natively: every program under tests/fixtures/npm/cases + * (plus the commander fixtures in npm-cases.ts) builds with the default + * static npm compile and runs against Node. A case matches Node (stdout and + * exit status for every argv list, stderr when it opts in with + * `// @stderr`) unless KNOWN records how it differs: + * + * - build: the compile refuses it with this diagnostic code; + * - run: it compiles, and the program's output names this diagnostic code + * (a refusal deferred to the operation that reached it), for every argv + * list or for the listed `argvs`, while the other lists match Node; + * - divergence: it runs but does not match Node yet. These run as expected + * failures, so a fix that makes one match fails here until KNOWN drops it. + * + * The fixture node_modules are committed test data. Binaries compile the + * package sources at build time and read nothing from node_modules at run + * time. + */ +import { execFile } from "node:child_process"; +import { createHash } from "node:crypto"; +import { globSync, mkdirSync, readFileSync } from "node:fs"; +import { dirname, join } from "node:path"; +import { promisify } from "node:util"; +import { describe, expect, test } from "vitest"; +import { compile } from "@scriptc/compiler"; +import { npmCases, type NpmCase } from "./npm-cases.js"; +import { shardSelect, shardSuffix } from "./shard.js"; + +const execFileAsync = promisify(execFile); +const repoRoot = join(import.meta.dirname, "../.."); +const fixturesRoot = join(repoRoot, "tests/fixtures"); +const cacheDir = join(repoRoot, "node_modules/.cache/scriptc-tests"); +const sanitize = process.env["SCRIPTC_SAN"] === "1"; + +type Known = { build: string } | { run: string; argvs?: string[][] } | { divergence: string }; + +const KNOWN: Record = { + "any-chaining": { build: "SC2003" }, + "assert-shims": { run: "SC1043" }, + "buffer-shims": { run: "SC2020" }, + "bytes-exit": { build: "SC2004" }, + "cjs-esm-named": { build: "SC2013" }, + "cjs-lexer-aligned": { build: "SC2013" }, + "cjs-lexer-invisible": { build: "SC2013" }, + "commander-calc": { run: "SC1090", argvs: [["--help"]] }, + "commander-calc-async": { run: "SC1090", argvs: [["--help"]] }, + "create-require": { run: "SC2009" }, + "dotfolder": { run: "SC2009" }, + "dual-package-kinds": { build: "SC2013" }, + "dynamic-import": { build: "SC2012" }, + "dynamic-import-local": { build: "SC2012" }, + "esbuild-require": { build: "SC2013" }, + "esm-missing-default": { build: "SC1090" }, + "esm-missing-export": { build: "SC1090" }, + "fetch-destructure": { build: "SC1090" }, + "fs-shims": { run: "SC2020" }, + "hoisted-workspace": { build: "SC2013" }, + "import-refusal": { build: "SC2020" }, + "island-registry": { build: "SC2011" }, + "island-timers": { + divergence: + "Timeout handles are numbers (typeof) and lose their ref flag once fired or cleared", + }, + "island-web-plumbing": { build: "SC2013" }, + "lazy-traps": { build: "SC2013" }, + "misc-shims": { build: "SC2013" }, + "namespace": { build: "SC1013" }, + "node24-async-events": { run: "SC2020" }, + "node24-commonjs-metadata": { run: "SC1090" }, + "node24-crypto-kdf": { run: "SC2020" }, + "node24-deep-equality": { run: "SC2020" }, + "node24-diagnostics-async": { run: "SC1090" }, + "node24-diagnostics-tracing": { run: "SC1090" }, + "node24-env": { run: "SC1090" }, + "node24-esm-metadata": { run: "SC1090" }, + "node24-event-emitter": { run: "SC2020" }, + "node24-file-url-conversion": { build: "SC1090" }, + "node24-fs-descriptors": { run: "SC1090" }, + "node24-mime": { run: "SC2020" }, + "node24-mime-errors": { run: "SC1090" }, + "node24-no-error-assertions": { run: "SC2020" }, + "node24-parseargs": { run: "SC1090" }, + "node24-promise-timers": { run: "SC2020" }, + "node24-string-decoder": { run: "SC2020" }, + "node24-style-text": { run: "SC2020" }, + "node24-system-errors": { run: "SC1090" }, + "node24-text-encoding": { run: "SC2020" }, + "node24-text-utilities": { run: "SC2020" }, + "node24-timers-unref": { run: "SC2020" }, + "node24-url-factories": { run: "SC2020" }, + "node24-url-mutation": { run: "SC1090" }, + "node24-util-adapters": { run: "SC2020" }, + "node24-util-types": { run: "SC2009" }, + "node24-web-stream-encoding": { build: "SC2013" }, + "node24-web-stream-errors": { build: "SC2013" }, + "node24-web-stream-lifecycle": { build: "SC2013" }, + "node24-web-stream-pipe-abort": { build: "SC2013" }, + "node24-web-stream-pipe-errors": { build: "SC2013" }, + "node24-web-stream-pipes": { build: "SC2013" }, + "node24-web-stream-queues": { build: "SC2013" }, + "node24-web-stream-readable-lifecycle": { build: "SC2013" }, + "node24-web-stream-readers": { build: "SC2013" }, + "node24-web-stream-tee": { build: "SC2013" }, + "node24-web-stream-transforms": { build: "SC2013" }, + "package-imports": { build: "SC2013" }, + "promise-bridge": { build: "SC1090" }, + "self-name": { build: "SC2013" }, + "stream-shims": { build: "SC1090" }, + "typed-callbacks": { + divergence: + "crashes converting a cyclic object into a recursive record type; a follow-up fix is in progress", + }, + "util-shims": { run: "SC2020" }, + "web-streams": { build: "SC2013" }, + "workspace-copied": { build: "SC2013" }, + "workspace-unclean": { build: "SC2013" }, + "zlib-shims": { run: "SC2020" }, +}; + +const cases = shardSelect(npmCases(fixturesRoot), (c) => c.name); + +interface RunResult { + stdout: Buffer; + stderr: Buffer; + exitCode: number; +} + +function programStderr(stderr: Buffer): Buffer { + if (!sanitize) return stderr; + // Linux ASan emits this instrumentation warning at the first fiber swap. + // Latin-1 preserves every other byte, including non-UTF-8 program output. + return Buffer.from( + stderr + .toString("latin1") + .replace( + /^==\d+==WARNING: ASan doesn't fully support makecontext\/swapcontext functions and may produce false positives in some cases!\n/gm, + "", + ), + "latin1", + ); +} + +async function runBinary(cmd: string, args: string[], cwd: string): Promise { + try { + // stdin closes immediately, as in differential.test.ts: a program that + // reads fd 0 to EOF must not hang on an open pipe. + const pending = execFileAsync(cmd, args, { cwd, encoding: "buffer" }); + pending.child.stdin?.end(); + const { stdout, stderr } = await pending; + return { stdout, stderr, exitCode: 0 }; + } catch (err) { + const e = err as { code?: unknown; signal?: unknown; stdout?: Buffer; stderr?: Buffer }; + if (!Buffer.isBuffer(e.stdout) || !Buffer.isBuffer(e.stderr)) throw err; + const exitCode = typeof e.code === "number" ? e.code : -1; + if (exitCode === -1 && typeof e.signal !== "string") throw err; + return { stdout: e.stdout, stderr: e.stderr, exitCode }; + } +} + +type Built = { ok: true; binary: string } | { ok: false; codes: string[]; text: string }; + +/** Compile once per (entry, lane); the cache key hashes the program and the + * fixture packages so edits to either rebuild. */ +async function build(entry: string): Promise { + const hash = createHash("sha256"); + const inputs = [ + entry, + ...globSync(join(dirname(entry), "*.{ts,mts,cts,js,mjs,cjs,json}")), + ...globSync(join(fixturesRoot, "npm/node_modules/**/*.{js,mjs,cjs,json,d.ts,node}")), + ].sort(); + for (const f of inputs) hash.update(f).update(readFileSync(f)); + const key = hash + .update(sanitize ? "san" : "plain") + .digest("hex") + .slice(0, 16); + const outDir = join(cacheDir, `npm-${key}`); + mkdirSync(outDir, { recursive: true }); + const result = await compile(entry, { outPath: join(outDir, "program"), outDir, sanitize }); + if (result.ok) return { ok: true, binary: result.binaryPath }; + return { + ok: false, + codes: result.diagnostics.map((d) => d.code), + text: result.diagnostics.map((d) => `${d.code}: ${d.message}`).join("\n"), + }; +} + +async function expectMatchesNode(c: NpmCase, binary: string): Promise { + const cwd = dirname(c.entry); + for (const argv of c.argvs ?? [[]]) { + const [nodeRes, nativeRes] = await Promise.all([ + runBinary(process.execPath, [c.entry, ...argv], cwd), + runBinary(binary, argv, cwd), + ]); + const label = argv.join(" "); + if (!nodeRes.stdout.equals(nativeRes.stdout)) { + expect(nativeRes.stdout.toString("utf8"), label).toBe(nodeRes.stdout.toString("utf8")); + expect.unreachable("stdout differed at byte level but not after utf8 decode"); + } + expect(nativeRes.exitCode, label).toBe(nodeRes.exitCode); + if (c.compareStderr) expect(programStderr(nativeRes.stderr), label).toEqual(nodeRes.stderr); + } +} + +test("every KNOWN entry names a case", () => { + const names = new Set(npmCases(fixturesRoot).map((c) => c.name)); + expect(Object.keys(KNOWN).filter((name) => !names.has(name))).toEqual([]); +}); + +describe(`npm native (${cases.length} programs${sanitize ? ", sanitized" : ""}${shardSuffix()})`, () => { + for (const c of cases) { + const known = KNOWN[c.name]; + if (known !== undefined && "divergence" in known) { + test.fails(`${c.name} (diverges: ${known.divergence})`, async () => { + const built = await build(c.entry); + if (!built.ok) throw new Error(built.text); + await expectMatchesNode(c, built.binary); + }, 120_000); + continue; + } + test( + c.name, + async () => { + const built = await build(c.entry); + if (known !== undefined && "build" in known) { + expect(built.ok, "the compile refuses the program").toBe(false); + if (!built.ok) expect(built.codes, built.text).toContain(known.build); + return; + } + if (!built.ok) throw new Error(`npm fixture failed to compile:\n${built.text}`); + if (known !== undefined && "run" in known) { + for (const argv of known.argvs ?? c.argvs ?? [[]]) { + const res = await runBinary(built.binary, argv, dirname(c.entry)); + const output = res.stdout.toString("utf8") + res.stderr.toString("utf8"); + expect(output, `${argv.join(" ")}: the deferred refusal`).toContain(known.run); + } + if (known.argvs === undefined) return; + const refused = new Set(known.argvs.map((argv) => JSON.stringify(argv))); + const argvs = (c.argvs ?? [[]]).filter((argv) => !refused.has(JSON.stringify(argv))); + await expectMatchesNode({ ...c, argvs }, built.binary); + return; + } + await expectMatchesNode(c, built.binary); + }, + 120_000, + ); + } +}); diff --git a/tests/harness/wasi-differential.test.ts b/tests/harness/wasi-differential.test.ts index 0ceb66579..7a898b19d 100644 --- a/tests/harness/wasi-differential.test.ts +++ b/tests/harness/wasi-differential.test.ts @@ -9,6 +9,8 @@ import { dirname, join } from "node:path"; import { promisify } from "node:util"; import { afterAll, beforeAll, describe, expect, test } from "vitest"; import { compile, compileLibrary } from "@scriptc/compiler"; +import { emitNativeArtifact } from "../../packages/compiler/src/backend/native-codegen.js"; +import { WASM32_WASI_TARGET } from "../../packages/compiler/src/backend/targets.js"; const execFileAsync = promisify(execFile); const repoRoot = join(import.meta.dirname, "../.."); @@ -318,6 +320,91 @@ describe.skipIf(!zigOnPath())("wasm32-wasi differential", () => { } }); + test("partitioned wasm32 objects link to the same program as one object", async () => { + const dir = await mkdtemp("/tmp/scriptc-wasi-partitions-"); + // Internal callees, internal state, indirect calls through a table of + // internal functions and a thread_local global, reached across partitions. + const adds = [0, 1, 2, 3, 4, 5]; + const llvm = `@total = internal global i32 0 +@zero = internal global i32 0 +@counter = internal thread_local global i32 0 +@handlers = internal constant [3 x ptr] [ptr @add0, ptr @add1, ptr @add2] +${adds + .map( + (index) => `define internal void @add${index}(i32 %value) noinline { + %old = load i32, ptr @total + %scaled = mul i32 %value, ${index + 1} + %new = add i32 %old, %scaled + store i32 %new, ptr @total + ret void +}`, + ) + .join("\n")} +define internal i32 @bump() noinline { + %old = load i32, ptr @counter + %new = add i32 %old, 1 + store i32 %new, ptr @counter + ret i32 %new +} +define i32 @__main_argc_argv(i32 %argc, ptr %argv) { +${adds.map((index) => ` call void @add${index}(i32 ${index + 2})`).join("\n")} + %base = load volatile i32, ptr @zero +${[0, 1, 2] + .map( + (slot) => ` %i${slot} = add i32 %base, ${slot} + %p${slot} = getelementptr [3 x ptr], ptr @handlers, i32 0, i32 %i${slot} + %f${slot} = load ptr, ptr %p${slot} + call void %f${slot}(i32 3)`, + ) + .join("\n")} + %b0 = call i32 @bump() + %b1 = call i32 @bump() + %sum = load i32, ptr @total + %code = add i32 %sum, %b1 + ret i32 %code +} +`; + const emit = async (output: string, partitions: number, optimization: "0" | "2") => { + const artifact = await emitNativeArtifact({ + outputPath: output, + llvm, + outputKind: "obj", + sourcePath: "/src/partitions.ts", + optimization, + target: WASM32_WASI_TARGET, + cacheRoot: null, + partitions, + }); + return artifact.outputPaths; + }; + const link = async (objects: string[], name: string) => { + const output = join(dir, name); + await execFileAsync("zig", ["cc", "-target", "wasm32-wasi", ...objects, "-o", output]); + return run(process.execPath, [ + "--no-warnings", + "--experimental-wasi-unstable-preview1", + "-e", + WASI_RUNNER, + output, + ]); + }; + // Release partitions import small functions from each other; dev + // partitions compile independently. + for (const optimization of ["2", "0"] as const) { + const partitions = await emit(join(dir, `program-${optimization}.o`), 3, optimization); + expect(partitions).toHaveLength(3); + const again = await emit(join(dir, `again-${optimization}.o`), 3, optimization); + expect(await Promise.all(again.map((path) => readFile(path)))).toEqual( + await Promise.all(partitions.map((path) => readFile(path))), + ); + const whole = await emit(join(dir, `whole-${optimization}.o`), 1, optimization); + const partitioned = await link(partitions, `partitioned-${optimization}.wasm`); + expect(partitioned).toEqual(await link(whole, `whole-${optimization}.wasm`)); + // 112 from the direct calls, 18 through the table, 2 from the counter. + expect(partitioned.exitCode).toBe(132); + } + }); + test("sanitized library mode reports a target diagnostic before invoking the WASI toolchain", async () => { const outDir = await mkdtemp("/tmp/scriptc-wasi-library-"); const result = await compileLibrary({