From 0d59abe1435447d1dd530f51ffcf47e227242b0c Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Fri, 16 Oct 2026 01:39:10 -0500 Subject: [PATCH] Pool small Buffers as Node does Buffer.from of strings, arrays and Uint8Arrays, Buffer.allocUnsafe and Buffer.concat now return views into a per-thread pool slab of Buffer.poolSize bytes (64 KiB by default) when the size is below Buffer.poolSize >>> 1, following Node's allocate, fromStringFast and fromArrayLike: 8-byte aligned offsets, a new slab when a request does not fit, and the four-bytes-per-unit reservation for strings. Buffer.poolSize can be read and set, and Buffer.allocUnsafeSlow returns an unpooled Buffer. A slab lives while any of its views does. Its ArrayBuffer cannot be transferred, and structuredClone of a typed array view copies the whole ArrayBuffer and keeps the view's offset. Buffer.alloc, allocUnsafe and allocUnsafeSlow validate sizes with Node's ERR_OUT_OF_RANGE error. The first pooled allocation finds the startup slab, which has the default 64 KiB size. A request it cannot hold gets a slab of the current Buffer.poolSize, as in Node, and the room is checked after every new slab, so a Buffer.poolSize raised before the first pooled allocation cannot place a reservation past the end of a 64 KiB slab. A reservation larger than a fresh slab traps instead of writing past it. Corpus 5531 (typed) and 5532 (untyped) raise Buffer.poolSize before the first pooled allocation and allocate just under the new threshold through each pooled path, each case in a fresh child process, and also shrink Buffer.poolSize and change it between allocations. The node-types fenced fixture no longer lists Buffer.poolSize, which now compiles. --- .../limitations/buffer-construction.mdx | 2 - .../arraybuffers-and-typed-arrays.mdx | 2 +- .../node-v24-compatibility-meta.json | 2 +- .../src/generated/node-v24-compatibility.json | 10 +- .../generated/node-v24-backlog.json | 20 +- .../generated/node-v24-internal.json | 16 +- internal/compatibility/static-support.json | 1 + .../ambient/scriptc-node-fallback.d.ts | 7 +- packages/compiler/src/backend/llvm/emitter.ts | 2 +- .../compiler/src/backend/llvm/expr-bytes.ts | 2 +- .../compiler/src/backend/llvm/lib-shared.ts | 15 +- .../src/frontend/lowering/containers/bytes.ts | 46 ++- .../src/frontend/lowering/lower-calls.ts | 24 ++ .../src/frontend/lowering/lower-exprs.ts | 2 + .../src/frontend/lowering/lower-stmts.ts | 12 + packages/compiler/src/ir/builtin-effects.ts | 9 + .../compiler/src/ir/builtin-signatures.ts | 19 + .../test/bytes-element-emission.test.ts | 2 +- .../compiler/test/llvm-runtime-abi.test.ts | 3 +- .../tests/corpus/5530-buffer-pool.ts.json | 6 + .../5531-buffer-pool-first-slab.ts.json | 6 + ...32-buffer-pool-first-slab-untyped.mjs.json | 6 + packages/runtime/src/scr_bytes.c | 383 +++++++++++++++++- packages/runtime/src/scr_bytes.h | 25 ++ packages/runtime/src/scr_bytes.test.c | 50 +++ packages/runtime/src/scr_bytes_io.c | 12 + packages/runtime/src/scr_console.c | 1 + packages/runtime/src/scr_dyn.h | 3 + packages/runtime/src/scr_json.c | 34 +- packages/runtime/test/test_bytes.c | 2 + .../corpus/5405-buffer-arraybuffer-window.ts | 9 +- tests/corpus/5530-buffer-pool.ts | 137 +++++++ tests/corpus/5531-buffer-pool-first-slab.ts | 151 +++++++ .../5532-buffer-pool-first-slab-untyped.mjs | 45 ++ tests/fixtures/node-types/fenced.ts | 1 - .../__snapshots__/node-types-fenced.txt | 203 +++++----- 36 files changed, 1101 insertions(+), 169 deletions(-) create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5530-buffer-pool.ts.json create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5531-buffer-pool-first-slab.ts.json create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5532-buffer-pool-first-slab-untyped.mjs.json create mode 100644 tests/corpus/5530-buffer-pool.ts create mode 100644 tests/corpus/5531-buffer-pool-first-slab.ts create mode 100644 tests/corpus/5532-buffer-pool-first-slab-untyped.mjs diff --git a/docs/content/limitations/buffer-construction.mdx b/docs/content/limitations/buffer-construction.mdx index 1d6ebefad..e92792cfc 100644 --- a/docs/content/limitations/buffer-construction.mdx +++ b/docs/content/limitations/buffer-construction.mdx @@ -1,3 +1 @@ `Buffer.from` accepts untyped strings, byte arrays, arrays, and plain array-like or Buffer JSON objects. Encoding arguments must be supported literals. Custom input `valueOf` hooks and opaque class or native-handle inputs throw an unsupported-operation error. - -Every Buffer owns an ArrayBuffer of its own length. Node allocates small Buffers from `Buffer.from`, `Buffer.allocUnsafe`, and `Buffer.concat` out of a shared 64 KiB pool, so for those Buffers `buf.buffer.byteLength` is the pool size in Node and the Buffer's length here, and `buf.byteOffset` is `0` here. Reading `buf.buffer` through `buf.byteOffset` and `buf.length` returns the same bytes in both. `Buffer.poolSize` is unsupported. diff --git a/docs/content/limitations/builtin-apis/arraybuffers-and-typed-arrays.mdx b/docs/content/limitations/builtin-apis/arraybuffers-and-typed-arrays.mdx index 9af300117..81dfded25 100644 --- a/docs/content/limitations/builtin-apis/arraybuffers-and-typed-arrays.mdx +++ b/docs/content/limitations/builtin-apis/arraybuffers-and-typed-arrays.mdx @@ -1 +1 @@ -- **ArrayBuffers and typed arrays:** Fixed-length ArrayBuffers support shared typed-array and DataView storage. Resizing, transferring, Float16 arrays, and BigInt arrays are unsupported. Numeric typed arrays and Buffers preserve their kind and shared storage through `unknown`, including when nested in records and arrays. Numeric typed-array constructors can be stored and used with `new`, including constructors read from an array's `constructor` property. JavaScript numeric-array parameters and returns preserve typed-array storage. +- **ArrayBuffers and typed arrays:** Fixed-length ArrayBuffers support shared typed-array and DataView storage. Resizing, transferring, Float16 arrays, and BigInt arrays are unsupported. Numeric typed arrays and Buffers preserve their kind and shared storage through `unknown`, including when nested in records and arrays. Numeric typed-array constructors can be stored and used with `new`, including constructors read from an array's `constructor` property. JavaScript numeric-array parameters and returns preserve typed-array storage. `Buffer.from`, `Buffer.allocUnsafe`, and `Buffer.concat` return views into a per-thread pool of `Buffer.poolSize` bytes (64 KiB by default) when the size is below `Buffer.poolSize >>> 1`, as in Node; a pool slab stays in memory while any of its Buffers does. Node's own startup code allocates from the first slab, so the `byteOffset` of Buffers in that slab differs from Node's. Buffers returned by other Node APIs, such as `fs.readFileSync` and `zlib`, own their `ArrayBuffer`, where Node allocates small ones from the pool. diff --git a/docs/src/generated/node-v24-compatibility-meta.json b/docs/src/generated/node-v24-compatibility-meta.json index 75b0e8613..7f49aebbb 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": "1c65fe186ed24618c053", + "artifactVersion": "aaa7e74233dc5f826dcd", "rowCount": 3880 } diff --git a/docs/src/generated/node-v24-compatibility.json b/docs/src/generated/node-v24-compatibility.json index d5b94a8cd..6ab3bd301 100644 --- a/docs/src/generated/node-v24-compatibility.json +++ b/docs/src/generated/node-v24-compatibility.json @@ -119,9 +119,9 @@ "apiEntries": 99, "static": { "supported": 16, - "partial": 6, + "partial": 7, "refused": 0, - "not-implemented": 77, + "not-implemented": 76, "by-design": 0, "unreviewed": 0, "not-applicable": 0 @@ -4584,9 +4584,9 @@ "inherited": true }, "static": { - "status": "not-implemented", - "detail": "No static compiler lowering is registered for this API yet.", - "verification": "registry-gap" + "status": "partial", + "detail": "Implemented for the documented scriptc module-loader subset.", + "verification": "test-backed" } }, { diff --git a/internal/compatibility/generated/node-v24-backlog.json b/internal/compatibility/generated/node-v24-backlog.json index 5c3c8f920..fdc2805d0 100644 --- a/internal/compatibility/generated/node-v24-backlog.json +++ b/internal/compatibility/generated/node-v24-backlog.json @@ -7,8 +7,8 @@ "tierItemCount": 3472, "tiers": { "static": { - "audit-partial": 1034, - "verify-gap": 2311, + "audit-partial": 1035, + "verify-gap": 2310, "replace-refusal": 40, "classify": 2, "implement": 85 @@ -3733,12 +3733,16 @@ "priority": "high", "tiers": { "static": { - "status": "not-implemented", - "action": "verify-gap", - "verification": "registry-gap", - "confidence": "low", - "source": "compiler-unmatched:buffer", - "tests": [] + "status": "partial", + "action": "audit-partial", + "verification": "test-backed", + "confidence": "high", + "source": "compiler-feature:buffer.poolSize", + "tests": [ + "tests/corpus/5530-buffer-pool.ts", + "tests/corpus/5531-buffer-pool-first-slab.ts", + "tests/corpus/5532-buffer-pool-first-slab-untyped.mjs" + ] } } }, diff --git a/internal/compatibility/generated/node-v24-internal.json b/internal/compatibility/generated/node-v24-internal.json index 1cecc5bdb..a56cf5671 100644 --- a/internal/compatibility/generated/node-v24-internal.json +++ b/internal/compatibility/generated/node-v24-internal.json @@ -71,9 +71,9 @@ "apiEntries": 99, "static": { "supported": 16, - "partial": 6, + "partial": 7, "refused": 0, - "not-implemented": 77, + "not-implemented": 76, "by-design": 0, "unreviewed": 0, "not-applicable": 0 @@ -3144,6 +3144,9 @@ "tests/corpus/5288-blob-validation.js", "tests/corpus/5289-blob-read-order.ts", "tests/corpus/5290-blob-dynamic.js", + "tests/corpus/5530-buffer-pool.ts", + "tests/corpus/5531-buffer-pool-first-slab.ts", + "tests/corpus/5532-buffer-pool-first-slab-untyped.mjs", "tests/corpus/node24-buffer-encoding-errors.ts", "tests/corpus/node24-buffer-encoding.ts", "tests/fixtures/node-types/blob-file.ts", @@ -5432,8 +5435,13 @@ "inherited": true }, "static": { - "status": "not-implemented", - "evidence": "compiler-unmatched:buffer" + "status": "partial", + "evidence": "compiler-feature:buffer.poolSize", + "tests": [ + "tests/corpus/5530-buffer-pool.ts", + "tests/corpus/5531-buffer-pool-first-slab.ts", + "tests/corpus/5532-buffer-pool-first-slab-untyped.mjs" + ] }, "anchorSource": "ancestor" }, diff --git a/internal/compatibility/static-support.json b/internal/compatibility/static-support.json index 0c63dde34..a0e0e3a7c 100644 --- a/internal/compatibility/static-support.json +++ b/internal/compatibility/static-support.json @@ -274,6 +274,7 @@ { "chapter": "http", "parent": "Class: `http.ServerResponse`", "signatures": ["`req` Type: {http.IncomingMessage}", "`sendDate` Type: {boolean}", "`strictContentLength` Type: {boolean} **Default:** `false`", "`statusMessage` Type: {string}", "`socket` Type: {stream.Duplex}", "`connection` Type: {stream.Duplex}", "`writableFinished` Type: {boolean}"], "status": "partial", "evidence": ["tests/corpus/2957-http-static-response-controls.ts"] }, { "chapter": "http", "signatures": ["`request.cork()`", "`request.flushHeaders()`", "`request.uncork()`", "`response.addTrailers(headers)`", "`response.cork()`", "`response.flushHeaders()`", "`response.uncork()`", "`headersDistinct` Type: {Object}", "`rawTrailers` Type: {string\\[]}", "`trailers` Type: {Object}", "`trailersDistinct` Type: {Object}", "`outgoingMessage.addTrailers(headers)`", "`outgoingMessage.cork()`", "`outgoingMessage.flushHeaders()`", "`outgoingMessage.uncork()`", "`writableCorked` Type: {number}"], "status": "partial", "evidence": ["tests/corpus/2700-http-static-trailers.ts", "tests/corpus/2701-http-static-trailer-wire.ts", "tests/corpus/2702-http-static-trailer-end.ts", "tests/corpus/2972-http-static-distinct-snapshots.ts", "tests/fixtures/server/cases/http-trailers/main.ts"] }, { "chapter": "http", "parent": "Class: `http.IncomingMessage`", "signatures": ["`rawHeaders` Type: {string\\[]}"], "status": "partial", "evidence": ["tests/corpus/2972-http-static-distinct-snapshots.ts"] }, + { "chapter": "buffer", "signatures": ["`poolSize` Type: {integer} **Default:** `65536`"], "status": "partial", "evidence": ["tests/corpus/5530-buffer-pool.ts", "tests/corpus/5531-buffer-pool-first-slab.ts", "tests/corpus/5532-buffer-pool-first-slab-untyped.mjs"] }, { "chapter": "buffer", "signatures": ["`buf.readBigInt64BE([offset])`", "`buf.readBigInt64LE([offset])`", "`buf.readBigUInt64BE([offset])`", "`buf.readBigUInt64LE([offset])`", "`buf.writeBigInt64BE(value[, offset])`", "`buf.writeBigInt64LE(value[, offset])`", "`buf.writeBigUInt64BE(value[, offset])`", "`buf.writeBigUInt64LE(value[, offset])`"], "status": "supported", "evidence": ["tests/corpus/2902-bigint-bytes.ts", "tests/corpus/2904-bigint-errors.ts"] }, { "chapter": "environment_variables", "symbols": ["export"], "status": "not-applicable" }, { "chapter": "globals", "symbols": ["fetch"], "status": "partial", "evidence": ["tests/harness/fetch-conformance.test.ts"] }, diff --git a/packages/compiler/ambient/scriptc-node-fallback.d.ts b/packages/compiler/ambient/scriptc-node-fallback.d.ts index 0f2c67c1a..afa9b0d31 100644 --- a/packages/compiler/ambient/scriptc-node-fallback.d.ts +++ b/packages/compiler/ambient/scriptc-node-fallback.d.ts @@ -635,9 +635,12 @@ interface BufferConstructor { from(data: Uint8Array | readonly number[]): Buffer; alloc(size: number, fill?: string | number | Uint8Array, encoding?: BufferEncoding): Buffer; compare(buf1: Uint8Array, buf2: Uint8Array): number; - /* alloc without the zero-fill guarantee — the lowering zero-fills - * anyway (never handing out uninitialized memory costs one memset). */ + /* alloc without the zero-fill guarantee. Below Buffer.poolSize >>> 1 + * bytes it returns a view into the shared pool slab. */ allocUnsafe(size: number): Buffer; + allocUnsafeSlow(size: number): Buffer; + /* The pool slab size for allocUnsafe, from, and concat. */ + poolSize: number; concat(list: readonly Uint8Array[], totalLength?: number): Buffer; isBuffer(obj: unknown): obj is Buffer; byteLength(input: string | Uint8Array, encoding?: BufferEncoding): number; diff --git a/packages/compiler/src/backend/llvm/emitter.ts b/packages/compiler/src/backend/llvm/emitter.ts index 63b5959bc..e06f3ca06 100644 --- a/packages/compiler/src/backend/llvm/emitter.ts +++ b/packages/compiler/src/backend/llvm/emitter.ts @@ -2103,7 +2103,7 @@ export class LlEmitter { // typed-array access GEPs through this directly: the IR type already // fixes elem, so the hot path needs neither a runtime kind load nor // the generic scr_bytes_get/set call. - `%ScrBytes = type { ${this.sizeType}, ${this.sizeType}, i32, ptr, ptr, i8, i8, i8, ptr }`, + `%ScrBytes = type { ${this.sizeType}, ${this.sizeType}, i32, ptr, ptr, i8, i8, i8, i8, ptr }`, // The capture box { rc, kind, obj_retain, obj_release, obj_trace, // slot } — TDZ reads peek the payload slot (offset 40) directly. `%ScrBox = type { ${this.sizeType}, i32, ptr, ptr, ptr, i64 }`, diff --git a/packages/compiler/src/backend/llvm/expr-bytes.ts b/packages/compiler/src/backend/llvm/expr-bytes.ts index 853f4e958..33c24fd54 100644 --- a/packages/compiler/src/backend/llvm/expr-bytes.ts +++ b/packages/compiler/src/backend/llvm/expr-bytes.ts @@ -183,7 +183,7 @@ function emitSharedBytesSplit(host: LlvmEmitterContext, receiver: string): strin storage = B.tmp(), isShared = B.tmp(), expected = B.tmp(); - B.line(`${slot} = getelementptr inbounds %ScrBytes, ptr ${receiver}, i64 0, i32 8`); + B.line(`${slot} = getelementptr inbounds %ScrBytes, ptr ${receiver}, i64 0, i32 9`); B.line(`${storage} = load ptr, ptr ${slot}`); B.line(`${isShared} = icmp ne ptr ${storage}, null`); B.line(`${expected} = call i1 @llvm.expect.i1(i1 ${isShared}, i1 false)`); diff --git a/packages/compiler/src/backend/llvm/lib-shared.ts b/packages/compiler/src/backend/llvm/lib-shared.ts index 02b4ea310..192c20aa8 100644 --- a/packages/compiler/src/backend/llvm/lib-shared.ts +++ b/packages/compiler/src/backend/llvm/lib-shared.ts @@ -399,8 +399,18 @@ export const LIB_FN_SYMS: Record = { "buffer.fromStrChecked": "scr_bytes_from_str_checked", "buffer.brand": "scr_bytes_as_buffer", "buffer.fromDyn": "scr_buffer_from_dyn", - "buffer.concat": "scr_bytes_concat", - "buffer.concatLen": "scr_bytes_concat_len", + "buffer.concat": "scr_buffer_concat", + "buffer.concatLen": "scr_buffer_concat_len", + "buffer.alloc": "scr_buffer_alloc", + "buffer.allocUnsafe": "scr_buffer_alloc_unsafe", + "buffer.allocUnsafeSlow": "scr_buffer_alloc_unsafe_slow", + "buffer.poolFromStr": "scr_buffer_from_str", + "buffer.poolFromStrChecked": "scr_buffer_from_str_checked", + "buffer.poolFromBytes": "scr_buffer_from_bytes", + "buffer.poolFromArray": "scr_buffer_from_arr", + "buffer.poolFromDyn": "scr_buffer_from_dyn_pooled", + "buffer.poolSize": "scr_buffer_pool_size", + "buffer.setPoolSize": "scr_buffer_set_pool_size", "buffer.byteLenDyn": "scr_buffer_byte_length_dyn", "buffer.byteLenStr": "scr_bytes_byte_length_str", "buffer.isEncoding": "scr_bytes_is_encoding", @@ -1286,6 +1296,7 @@ export const LIB_FN_SYMS: Record = { "dyn.structuredClone": "scr_structured_clone", "dyn.cloneMissing": "scr_structured_clone_missing", "dyn.cloneTransferFail": "scr_structured_clone_transfer_fail", + "dyn.cloneTransferFailWith": "scr_structured_clone_transfer_fail_with", "regex.new": "scr_regex_new", "regex.resetLastIndex": "scr_regex_reset_last_index", "regex.newChecked": "scr_regex_new_checked", diff --git a/packages/compiler/src/frontend/lowering/containers/bytes.ts b/packages/compiler/src/frontend/lowering/containers/bytes.ts index 74d0716b9..b9400cbbd 100644 --- a/packages/compiler/src/frontend/lowering/containers/bytes.ts +++ b/packages/compiler/src/frontend/lowering/containers/bytes.ts @@ -1357,7 +1357,7 @@ function lowerBufferStaticValue( if (args.length === 1) { const elems = argNode.elements.map((el) => lowerer.lowerExprExpecting(el, F64)); const seed: IrExpr = { kind: "arrayLit", elems, type: arrayOf(F64), loc }; - return { kind: "bytesNew", source: seed, type: BYTES_U8, loc }; + return { kind: "libCall", fn: "buffer.poolFromArray", args: [seed], type: BYTES_U8, loc }; } } else { const srcIr = lowerer.mapTypeOf(lowerer.typeOf(argNode)); @@ -1367,13 +1367,19 @@ function lowerBufferStaticValue( if (value.type.kind === "dyn") { const encName = args[1] ? bufEncoding(lowerer, "Buffer.from", args[1]) : "utf8"; const enc: IrExpr = { kind: "strLit", value: encName, type: STRING, loc }; - return { kind: "libCall", fn: "buffer.fromDyn", args: [value, enc], type: BYTES_U8, loc }; + return { + kind: "libCall", + fn: "buffer.poolFromDyn", + args: [value, enc], + type: BYTES_U8, + loc, + }; } if (srcIr?.kind === "string") { const s = lowerer.coerceInto(argNode, value, STRING); const encNode = args[1]; let enc: IrExpr = { kind: "strLit", value: "utf8", type: STRING, loc }; - let fn: IrLibFn = "buffer.fromStr"; + let fn: IrLibFn = "buffer.poolFromStr"; if (encNode) { const encType = lowerer.typeOf(encNode); const encName = encType.isStringLiteralType() @@ -1392,17 +1398,29 @@ function lowerBufferStaticValue( enc = defaultAfterUndefined(undefinedArg, enc); } else { enc = lowerOptionalArgument(lowerer, encNode, STRING, enc); - fn = "buffer.fromStrChecked"; + fn = "buffer.poolFromStrChecked"; } } } return { kind: "libCall", fn, args: [s, enc], type: BYTES_U8, loc }; } if (args.length === 1 && srcIr?.kind === "bytes" && srcIr.elem === "u8") { - return { kind: "bytesNew", source: value, type: BYTES_U8, loc }; + return { + kind: "libCall", + fn: "buffer.poolFromBytes", + args: [value], + type: BYTES_U8, + loc, + }; } if (args.length === 1 && srcIr?.kind === "array" && srcIr.elem.kind === "f64") { - return { kind: "bytesNew", source: value, type: BYTES_U8, loc }; + return { + kind: "libCall", + fn: "buffer.poolFromArray", + args: [value], + type: BYTES_U8, + loc, + }; } } } @@ -1414,17 +1432,21 @@ function lowerBufferStaticValue( "the buffer's storage; narrow unions first", ); } - if (member === "alloc" || member === "allocUnsafe") { + if (member === "alloc" || member === "allocUnsafe" || member === "allocUnsafeSlow") { const maxArgs = member === "alloc" ? 3 : 1; if (args.length < 1 || args.length > maxArgs || args.some(ts.isSpreadElement)) { lowerer.noLowering(`Buffer.${member} with ${args.length} arguments`, call); } - // allocUnsafe's contents are UNSPECIFIED in Node (uninitialized pool - // memory); a zero-filled buffer is a valid instance of unspecified, - // and the deterministic choice — a program observing the difference - // is depending on garbage. + // allocUnsafe's contents are unspecified in Node (uninitialized pool + // memory). Pool slabs and unpooled buffers start zero-filled. const size = lowerer.lowerExprExpecting(args[0]!, F64); - const fresh: IrExpr = { kind: "bytesNew", source: size, type: BYTES_U8, loc }; + const fn: IrLibFn = + member === "alloc" + ? "buffer.alloc" + : member === "allocUnsafe" + ? "buffer.allocUnsafe" + : "buffer.allocUnsafeSlow"; + const fresh: IrExpr = { kind: "libCall", fn, args: [size], type: BYTES_U8, loc }; if (args.length === 1) return fresh; // alloc(size, fill, encoding?): the fill semantics ARE fill()'s — // the fresh buffer is the receiver of a whole-range fill. diff --git a/packages/compiler/src/frontend/lowering/lower-calls.ts b/packages/compiler/src/frontend/lowering/lower-calls.ts index 07322cd06..1020b6160 100644 --- a/packages/compiler/src/frontend/lowering/lower-calls.ts +++ b/packages/compiler/src/frontend/lowering/lower-calls.ts @@ -548,6 +548,30 @@ export function lowerCall(lowerer: Lowerer, expr: ts.CallExpression): IrExpr { ts.isArrayLiteralExpression(tr.initializer) && tr.initializer.elements.length > 0 ) { + // An ArrayBuffer first member may be a Buffer pool slab, which + // Node reports as untransferable. + const first = tr.initializer.elements[0]!; + const firstType = ts.isSpreadElement(first) ? undefined : lowerer.typeOf(first); + const parts = !firstType + ? [] + : firstType.isUnionType() + ? ts.constituentTypes(firstType) + : [firstType]; + const isStorage = + parts.length > 0 && + parts.every((part) => { + const name = part.getSymbol()?.name; + return name === "ArrayBuffer" || name === "SharedArrayBuffer"; + }); + if (isStorage) { + return { + kind: "libCall", + fn: "dyn.cloneTransferFailWith", + args: [toDynArg(first)], + type: DYN, + loc, + }; + } return { kind: "libCall", fn: "dyn.cloneTransferFail", args: [], type: DYN, loc }; } } diff --git a/packages/compiler/src/frontend/lowering/lower-exprs.ts b/packages/compiler/src/frontend/lowering/lower-exprs.ts index f5ecbe5c9..740dcbcd5 100644 --- a/packages/compiler/src/frontend/lowering/lower-exprs.ts +++ b/packages/compiler/src/frontend/lowering/lower-exprs.ts @@ -4903,6 +4903,8 @@ export function lowerIntrinsicProperty( const numberMember = lowerer.stdlibGlobalMember(expr, "Number"); if (numberMember === "parseInt" || numberMember === "parseFloat") return lowerNumberParserValue(lowerer, numberMember, locOf(expr)); + if (lowerer.stdlibGlobalMember(expr, "Buffer") === "poolSize") + return { kind: "libCall", fn: "buffer.poolSize", args: [], type: F64, loc: locOf(expr) }; if (lowerer.stdlibGlobalMember(expr, "Buffer") === "isBuffer") return lowerCheckedPredicateValue(lowerer, "Buffer.isBuffer", "buffer", locOf(expr)); if (lowerer.stdlibGlobalMember(expr, "Date") === "now") diff --git a/packages/compiler/src/frontend/lowering/lower-stmts.ts b/packages/compiler/src/frontend/lowering/lower-stmts.ts index 303035891..4d34496d5 100644 --- a/packages/compiler/src/frontend/lowering/lower-stmts.ts +++ b/packages/compiler/src/frontend/lowering/lower-stmts.ts @@ -6354,6 +6354,18 @@ export function lowerExprStatement(lowerer: Lowerer, expr: ts.Expression): IrStm } } } + if ( + !expr.left.questionDotToken && + lowerer.stdlibGlobalMember(expr.left, "Buffer") === "poolSize" + ) { + const loc = locOf(expr); + const value = lowerer.lowerExprExpecting(expr.right, F64); + return { + kind: "exprStmt", + expr: { kind: "libCall", fn: "buffer.setPoolSize", args: [value], type: VOID, loc }, + loc, + }; + } if ( !expr.left.questionDotToken && lowerer.stdlibGlobalMember(expr.left, "process") === "exitCode" diff --git a/packages/compiler/src/ir/builtin-effects.ts b/packages/compiler/src/ir/builtin-effects.ts index 7801d4be8..9cb5db683 100644 --- a/packages/compiler/src/ir/builtin-effects.ts +++ b/packages/compiler/src/ir/builtin-effects.ts @@ -454,6 +454,7 @@ export const MAY_THROW_LIB_FNS: ReadonlySet = new Set([ "dyn.structuredClone", "dyn.cloneMissing", "dyn.cloneTransferFail", + "dyn.cloneTransferFailWith", // the dyn Object walks throw on null/undefined receivers "dyn.objKeys", "dyn.forInKeys", @@ -630,6 +631,14 @@ export const MAY_THROW_LIB_FNS: ReadonlySet = new Set([ "buffer.concatLen", "buffer.fromDyn", "buffer.fromStrChecked", + "buffer.alloc", + "buffer.allocUnsafe", + "buffer.allocUnsafeSlow", + "buffer.poolFromStr", + "buffer.poolFromStrChecked", + "buffer.poolFromBytes", + "buffer.poolFromArray", + "buffer.poolFromDyn", // The checked-dynamic compare/equals validators: Node's argument // ladders throw ERR_INVALID_ARG_TYPE / ERR_OUT_OF_RANGE catchably. "buffer.compareChk", diff --git a/packages/compiler/src/ir/builtin-signatures.ts b/packages/compiler/src/ir/builtin-signatures.ts index afc919a02..80ccf9a3e 100644 --- a/packages/compiler/src/ir/builtin-signatures.ts +++ b/packages/compiler/src/ir/builtin-signatures.ts @@ -2435,6 +2435,22 @@ export const LIB_FN_SIGS = defineLibFnSignatures({ * opaque reference inputs retain explicit runtime refusals. */ "buffer.fromDyn": { argTypes: [DYN, STRING], result: BYTES_U8 }, "buffer.concat": { argTypes: [arrayOf(BYTES_U8)], result: BYTES_U8 }, + /** The pooled Buffer factories (Node's allocate/fromStringFast/ + * fromArrayLike): allocUnsafe, Buffer.from of strings, Uint8Arrays, + * number[] and checked values, and concat return views into the + * per-thread pool slab below Buffer.poolSize >>> 1 bytes. alloc and + * allocUnsafeSlow never pool. The size forms validate like Node + * (ERR_OUT_OF_RANGE). poolSize reads and writes Buffer.poolSize. */ + "buffer.alloc": { argTypes: [F64], result: BYTES_U8 }, + "buffer.allocUnsafe": { argTypes: [F64], result: BYTES_U8 }, + "buffer.allocUnsafeSlow": { argTypes: [F64], result: BYTES_U8 }, + "buffer.poolFromStr": { argTypes: [STRING, STRING], result: BYTES_U8 }, + "buffer.poolFromStrChecked": { argTypes: [STRING, STRING], result: BYTES_U8 }, + "buffer.poolFromBytes": { argTypes: [BYTES_U8], result: BYTES_U8 }, + "buffer.poolFromArray": { argTypes: [arrayOf(F64)], result: BYTES_U8 }, + "buffer.poolFromDyn": { argTypes: [DYN, STRING], result: BYTES_U8 }, + "buffer.poolSize": { argTypes: [], result: F64 }, + "buffer.setPoolSize": { argTypes: [F64], result: VOID }, /** Buffer.byteLength(string, enc) — enc a NORMALIZED literal like * fromStr's — and Buffer.isEncoding(name) over a runtime string * (case-insensitive against Node's alias set). Pure; never throw. */ @@ -3082,6 +3098,9 @@ export const LIB_FN_SIGS = defineLibFnSignatures({ * lowered directly (the list's values need no dyn representation to * fail). */ "dyn.cloneTransferFail": { argTypes: [], result: DYN }, + /** The same refusal given the transfer list's first member, an + * ArrayBuffer: a Buffer pool slab gets Node's untransferable message. */ + "dyn.cloneTransferFailWith": { argTypes: [DYN], result: DYN }, /** `new RegExp(pattern, flags?)` (scr_regex.c): a heap regex over the * same libregexp engine the literals use. The pattern compiles EAGERLY * so an invalid pattern (or an unknown flag letter) throws Node's diff --git a/packages/compiler/test/bytes-element-emission.test.ts b/packages/compiler/test/bytes-element-emission.test.ts index d3faa83c9..ee099c496 100644 --- a/packages/compiler/test/bytes-element-emission.test.ts +++ b/packages/compiler/test/bytes-element-emission.test.ts @@ -271,7 +271,7 @@ test("LLVM emission performs typed-array element access directly on the valid pa const mod = fixture(); expect(validateModule(mod)).toEqual([]); const ll = emitLlvmModule(mod); - expect(ll).toContain("%ScrBytes = type { i64, i64, i32, ptr, ptr, i8, i8, i8, ptr }"); + expect(ll).toContain("%ScrBytes = type { i64, i64, i32, ptr, ptr, i8, i8, i8, i8, ptr }"); expect(ll).toContain("getelementptr inbounds i8"); expect(ll).toContain("getelementptr inbounds i32"); expect(ll).toContain("getelementptr inbounds float"); diff --git a/packages/compiler/test/llvm-runtime-abi.test.ts b/packages/compiler/test/llvm-runtime-abi.test.ts index 167c77565..9a7ccbb2f 100644 --- a/packages/compiler/test/llvm-runtime-abi.test.ts +++ b/packages/compiler/test/llvm-runtime-abi.test.ts @@ -345,7 +345,7 @@ describe("LLVM backend declares match scr_runtime.h prototypes", () => { #include #include "scr_runtime.h" -/* LLVM emits: %ScrBytes = type { i64, i64, i32, ptr, ptr, i8, i8, i8, ptr }. +/* LLVM emits: %ScrBytes = type { i64, i64, i32, ptr, ptr, i8, i8, i8, i8, ptr }. * Keep every ABI fact used by its field-index GEPs explicit here. */ _Static_assert(sizeof(size_t) == 8, "LLVM ScrBytes expects 64-bit size_t"); _Static_assert(sizeof(void *) == 8, "LLVM ScrBytes expects 64-bit pointers"); @@ -360,6 +360,7 @@ _Static_assert(offsetof(ScrBytes, backing) == 32, "LLVM ScrBytes.backing offset _Static_assert(offsetof(ScrBytes, is_buffer) == 40, "LLVM ScrBytes.is_buffer offset changed"); _Static_assert(offsetof(ScrBytes, is_data_view) == 41, "LLVM ScrBytes.is_data_view offset changed"); _Static_assert(offsetof(ScrBytes, external) == 42, "LLVM ScrBytes.external offset changed"); +_Static_assert(offsetof(ScrBytes, untransferable) == 43, "LLVM ScrBytes.untransferable offset changed"); _Static_assert(offsetof(ScrBytes, shared) == 48, "LLVM ScrBytes.shared offset changed"); `, ); diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5530-buffer-pool.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5530-buffer-pool.ts.json new file mode 100644 index 000000000..721a0813d --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5530-buffer-pool.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/5530-buffer-pool.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5531-buffer-pool-first-slab.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5531-buffer-pool-first-slab.ts.json new file mode 100644 index 000000000..cf1152362 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5531-buffer-pool-first-slab.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/5531-buffer-pool-first-slab.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5532-buffer-pool-first-slab-untyped.mjs.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5532-buffer-pool-first-slab-untyped.mjs.json new file mode 100644 index 000000000..0273a505e --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5532-buffer-pool-first-slab-untyped.mjs.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/5532-buffer-pool-first-slab-untyped.mjs" + ], + "diags": [] +} diff --git a/packages/runtime/src/scr_bytes.c b/packages/runtime/src/scr_bytes.c index e6085b4de..8c699e3ff 100644 --- a/packages/runtime/src/scr_bytes.c +++ b/packages/runtime/src/scr_bytes.c @@ -132,6 +132,7 @@ ScrBytes *scr_bytes_take_data(uint8_t *data, size_t len) { b->is_buffer = false; b->is_data_view = false; b->external = false; + b->untransferable = false; b->shared = NULL; #ifdef SCR_RC_AUDIT scr_live_bytes++; @@ -654,6 +655,7 @@ ScrBytes *scr_bytes_subarray(ScrBytes *b, double start, double end) { v->is_buffer = b->is_buffer; v->is_data_view = false; v->external = false; + v->untransferable = false; v->shared = owner->shared; #ifdef SCR_RC_AUDIT scr_live_bytes++; @@ -890,6 +892,7 @@ ScrBytes *scr_dataview_new(ScrBytes *src, double byte_off, bool has_len, double v->is_buffer = false; v->is_data_view = true; v->external = false; + v->untransferable = false; v->shared = owner->shared; #ifdef SCR_RC_AUDIT scr_live_bytes++; @@ -2478,14 +2481,30 @@ static const int8_t scr_b64_values[256] = { /* Conversion destinations are private until their initialized length is set. * General typed-array allocation must remain zero-filled. */ -static ScrBytes *scr_bytes_conversion_storage(size_t capacity) { +/* Where scr_bytes_encode_str writes: room bytes at `at` in the current pool + * slab, or (at == NULL, or more than room needed) fresh private storage. */ +typedef struct { + uint8_t *at; + size_t room; + ScrBytes header; /* the temporary view handed to the encoders */ +} ScrBytesSink; + +static ScrBytes *scr_bytes_conversion_storage_in(ScrBytesSink *sink, size_t capacity) { + if (sink && sink->at && capacity <= sink->room) { + sink->header = (ScrBytes){.rc = 1, .len = 0, .elem = SCR_BYTES_U8, .data = sink->at}; + return &sink->header; + } uint8_t *data = malloc(capacity ? capacity : 1); if (!data) scr_bytes_oom(); return scr_bytes_take_data(data, 0); } -static ScrBytes *scr_bytes_decode_b64(const uint8_t *in, size_t n) { - ScrBytes *b = scr_bytes_conversion_storage(n / 4 * 3 + 2); +static ScrBytes *scr_bytes_conversion_storage(size_t capacity) { + return scr_bytes_conversion_storage_in(NULL, capacity); +} + +static ScrBytes *scr_bytes_decode_b64(ScrBytesSink *sink, const uint8_t *in, size_t n) { + ScrBytes *b = scr_bytes_conversion_storage_in(sink, n / 4 * 3 + 2); size_t i = 0, o = 0; unsigned acc = 0, have = 0; while (i < n) { @@ -2524,13 +2543,16 @@ static ScrBytes *scr_bytes_decode_b64(const uint8_t *in, size_t n) { return b; } -ScrBytes *scr_bytes_from_str(const ScrStr *s, const ScrStr *enc) { +/* Encodes s into sink's pool room when its upper bound fits there, else into + * fresh private storage. The result's len is the bytes written; a pool + * result is &sink->header, never released. */ +static ScrBytes *scr_bytes_encode_str(ScrBytesSink *sink, const ScrStr *s, const ScrStr *enc) { const uint8_t *in = (const uint8_t *)s->data; size_t n = s->len; if (scr_enc_is(enc, "latin1") || scr_enc_is(enc, "ascii")) { /* Node writes charCodeAt(i) & 0xFF for BOTH spellings — an astral * code point contributes its two surrogates' low bytes. */ - ScrBytes *b = scr_bytes_conversion_storage(n); + ScrBytes *b = scr_bytes_conversion_storage_in(sink, n); size_t o = 0, i = 0; while (i < n) { uint32_t cp = scr_bytes_next_cp(in, &i); @@ -2547,7 +2569,7 @@ ScrBytes *scr_bytes_from_str(const ScrStr *s, const ScrStr *enc) { } if (scr_enc_is(enc, "utf16le")) { if (n > SIZE_MAX / 2) scr_bytes_oom(); - ScrBytes *b = scr_bytes_conversion_storage(n * 2); + ScrBytes *b = scr_bytes_conversion_storage_in(sink, n * 2); size_t o = 0, i = 0; while (i < n) { uint32_t cp = scr_bytes_next_cp(in, &i); @@ -2573,7 +2595,7 @@ ScrBytes *scr_bytes_from_str(const ScrStr *s, const ScrStr *enc) { ScrBytes *narrowed = NULL; if (scr_bytes_ascii_prefix(in, n) != n) { ScrStr *latin1 = scr_str_new("latin1", 6); - narrowed = scr_bytes_from_str(s, latin1); + narrowed = scr_bytes_encode_str(NULL, s, latin1); scr_str_release(latin1); in = narrowed->data; n = narrowed->len; @@ -2581,7 +2603,7 @@ ScrBytes *scr_bytes_from_str(const ScrStr *s, const ScrStr *enc) { ScrBytes *b; if (hex) { /* Stop at the first invalid pair, discarding an odd tail. */ - b = scr_bytes_conversion_storage(n / 2); + b = scr_bytes_conversion_storage_in(sink, n / 2); size_t o = 0; for (size_t i = 0; i + 1 < n; i += 2) { int hi = scr_hex_val(in[i]), lo = scr_hex_val(in[i + 1]); @@ -2590,20 +2612,352 @@ ScrBytes *scr_bytes_from_str(const ScrStr *s, const ScrStr *enc) { } b->len = o; } else { - b = scr_bytes_decode_b64(in, n); + b = scr_bytes_decode_b64(sink, in, n); } scr_bytes_release(narrowed); return b; } /* utf8: ScrStr storage IS the bytes, once lone surrogates become U+FFFD * (same length) */ - ScrBytes *b = scr_bytes_conversion_storage(n); + ScrBytes *b = scr_bytes_conversion_storage_in(sink, n); memcpy(b->data, in, n); if (!s->ascii) scr_wtf8_to_utf8((char *)b->data, n); b->len = n; return b; } +ScrBytes *scr_bytes_from_str(const ScrStr *s, const ScrStr *enc) { + return scr_bytes_encode_str(NULL, s, enc); +} + +/* ── the Buffer pool ──────────────────────────────────────────────────── + * Node's lib/buffer.js: Buffer.poolSize starts at 64 KiB, and the first + * slab exists from startup with that size. A pooled request (size < + * Buffer.poolSize >>> 1) that does not fit the current slab's remainder + * starts a new slab of Buffer.poolSize bytes; a slab created later uses the + * value at that time. Views start at 8-byte aligned offsets. The pool holds + * one reference to the current slab and each view another, so a slab is + * freed once it is replaced and its last view is released (Node keeps a + * slab alive the same way). */ +#define SCR_BUFFER_POOL_DEFAULT 65536.0 +size_t scr_num_received(double v, char out[48]); +static SCR_TL double scr_pool_setting = SCR_BUFFER_POOL_DEFAULT; +static SCR_TL ScrBytes *scr_pool_slab; +static SCR_TL size_t scr_pool_offset; +static SCR_TL bool scr_pool_started; +static SCR_TL bool scr_pool_registered; /* scr_buffer_pool_reset runs at context exit */ + +/* Buffer.poolSize >>> 1, kept with the setting. */ +static SCR_TL size_t scr_pool_half = 32768; + +double scr_buffer_pool_size(void) { return scr_pool_setting; } + +void scr_buffer_set_pool_size(double size) { + scr_pool_setting = size; + double t = isfinite(size) ? fmod(trunc(size), 4294967296.0) : 0; + if (t < 0) t += 4294967296.0; + scr_pool_half = (size_t)((uint32_t)t >> 1); +} + +static size_t scr_pool_threshold(void) { return scr_pool_half; } + +/* Bytes left in the slab. Aligning can move the offset past the end of a + * slab whose size is not a multiple of 8; nothing is left then. */ +static size_t scr_pool_room(void) { + if (!scr_pool_slab || scr_pool_offset >= scr_pool_slab->len) return 0; + return scr_pool_slab->len - scr_pool_offset; +} + +/* Replaces the slab with one of `size` bytes (Node's createPool). false + * with a pending RangeError when size is not a valid typed array length. */ +static bool scr_pool_create(double size) { + ScrBytes *slab = scr_bytes_new(SCR_BYTES_U8, size); + if (!slab) return false; + slab->untransferable = true; + if (!scr_pool_registered) { + scr_atexit(scr_buffer_pool_reset); + scr_pool_registered = true; + } + scr_bytes_release(scr_pool_slab); + scr_pool_slab = slab; + scr_pool_offset = 0; + scr_pool_started = true; + return true; +} + +/* Ensures the current slab holds `reserve` more bytes (Node's `size > + * poolSize - poolOffset` test before createPool). Node creates the first + * slab at startup with the default size, so a request it holds goes there + * even after Buffer.poolSize changed, and one it does not hold replaces it + * with a slab of the current Buffer.poolSize. Callers write `reserve` + * bytes at the pool offset, so the room is checked again after every new + * slab: a pooled request is below Buffer.poolSize >>> 1, which a slab of + * Buffer.poolSize bytes always holds, and anything else is an internal + * error rather than a write past the slab. */ +static bool scr_pool_reserve(size_t reserve) { + if (scr_pool_started && reserve <= scr_pool_room()) return true; + bool startup = !scr_pool_started && (double)reserve <= SCR_BUFFER_POOL_DEFAULT; + if (!scr_pool_create(startup ? SCR_BUFFER_POOL_DEFAULT : scr_pool_setting)) return false; + if (reserve > scr_pool_room()) scr_trap("scriptc: internal error: Buffer pool slab smaller than its reservation\n"); + return true; +} + +/* A +1 Buffer view of `len` bytes at the pool offset, which then advances + * past it to the next 8-byte boundary (Node's alignPool). */ +static ScrBytes *scr_pool_take(size_t len) { + ScrBytes *v = malloc(sizeof(ScrBytes)); + if (!v) scr_bytes_oom(); + v->rc = 1; + v->len = len; + v->elem = SCR_BYTES_U8; + v->data = scr_pool_slab->data + scr_pool_offset; + v->backing = scr_bytes_retain(scr_pool_slab); + v->is_buffer = true; + v->is_data_view = false; + v->external = false; + v->untransferable = false; + v->shared = NULL; +#ifdef SCR_RC_AUDIT + scr_live_bytes++; +#endif + scr_pool_offset += len; + if (scr_pool_offset & 7) scr_pool_offset = (scr_pool_offset | 7) + 1; + return v; +} + +/* An empty Buffer (Node's new FastBuffer()): its own zero-length buffer. */ +static ScrBytes *scr_buffer_empty(void) { + ScrBytes *b = scr_bytes_alloc(SCR_BYTES_U8, 0); + b->is_buffer = true; + return b; +} + +/* validateNumber(size, 'size', 0, kMaxLength). */ +static bool scr_buffer_check_size(double size) { + if (size >= 0 && size <= 9007199254740991.0) return true; + char recv[48], msg[160]; + scr_num_received(size, recv); + int len = snprintf(msg, sizeof msg, + "The value of \"size\" is out of range. It must be >= 0 && <= 9007199254740991. " + "Received %s", + recv); + scr_throw_error_msg_code(SCR_ERR_RANGE, msg, (size_t)len, "ERR_OUT_OF_RANGE"); + return false; +} + +ScrBytes *scr_buffer_alloc(double size) { + if (!scr_buffer_check_size(size)) return NULL; + /* Sizes past 2^31 keep scr_bytes_new's typed array length rules. */ + ScrBytes *b = size < 2147483648.0 ? scr_bytes_alloc(SCR_BYTES_U8, (size_t)size) + : scr_bytes_new(SCR_BYTES_U8, size); + if (b) b->is_buffer = true; + return b; +} + +ScrBytes *scr_buffer_alloc_unsafe_slow(double size) { return scr_buffer_alloc(size); } + +/* Node's allocate(size) for a validated size. */ +static ScrBytes *scr_buffer_allocate(double size) { + if (!(size >= 1)) return scr_buffer_empty(); + if (size < (double)scr_pool_threshold()) { + size_t n = (size_t)size; + if (!scr_pool_reserve(n)) return NULL; + return scr_pool_take(n); + } + return scr_buffer_alloc(size); +} + +ScrBytes *scr_buffer_alloc_unsafe(double size) { + if (!scr_buffer_check_size(size)) return NULL; + return scr_buffer_allocate(size); +} + +/* Node's fromArrayLike: the values of a typed array or number[] (ToUint8). */ +ScrBytes *scr_buffer_from_bytes(const ScrBytes *src) { + size_t n = src->len; + if (n == 0) return scr_buffer_empty(); + if (n < scr_pool_threshold()) { + if (!scr_pool_reserve(n)) return NULL; + ScrBytes *v = scr_pool_take(n); + if (src->elem == SCR_BYTES_U8 || src->elem == SCR_BYTES_U8C || src->elem == SCR_BYTES_I8) { + scr_bytes_raw_in(src, v->data, src->data, n); + } else { + for (size_t i = 0; i < n; i++) v->data[i] = (uint8_t)scr_to_uint32(scr_bytes_get(src, (double)i)); + } + return v; + } + ScrBytes *b = scr_bytes_convert(SCR_BYTES_U8, src); + b->is_buffer = true; + return b; +} + +static void scr_bytes_fill_from_arr(uint8_t *out, ScrBytesElem elem, const ScrArr *arr); + +ScrBytes *scr_buffer_from_arr(const ScrArr *arr) { + size_t n = arr->len; + if (n == 0) return scr_buffer_empty(); + if (n < scr_pool_threshold()) { + /* Converting numbers runs no user code, so the values are written + * straight into the slab. */ + if (!scr_pool_reserve(n)) return NULL; + ScrBytes *v = scr_pool_take(n); + scr_bytes_fill_from_arr(v->data, SCR_BYTES_U8, arr); + return v; + } + ScrBytes *b = scr_bytes_from_arr(SCR_BYTES_U8, arr); + b->is_buffer = true; + return b; +} + +double scr_bytes_byte_length_str(ScrStr *s, const ScrStr *enc); + +/* Node's createFromString: an unpooled Buffer of the encoding's byte + * length; when fewer bytes are written it views the start of that + * ArrayBuffer. */ +static ScrBytes *scr_buffer_from_str_unpooled(const ScrStr *s, const ScrStr *enc, size_t length) { + ScrBytes *b = scr_bytes_encode_str(NULL, s, enc); + b->is_buffer = true; + if (b->len >= length) return b; + /* The encoders' storage bound covers the byte length. */ + size_t actual = b->len; + memset(b->data + actual, 0, length - actual); + b->len = length; + ScrBytes *view = scr_bytes_subarray(b, 0, (double)actual); + scr_bytes_release(b); + return view; +} + +/* Node's fromString/fromStringFast for a normalized encoding name. */ +ScrBytes *scr_buffer_from_str(const ScrStr *s, const ScrStr *enc) { + if (s->len == 0) return scr_buffer_empty(); + size_t max = scr_pool_threshold(); + /* The UTF-16 length lies between len / 3 and len (each unit is one to + * three stored bytes; an astral character's four bytes are two units). + * Node's reservation is four bytes per unit; when four bytes per stored + * byte already fits the threshold and the slab, the exact length cannot + * change the outcome. */ + size_t units; + if (s->ascii) { + units = s->len; + } else if (s->len < max / 4 && scr_pool_started && s->len * 4 <= scr_pool_room()) { + units = s->len; /* an upper bound: reserve() succeeds without a new slab */ + } else { + units = (size_t)scr_str_utf16_len((ScrStr *)s); + } + if (units >= max) + return scr_buffer_from_str_unpooled(s, enc, (size_t)scr_bytes_byte_length_str((ScrStr *)s, enc)); + size_t length = units * 4; + if (length >= max) { + length = (size_t)scr_bytes_byte_length_str((ScrStr *)s, enc); + if (length >= max) return scr_buffer_from_str_unpooled(s, enc, length); + } + if (!scr_pool_reserve(length)) return NULL; + ScrBytesSink sink = {scr_pool_slab->data + scr_pool_offset, scr_pool_room()}; + ScrBytes *b = scr_bytes_encode_str(&sink, s, enc); + size_t actual = b->len; + if (b != &sink.header) { + /* The encoder's storage bound exceeded the room; the bytes written + * never exceed Node's reservation, which fits. */ + memcpy(scr_pool_slab->data + scr_pool_offset, b->data, actual); + scr_bytes_release(b); + } + return scr_pool_take(actual); +} + +ScrBytes *scr_buffer_from_str_checked(const ScrStr *s, const ScrStr *enc) { + ScrStr *normalized = enc->len ? scr_bytes_normalize_encoding(enc) : scr_str_new("utf8", 4); + if (!normalized) return NULL; + ScrBytes *out = scr_buffer_from_str(s, normalized); + scr_str_release(normalized); + return out; +} + +static ScrBytes *scr_bytes_concat_item(const ScrArr *list, size_t i); + +/* Buffer.concat(list[, totalLength]) into Node's allocate(totalLength). */ +static ScrBytes *scr_buffer_concat_into(const ScrArr *list, double total) { + ScrBytes *out = scr_buffer_allocate(total); + if (!out) return NULL; + size_t pos = 0; + for (size_t i = 0; i < list->len && pos < out->len; i++) { + ScrBytes *part = scr_bytes_concat_item(list, i); + if (!part) { + scr_bytes_release(out); + return NULL; + } + size_t take = part->len < out->len - pos ? part->len : out->len - pos; + scr_bytes_read(part, 0, out->data + pos, take); + pos += take; + scr_bytes_release(part); + } + if (pos < out->len) memset(out->data + pos, 0, out->len - pos); + return out; +} + +ScrBytes *scr_buffer_concat(const ScrArr *list) { + if (list->len == 0) return scr_buffer_empty(); + size_t total = 0; + bool borrowed = true; + for (size_t i = 0; i < list->len; i++) { + /* Present parts are read borrowed; copying runs no script code. */ + const ScrBytes *part = scr_arr_has(list, (double)i) ? scr_arr_peek_ref(list, (double)i) : NULL; + if (!part) { + borrowed = false; + break; + } + total += part->len; + } + if (!borrowed) { + total = 0; + for (size_t i = 0; i < list->len; i++) { + ScrBytes *part = scr_bytes_concat_item(list, i); + if (!part) return NULL; + total += part->len; + scr_bytes_release(part); + } + return scr_buffer_concat_into(list, (double)total); + } + ScrBytes *out = scr_buffer_allocate((double)total); + if (!out) return NULL; + size_t pos = 0; + for (size_t i = 0; i < list->len; i++) { + const ScrBytes *part = scr_arr_peek_ref(list, (double)i); + scr_bytes_read(part, 0, out->data + pos, part->len); + pos += part->len; + } + return out; +} + +ScrBytes *scr_buffer_concat_len(const ScrArr *list, double total) { + /* An empty list returns before the total validates, as in Node. */ + if (list->len == 0) return scr_buffer_empty(); + if (!scr_bytes_validate_off("length", total, 9007199254740991.0)) return NULL; + return scr_buffer_concat_into(list, total); +} + +/* Moves an unpooled array-like copy into the pool when it is small enough + * (Node's fromArrayLike). Consumes `b`. */ +ScrBytes *scr_buffer_pool_adopt(ScrBytes *b) { + size_t n = b->len; + if (b->backing || b->shared || n == 0 || n >= scr_pool_threshold()) return b; + if (!scr_pool_reserve(n)) { + scr_bytes_release(b); + return NULL; + } + ScrBytes *v = scr_pool_take(n); + memcpy(v->data, b->data, n); + scr_bytes_release(b); + return v; +} + +void scr_buffer_pool_reset(void) { + scr_bytes_release(scr_pool_slab); + scr_pool_slab = NULL; + scr_pool_offset = 0; + scr_pool_started = false; + scr_buffer_set_pool_size(SCR_BUFFER_POOL_DEFAULT); +} + /* Runtime encoding variables share the output conversion alias/error table. * Buffer.from treats an empty encoding as utf8, even for an empty source. */ ScrBytes *scr_bytes_from_str_checked(const ScrStr *s, const ScrStr *enc) { @@ -2616,6 +2970,12 @@ ScrBytes *scr_bytes_from_str_checked(const ScrStr *s, const ScrStr *enc) { ScrBytes *scr_bytes_from_arr(ScrBytesElem elem, const ScrArr *arr) { ScrBytes *b = scr_bytes_alloc_private(elem, arr->len); + scr_bytes_fill_from_arr(b->data, elem, arr); + return b; +} + +/* Writes arr's values, converted to `elem`, at `out` (arr->len elements). */ +static void scr_bytes_fill_from_arr(uint8_t *out, ScrBytesElem elem, const ScrArr *arr) { size_t width = scr_bytes_elem_size(elem); double values[128]; for (size_t start = 0; start < arr->len;) { @@ -2630,10 +2990,9 @@ ScrBytes *scr_bytes_from_arr(ScrBytesElem elem, const ScrArr *arr) { else values[i] = NAN; } else values[i] = scr_arr_get_number(arr, (double)index); } - scr_bytes_write_numbers(b->data + start * width, elem, values, count); + scr_bytes_write_numbers(out + start * width, elem, values, count); start += count; } - return b; } /* ── equals / compare / indexOf / fill / copy / swap / write (u8; the diff --git a/packages/runtime/src/scr_bytes.h b/packages/runtime/src/scr_bytes.h index 69fb7dd94..5050e3a1b 100644 --- a/packages/runtime/src/scr_bytes.h +++ b/packages/runtime/src/scr_bytes.h @@ -54,6 +54,7 @@ typedef struct ScrBytes { bool is_buffer; /* Buffer brand belongs to the view, not its backing. */ bool is_data_view; /* DataView is distinct from a numeric typed array. */ bool external; /* Root aliases foreign memory; releasing it never frees data. */ + bool untransferable; /* A Buffer pool slab: its ArrayBuffer cannot be transferred. */ ScrSharedBytes *shared; /* Neutral backing, borrowed by views from their local owner. */ } ScrBytes; @@ -334,6 +335,30 @@ ScrStr *scr_text_decode_legacy(const ScrBytes *b, double encoding); * "utf16le" writes LE code units, surrogate pairs included. Borrows * both; +1 u8 result. Never throws. */ ScrBytes *scr_bytes_from_str(const ScrStr *s, const ScrStr *enc); +/* The Buffer pool (Node's lib/buffer.js allocate/fromStringFast/ + * fromArrayLike): Buffer.allocUnsafe, Buffer.from of strings, arrays and + * array-likes, and Buffer.concat return views into a per-thread slab of + * Buffer.poolSize bytes when the size is below poolSize >>> 1. Views start + * at 8-byte aligned offsets; a request that does not fit starts a new + * slab. A slab lives while any of its views does. All return +1 Buffers. */ +double scr_buffer_pool_size(void); +void scr_buffer_set_pool_size(double size); +ScrBytes *scr_buffer_alloc(double size); /* zeroed, never pooled */ +ScrBytes *scr_buffer_alloc_unsafe(double size); /* pooled */ +ScrBytes *scr_buffer_alloc_unsafe_slow(double size); /* never pooled */ +ScrBytes *scr_buffer_from_str(const ScrStr *s, const ScrStr *enc); +ScrBytes *scr_buffer_from_str_checked(const ScrStr *s, const ScrStr *enc); +ScrBytes *scr_buffer_from_bytes(const ScrBytes *src); /* a copy of any typed array's values */ +ScrBytes *scr_buffer_from_arr(const ScrArr *arr); /* number[] */ +ScrBytes *scr_buffer_from_dyn_pooled(const ScrDyn *value, const ScrStr *encoding); +ScrBytes *scr_buffer_concat(const ScrArr *list); +ScrBytes *scr_buffer_concat_len(const ScrArr *list, double total); +/* Drops the pool's slab reference and restores Buffer.poolSize (process + * exit audits and library session resets). */ +void scr_buffer_pool_reset(void); +/* A pooled copy of an unpooled u8 Buffer below the pool threshold; others + * are returned as they are. Consumes `b`. */ +ScrBytes *scr_buffer_pool_adopt(ScrBytes *b); ScrBytes *scr_bytes_from_str_checked(const ScrStr *s, const ScrStr *enc); /* Buffer.byteLength(string, enc) — enc NORMALIZED like from_str — and diff --git a/packages/runtime/src/scr_bytes.test.c b/packages/runtime/src/scr_bytes.test.c index f06a29e5e..58f52fa63 100644 --- a/packages/runtime/src/scr_bytes.test.c +++ b/packages/runtime/src/scr_bytes.test.c @@ -83,3 +83,53 @@ void scr_bytes_test_bulk_storage(void) { filled_storage(); assert(scr_bytes_live_count() == before); } + +/* The Buffer pool: views into one slab at 8-byte aligned offsets, the + * threshold, a slab whose size is not a multiple of 8, the slab's lifetime + * through its views, and the reset that drops the pool's own reference. */ +void scr_bytes_test_buffer_pool(void) { + long before = scr_bytes_live_count(); + scr_buffer_pool_reset(); + ScrBytes *a = scr_buffer_alloc_unsafe(3); + ScrBytes *b = scr_buffer_alloc_unsafe(9); + ScrBytes *c = scr_buffer_alloc_unsafe(1); + assert(a->backing && a->backing == b->backing && b->backing == c->backing); + assert(a->backing->len == 65536 && a->backing->untransferable); + assert(scr_bytes_byte_offset(a) == 0 && scr_bytes_byte_offset(b) == 8 && + scr_bytes_byte_offset(c) == 24); + ScrBytes *slab = scr_bytes_retain(a->backing); + ScrBytes *big = scr_buffer_alloc_unsafe(32768); + assert(big && !big->backing && big->len == 32768); + ScrBytes *edge = scr_buffer_alloc_unsafe(32767); + assert(edge->backing == slab); + + /* A 60-byte slab: after offset 56 four bytes remain, then none. */ + scr_buffer_set_pool_size(60); + ScrBytes *t = scr_buffer_alloc_unsafe(29); + while (scr_bytes_byte_offset(t) != 0) { + scr_bytes_release(t); + t = scr_buffer_alloc_unsafe(29); + } + assert(t->backing->len == 60); + ScrBytes *u = scr_buffer_alloc_unsafe(20); + ScrStr *euro = scr_str_new("\xe2\x82\xac", 3), *utf16 = scr_str_new("utf16le", 7); + ScrBytes *w = scr_buffer_from_str(euro, utf16); + assert(w->backing == t->backing && scr_bytes_byte_offset(w) == 56 && w->len == 2); + assert(w->data[0] == 0xac && w->data[1] == 0x20); + ScrBytes *x = scr_buffer_alloc_unsafe(1); + assert(x->backing != t->backing && scr_bytes_byte_offset(x) == 0); + scr_buffer_set_pool_size(0); + ScrBytes *y = scr_buffer_alloc_unsafe(1); + assert(!y->backing); + + ScrBytes *views[] = {a, b, c, edge, t, u, w, x, y, big}; + for (size_t i = 0; i < sizeof views / sizeof views[0]; i++) scr_bytes_release(views[i]); + /* The first slab lives while a reference to it does. */ + assert(slab->rc == 1); + scr_bytes_release(slab); + scr_str_release(euro); + scr_str_release(utf16); + scr_buffer_pool_reset(); + assert(scr_buffer_pool_size() == 65536); + assert(scr_bytes_live_count() == before); +} diff --git a/packages/runtime/src/scr_bytes_io.c b/packages/runtime/src/scr_bytes_io.c index 0324842fa..d1453829f 100644 --- a/packages/runtime/src/scr_bytes_io.c +++ b/packages/runtime/src/scr_bytes_io.c @@ -1276,6 +1276,18 @@ ScrBytes *scr_buffer_from_dyn(const ScrDyn *value, const ScrStr *encoding) { return NULL; } +/* Buffer.from over a checked value, with Node's pooling: strings take + * fromStringFast, typed arrays and array-likes fromArrayLike. */ +ScrBytes *scr_buffer_from_dyn_pooled(const ScrDyn *value, const ScrStr *encoding) { + if (value->kind == SCR_DYN_STR) return scr_buffer_from_str(value->v.str, encoding); + if (value->kind == SCR_DYN_BYTES && !value->v.bytes->is_data_view) + return scr_buffer_from_bytes(value->v.bytes); + ScrBytes *out = scr_buffer_from_dyn(value, encoding); + if (!out) return NULL; + out->is_buffer = true; + return scr_buffer_pool_adopt(out); +} + /* ── the checked-dynamic Buffer compare/equals validators ────────────── * Node's argument ladders for buf.equals / buf.compare / Buffer.compare * over dyn-boxed arguments (the invalid-input probes: string needles, diff --git a/packages/runtime/src/scr_console.c b/packages/runtime/src/scr_console.c index 2d3cb956e..daab4e0bc 100644 --- a/packages/runtime/src/scr_console.c +++ b/packages/runtime/src/scr_console.c @@ -49,6 +49,7 @@ extern long scr_dyn_live_count(void); /* scr_json.c */ extern long scr_bytes_live_count(void); /* scr_bytes.c */ static void scr_rc_audit_at_exit(void) { + scr_buffer_pool_reset(); /* the pool's own slab reference */ /* Fibers suspended forever at loop exhaustion (awaits nobody resolves — * Node exits 0 there too) still hold their stacks and owned values by * design; a strict audit would only report that deliberate abandonment. */ diff --git a/packages/runtime/src/scr_dyn.h b/packages/runtime/src/scr_dyn.h index d3839b22d..e4a4adcf7 100644 --- a/packages/runtime/src/scr_dyn.h +++ b/packages/runtime/src/scr_dyn.h @@ -793,6 +793,9 @@ void scr_dyn_throw(ScrDyn *d); /* moves; preserves boxed native Error classifica ScrDyn *scr_structured_clone(const ScrDyn *value, const ScrDyn *options); ScrDyn *scr_structured_clone_missing(void); ScrDyn *scr_structured_clone_transfer_fail(void); /* always throws DataCloneError */ +/* The same, naming a Buffer pool slab's ArrayBuffer as Node does when it is + * the transfer list's first member (borrowed). */ +ScrDyn *scr_structured_clone_transfer_fail_with(const ScrDyn *first); /* Validates a structuredClone options dyn value (throws on failure — * callers must check scr_exc_pending). Borrowed, NULL-tolerant. */ void scr_sc_validate_options(const ScrDyn *options); diff --git a/packages/runtime/src/scr_json.c b/packages/runtime/src/scr_json.c index 0c53afeb6..d40ca9a9e 100644 --- a/packages/runtime/src/scr_json.c +++ b/packages/runtime/src/scr_json.c @@ -8325,8 +8325,13 @@ void scr_sc_validate_options(const ScrDyn *options) { } if (tr->v.arr.len > 0) { /* Nothing in the static world is transferable — Node's own error for - * a non-transferable list member. */ - scr_throw_domex("DataCloneError", "Found invalid value in transferList."); + * a non-transferable list member. A Buffer pool slab's ArrayBuffer is + * marked untransferable in Node, which reports it differently. */ + const ScrDyn *first = tr->v.arr.items[0]; + if (first && scr_array_buffer_is(first) && ((ScrBytes *)first->v.handle.ptr)->untransferable) + scr_throw_domex("DataCloneError", "Cannot transfer object of unsupported type."); + else + scr_throw_domex("DataCloneError", "Found invalid value in transferList."); } scr_dyn_release(transferView); } @@ -8368,8 +8373,19 @@ static ScrDyn *scr_sc_clone(const ScrDyn *v, const ScrScParent *up) { return scr_dyn_new_str(v->v.str); case SCR_DYN_BYTES: { /* A fresh byte copy; the Buffer flavor drops (Node: structuredClone - * of a Buffer answers a plain Uint8Array). */ - ScrBytes *copy = scr_bytes_copy(v->v.bytes); + * of a Buffer answers a plain Uint8Array). A typed array view over a + * larger ArrayBuffer, such as a pooled Buffer, clones the whole + * ArrayBuffer and keeps its offset and length, as Node does. */ + ScrBytes *src = v->v.bytes; + ScrBytes *copy; + if (src->backing && !src->is_data_view && !src->shared) { + ScrBytes *storage = scr_bytes_copy(src->backing); + copy = scr_bytes_buffer_view(storage, src->elem, scr_bytes_byte_offset(src), true, + (double)src->len); + scr_bytes_release(storage); + } else { + copy = scr_bytes_copy(src); + } ScrDyn *result = scr_dyn_new_bytes(copy); scr_bytes_release(copy); return result; @@ -8454,6 +8470,16 @@ ScrDyn *scr_structured_clone_transfer_fail(void) { return NULL; } +/* A literal transfer list whose first member is `first`: a Buffer pool + * slab's ArrayBuffer is untransferable in Node. Borrowed. */ +ScrDyn *scr_structured_clone_transfer_fail_with(const ScrDyn *first) { + if (scr_array_buffer_is(first) && ((ScrBytes *)first->v.handle.ptr)->untransferable) { + scr_throw_domex("DataCloneError", "Cannot transfer object of unsupported type."); + return NULL; + } + return scr_structured_clone_transfer_fail(); +} + ScrDyn *scr_structured_clone_missing(void) { /* Node's message, verbatim (its own template double-wraps the text). */ static const char msg[] = diff --git a/packages/runtime/test/test_bytes.c b/packages/runtime/test/test_bytes.c index 9b4b68ffc..841b9a2f6 100644 --- a/packages/runtime/test/test_bytes.c +++ b/packages/runtime/test/test_bytes.c @@ -26,6 +26,7 @@ long scr_bytes_live_count(void); static long total = 0, failed = 0; void scr_bytes_test_bulk_storage(void); +void scr_bytes_test_buffer_pool(void); static void check(bool ok, const char *what) { total++; @@ -405,6 +406,7 @@ int main(int argc, char **argv) { test_coercion_matrix(); test_slice_set_copy(); scr_bytes_test_bulk_storage(); + scr_bytes_test_buffer_pool(); test_encodings(); test_concat_and_u32be(); test_from_arr_and_random(); diff --git a/tests/corpus/5405-buffer-arraybuffer-window.ts b/tests/corpus/5405-buffer-arraybuffer-window.ts index ac3680fbd..df487ac58 100644 --- a/tests/corpus/5405-buffer-arraybuffer-window.ts +++ b/tests/corpus/5405-buffer-arraybuffer-window.ts @@ -1,8 +1,7 @@ -// Small Buffers own exact-size ArrayBuffers here, while Node allocates them -// from a shared 64 KiB pool, so buf.buffer.byteLength and buf.byteOffset -// differ. Reading a Buffer's window of its ArrayBuffer through byteOffset -// and length gives the same bytes either way, and Buffer.alloc is never -// pooled. +// Small Buffers from Buffer.from, Buffer.concat and Buffer.allocUnsafe are +// views into a shared pool slab. Reading a Buffer's window of its +// ArrayBuffer through byteOffset and length gives its own bytes, and +// Buffer.alloc is never pooled. const small = Buffer.from("hello"); const copy = small.buffer.slice(small.byteOffset, small.byteOffset + small.length); console.log(copy.byteLength, new TextDecoder().decode(new Uint8Array(copy))); diff --git a/tests/corpus/5530-buffer-pool.ts b/tests/corpus/5530-buffer-pool.ts new file mode 100644 index 000000000..e6f0c9772 --- /dev/null +++ b/tests/corpus/5530-buffer-pool.ts @@ -0,0 +1,137 @@ +// Buffer pooling: Buffer.from of strings, arrays and Uint8Arrays, +// Buffer.allocUnsafe and Buffer.concat return views into a shared slab of +// Buffer.poolSize bytes below poolSize >>> 1, at 8-byte aligned offsets; +// Buffer.alloc and allocUnsafeSlow own their storage. Every observation is +// recorded before printing: console output allocates from the pool too. +const rows: string[] = []; +const slabs: ArrayBufferLike[] = []; +function slabOf(b: Buffer): number { + const size = b.buffer.byteLength; + if (size !== 65536 && size !== 1024 && size !== 60) return -1; + let index = slabs.indexOf(b.buffer); + if (index < 0) { + slabs.push(b.buffer); + index = slabs.length - 1; + } + return index; +} +const note = (label: string, b: Buffer): void => { + rows.push(`${label}: length ${b.length}, buffer ${b.buffer.byteLength}, offset ${b.byteOffset}, slab ${slabOf(b)}`); +}; + +// Node's module loader allocates from the pool before the program runs. +// Start from a fresh slab: a 32767-byte request at offset 0 opens one, a +// second fills it, and the next request opens another. +let start = Buffer.allocUnsafe(32767); +while (start.byteOffset !== 0) start = Buffer.allocUnsafe(32767); +Buffer.allocUnsafe(32767); +rows.push(`poolSize ${Buffer.poolSize}`); +note("allocUnsafe(1)", Buffer.allocUnsafe(1)); +note("from('abc')", Buffer.from("abc")); +note("from('héllo')", Buffer.from("héllo")); +note("from('€€')", Buffer.from("€€")); +note("from('\\ud800abc')", Buffer.from("\ud800abc")); +note("allocUnsafe(3)", Buffer.allocUnsafe(3)); +note("allocUnsafe(8)", Buffer.allocUnsafe(8)); +note("allocUnsafe(9)", Buffer.allocUnsafe(9)); +note("from([1, 2, 3])", Buffer.from([1, 2, 3])); +const values = [256, -1, 1.9, NaN]; +note("from(number[])", Buffer.from(values)); +note("from(Uint8Array)", Buffer.from(new Uint8Array([7, 8]))); +note("from(Buffer)", Buffer.from(Buffer.from("copy"))); +note("from(hex)", Buffer.from("abcd", "hex")); +note("from(bad hex)", Buffer.from("abzz", "hex")); +note("from(base64)", Buffer.from("aGVsbG8=", "base64")); +note("from(latin1)", Buffer.from("hé", "latin1")); +note("from(utf16le)", Buffer.from("€", "utf16le")); +const encoding: BufferEncoding = process.argv.length > 99 ? "hex" : "utf8"; +note("from(runtime encoding)", Buffer.from("xyz", encoding)); +note("concat", Buffer.concat([Buffer.from("a"), Buffer.from("bc")])); +note("concat(total)", Buffer.concat([Buffer.from("ab"), Buffer.from("cd")], 7)); +note("concat([])", Buffer.concat([])); +note("from('')", Buffer.from("")); +note("allocUnsafe(0)", Buffer.allocUnsafe(0)); +note("alloc(8)", Buffer.alloc(8)); +note("alloc(8, 1)", Buffer.alloc(8, 1)); +note("allocUnsafeSlow(8)", Buffer.allocUnsafeSlow(8)); +note("subarray", Buffer.from("hello").subarray(1, 3)); +note("from(ArrayBuffer)", Buffer.from(new ArrayBuffer(8))); + +// Sizes at the pool threshold, slab rollover, and Buffer.poolSize. +note("from(32767 chars)", Buffer.from("a".repeat(32767))); +note("from(32768 chars)", Buffer.from("a".repeat(32768))); +note("from(11000 euro signs)", Buffer.from("€".repeat(11000))); +note("from(8191 euro signs)", Buffer.from("€".repeat(8191))); +note("allocUnsafe(32767)", Buffer.allocUnsafe(32767)); +note("allocUnsafe(32768)", Buffer.allocUnsafe(32768)); +note("allocUnsafe(20000)", Buffer.allocUnsafe(20000)); +note("allocUnsafe(20000)", Buffer.allocUnsafe(20000)); +note("from(large bad hex)", Buffer.from("zz" + "a".repeat(70000), "hex")); +Buffer.poolSize = 1024; +rows.push(`poolSize ${Buffer.poolSize}`); +note("allocUnsafe(10) after poolSize = 1024", Buffer.allocUnsafe(10)); +note("allocUnsafe(600)", Buffer.allocUnsafe(600)); +note("allocUnsafe(511)", Buffer.allocUnsafe(511)); +// The 64 KiB slab stays current until a request does not fit; the next +// slab then has the new size. +let small = Buffer.allocUnsafe(500); +let filled = 0; +while (small.byteOffset !== 0) { + small = Buffer.allocUnsafe(500); + filled++; +} +note(`allocUnsafe(500) after ${filled} more`, small); +note("allocUnsafe(500)", Buffer.allocUnsafe(500)); +note("allocUnsafe(500)", Buffer.allocUnsafe(500)); +// A 60-byte slab leaves 4 bytes after an aligned offset of 56: enough for +// the 4-bytes-per-unit reservation of one UTF-16 unit. +Buffer.poolSize = 60; +let tiny = Buffer.allocUnsafe(29); +while (tiny.byteOffset !== 0) tiny = Buffer.allocUnsafe(29); +note("allocUnsafe(29) in a 60-byte slab", tiny); +note("allocUnsafe(20)", Buffer.allocUnsafe(20)); +note("from('€', 'utf16le') in the last 4 bytes", Buffer.from("€", "utf16le")); +note("from('€€', 'utf16le')", Buffer.from("€€", "utf16le")); +note("from('é') in a new slab's room", Buffer.from("é")); +Buffer.poolSize = 0; +note("allocUnsafe(1) with poolSize 0", Buffer.allocUnsafe(1)); +Buffer.poolSize = 65536; + +// Views over one slab share an ArrayBuffer and see each other's bytes. +const first = Buffer.from("left"); +const second = Buffer.from("right"); +rows.push(`shared ArrayBuffer ${first.buffer === second.buffer}`); +const whole = new Uint8Array(first.buffer); +whole[second.byteOffset] = "R".charCodeAt(0); +rows.push(`second through the slab ${second.toString()}`); +first.fill("L"); +rows.push(`first ${first.toString()} second ${second.toString()}`); + +// structuredClone copies the whole slab and keeps the offset; a transfer +// of the slab is refused. +const cloned = structuredClone(first); +rows.push(`clone ${cloned.length} ${cloned.buffer.byteLength} ${cloned.byteOffset === first.byteOffset} ${new TextDecoder().decode(cloned)}`); +try { + structuredClone(first, { transfer: [first.buffer] }); + rows.push("transfer allowed"); +} catch (error) { + rows.push(`transfer ${(error as Error).name}: ${(error as Error).message}`); +} + +// Invalid sizes. +for (const size of [-1, NaN, 2 ** 53, 1.5]) { + for (const [name, make] of [ + ["alloc", (n: number) => Buffer.alloc(n)], + ["allocUnsafe", (n: number) => Buffer.allocUnsafe(n)], + ["allocUnsafeSlow", (n: number) => Buffer.allocUnsafeSlow(n)], + ] as const) { + try { + rows.push(`${name}(${size}) length ${make(size).length}`); + } catch (error) { + const e = error as Error & { code?: string }; + rows.push(`${name}(${size}) ${e.name} ${e.code}: ${e.message}`); + } + } +} + +for (const row of rows) console.log(row); diff --git a/tests/corpus/5531-buffer-pool-first-slab.ts b/tests/corpus/5531-buffer-pool-first-slab.ts new file mode 100644 index 000000000..7f4cf4f9e --- /dev/null +++ b/tests/corpus/5531-buffer-pool-first-slab.ts @@ -0,0 +1,151 @@ +// Buffer.poolSize changes before and between pooled allocations. Node +// creates the first 64 KiB slab at startup; a pooled request (size < +// Buffer.poolSize >>> 1) that does not fit the current slab replaces it +// with a slab of the current Buffer.poolSize. Each case runs in a fresh +// child process, so its first pooled allocation meets the startup slab. +// Node's own startup code allocates from that slab, so offsets into it +// are not printed. +import { spawnSync } from "node:child_process"; + +const KiB = 1024; + +function describe(label: string, b: Buffer, offset = true): void { + let sum = 0; + for (let i = 0; i < b.length; i++) sum = (sum + b[i]! * (i + 1)) % 1000003; + console.log(label, b.length, offset ? b.byteOffset : "-", b.buffer.byteLength, sum); +} + +// A follow-up allocation shows whether the slab is shared. +function follow(first: Buffer): void { + const next = Buffer.allocUnsafe(8); + console.log("next", next.byteOffset, next.buffer.byteLength, next.buffer === first.buffer); +} + +function run(name: string): void { + if (name === "string-ascii") { + Buffer.poolSize = 1024 * KiB; + const b = Buffer.from("x".repeat(100000)); + describe(name, b); + follow(b); + } else if (name === "string-utf8") { + Buffer.poolSize = 1024 * KiB; + const b = Buffer.from("é".repeat(50000)); + describe(name, b); + follow(b); + } else if (name === "string-hex") { + Buffer.poolSize = 1024 * KiB; + const b = Buffer.from("a5".repeat(100000), "hex"); + describe(name, b); + follow(b); + } else if (name === "string-base64") { + Buffer.poolSize = 1024 * KiB; + const b = Buffer.from("AQID".repeat(33334), "base64"); + describe(name, b); + follow(b); + } else if (name === "string-latin1") { + Buffer.poolSize = 1024 * KiB; + const b = Buffer.from("ÿ".repeat(100000), "latin1"); + describe(name, b); + follow(b); + } else if (name === "array") { + Buffer.poolSize = 1024 * KiB; + const values: number[] = []; + for (let i = 0; i < 100000; i++) values.push(i); + const b = Buffer.from(values); + describe(name, b); + follow(b); + } else if (name === "uint8array") { + Buffer.poolSize = 1024 * KiB; + const source = new Uint8Array(100000); + for (let i = 0; i < source.length; i++) source[i] = i * 7; + const b = Buffer.from(source); + describe(name, b); + follow(b); + } else if (name === "buffer") { + Buffer.poolSize = 1024 * KiB; + const b = Buffer.from(Buffer.alloc(100000, 3)); + describe(name, b); + follow(b); + } else if (name === "alloc-unsafe") { + Buffer.poolSize = 1024 * KiB; + const b = Buffer.allocUnsafe(100000).fill(9); + describe(name, b); + follow(b); + } else if (name === "concat") { + Buffer.poolSize = 1024 * KiB; + const b = Buffer.concat([Buffer.alloc(60000, 1), Buffer.alloc(40000, 2)]); + describe(name, b); + follow(b); + } else if (name === "concat-length") { + Buffer.poolSize = 1024 * KiB; + const b = Buffer.concat([Buffer.alloc(60000, 1), Buffer.alloc(30000, 2)], 100000); + describe(name, b); + follow(b); + } else if (name === "threshold") { + // Just under and at the enlarged threshold. + Buffer.poolSize = 300000; + const under = Buffer.allocUnsafe(149999).fill(1); + describe("under", under); + const at = Buffer.allocUnsafe(150000).fill(2); + describe("at", at); + follow(under); + } else if (name === "shrink") { + // A smaller setting keeps using the startup slab while requests fit; + // the next slab has the new size. + Buffer.poolSize = 64; + const small = Buffer.from([1, 2, 3]); + describe("small", small, false); + const unpooled = Buffer.allocUnsafe(32).fill(4); + describe("unpooled", unpooled); + let last = Buffer.allocUnsafe(31); + while (last.buffer === small.buffer) last = Buffer.allocUnsafe(31); + const after = Buffer.allocUnsafe(20); + console.log("moved", last.byteOffset, last.buffer.byteLength, after.byteOffset, after.buffer === last.buffer); + } else if (name === "between") { + // Growing and shrinking between allocations. + const first = Buffer.allocUnsafe(1000).fill(1); + describe("first", first, false); + Buffer.poolSize = 1024 * KiB; + const big = Buffer.allocUnsafe(400000).fill(5); + describe("big", big); + Buffer.poolSize = 128; + const tiny = Buffer.allocUnsafe(60).fill(6); + describe("tiny", tiny); + console.log("same slab", tiny.buffer === big.buffer); + Buffer.poolSize = 512 * KiB; + const mid = Buffer.allocUnsafe(200000).fill(7); + describe("mid", mid); + console.log("same slab", mid.buffer === big.buffer); + const over = Buffer.allocUnsafe(500000).fill(8); + describe("over", over); + } +} + +const cases = [ + "string-ascii", + "string-utf8", + "string-hex", + "string-base64", + "string-latin1", + "array", + "uint8array", + "buffer", + "alloc-unsafe", + "concat", + "concat-length", + "threshold", + "shrink", + "between", +]; + +const mode = process.argv.at(2); +if (mode !== undefined) { + run(mode); +} else { + for (const name of cases) { + const child = spawnSync(process.execPath, [process.argv[1]!, name], { encoding: "utf8" }); + console.log(`${name}: status ${child.status}`); + process.stdout.write(child.stdout); + if (child.stderr.length > 0) console.log(`stderr: ${child.stderr}`); + } +} diff --git a/tests/corpus/5532-buffer-pool-first-slab-untyped.mjs b/tests/corpus/5532-buffer-pool-first-slab-untyped.mjs new file mode 100644 index 000000000..53e0a00a4 --- /dev/null +++ b/tests/corpus/5532-buffer-pool-first-slab-untyped.mjs @@ -0,0 +1,45 @@ +// The dynamic Buffer.from paths (untyped strings, arrays, array-like +// objects, Buffer JSON and Buffers) after Buffer.poolSize grows before +// the first pooled allocation. Each case runs in a fresh child process; +// see 5531 for the typed paths. +import { spawnSync } from "node:child_process"; + +function describe(label, b) { + let sum = 0; + for (let i = 0; i < b.length; i++) sum = (sum + b[i] * (i + 1)) % 1000003; + console.log(label, b.length, b.byteOffset, b.buffer.byteLength, sum); + const next = Buffer.allocUnsafe(8); + console.log("next", next.byteOffset, next.buffer.byteLength, next.buffer === b.buffer); +} + +const values = () => { + const out = []; + for (let i = 0; i < 100000; i++) out.push(i % 251); + return out; +}; + +const cases = { + string: () => Buffer.from("y".repeat(100000)), + "string-encoding": () => Buffer.from("0f".repeat(100000), "hex"), + array: () => Buffer.from(values()), + "array-like": () => { + const like = { length: 100000 }; + for (let i = 0; i < 100000; i++) like[i] = i % 13; + return Buffer.from(like); + }, + "buffer-json": () => Buffer.from({ type: "Buffer", data: values() }), + buffer: () => Buffer.from(Buffer.alloc(100000, 6)), +}; + +const mode = process.argv[2]; +if (mode !== undefined) { + Buffer.poolSize = 1024 * 1024; + describe(mode, cases[mode]()); +} else { + for (const name of Object.keys(cases)) { + const child = spawnSync(process.execPath, [process.argv[1], name], { encoding: "utf8" }); + console.log(`${name}: status ${child.status}`); + process.stdout.write(child.stdout); + if (child.stderr.length > 0) console.log(`stderr: ${child.stderr}`); + } +} diff --git a/tests/fixtures/node-types/fenced.ts b/tests/fixtures/node-types/fenced.ts index b8418c827..b2a3ce8ce 100644 --- a/tests/fixtures/node-types/fenced.ts +++ b/tests/fixtures/node-types/fenced.ts @@ -3,7 +3,6 @@ * the SC2020-family fence naming @types/node instead of a raw TS error * or a binary that runs with the wrong behavior. */ console.log(process.hasUncaughtExceptionCaptureCallback()); // the process introspection statics lower now; the capture-callback probe does not -console.log(Buffer.poolSize); /* setInterval lowers, and under @types/node its Timeout RETURN now maps * to the numeric handle — holding one and unref()/ref()/hasRef()/refresh() * all compile. The Timeout surface BEYOND that (close, [Symbol.toPrimitive], diff --git a/tests/harness/__snapshots__/node-types-fenced.txt b/tests/harness/__snapshots__/node-types-fenced.txt index 2e55219c1..37aa2d4bc 100644 --- a/tests/harness/__snapshots__/node-types-fenced.txt +++ b/tests/harness/__snapshots__/node-types-fenced.txt @@ -3,231 +3,222 @@ fenced.ts:5:13 - error SC2020: 'process.hasUncaughtExceptionCaptureCallback' is 4 | * or a binary that runs with the wrong behavior. */ 5 | console.log(process.hasUncaughtExceptionCaptureCallback()); // the process introspection statics lower now; the capture-callback probe does not | ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - 6 | console.log(Buffer.poolSize); + 6 | /* setInterval lowers, and under @types/node its Timeout RETURN now maps hint: the type checker sees everything @types/node declares, but only the supported surface compiles (https://scriptc.dev/limitations) -fenced.ts:6:13 - error SC2020: 'Buffer.poolSize' is typed by @types/node but has no scriptc lowering yet +fenced.ts:15:1 - error SC2020: 'Timeout.close' is typed by @types/node but has no scriptc lowering yet - 5 | console.log(process.hasUncaughtExceptionCaptureCallback()); // the process introspection statics lower now; the capture-callback probe does not - 6 | console.log(Buffer.poolSize); - | ^~~~~~~~~~~~~~~ - 7 | /* setInterval lowers, and under @types/node its Timeout RETURN now maps - - hint: the type checker sees everything @types/node declares, but only the supported surface compiles (https://scriptc.dev/limitations) - -fenced.ts:16:1 - error SC2020: 'Timeout.close' is typed by @types/node but has no scriptc lowering yet - - 15 | timer.refresh(); - 16 | timer.close(); + 14 | timer.refresh(); + 15 | timer.close(); | ^~~~~~~~~~~~~ - 17 | /* The web-platform globals ride in with @types/node (undici). Fetch, + 16 | /* The web-platform globals ride in with @types/node (undici). Fetch, hint: unref(), ref(), hasRef(), and refresh() are the supported Timeout methods -fenced.ts:28:15 - error SC2020: 'Response.type' is typed by @types/node but has no scriptc lowering yet +fenced.ts:27:15 - error SC2020: 'Response.type' is typed by @types/node but has no scriptc lowering yet - 27 | const response = await fetch(url); - 28 | console.log(response.type); + 26 | const response = await fetch(url); + 27 | console.log(response.type); | ^~~~~~~~~~~~~ - 29 | response.clone(); + 28 | response.clone(); hint: the member is outside the native static handle projection -fenced.ts:29:3 - error SC2020: 'Response.clone' is typed by @types/node but has no scriptc lowering yet +fenced.ts:28:3 - error SC2020: 'Response.clone' is typed by @types/node but has no scriptc lowering yet - 28 | console.log(response.type); - 29 | response.clone(); + 27 | console.log(response.type); + 28 | response.clone(); | ^~~~~~~~~~~~~~ - 30 | } + 29 | } hint: the native fetch runtime does not implement this Response operation -fenced.ts:32:21 - error SC2020: 'ReadableStream.from() over Set iterables' is part of the standard library types but has no scriptc lowering yet +fenced.ts:31:21 - error SC2020: 'ReadableStream.from() over Set iterables' is part of the standard library types but has no scriptc lowering yet - 31 | void inspectResponse; - 32 | ReadableStream.from(new Set([1, 2])); + 30 | void inspectResponse; + 31 | ReadableStream.from(new Set([1, 2])); | ^~~~~~~~~~~~~~~ - 33 | /* Members of SUPPORTED builtin modules beyond the lowered tables: they + 32 | /* Members of SUPPORTED builtin modules beyond the lowered tables: they hint: arrays, Uint8Array, and strings are supported — spread other synchronous iterables into an array first: [...iterable] -fenced.ts:39:1 - error SC2020: 'fs.watchFile' is typed by @types/node but has no scriptc lowering yet +fenced.ts:38:1 - error SC2020: 'fs.watchFile' is typed by @types/node but has no scriptc lowering yet - 38 | import { win32 } from "path"; - 39 | watchFile("x", () => {}); + 37 | import { win32 } from "path"; + 38 | watchFile("x", () => {}); | ^~~~~~~~~~~~~~~~~~~~~~~~ - 40 | setPriority(0); + 39 | setPriority(0); hint: the type checker sees everything @types/node declares, but only the supported surface compiles (https://scriptc.dev/limitations) -fenced.ts:40:1 - error SC2020: 'os.setPriority' is typed by @types/node but has no scriptc lowering yet +fenced.ts:39:1 - error SC2020: 'os.setPriority' is typed by @types/node but has no scriptc lowering yet - 39 | watchFile("x", () => {}); - 40 | setPriority(0); + 38 | watchFile("x", () => {}); + 39 | setPriority(0); | ^~~~~~~~~~~~~~ - 41 | console.log(win32.sep); + 40 | console.log(win32.sep); hint: the type checker sees everything @types/node declares, but only the supported surface compiles (https://scriptc.dev/limitations) -fenced.ts:41:13 - error SC2020: 'PlatformPath.sep' is typed by @types/node but has no scriptc lowering yet +fenced.ts:40:13 - error SC2020: 'PlatformPath.sep' is typed by @types/node but has no scriptc lowering yet - 40 | setPriority(0); - 41 | console.log(win32.sep); + 39 | setPriority(0); + 40 | console.log(win32.sep); | ^~~~~~~~~ - 42 | /* URL getters and serialization lower under @types/node, but object-URL + 41 | /* URL getters and serialization lower under @types/node, but object-URL hint: the type checker sees everything @types/node declares, but only the supported surface compiles (https://scriptc.dev/limitations) -fenced.ts:47:13 - error SC2020: 'URL.createObjectURL' is typed by @types/node but has no scriptc lowering yet +fenced.ts:46:13 - error SC2020: 'URL.createObjectURL' is typed by @types/node but has no scriptc lowering yet - 46 | const u = new URL("https://example.com/x?a=1"); - 47 | console.log(URL.createObjectURL(new Blob(["data"]))); + 45 | const u = new URL("https://example.com/x?a=1"); + 46 | console.log(URL.createObjectURL(new Blob(["data"]))); | ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - 48 | u.searchParams.get("a"); + 47 | u.searchParams.get("a"); hint: protocol, origin, username, password, pathname, href, host, hostname, port, search, hash, searchParams, toString(), and toJSON() are the supported URL members -fenced.ts:53:21 - error SC2020: 'deflateSync with these options' is part of the standard library types but has no scriptc lowering yet +fenced.ts:52:21 - error SC2020: 'deflateSync with these options' is part of the standard library types but has no scriptc lowering yet - 52 | import { brotliCompressSync, deflateSync } from "zlib"; - 53 | deflateSync("data", { level: 9, strategy: 1 }); + 51 | import { brotliCompressSync, deflateSync } from "zlib"; + 52 | deflateSync("data", { level: 9, strategy: 1 }); | ^~~~~~~~~~~~~~~~~~~~~~~~~ - 54 | brotliCompressSync(Buffer.from("data")); + 53 | brotliCompressSync(Buffer.from("data")); hint: the static compression options form is { level: } -fenced.ts:54:1 - error SC2020: 'zlib.brotliCompressSync' is typed by @types/node but has no scriptc lowering yet +fenced.ts:53:1 - error SC2020: 'zlib.brotliCompressSync' is typed by @types/node but has no scriptc lowering yet - 53 | deflateSync("data", { level: 9, strategy: 1 }); - 54 | brotliCompressSync(Buffer.from("data")); + 52 | deflateSync("data", { level: 9, strategy: 1 }); + 53 | brotliCompressSync(Buffer.from("data")); | ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - 55 | /* The http2 compatibility slice's @types/node-world fences (divergence + 54 | /* The http2 compatibility slice's @types/node-world fences (divergence hint: Brotli needs a Brotli codec, which the static runtime does not vendor — the default-options deflate/inflate, raw, gzip/gunzip, and unzip sync/callback forms, crc32, and the Deflate/Inflate/Gzip/Gunzip/DeflateRaw/InflateRaw/Unzip streams are the lowered zlib surface -fenced.ts:63:71 - error SC2020: 'a createSecureServer SNICallback of 'undefined' values' is part of the standard library types but has no scriptc lowering yet +fenced.ts:62:71 - error SC2020: 'a createSecureServer SNICallback of 'undefined' values' is part of the standard library types but has no scriptc lowering yet - 62 | import * as http2 from "node:http2"; - 63 | http2.createSecureServer({ allowHTTP1: true, cert: "pem", key: "pem", SNICallback: undefined }); + 61 | import * as http2 from "node:http2"; + 62 | http2.createSecureServer({ allowHTTP1: true, cert: "pem", key: "pem", SNICallback: undefined }); | ^~~~~~~~~~~~~~~~~~~~~~ - 64 | http2.createSecureServer({ + 63 | http2.createSecureServer({ hint: SNICallback lowers as a direct option or the conditional spread ...(x ? { SNICallback: x } : {}) — the callback is (servername, cb) => void with cb: (err: Error | null, ctx?: tls.SecureContext) => void -fenced.ts:68:3 - error SC2020: 'createSecureServer options with a conditional spread' is part of the standard library types but has no scriptc lowering yet +fenced.ts:67:3 - error SC2020: 'createSecureServer options with a conditional spread' is part of the standard library types but has no scriptc lowering yet - 67 | key: "pem", - 68 | ...(1 ? { SNICallback: undefined } : {}), + 66 | key: "pem", + 67 | ...(1 ? { SNICallback: undefined } : {}), | ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - 69 | }); + 68 | }); hint: the lowered conditional spread is exactly ...(x ? { SNICallback: x } : {}) — the included value must BE the condition -fenced.ts:70:71 - error SC2020: 'createSecureServer option 'streamResetBurst' with a non-literal value' is part of the standard library types but has no scriptc lowering yet +fenced.ts:69:71 - error SC2020: 'createSecureServer option 'streamResetBurst' with a non-literal value' is part of the standard library types but has no scriptc lowering yet - 69 | }); - 70 | http2.createSecureServer({ allowHTTP1: true, cert: "pem", key: "pem", streamResetBurst: 1 + 0 }); + 68 | }); + 69 | http2.createSecureServer({ allowHTTP1: true, cert: "pem", key: "pem", streamResetBurst: 1 + 0 }); | ^~~~~~~~~~~~~~~~~~~~~~~ - 71 | http2.connect("https://localhost"); + 70 | http2.connect("https://localhost"); hint: h2 session-tuning options are accepted with literal values but are not applied yet -fenced.ts:77:1 - error SC2020: 'crypto.generateKeyPair' is typed by @types/node but has no scriptc lowering yet +fenced.ts:76:1 - error SC2020: 'crypto.generateKeyPair' is typed by @types/node but has no scriptc lowering yet - 76 | import { checkPrimeSync, createCipheriv, generateKeyPair, setFips } from "node:crypto"; - 77 | generateKeyPair("rsa", { modulusLength: 2048 }, () => {}); + 75 | import { checkPrimeSync, createCipheriv, generateKeyPair, setFips } from "node:crypto"; + 76 | generateKeyPair("rsa", { modulusLength: 2048 }, () => {}); | ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - 78 | createCipheriv("aes-128-ccm", Buffer.alloc(16), Buffer.alloc(12), { authTagLength: 16 }); + 77 | createCipheriv("aes-128-ccm", Buffer.alloc(16), Buffer.alloc(12), { authTagLength: 16 }); hint: asymmetric-key operations need a public-key stack (bignum, RSA/EC/EdDSA math) and public and private KeyObjects — neither exists in the static runtime, so no faithful lowering can be small; the lowered crypto surface is hashing, randomness, secret KeyObjects, and the introspection statics -fenced.ts:78:16 - error SC2020: 'crypto.createCipheriv with cipher 'aes-128-ccm'' is part of the standard library types but has no scriptc lowering yet +fenced.ts:77:16 - error SC2020: 'crypto.createCipheriv with cipher 'aes-128-ccm'' is part of the standard library types but has no scriptc lowering yet - 77 | generateKeyPair("rsa", { modulusLength: 2048 }, () => {}); - 78 | createCipheriv("aes-128-ccm", Buffer.alloc(16), Buffer.alloc(12), { authTagLength: 16 }); + 76 | generateKeyPair("rsa", { modulusLength: 2048 }, () => {}); + 77 | createCipheriv("aes-128-ccm", Buffer.alloc(16), Buffer.alloc(12), { authTagLength: 16 }); | ^~~~~~~~~~~~~ - 79 | checkPrimeSync(7n); + 78 | checkPrimeSync(7n); hint: the supported ciphers are aes-128-cbc, aes-192-cbc, aes-256-cbc, aes-128-ctr, aes-192-ctr, aes-256-ctr, aes-128-gcm, aes-192-gcm, aes-256-gcm, and chacha20-poly1305 -fenced.ts:79:1 - error SC2020: 'crypto.checkPrimeSync' is typed by @types/node but has no scriptc lowering yet +fenced.ts:78:1 - error SC2020: 'crypto.checkPrimeSync' is typed by @types/node but has no scriptc lowering yet - 78 | createCipheriv("aes-128-ccm", Buffer.alloc(16), Buffer.alloc(12), { authTagLength: 16 }); - 79 | checkPrimeSync(7n); + 77 | createCipheriv("aes-128-ccm", Buffer.alloc(16), Buffer.alloc(12), { authTagLength: 16 }); + 78 | checkPrimeSync(7n); | ^~~~~~~~~~~~~~~~~~ - 80 | setFips(false); + 79 | setFips(false); hint: the type checker sees everything @types/node declares, but only the supported surface compiles (https://scriptc.dev/limitations) -fenced.ts:80:1 - error SC2020: 'crypto.setFips' is typed by @types/node but has no scriptc lowering yet +fenced.ts:79:1 - error SC2020: 'crypto.setFips' is typed by @types/node but has no scriptc lowering yet - 79 | checkPrimeSync(7n); - 80 | setFips(false); + 78 | checkPrimeSync(7n); + 79 | setFips(false); | ^~~~~~~~~~~~~~ - 81 | fetch("https://example.invalid/", { + 80 | fetch("https://example.invalid/", { hint: a compiled binary has no FIPS provider to enable, and Node itself throws on setFips(true) in a non-FIPS build — getFips() answers 0 here -fenced.ts:82:3 - error SC2020: 'RequestInit option 'integrity' in a static build' is typed by @types/node but has no scriptc lowering yet +fenced.ts:81:3 - error SC2020: 'RequestInit option 'integrity' in a static build' is typed by @types/node but has no scriptc lowering yet - 81 | fetch("https://example.invalid/", { - 82 | integrity: "sha256-AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=", + 80 | fetch("https://example.invalid/", { + 81 | integrity: "sha256-AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=", | ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - 83 | }); + 82 | }); hint: the compiler does not preserve this RequestInit member's conversion or transport behavior -fenced.ts:86:3 - error SC2020: 'ReadableStream.tee' is typed by @types/node but has no scriptc lowering yet +fenced.ts:85:3 - error SC2020: 'ReadableStream.tee' is typed by @types/node but has no scriptc lowering yet - 85 | const body = (await fetch(url)).body!; - 86 | body.tee(); + 84 | const body = (await fetch(url)).body!; + 85 | body.tee(); | ^~~~~~~~ - 87 | const tee = body.tee; + 86 | const tee = body.tee; hint: this operation is outside the native readable-stream implementation -fenced.ts:87:15 - error SC2020: 'ReadableStream.tee' is typed by @types/node but has no scriptc lowering yet +fenced.ts:86:15 - error SC2020: 'ReadableStream.tee' is typed by @types/node but has no scriptc lowering yet - 86 | body.tee(); - 87 | const tee = body.tee; + 85 | body.tee(); + 86 | const tee = body.tee; | ^~~~~~~~ - 88 | body["tee"](); + 87 | body["tee"](); hint: this operation is outside the native readable-stream implementation -fenced.ts:88:3 - error SC2020: 'ReadableStream.tee' is typed by @types/node but has no scriptc lowering yet +fenced.ts:87:3 - error SC2020: 'ReadableStream.tee' is typed by @types/node but has no scriptc lowering yet - 87 | const tee = body.tee; - 88 | body["tee"](); + 86 | const tee = body.tee; + 87 | body["tee"](); | ^~~~~~~~~~~ - 89 | const bracketTee = body["tee"]; + 88 | const bracketTee = body["tee"]; hint: this operation is outside the native readable-stream implementation -fenced.ts:89:22 - error SC2020: 'ReadableStream.tee' is typed by @types/node but has no scriptc lowering yet +fenced.ts:88:22 - error SC2020: 'ReadableStream.tee' is typed by @types/node but has no scriptc lowering yet - 88 | body["tee"](); - 89 | const bracketTee = body["tee"]; + 87 | body["tee"](); + 88 | const bracketTee = body["tee"]; | ^~~~~~~~~~~ - 90 | } + 89 | } hint: this operation is outside the native readable-stream implementation -fenced.ts:95:23 - error SC2020: 'util.callbackify into a callback whose error parameter excludes null' is part of the standard library types but has no scriptc lowering yet +fenced.ts:94:23 - error SC2020: 'util.callbackify into a callback whose error parameter excludes null' is part of the standard library types but has no scriptc lowering yet - 94 | import { callbackify } from "node:util"; - 95 | const callbackified = callbackify(async (n: number) => n); + 93 | import { callbackify } from "node:util"; + 94 | const callbackified = callbackify(async (n: number) => n); | ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - 96 | void callbackified; + 95 | void callbackified; hint: Node calls back with a null error on success; type the binding's callback as (err: NodeJS.ErrnoException | null, ...) => void -fenced.ts:100:1 - error SC2020: 'Cipheriv/Decipheriv.on' is part of the standard library types but has no scriptc lowering yet +fenced.ts:99:1 - error SC2020: 'Cipheriv/Decipheriv.on' is part of the standard library types but has no scriptc lowering yet - 99 | const cipherStream = createCipheriv("aes-128-cbc", Buffer.alloc(16), Buffer.alloc(16)); - 100 | cipherStream.on("data", () => {}); + 98 | const cipherStream = createCipheriv("aes-128-cbc", Buffer.alloc(16), Buffer.alloc(16)); + 99 | cipherStream.on("data", () => {}); | ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - 101 | // End of the declared-but-not-lowered surface. + 100 | // End of the declared-but-not-lowered surface. hint: update(), final(), setAAD(), setAutoPadding(), getAuthTag(), and setAuthTag() are supported; cipher streams are not \ No newline at end of file