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 85fe0ecb3..7323db110 100644 --- a/packages/compiler/src/backend/llvm/emitter.ts +++ b/packages/compiler/src/backend/llvm/emitter.ts @@ -2120,7 +2120,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 1d3db79b2..e32a85a82 100644 --- a/packages/runtime/src/scr_console.c +++ b/packages/runtime/src/scr_console.c @@ -68,6 +68,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