Pool small Buffers as Node does (#1101)

This commit is contained in:
Chris Tate
2026-10-16 03:50:17 -05:00
committed by GitHub
36 changed files with 1101 additions and 169 deletions
@@ -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.
@@ -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.
@@ -2,6 +2,6 @@
"schemaVersion": 3,
"nodeVersion": "24.22.0",
"nodeCommit": "f14cd3aa7bf9ca8df2cc5b8bb14beb0f335b1d93",
"artifactVersion": "1c65fe186ed24618c053",
"artifactVersion": "aaa7e74233dc5f826dcd",
"rowCount": 3880
}
@@ -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"
}
},
{
@@ -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"
]
}
}
},
@@ -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"
},
@@ -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"] },
+5 -2
View File
@@ -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;
@@ -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 }`,
@@ -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)`);
@@ -399,8 +399,18 @@ export const LIB_FN_SYMS: Record<string, string> = {
"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<string, string> = {
"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",
@@ -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.
@@ -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 };
}
}
@@ -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")
@@ -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"
@@ -454,6 +454,7 @@ export const MAY_THROW_LIB_FNS: ReadonlySet<IrLibFn> = 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<IrLibFn> = 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",
@@ -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
@@ -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");
@@ -345,7 +345,7 @@ describe("LLVM backend declares match scr_runtime.h prototypes", () => {
#include <stdint.h>
#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");
`,
);
@@ -0,0 +1,6 @@
{
"order": [
"<repo>/tests/corpus/5530-buffer-pool.ts"
],
"diags": []
}
@@ -0,0 +1,6 @@
{
"order": [
"<repo>/tests/corpus/5531-buffer-pool-first-slab.ts"
],
"diags": []
}
@@ -0,0 +1,6 @@
{
"order": [
"<repo>/tests/corpus/5532-buffer-pool-first-slab-untyped.mjs"
],
"diags": []
}
+371 -12
View File
@@ -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
+25
View File
@@ -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
+50
View File
@@ -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);
}
+12
View File
@@ -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,
+1
View File
@@ -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. */
+3
View File
@@ -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);
+30 -4
View File
@@ -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[] =
+2
View File
@@ -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();
@@ -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)));
+137
View File
@@ -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);
+151
View File
@@ -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}`);
}
}
@@ -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}`);
}
}
-1
View File
@@ -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],
+97 -106
View File
@@ -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<number> iterables' is part of the standard library types but has no scriptc lowering yet
fenced.ts:31:21 - error SC2020: 'ReadableStream.from() over Set<number> 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: <integer literal from -1 through 9> }
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