Keep numeric typed arrays native across JavaScript boundaries

- Support integer widths and clamped arrays with shared storage and exact runtime brands.
- Preserve typed-array values through untyped fields, iteration, and byte I/O.
- Compile native ArrayBuffer response bodies and checked Number predicates.
This commit is contained in:
Chris Tate
2026-09-28 21:15:01 -05:00
committed by GitHub
parent 6aa0d0015b
commit 24ab670305
51 changed files with 691 additions and 355 deletions
-1
View File
@@ -105,7 +105,6 @@ Blocker lines carry `SC` codes — the same codes `scriptc build` errors with, s
- `'X' is part of the standard library types but has no scriptc lowering yet (SC2020)` — the type checker sees the full standard library, but only the supported surface compiles. The build error at that site includes the supported-alternatives hint. - `'X' is part of the standard library types but has no scriptc lowering yet (SC2020)` — the type checker sees the full standard library, but only the supported surface compiles. The build error at that site includes the supported-alternatives hint.
- `importing 'pkg' requires the embedded dynamic engine (SC2013)` — npm package implementations run in the engine; add `--dynamic` or drop the dependency. - `importing 'pkg' requires the embedded dynamic engine (SC2013)` — npm package implementations run in the engine; add `--dynamic` or drop the dependency.
- `'Response.arrayBuffer() in a static build' is typed but has no scriptc lowering yet (SC2020)` — `fetch` itself is native static; use `Response.bytes()` for a byte body, or `--dynamic` for the wider Web API.
- `passing 'unknown' values where 'any' is expected (SC1100)` — the `any`/`unknown` boundary rules; narrow or cast first. - `passing 'unknown' values where 'any' is expected (SC1100)` — the `any`/`unknown` boundary rules; narrow or cast first.
## Type errors gate the analysis ## Type errors gate the analysis
+2 -2
View File
@@ -53,7 +53,7 @@ const results = await Promise.all(jobs); // number[] — compiles
- `fs.openSync` accepts inline bitwise OR expressions of `fs.constants.O_RDONLY`, `O_WRONLY`, `O_RDWR`, `O_CREAT`, `O_EXCL`, `O_NOFOLLOW`, `O_NONBLOCK`, `O_TRUNC`, and `O_APPEND`, with an optional creation mode. Computed numeric flags remain fenced because their bit values differ by target. `fstatSync`, `fchmodSync`, `fsyncSync`, and `linkSync` compile at direct call sites. On Windows, a numeric open with `O_NOFOLLOW` and `fchmodSync` throw `ENOSYS` because the CRT cannot enforce those contracts; programs requiring them should use a POSIX target. - `fs.openSync` accepts inline bitwise OR expressions of `fs.constants.O_RDONLY`, `O_WRONLY`, `O_RDWR`, `O_CREAT`, `O_EXCL`, `O_NOFOLLOW`, `O_NONBLOCK`, `O_TRUNC`, and `O_APPEND`, with an optional creation mode. Computed numeric flags remain fenced because their bit values differ by target. `fstatSync`, `fchmodSync`, `fsyncSync`, and `linkSync` compile at direct call sites. On Windows, a numeric open with `O_NOFOLLOW` and `fchmodSync` throw `ENOSYS` because the CRT cannot enforce those contracts; programs requiring them should use a POSIX target.
- Numeric `process.exitCode = value` writes in statement position set the implicit exit status; `process.exit()` with no argument uses that status. Reading or resetting `process.exitCode`, and assignments used as values, remain fenced. - Numeric `process.exitCode = value` writes in statement position set the implicit exit status; `process.exit()` with no argument uses that status. Reading or resetting `process.exitCode`, and assignments used as values, remain fenced.
- `process.getBuiltinModule()` exposes native subsets of `path`, `path/posix`, and `path/win32`, native `os` functions, and main-thread `worker_threads` metadata. Module references and stored functions retain identity. Other modules and exports, including `Worker`, throw `SC2020`. `process.versions` is a shared dictionary containing `node` and `openssl`; other component versions are absent unless defined by the program. - `process.getBuiltinModule()` exposes native subsets of `path`, `path/posix`, and `path/win32`, native `os` functions, and main-thread `worker_threads` metadata. Module references and stored functions retain identity. Other modules and exports, including `Worker`, throw `SC2020`. `process.versions` is a shared dictionary containing `node` and `openssl`; other component versions are absent unless defined by the program.
- Fixed-length `ArrayBuffer` values support shared typed-array and DataView storage. Resizing and transferring buffers remain unsupported. `Uint8Array` and Buffer values preserve shared storage when passed through `unknown`. - Fixed-length `ArrayBuffer` values support shared typed-array and DataView storage. Resizing and transferring buffers remain unsupported. Numeric typed arrays and Buffer values preserve their element kind and shared storage when passed through `unknown`, including typed arrays nested in records and arrays. Float16 and BigInt typed arrays remain unsupported.
- `Intl.Segmenter` supports default Unicode grapheme segmentation, segment iteration, and `containing()`. Locale negotiation, word and sentence segmentation, `resolvedOptions()`, and detached method values remain unsupported. - `Intl.Segmenter` supports default Unicode grapheme segmentation, segment iteration, and `containing()`. Locale negotiation, word and sentence segmentation, `resolvedOptions()`, and detached method values remain unsupported.
- Stored `globalThis` references support identity and probes for absent host capabilities. Most builtin members still require direct global access; reading them through a stored global object can raise `SC2020`. - Stored `globalThis` references support identity and probes for absent host capabilities. Most builtin members still require direct global access; reading them through a stored global object can raise `SC2020`.
- Map keys and Set elements support numbers and strings by value, and records, class instances, arrays, symbols, and server handles by identity. Unions consisting only of these identity types also compile. Collections with `unknown` slots, including JavaScript's unannotated `new Map()` and `new Set()`, use native checked values: supported primitives compare by value and supported object references retain their identity. BigInts, symbols, nested collections, Error objects, and promises whose payload requires an adapter cannot cross into those checked slots; `Promise<unknown>` retains its original identity. Other statically typed key forms, mixed scalar/reference unions, nullable key unions, and key conversions that copy an existing reference remain fenced. - Map keys and Set elements support numbers and strings by value, and records, class instances, arrays, symbols, and server handles by identity. Unions consisting only of these identity types also compile. Collections with `unknown` slots, including JavaScript's unannotated `new Map()` and `new Set()`, use native checked values: supported primitives compare by value and supported object references retain their identity. BigInts, symbols, nested collections, Error objects, and promises whose payload requires an adapter cannot cross into those checked slots; `Promise<unknown>` retains its original identity. Other statically typed key forms, mixed scalar/reference unions, nullable key unions, and key conversions that copy an existing reference remain fenced.
@@ -81,7 +81,7 @@ A static-tier program otherwise produces byte-identical stdout and the same exit
**Untyped Buffer construction accepts native data.** `Buffer.from` accepts untyped strings, byte arrays, arrays, and plain array-like or Buffer JSON objects. Encoding arguments must be supported literals. Custom `valueOf` hooks on the input object and opaque class or native-handle inputs throw an explicit unsupported-operation error. **Untyped Buffer construction accepts native data.** `Buffer.from` accepts untyped strings, byte arrays, arrays, and plain array-like or Buffer JSON objects. Encoding arguments must be supported literals. Custom `valueOf` hooks on the input object and opaque class or native-handle inputs throw an explicit unsupported-operation error.
**JSON callbacks have a native subset.** Function replacers and two-argument revivers run statically, including nested replacements, object-property deletion, and thrown exceptions. A replacer that omits the root returns `undefined`. Replacer property lists, reviver source contexts, and computed indentation remain unsupported; a reviver that deletes an array element throws because checked-dynamic arrays cannot represent holes. Callback values use the checked-dynamic boundary: typed records and arrays become snapshots, so callback mutations do not update the original typed containers, record fields retain declaration order, and typed Buffer values lose their Buffer brand when converted to bytes. `Buffer.isBuffer` observes that brand loss when typed Buffers cross into `unknown`; untyped stream chunks retain their Buffer brand. JavaScript callbacks receive the holder as `this`; TypeScript callbacks that access a dynamic `this` remain unsupported. **JSON callbacks have a native subset.** Function replacers and two-argument revivers run statically, including nested replacements, object-property deletion, and thrown exceptions. A replacer that omits the root returns `undefined`. Replacer property lists, reviver source contexts, and computed indentation remain unsupported; a reviver that deletes an array element throws because checked-dynamic arrays cannot represent holes. Callback values use the checked-dynamic boundary: typed records and arrays become snapshots, so callback mutations do not update the original typed containers, record fields retain declaration order, and typed-array views retain their backing storage and brands. `Buffer.isBuffer` preserves the Buffer brand across `unknown` storage. JavaScript callbacks receive the holder as `this`; TypeScript callbacks that access a dynamic `this` remain unsupported.
**A lying cast on dynamic data throws instead of corrupting memory** — the headline divergence, and the point. `JSON.parse(s) as Config` with mismatched data throws a catchable error naming the offending path (`expected number at $.port, got string`) where JS would silently hand you garbage. **A lying cast on dynamic data throws instead of corrupting memory** — the headline divergence, and the point. `JSON.parse(s) as Config` with mismatched data throws a catchable error naming the offending path (`expected number at $.port, got string`) where JS would silently hand you garbage.
+7 -7
View File
@@ -41,7 +41,7 @@ import type {
IrUnionDef, IrUnionDef,
SrcLoc, SrcLoc,
} from "../../ir/ir.js"; } from "../../ir/ir.js";
import { ffiCallbackType, isFfiCallbackParam, isFfiContextParam, isFfiReleaseParam, isRefCounted, isUnitType, moduleEmbedsCompressedNpm, moduleUsesChildProcess, moduleUsesDgram, moduleUsesDynInvoke, moduleEmbedsBuiltin, moduleUsesFetch, moduleUsesFsWatch, moduleUsesHttp2, moduleUsesHttpServer, moduleUsesNet, moduleUsesNodeTest, moduleUsesProcessEvents, moduleUsesStream, moduleUsesTls, moduleUsesTlsCa, POINTER_KINDS, type PointerKind, RUNTIME_EMITTER_CLASS, VOID } from "../../ir/ir.js"; import { BYTES_ELEMENT_SIZE, ffiCallbackType, isFfiCallbackParam, isFfiContextParam, isFfiReleaseParam, isRefCounted, isUnitType, moduleEmbedsCompressedNpm, moduleUsesChildProcess, moduleUsesDgram, moduleUsesDynInvoke, moduleEmbedsBuiltin, moduleUsesFetch, moduleUsesFsWatch, moduleUsesHttp2, moduleUsesHttpServer, moduleUsesNet, moduleUsesNodeTest, moduleUsesProcessEvents, moduleUsesStream, moduleUsesTls, moduleUsesTlsCa, POINTER_KINDS, type PointerKind, RUNTIME_EMITTER_CLASS, VOID } from "../../ir/ir.js";
import { undefinedArmTag } from "../../ir/analysis.js"; import { undefinedArmTag } from "../../ir/analysis.js";
import { scalarizeNumericRecords } from "../../ir/scalar-records.js"; import { scalarizeNumericRecords } from "../../ir/scalar-records.js";
import type { IntegerRanges } from "../../ir/integer-ranges.js"; import type { IntegerRanges } from "../../ir/integer-ranges.js";
@@ -1424,7 +1424,7 @@ export class CEmitter {
`}`, `}`,
); );
} }
const elements: ("u8" | "u32" | "i32" | "f32" | "f64")[] = ["u8", "u32", "i32", "f32", "f64"]; const elements: IrBytesElem[] = ["u8", "u8c", "i8", "u16", "i16", "u32", "i32", "f32", "f64"];
const modes: ("f64" | "u64")[] = ["f64", "u64"]; const modes: ("f64" | "u64")[] = ["f64", "u64"];
for (const elem of elements) { for (const elem of elements) {
for (const mode of modes) { for (const mode of modes) {
@@ -1439,8 +1439,8 @@ export class CEmitter {
`}`, `}`,
); );
} else { } else {
const valueType = elem === "f32" ? "float" : elem === "f64" ? "double" : elem === "i32" ? "int32_t" : "uint32_t"; const size = BYTES_ELEMENT_SIZE[elem];
const size = elem === "f64" ? 8 : 4; const valueType = elem === "f32" ? "float" : elem === "f64" ? "double" : `${elem.startsWith("i") ? "int" : "uint"}${size * 8}_t`;
out.push( out.push(
`static inline double sc_bytes_get_${elem}${suffix}(const ScrBytes *b, ${indexType} i) {`, `static inline double sc_bytes_get_${elem}${suffix}(const ScrBytes *b, ${indexType} i) {`,
` ${valueType} v;`, ` ${valueType} v;`,
@@ -1458,9 +1458,9 @@ export class CEmitter {
`}`, `}`,
); );
} else { } else {
const valueType = elem === "f32" ? "float" : elem === "f64" ? "double" : "uint32_t"; const size = BYTES_ELEMENT_SIZE[elem];
const init = elem === "f32" ? `(float)v` : elem === "f64" ? "v" : `sc_bytes_coerce_u32(v)`; const valueType = elem === "f32" ? "float" : elem === "f64" ? "double" : `uint${size * 8}_t`;
const size = elem === "f64" ? 8 : 4; const init = elem === "f32" ? `(float)v` : elem === "f64" ? "v" : elem === "u8c" ? `(uint8_t)scr_bytes_to_u8_clamp(v)` : `(${valueType})sc_bytes_coerce_u32(v)`;
out.push( out.push(
`static inline void sc_bytes_set_${elem}${suffix}(ScrBytes *b, ${indexType} i, double v) {`, `static inline void sc_bytes_set_${elem}${suffix}(ScrBytes *b, ${indexType} i, double v) {`,
` size_t idx = ${checked}(b, i);`, ` size_t idx = ${checked}(b, i);`,
+16 -2
View File
@@ -3,7 +3,7 @@ import { InternalCompilerError } from "../../errors.js";
* expression lands in a fresh C temp, with RC ownership tracked on the * expression lands in a fresh C temp, with RC ownership tracked on the
* emitter's frames (see the discipline comment in emitter core). */ * emitter's frames (see the discipline comment in emitter core). */
import type { CEmitter, Temp } from "./c-emitter.js"; import type { CEmitter, Temp } from "./c-emitter.js";
import { arrayOf, BOOL, BYTES_U8, bytesOf, canMarshalFuncIntoIsland, CHILDSTREAM_T, CHILDWRITER_T, DYN, F64, type IrExpr, type IrLibFn, type IrRecordShape, type IrType, islandPromisePayloadTag, classDynViewSupported, isClassOwnEnumerableFieldName, isDynTypedRefType, isFfiCallbackParam, isFfiContextParam, isFfiReleaseParam, isRefCounted, isUnitType, MAY_THROW_LIB_FNS, NETSOCKET_T, RUNTIME_ERROR_CLASSES, STRING, typeEquals, typeKey } from "../../ir/ir.js"; import { BYTES_ELEMENT_SIZE, arrayOf, BOOL, BYTES_U8, bytesOf, canMarshalFuncIntoIsland, CHILDSTREAM_T, CHILDWRITER_T, DYN, F64, type IrExpr, type IrLibFn, type IrRecordShape, type IrType, islandPromisePayloadTag, classDynViewSupported, isClassOwnEnumerableFieldName, isDynTypedRefType, isFfiCallbackParam, isFfiContextParam, isFfiReleaseParam, isRefCounted, isUnitType, MAY_THROW_LIB_FNS, NETSOCKET_T, RUNTIME_ERROR_CLASSES, STRING, typeEquals, typeKey } from "../../ir/ir.js";
import { BYTES_NUM_KIND_C, BYTES_NUM_VAR_C, bytesElemKindC, cDecl, cFnPtrCast, cNumberLiteral, cStringLiteral, cType, DV_GET_KIND_C, DV_SET_KIND_C, elemAccess, mapKeyAccess, mapKeyKindC, mapValKindC, releaseCallC, retainCallC, vAdapters } from "./types.js"; import { BYTES_NUM_KIND_C, BYTES_NUM_VAR_C, bytesElemKindC, cDecl, cFnPtrCast, cNumberLiteral, cStringLiteral, cType, DV_GET_KIND_C, DV_SET_KIND_C, elemAccess, mapKeyAccess, mapKeyKindC, mapValKindC, releaseCallC, retainCallC, vAdapters } from "./types.js";
import { mangleClassNew, mangleClassRetain, mangleClassStruct, mangleField, mangleFnClosure, mangleFunction, mangleGlobal, mangleLocal, mangleRecordClone, mangleRecordNew, mangleRecordStruct, mangleVtStruct } from "../mangle.js"; import { mangleClassNew, mangleClassRetain, mangleClassStruct, mangleField, mangleFnClosure, mangleFunction, mangleGlobal, mangleLocal, mangleRecordClone, mangleRecordNew, mangleRecordStruct, mangleVtStruct } from "../mangle.js";
import { OVERFLOW_MEMBER } from "./shapes.js"; import { OVERFLOW_MEMBER } from "./shapes.js";
@@ -1993,7 +1993,7 @@ function emitContainerExpr(
} }
return emitter.newTemp( return emitter.newTemp(
e.type, e.type,
`(double)(${r.name}->len * ${e.receiver.type.elem === "u8" ? "1" : e.receiver.type.elem === "f64" ? "8" : "4"})`, `(double)(${r.name}->len * ${BYTES_ELEMENT_SIZE[e.receiver.type.elem]})`,
); );
case "get": case "get":
// Any invalid index traps (the array runtime's discipline). // Any invalid index traps (the array runtime's discipline).
@@ -3253,6 +3253,10 @@ function emitDynamicExpr(
// kind, so the calls stay unconditional); narrowing never changes // kind, so the calls stay unconditional); narrowing never changes
// representation (SEMANTICS.md). // representation (SEMANTICS.md).
const d = emitter.emitExpr(e.value); const d = emitter.emitExpr(e.value);
if (e.test === "bytes") {
const test = `scr_dyn_bytes_is(${d.name}, ${bytesElemKindC(e.bytesElem ?? "u8")})`;
return emitter.newTemp(e.type, e.negated ? `!${test}` : test);
}
if (e.test === "buffer") { if (e.test === "buffer") {
const test = `(${d.name}->kind == SCR_DYN_BYTES && ${d.name}->buffer)`; const test = `(${d.name}->kind == SCR_DYN_BYTES && ${d.name}->buffer)`;
return emitter.newTemp(e.type, e.negated ? `!${test}` : test); return emitter.newTemp(e.type, e.negated ? `!${test}` : test);
@@ -4331,6 +4335,8 @@ function emitWebLibCall(state: LibCallState): Temp {
return finish(`scr_fetch_static(${arg(0)}, ${arg(1)})`); return finish(`scr_fetch_static(${arg(0)}, ${arg(1)})`);
case "fetch.responseNew": case "fetch.responseNew":
return finish(`scr_fetch_response_new(${arg(0)}, ${arg(1)})`); return finish(`scr_fetch_response_new(${arg(0)}, ${arg(1)})`);
case "fetch.responseArrayBuffer":
return finish(`scr_fetch_response_array_buffer(${arg(0)})`);
case "fetch.responseJson": case "fetch.responseJson":
return finish(`scr_fetch_response_json(${arg(0)})`); return finish(`scr_fetch_response_json(${arg(0)})`);
case "fetch.responseText": case "fetch.responseText":
@@ -4533,6 +4539,14 @@ function emitDynamicLibCall(state: LibCallState): Temp {
return finish(`scr_array_buffer_byte_length_getter()`); return finish(`scr_array_buffer_byte_length_getter()`);
case "arrayBuffer.byteLengthDescriptor": case "arrayBuffer.byteLengthDescriptor":
return finish(`scr_array_buffer_byte_length_descriptor(${arg(0)})`); return finish(`scr_array_buffer_byte_length_descriptor(${arg(0)})`);
case "arrayBuffer.viewU8C":
return finish(`scr_array_buffer_view_u8c(${arg(0)}, ${arg(1)}, ${arg(2)})`);
case "arrayBuffer.viewI8":
return finish(`scr_array_buffer_view_i8(${arg(0)}, ${arg(1)}, ${arg(2)})`);
case "arrayBuffer.viewU16":
return finish(`scr_array_buffer_view_u16(${arg(0)}, ${arg(1)}, ${arg(2)})`);
case "arrayBuffer.viewI16":
return finish(`scr_array_buffer_view_i16(${arg(0)}, ${arg(1)}, ${arg(2)})`);
case "arrayBuffer.viewU8": case "arrayBuffer.viewU8":
return finish(`scr_array_buffer_view_u8(${arg(0)}, ${arg(1)}, ${arg(2)})`); return finish(`scr_array_buffer_view_u8(${arg(0)}, ${arg(1)}, ${arg(2)})`);
case "arrayBuffer.viewU32": case "arrayBuffer.viewU32":
+4
View File
@@ -334,6 +334,10 @@ export function elemKindC(elem: IrType): string {
/** The runtime's element-kind tag for a bytes (typed array) type. */ /** The runtime's element-kind tag for a bytes (typed array) type. */
const BYTES_ELEM_KIND_C: Record<IrBytesElem, string> = { const BYTES_ELEM_KIND_C: Record<IrBytesElem, string> = {
u8: "SCR_BYTES_U8", u8: "SCR_BYTES_U8",
u8c: "SCR_BYTES_U8C",
i8: "SCR_BYTES_I8",
u16: "SCR_BYTES_U16",
i16: "SCR_BYTES_I16",
u32: "SCR_BYTES_U32", u32: "SCR_BYTES_U32",
i32: "SCR_BYTES_I32", i32: "SCR_BYTES_I32",
f32: "SCR_BYTES_F32", f32: "SCR_BYTES_F32",
+9 -14
View File
@@ -9,7 +9,7 @@ import { InternalCompilerError } from "../../errors.js";
import type { CEmitter } from "./c-emitter.js"; import type { CEmitter } from "./c-emitter.js";
import { DYN_HANDLE_KINDS, type IrType, type IrUnionDef, isDynTypedRefType, isRefCounted, typeEquals, typeKey } from "../../ir/ir.js"; import { DYN_HANDLE_KINDS, type IrType, type IrUnionDef, isDynTypedRefType, isRefCounted, typeEquals, typeKey } from "../../ir/ir.js";
import { dynDesc, undefinedArmTag, unionWideningTags } from "../../ir/analysis.js"; import { dynDesc, undefinedArmTag, unionWideningTags } from "../../ir/analysis.js";
import { cCommentText, cDecl, cStringLiteral, cType, elemAccess, releaseCallC, retainCallC, vAdapters } from "./types.js"; import { bytesElemKindC, cCommentText, cDecl, cStringLiteral, cType, elemAccess, releaseCallC, retainCallC, vAdapters } from "./types.js";
import { mangleField, mangleRecordNew, mangleRecordStruct } from "../mangle.js"; import { mangleField, mangleRecordNew, mangleRecordStruct } from "../mangle.js";
import { jsonObjectKeyLabel } from "../json-literal.js"; import { jsonObjectKeyLabel } from "../json-literal.js";
import { OVERFLOW_MEMBER } from "./shapes.js"; import { OVERFLOW_MEMBER } from "./shapes.js";
@@ -350,9 +350,9 @@ export function unionWidenHelper(emitter: CEmitter, fromId: string, toId: string
` }`, ` }`,
` for (size_t i = 0; i < d->v.bytes->len; i++) {`, ` for (size_t i = 0; i < d->v.bytes->len; i++) {`,
` if (i > 0) scr_jb_putc(b, ',');`, ` if (i > 0) scr_jb_putc(b, ',');`,
` char n[16];`, ` ScrStr *n = scr_f64_to_scrstr(scr_bytes_get(d->v.bytes, (double)i));`,
` snprintf(n, sizeof n, "%u", (unsigned)d->v.bytes->data[i]);`, ` for (size_t j = 0; j < n->len; j++) scr_jb_putc(b, n->data[j]);`,
` scr_jb_puts(b, n);`, ` scr_str_release(n);`,
` }`, ` }`,
` break;`, ` break;`,
` }`, ` }`,
@@ -800,9 +800,7 @@ export function jsonWriteHelper(emitter: CEmitter, t: IrType): string {
d.push(` return true;`); d.push(` return true;`);
break; break;
case "bytes": case "bytes":
// A Uint8Array target (the checked-dynamic tree carries u8 payloads only). d.push(` return scr_dyn_bytes_is(d, ${bytesElemKindC(t.elem)});`);
if (t.elem !== "u8") throw new InternalCompilerError(`emitter bug: dynMatch of bytes<${t.elem}>`);
d.push(` return d->kind == SCR_DYN_BYTES;`);
break; break;
case "func": case "func":
d.push(` return d->kind == SCR_DYN_FUNC;`); d.push(` return d->kind == SCR_DYN_FUNC;`);
@@ -1013,7 +1011,7 @@ export function jsonWriteHelper(emitter: CEmitter, t: IrType): string {
` if (d->kind == SCR_DYN_ARR) {`, ` if (d->kind == SCR_DYN_ARR) {`,
` item = i < d->v.arr.len ? scr_dyn_retain(d->v.arr.items[i]) : scr_dyn_retain(scr_dyn_undefined());`, ` item = i < d->v.arr.len ? scr_dyn_retain(d->v.arr.items[i]) : scr_dyn_retain(scr_dyn_undefined());`,
` } else if (d->kind == SCR_DYN_BYTES) {`, ` } else if (d->kind == SCR_DYN_BYTES) {`,
` item = i < d->v.bytes->len ? scr_dyn_new_num((double)d->v.bytes->data[i]) : scr_dyn_retain(scr_dyn_undefined());`, ` item = i < d->v.bytes->len ? scr_dyn_new_num(scr_bytes_get(d->v.bytes, (double)i)) : scr_dyn_retain(scr_dyn_undefined());`,
` } else {`, ` } else {`,
` /* String iteration: whole code POINTS (astral chars arrive`, ` /* String iteration: whole code POINTS (astral chars arrive`,
` * unsplit — the string iterator, not charAt). */`, ` * unsplit — the string iterator, not charAt). */`,
@@ -1246,9 +1244,8 @@ export function jsonWriteHelper(emitter: CEmitter, t: IrType): string {
d.push(` return scr_dyn_retain((ScrDyn *)d);`); d.push(` return scr_dyn_retain((ScrDyn *)d);`);
break; break;
case "bytes": case "bytes":
// `u as Uint8Array`: kind check, then retain the shared view. // Check the exact element brand before retaining the shared view.
if (t.elem !== "u8") throw new InternalCompilerError(`emitter bug: dynCheck of bytes<${t.elem}>`); d.push(` if (!scr_dyn_bytes_is(d, ${bytesElemKindC(t.elem)})) { scr_dyn_check_fail(path, ${want}, d); return NULL; }`);
d.push(` if (d->kind != SCR_DYN_BYTES) { scr_dyn_check_fail(path, ${want}, d); return NULL; }`);
d.push(` return scr_dyn_bytes_unbox(d);`); d.push(` return scr_dyn_bytes_unbox(d);`);
break; break;
case "object": case "object":
@@ -1572,9 +1569,7 @@ export function jsonWriteHelper(emitter: CEmitter, t: IrType): string {
d.push(` return ${dynFuncBoxHelper(emitter, t)}(v, NULL);`); d.push(` return ${dynFuncBoxHelper(emitter, t)}(v, NULL);`);
break; break;
case "bytes": case "bytes":
// bytes<u8> → a retained native view (the boundary // Preserve the view, its element brand, and its backing allocation.
// stance; stdin chunks into unknown-typed helpers).
if (t.elem !== "u8") throw new InternalCompilerError(`emitter bug: to-dyn of bytes<${t.elem}>`);
d.push(` return scr_dyn_new_bytes(v);`); d.push(` return scr_dyn_new_bytes(v);`);
break; break;
case "record": { case "record": {
+14 -1
View File
@@ -1,6 +1,6 @@
import { commentText, unsignedHex } from "../literals.js"; import { commentText, unsignedHex } from "../literals.js";
import { InternalCompilerError } from "../../errors.js"; import { InternalCompilerError } from "../../errors.js";
import type { IrFfiCallbackParamClass, IrFfiReturnClass, IrFfiValueParamClass } from "../../ir/ir.js"; import type { IrBytesElem, IrFfiCallbackParamClass, IrFfiReturnClass, IrFfiValueParamClass } from "../../ir/ir.js";
/** User-controlled text embedded after an LLVM `;` comment marker. Preserve /** User-controlled text embedded after an LLVM `;` comment marker. Preserve
* ordinary output byte-for-byte, but encode control and line-separator code * ordinary output byte-for-byte, but encode control and line-separator code
@@ -62,3 +62,16 @@ export function f64Lit(n: number): string {
} }
export const F64_INF = f64Lit(Infinity); export const F64_INF = f64Lit(Infinity);
/** Stable ScrBytesElem tags from scr_runtime.h. */
export const BYTES_ELEM_NUM: Record<IrBytesElem, number> = {
u8: 0,
u32: 1,
f32: 2,
i32: 3,
f64: 4,
i8: 5,
u16: 6,
i16: 7,
u8c: 8,
};
+27 -47
View File
@@ -1,4 +1,4 @@
import { f64Lit } from "./common.js"; import { BYTES_ELEM_NUM, f64Lit } from "./common.js";
import { InternalCompilerError } from "../../errors.js"; import { InternalCompilerError } from "../../errors.js";
/* The dyn (ScrDyn dyn) helper EMITTERS for the LLVM backend — the .ll /* The dyn (ScrDyn dyn) helper EMITTERS for the LLVM backend — the .ll
* mirror of walkers.ts's dyn slice: per-type match predicates * mirror of walkers.ts's dyn slice: per-type match predicates
@@ -389,10 +389,13 @@ export class LlDyn {
// An `unknown` target: every dyn value fits, undefined included. // An `unknown` target: every dyn value fits, undefined included.
B.terminate(`ret i1 true`); B.terminate(`ret i1 true`);
break; break;
case "bytes": case "bytes": {
if (t.elem !== "u8") throw new InternalCompilerError(`llvm emitter bug: dynMatch of bytes<${t.elem}>`); this.host.declare(`declare zeroext i1 @scr_dyn_bytes_is(ptr, i32)`);
kindIs(DYN_KIND.BYTES); const matched = B.tmp();
B.line(`${matched} = call zeroext i1 @scr_dyn_bytes_is(ptr %d, i32 ${BYTES_ELEM_NUM[t.elem]})`);
B.terminate(`ret i1 ${matched}`);
break; break;
}
case "func": case "func":
kindIs(DYN_KIND.FUNC); kindIs(DYN_KIND.FUNC);
break; break;
@@ -798,9 +801,16 @@ export class LlDyn {
break; break;
} }
case "bytes": { case "bytes": {
// `u as Uint8Array`: kind check, then retain the shared view. host.declare(`declare zeroext i1 @scr_dyn_bytes_is(ptr, i32)`);
if (t.elem !== "u8") throw new InternalCompilerError(`llvm emitter bug: dynCheck of bytes<${t.elem}>`); const matched = B.tmp();
requireKind(DYN_KIND.BYTES, "dc"); B.line(`${matched} = call zeroext i1 @scr_dyn_bytes_is(ptr %d, i32 ${BYTES_ELEM_NUM[t.elem]})`);
const yes = B.newLabel("dc.bytes");
const no = B.newLabel("dc.bytes.fail");
B.condBr(matched, yes, no);
B.startBlock(no);
B.line(`call void @scr_dyn_check_fail(ptr %path, ptr ${want}, ptr %d)`);
B.terminate(`ret ptr null`);
B.startBlock(yes);
host.declare(`declare ptr @scr_dyn_bytes_unbox(ptr)`); host.declare(`declare ptr @scr_dyn_bytes_unbox(ptr)`);
const r = B.tmp(); const r = B.tmp();
B.line(`${r} = call ptr @scr_dyn_bytes_unbox(ptr %d)`); B.line(`${r} = call ptr @scr_dyn_bytes_unbox(ptr %d)`);
@@ -1360,7 +1370,6 @@ export class LlDyn {
break; break;
} }
case "bytes": { case "bytes": {
if (t.elem !== "u8") throw new InternalCompilerError(`llvm emitter bug: to-dyn of bytes<${t.elem}>`);
host.declare(`declare ptr @scr_dyn_new_bytes(ptr)`); host.declare(`declare ptr @scr_dyn_new_bytes(ptr)`);
const r = B.tmp(); const r = B.tmp();
B.line(`${r} = call ptr @scr_dyn_new_bytes(ptr %v)`); B.line(`${r} = call ptr @scr_dyn_new_bytes(ptr %v)`);
@@ -2017,35 +2026,11 @@ export class LlDyn {
B.br(done); B.br(done);
B.startBlock(lJoin); B.startBlock(lJoin);
const bts = this.payloadOf(B, "%d", "ptr"); const bts = this.payloadOf(B, "%d", "ptr");
const blenp = B.tmp(); host.declare(`declare ptr @scr_bytes_join(ptr, ptr)`);
const blen = B.tmp(); const joined = B.tmp();
const bdatap = B.tmp(); B.line(`${joined} = call ptr @scr_bytes_join(ptr ${bts}, ptr ${host.internLiteral(",")})`);
const bdata = B.tmp(); this.putScrStr(B, "%b", joined);
B.line(`${blenp} = getelementptr inbounds i8, ptr ${bts}, i64 ${this.abiOffset(8, 4)} ; ->len`); B.line(`call void @scr_str_release(ptr ${joined})`);
B.line(`${blen} = load ${this.S}, ptr ${blenp}`);
B.line(`${bdatap} = getelementptr inbounds i8, ptr ${bts}, i64 ${this.abiOffset(24, 12)} ; ->data`);
B.line(`${bdata} = load ptr, ptr ${bdatap}`);
this.i64Loop(B, "ds.by", blen, (i) => {
const nz = B.tmp();
B.line(`${nz} = icmp ugt ${this.S} ${i}, 0`);
const lcm = B.newLabel("ds.bc");
const lv = B.newLabel("ds.bv");
B.condBr(nz, lcm, lv);
B.startBlock(lcm);
B.line(`call void @scr_jb_putc(ptr %b, i8 44)`);
B.br(lv);
B.startBlock(lv);
const cp = B.tmp();
const c = B.tmp();
const cd = B.tmp();
B.line(`${cp} = getelementptr inbounds i8, ptr ${bdata}, ${this.S} ${i}`);
B.line(`${c} = load i8, ptr ${cp}`);
B.line(`${cd} = uitofp i8 ${c} to double`);
const s = B.tmp();
B.line(`${s} = call ptr @scr_f64_to_scrstr(double ${cd})`);
this.putScrStr(B, "%b", s);
B.line(`call void @scr_str_release(ptr ${s})`);
});
B.br(done); B.br(done);
} }
B.startBlock(labels.get(DYN_KIND.FUNC)!); B.startBlock(labels.get(DYN_KIND.FUNC)!);
@@ -2814,7 +2799,7 @@ export class LlDyn {
B.line(`store ptr ${r2}, ptr ${itemSlot}`); B.line(`store ptr ${r2}, ptr ${itemSlot}`);
B.br(lPush); B.br(lPush);
} }
// BYTES: by byte. // BYTES: by numeric element.
B.startBlock(lBy); B.startBlock(lBy);
{ {
const bts = this.payloadOf(B, "%d", "ptr"); const bts = this.payloadOf(B, "%d", "ptr");
@@ -2828,17 +2813,12 @@ export class LlDyn {
const lMiss = B.newLabel("din.bm"); const lMiss = B.newLabel("din.bm");
B.condBr(inR, lHit, lMiss); B.condBr(inR, lHit, lMiss);
B.startBlock(lHit); B.startBlock(lHit);
const bdatap = B.tmp(); host.declare(`declare double @scr_bytes_get(ptr, double)`);
const bdata = B.tmp(); const index = B.tmp();
B.line(`${bdatap} = getelementptr inbounds i8, ptr ${bts}, i64 ${this.abiOffset(24, 12)} ; ->data`);
B.line(`${bdata} = load ptr, ptr ${bdatap}`);
const bp = B.tmp();
const bv = B.tmp();
const bd = B.tmp(); const bd = B.tmp();
const r = B.tmp(); const r = B.tmp();
B.line(`${bp} = getelementptr inbounds i8, ptr ${bdata}, ${host.sizeType} ${i}`); B.line(`${index} = uitofp ${host.sizeType} ${i} to double`);
B.line(`${bv} = load i8, ptr ${bp}`); B.line(`${bd} = call double @scr_bytes_get(ptr ${bts}, double ${index})`);
B.line(`${bd} = uitofp i8 ${bv} to double`);
B.line(`${r} = call ptr @scr_dyn_new_num(double ${bd})`); B.line(`${r} = call ptr @scr_dyn_new_num(double ${bd})`);
B.line(`store ptr ${r}, ptr ${itemSlot}`); B.line(`store ptr ${r}, ptr ${itemSlot}`);
B.br(lPush); B.br(lPush);
@@ -1,6 +1,6 @@
/* Focused LLVM expression emission extracted from emitter.ts. */ /* Focused LLVM expression emission extracted from emitter.ts. */
import { InternalCompilerError } from "../../errors.js"; import { InternalCompilerError } from "../../errors.js";
import { F64, type IrBytesElem, type IrExpr } from "../../ir/ir.js"; import { BYTES_ELEMENT_SIZE, F64, type IrBytesElem, type IrExpr } from "../../ir/ir.js";
import type { LlvmEmitterContext, LlValue } from "./expr-context.js"; import type { LlvmEmitterContext, LlValue } from "./expr-context.js";
import { F64_INF, f64Lit } from "./common.js"; import { F64_INF, f64Lit } from "./common.js";
@@ -128,8 +128,9 @@ export function emitBytesLength(host: LlvmEmitterContext, elem: IrBytesElem, rec
const len = B.tmp(); const len = B.tmp();
B.line(`${p} = getelementptr inbounds %ScrBytes, ptr ${receiver}, i64 0, i32 1`); B.line(`${p} = getelementptr inbounds %ScrBytes, ptr ${receiver}, i64 0, i32 1`);
B.line(`${len} = load ${host.sizeType}, ptr ${p}`); B.line(`${len} = load ${host.sizeType}, ptr ${p}`);
const count = bytes && elem !== "u8" ? B.tmp() : len; const size = BYTES_ELEMENT_SIZE[elem];
if (count !== len) B.line(`${count} = shl ${host.sizeType} ${len}, ${elem === "f64" ? 3 : 2}`); const count = bytes && size !== 1 ? B.tmp() : len;
if (count !== len) B.line(`${count} = shl ${host.sizeType} ${len}, ${Math.log2(size)}`);
const out = B.tmp(); const out = B.tmp();
B.line(`${out} = uitofp ${host.sizeType} ${count} to double`); B.line(`${out} = uitofp ${host.sizeType} ${count} to double`);
return { name: out, type: F64 }; return { name: out, type: F64 };
@@ -140,14 +141,16 @@ export function emitBytesGet(host: LlvmEmitterContext, elem: IrBytesElem, receiv
const idx = host.emitBytesIndex(receiver, index, integerIndex); const idx = host.emitBytesIndex(receiver, index, integerIndex);
const data = host.emitBytesData(receiver); const data = host.emitBytesData(receiver);
const p = B.tmp(); const p = B.tmp();
if (elem === "u8") { if (BYTES_ELEMENT_SIZE[elem] < 4) {
const bits = BYTES_ELEMENT_SIZE[elem] * 8;
const signed = elem === "i8" || elem === "i16";
const raw = B.tmp(); const raw = B.tmp();
const wide = B.tmp(); const wide = B.tmp();
const out = B.tmp(); const out = B.tmp();
B.line(`${p} = getelementptr inbounds i8, ptr ${data}, ${host.sizeType} ${idx}`); B.line(`${p} = getelementptr inbounds i${bits}, ptr ${data}, ${host.sizeType} ${idx}`);
B.line(`${raw} = load i8, ptr ${p}, align 1`); B.line(`${raw} = load i${bits}, ptr ${p}, align 1`);
B.line(`${wide} = zext i8 ${raw} to i32`); B.line(`${wide} = ${signed ? "sext" : "zext"} i${bits} ${raw} to i32`);
B.line(`${out} = uitofp i32 ${wide} to double`); B.line(`${out} = ${signed ? "sitofp" : "uitofp"} i32 ${wide} to double`);
return { name: out, type: F64 }; return { name: out, type: F64 };
} }
if (elem === "f32") { if (elem === "f32") {
@@ -249,14 +252,22 @@ export function emitToUint32(host: LlvmEmitterContext, value: string, expr?: IrE
export function emitBytesSet(host: LlvmEmitterContext, elem: IrBytesElem, receiver: string, index: string, value: string, integerIndex = false): void { export function emitBytesSet(host: LlvmEmitterContext, elem: IrBytesElem, receiver: string, index: string, value: string, integerIndex = false): void {
const B = host.B; const B = host.B;
const idx = host.emitBytesIndex(receiver, index, integerIndex); const idx = host.emitBytesIndex(receiver, index, integerIndex);
const stored = elem === "f32" || elem === "f64" ? null : host.emitToUint32(value); let stored: string | null;
if (elem === "u8c") {
host.declare(`declare double @scr_bytes_to_u8_clamp(double)`);
const clamped = B.tmp();
stored = B.tmp();
B.line(`${clamped} = call double @scr_bytes_to_u8_clamp(double ${value})`);
B.line(`${stored} = fptoui double ${clamped} to i32`);
} else stored = elem === "f32" || elem === "f64" ? null : host.emitToUint32(value);
const data = host.emitBytesData(receiver); const data = host.emitBytesData(receiver);
const p = B.tmp(); const p = B.tmp();
if (elem === "u8") { if (BYTES_ELEMENT_SIZE[elem] < 4) {
const bits = BYTES_ELEMENT_SIZE[elem] * 8;
const byte = B.tmp(); const byte = B.tmp();
B.line(`${byte} = trunc i32 ${stored!} to i8`); B.line(`${byte} = trunc i32 ${stored!} to i${bits}`);
B.line(`${p} = getelementptr inbounds i8, ptr ${data}, ${host.sizeType} ${idx}`); B.line(`${p} = getelementptr inbounds i${bits}, ptr ${data}, ${host.sizeType} ${idx}`);
B.line(`store i8 ${byte}, ptr ${p}, align 1`); B.line(`store i${bits} ${byte}, ptr ${p}, align 1`);
return; return;
} }
if (elem === "f32") { if (elem === "f32") {
@@ -2,6 +2,7 @@
import { InternalCompilerError } from "../../errors.js"; import { InternalCompilerError } from "../../errors.js";
import { streamTypedRefEligible } from "../../ir/analysis.js"; import { streamTypedRefEligible } from "../../ir/analysis.js";
import { DYN, isDynTypedRefType, isRefCounted, isUnitType, typeEquals, typeKey } from "../../ir/ir.js"; import { DYN, isDynTypedRefType, isRefCounted, isUnitType, typeEquals, typeKey } from "../../ir/ir.js";
import { BYTES_ELEM_NUM } from "./common.js";
import { DYN_KIND } from "./dyn.js"; import { DYN_KIND } from "./dyn.js";
import { elemAccess, vAdapters } from "./shapes.js"; import { elemAccess, vAdapters } from "./shapes.js";
import { LlvmUnsupportedError } from "./unsupported.js"; import { LlvmUnsupportedError } from "./unsupported.js";
@@ -494,7 +495,11 @@ export function emitDynamicExpr(host: LlvmEmitterContext, e: ExprOf<"dynFrom" |
// form also reads a scalar payload (the runtime's ToBoolean). // form also reads a scalar payload (the runtime's ToBoolean).
const d = host.emitExpr(e.value); const d = host.emitExpr(e.value);
let test: string; let test: string;
if (e.test === "truthy") { if (e.test === "bytes") {
host.declare(`declare zeroext i1 @scr_dyn_bytes_is(ptr, i32)`);
test = B.tmp();
B.line(`${test} = call zeroext i1 @scr_dyn_bytes_is(ptr ${d.name}, i32 ${BYTES_ELEM_NUM[e.bytesElem ?? "u8"]})`);
} else if (e.test === "truthy") {
host.declare(`declare zeroext i1 @scr_dyn_truthy(ptr)`); host.declare(`declare zeroext i1 @scr_dyn_truthy(ptr)`);
test = B.tmp(); test = B.tmp();
B.line(`${test} = call zeroext i1 @scr_dyn_truthy(ptr ${d.name})`); B.line(`${test} = call zeroext i1 @scr_dyn_truthy(ptr ${d.name})`);
@@ -1,21 +1,13 @@
/* Focused LLVM expression emission extracted from emitter.ts. */ /* Focused LLVM expression emission extracted from emitter.ts. */
import { InternalCompilerError } from "../../errors.js"; import { InternalCompilerError } from "../../errors.js";
import { matchStringSelfConcat, undefinedArmTag } from "../../ir/analysis.js"; import { matchStringSelfConcat, undefinedArmTag } from "../../ir/analysis.js";
import { isRefCounted, type IrBytesElem } from "../../ir/ir.js"; import { isRefCounted } from "../../ir/ir.js";
import { mangleRecordClone, mangleRecordNew } from "../mangle.js"; import { mangleRecordClone, mangleRecordNew } from "../mangle.js";
import { arrNewCall, elemAccess } from "./shapes.js"; import { arrNewCall, elemAccess } from "./shapes.js";
import { LlvmUnsupportedError } from "./unsupported.js"; import { LlvmUnsupportedError } from "./unsupported.js";
import type { LlvmEmitterContext, ExprOf, LlValue } from "./expr-context.js"; import type { LlvmEmitterContext, ExprOf, LlValue } from "./expr-context.js";
/** ScrBytesElem (scr_runtime.h): U8, U32, F32, I32, F64. */ import { BYTES_ELEM_NUM, f64Lit } from "./common.js";
const BYTES_ELEM_NUM: Record<IrBytesElem, number> = {
u8: 0,
u32: 1,
f32: 2,
i32: 3,
f64: 4,
};
import { f64Lit } from "./common.js";
export function emitLiteralExpr(host: LlvmEmitterContext, e: ExprOf<"numLit" | "boolLit" | "strLit" | "moduleNsRef" | "unitLit" | "varRef">): LlValue { export function emitLiteralExpr(host: LlvmEmitterContext, e: ExprOf<"numLit" | "boolLit" | "strLit" | "moduleNsRef" | "unitLit" | "varRef">): LlValue {
const B = host.B; const B = host.B;
@@ -30,6 +30,10 @@ export const LIB_FN_SYMS: Record<string, string> = {
"arrayBuffer.isView": "scr_array_buffer_is_view", "arrayBuffer.isView": "scr_array_buffer_is_view",
"arrayBuffer.byteLengthGetter": "scr_array_buffer_byte_length_getter", "arrayBuffer.byteLengthGetter": "scr_array_buffer_byte_length_getter",
"arrayBuffer.byteLengthDescriptor": "scr_array_buffer_byte_length_descriptor", "arrayBuffer.byteLengthDescriptor": "scr_array_buffer_byte_length_descriptor",
"arrayBuffer.viewU8C": "scr_array_buffer_view_u8c",
"arrayBuffer.viewI8": "scr_array_buffer_view_i8",
"arrayBuffer.viewU16": "scr_array_buffer_view_u16",
"arrayBuffer.viewI16": "scr_array_buffer_view_i16",
"arrayBuffer.viewU8": "scr_array_buffer_view_u8", "arrayBuffer.viewU8": "scr_array_buffer_view_u8",
"arrayBuffer.viewU32": "scr_array_buffer_view_u32", "arrayBuffer.viewU32": "scr_array_buffer_view_u32",
"arrayBuffer.viewI32": "scr_array_buffer_view_i32", "arrayBuffer.viewI32": "scr_array_buffer_view_i32",
@@ -517,6 +521,7 @@ export const LIB_FN_SYMS: Record<string, string> = {
// ambient-this read never throw. The fs dyn read is the sync-fs story. // ambient-this read never throw. The fs dyn read is the sync-fs story.
"fetch.start": "scr_fetch_static", "fetch.start": "scr_fetch_static",
"fetch.responseNew": "scr_fetch_response_new", "fetch.responseNew": "scr_fetch_response_new",
"fetch.responseArrayBuffer": "scr_fetch_response_array_buffer",
"fetch.responseJson": "scr_fetch_response_json", "fetch.responseJson": "scr_fetch_response_json",
"fetch.responseText": "scr_fetch_response_text", "fetch.responseText": "scr_fetch_response_text",
"fetch.responseBytes": "scr_fetch_response_bytes", "fetch.responseBytes": "scr_fetch_response_bytes",
@@ -500,12 +500,12 @@ export const NODE24_FETCH_COMPAT_PROFILE = {
facets: ["property-read"], facets: ["property-read"],
evidence: [fixture("static")], evidence: [fixture("static")],
})), })),
...["json", "text", "bytes"].map((member): FetchCompatOperation => ({ ...["json", "text", "bytes", "arrayBuffer"].map((member): FetchCompatOperation => ({
id: `stdlib.response.${member}`, id: `stdlib.response.${member}`,
name: `Response.${member}`, name: `Response.${member}`,
kind: "method", kind: "method",
facets: ["body-consumption", "promise-settlement", "state-machine", "error-shape"], facets: ["body-consumption", "promise-settlement", "state-machine", "error-shape"],
evidence: [fixture("static"), fixture("static-stream")], evidence: member === "arrayBuffer" ? [fixture("static")] : [fixture("static"), fixture("static-stream")],
})), })),
], ],
inventory: { inventory: {
@@ -711,13 +711,7 @@ export const NODE24_FETCH_COMPAT_PROFILE = {
...["url", "redirected", "status", "ok", "statusText", "headers", "body", "bodyUsed"].map( ...["url", "redirected", "status", "ok", "statusText", "headers", "body", "bodyUsed"].map(
(member) => staticEntry(`stdlib.response.${member}`, "Response", member, "prototype"), (member) => staticEntry(`stdlib.response.${member}`, "Response", member, "prototype"),
), ),
dynamicEntry( staticEntry("stdlib.response.arrayBuffer", "Response", "arrayBuffer", "prototype"),
"stdlib.response.arrayBuffer",
"Response",
"arrayBuffer",
"prototype",
"free-standing ArrayBuffer values have no static representation; use Response.bytes()",
),
...["clone", "blob", "formData"].map((member) => ...["clone", "blob", "formData"].map((member) =>
unsupportedEntry( unsupportedEntry(
`stdlib.response.${member}`, `stdlib.response.${member}`,
@@ -1,6 +1,6 @@
import { BUF_NUM_METHODS } from "./buffer-numeric-methods.js"; import { BUF_NUM_METHODS } from "./buffer-numeric-methods.js";
import * as ts from "../../ts7/adapter.js"; import * as ts from "../../ts7/adapter.js";
import { BIGINT_T, BOOL, BYTES_U8, DYN, F64, IrBytesElem, IrBytesIntrinsicMethod, IrExpr, IrType, STRING, SrcLoc, UNDEFINED_T, VOID, arrayOf, bytesOf, typeEquals } from "../../../ir/ir.js"; import { BYTES_ELEMENT_SIZE, BIGINT_T, BOOL, BYTES_U8, DYN, F64, IrBytesElem, IrBytesIntrinsicMethod, IrExpr, IrType, STRING, SrcLoc, UNDEFINED_T, VOID, arrayOf, bytesOf, typeEquals } from "../../../ir/ir.js";
import { locOf } from "../../program.js"; import { locOf } from "../../program.js";
import type { Lowerer } from "../lowerer.js"; import type { Lowerer } from "../lowerer.js";
import { dynUndefinedExpr, own } from "../lowerer.js"; import { dynUndefinedExpr, own } from "../lowerer.js";
@@ -97,17 +97,21 @@ function lowerBytesToSortedCall(
/* ── typed arrays / Buffer ─────────────────────────────────────────────── */ /* ── typed arrays / Buffer ─────────────────────────────────────────────── */
/** The typed-array constructors with a runtime representation, by lib /** The typed-array constructors with a runtime representation, by lib
* interface name. Other flavors (Int8Array, Float16Array, ...) fall through * interface name. Other flavors (Float16Array, BigInt64Array, ...) fall through
* to the generic stdlib-constructor fence. DataView is handled separately. */ * to the generic stdlib-constructor fence. DataView is handled separately. */
const BYTES_CTORS: Record<string, IrBytesElem | undefined> = { const BYTES_CTORS: Record<string, IrBytesElem | undefined> = {
Uint8Array: "u8", Uint8Array: "u8",
Uint8ClampedArray: "u8c",
Int8Array: "i8",
Uint16Array: "u16",
Int16Array: "i16",
Uint32Array: "u32", Uint32Array: "u32",
Int32Array: "i32", Int32Array: "i32",
Float32Array: "f32", Float32Array: "f32",
Float64Array: "f64", Float64Array: "f64",
}; };
/** `new Uint8Array(...)` / `new Uint32Array(...)` / `new Float32Array(...)` / `new Float64Array(...)` /** Numeric typed-array constructors
* (stdlib provenance — a user's own class with the name resolves through * (stdlib provenance — a user's own class with the name resolves through
* classBySymbol). Lowered argument shapes: none (empty), a length * classBySymbol). Lowered argument shapes: none (empty), a length
* (zero-filled; ToIndex at runtime — invalid lengths throw Node's * (zero-filled; ToIndex at runtime — invalid lengths throw Node's
@@ -116,7 +120,7 @@ const BYTES_CTORS: Record<string, IrBytesElem | undefined> = {
* literal (element-coerced; its contextual type is the lib's * literal (element-coerced; its contextual type is the lib's
* ArrayLike/Iterable union, which cannot map — the Set-seed pattern), or * ArrayLike/Iterable union, which cannot map — the Set-seed pattern), or
* a number[]-typed value, or checked native input with runtime length / * a number[]-typed value, or checked native input with runtime length /
* array-like dispatch. Free-standing ArrayBuffer forms remain fenced. * array-like dispatch. ArrayBuffer inputs create shared views.
* Null when this isn't a stdlib * Null when this isn't a stdlib
* typed-array construction. */ * typed-array construction. */
export function lowerBytesNew(lowerer: Lowerer, expr: ts.NewExpression, symbol: ts.Symbol | null | undefined): IrExpr | null { export function lowerBytesNew(lowerer: Lowerer, expr: ts.NewExpression, symbol: ts.Symbol | null | undefined): IrExpr | null {
@@ -174,7 +178,7 @@ export function lowerBytesNew(lowerer: Lowerer, expr: ts.NewExpression, symbol:
"SharedArrayBuffer here erases into the view: drop the options bag", "SharedArrayBuffer here erases into the view: drop the options bag",
); );
} }
const elemSize = elem === "u8" ? 1 : elem === "f64" ? 8 : 4; const elemSize = BYTES_ELEMENT_SIZE[elem];
const lenArg = argNode.arguments?.length === 1 ? argNode.arguments[0] : undefined; const lenArg = argNode.arguments?.length === 1 ? argNode.arguments[0] : undefined;
const lenT = lenArg ? lowerer.typeOf(lenArg) : null; const lenT = lenArg ? lowerer.typeOf(lenArg) : null;
const byteLen = lenT?.isNumberLiteralType() ? lenT.value : null; const byteLen = lenT?.isNumberLiteralType() ? lenT.value : null;
@@ -219,7 +223,7 @@ export function lowerBytesNew(lowerer: Lowerer, expr: ts.NewExpression, symbol:
`new ${name} over '${lowerer.fmt(src.type)}' values`, `new ${name} over '${lowerer.fmt(src.type)}' values`,
argNode, argNode,
`supported: new ${name}(), (length), (typedArray) — always a copy — or (number[]); ` + `supported: new ${name}(), (length), (typedArray) — always a copy — or (number[]); ` +
"ArrayBuffers and views do not exist here (narrow unions first)", "or (ArrayBuffer, byteOffset?, length?) for shared storage (narrow unions first)",
); );
} }
lowerer.noLowering( lowerer.noLowering(
@@ -260,7 +264,7 @@ function lowerArrayBufferView(lowerer: Lowerer, args: readonly ts.Expression[],
const node = args[index]; const node = args[index];
return node ? lowerer.coerceInto(node, lowerer.lowerExpr(node), DYN) : dynUndefinedExpr(loc); return node ? lowerer.coerceInto(node, lowerer.lowerExpr(node), DYN) : dynUndefinedExpr(loc);
}); });
const fn = { u8: "arrayBuffer.viewU8", u32: "arrayBuffer.viewU32", i32: "arrayBuffer.viewI32", f32: "arrayBuffer.viewF32", f64: "arrayBuffer.viewF64", dv: "arrayBuffer.viewDV" } as const; const fn = { u8c: "arrayBuffer.viewU8C", i8: "arrayBuffer.viewI8", u16: "arrayBuffer.viewU16", i16: "arrayBuffer.viewI16", u8: "arrayBuffer.viewU8", u32: "arrayBuffer.viewU32", i32: "arrayBuffer.viewI32", f32: "arrayBuffer.viewF32", f64: "arrayBuffer.viewF64", dv: "arrayBuffer.viewDV" } as const;
return { kind: "libCall", fn: fn[elem], args: values, type: elem === "dv" ? BYTES_U8 : bytesOf(elem), loc }; return { kind: "libCall", fn: fn[elem], args: values, type: elem === "dv" ? BYTES_U8 : bytesOf(elem), loc };
} }
@@ -318,9 +322,9 @@ export function lowerBytesMethodCall(lowerer: Lowerer, call: ts.CallExpression,
loc, loc,
}; };
} }
if (receiverIr.elem === "u8" && name === "join") { if (name === "join") {
if (nArgs > 1 || call.arguments.some(ts.isSpreadElement)) { if (nArgs > 1 || call.arguments.some(ts.isSpreadElement)) {
lowerer.noLowering(`.join with ${nArgs} arguments on Uint8Array`, call); lowerer.noLowering(`.join with ${nArgs} arguments on typed arrays`, call);
} }
const separatorDefault: IrExpr = { const separatorDefault: IrExpr = {
kind: "strLit", kind: "strLit",
@@ -158,10 +158,10 @@ function lowerOptionalNumberPredicate(
// arm runs the predicate; every other arm answers false after evaluating // arm runs the predicate; every other arm answers false after evaluating
// the argument once. This includes the undefined added by array reads. // the argument once. This includes the undefined added by array reads.
const widened = value.type; const widened = value.type;
if (widened.kind !== "union") return null; if (widened.kind !== "union" && widened.kind !== "dyn") return null;
const numberTag = lowerer.armTag(widened.unionId, F64); const numberTag = widened.kind === "union" ? lowerer.armTag(widened.unionId, F64) : -1;
if (numberTag < 0) return null; if (widened.kind === "union" && numberTag < 0) return null;
const key = `number.optionalPredicate:${fn}:${widened.unionId}`; const key = `number.optionalPredicate:${fn}:${typeKey(widened)}`;
let helper = lowerer.widthHelpers.get(key); let helper = lowerer.widthHelpers.get(key);
if (!helper) { if (!helper) {
helper = `%number.optionalPredicate.${lowerer.widthHelpers.size}`; helper = `%number.optionalPredicate.${lowerer.widthHelpers.size}`;
@@ -175,7 +175,9 @@ function lowerOptionalNumberPredicate(
body: [ body: [
{ {
kind: "if", kind: "if",
cond: { kind: "unionIsTag", unionId: widened.unionId, tag: numberTag, negated: true, value: input, type: BOOL, loc }, cond: widened.kind === "dyn"
? { kind: "dynTest", test: "number", negated: true, value: input, type: BOOL, loc }
: { kind: "unionIsTag", unionId: widened.unionId, tag: numberTag, negated: true, value: input, type: BOOL, loc },
then: [{ kind: "return", value: boolLit(false, loc), loc }], then: [{ kind: "return", value: boolLit(false, loc), loc }],
else_: null, else_: null,
loc, loc,
@@ -185,7 +187,9 @@ function lowerOptionalNumberPredicate(
value: { value: {
kind: "libCall", kind: "libCall",
fn, fn,
args: [{ kind: "unionNarrow", unionId: widened.unionId, tag: numberTag, value: input, type: F64, loc }], args: [widened.kind === "dyn"
? { kind: "dynCheck", value: input, type: F64, loc }
: { kind: "unionNarrow", unionId: widened.unionId, tag: numberTag, value: input, type: F64, loc }],
type: BOOL, type: BOOL,
loc, loc,
}, },
@@ -7,7 +7,7 @@ import * as ts from "../ts7/adapter.js";
import { bodyReadsArguments } from "../arguments-usage.js"; import { bodyReadsArguments } from "../arguments-usage.js";
import type { Lowerer } from "./lowerer.js"; import type { Lowerer } from "./lowerer.js";
import { lowerGenMethodCall } from "./lower-generators.js"; import { lowerGenMethodCall } from "./lower-generators.js";
import { BIGINT_T, BOOL, CAUGHT, DYN, F64, IrExpr, IrFunction, IrLocal, IrParam, IrStmt, IrType, JSVAL, STRING, SYMBOL_T, SrcLoc, UNDEFINED_T, VOID, arrayOf, canBoxFuncIntoDyn, canConvertToDyn, canDynCheckTo, canMarshalTypedFuncIntoIsland, ffiClassType, ffiSourceParamTypes, funcOf, isFfiCallbackParam, isFfiContextParam, isFfiReleaseParam, isUnitType, shapeHasAccessorSlots, typeEquals } from "../../ir/ir.js"; import { BYTES_ELEMENT_NAME, BIGINT_T, BOOL, CAUGHT, DYN, F64, IrExpr, IrFunction, IrLocal, IrParam, IrStmt, IrType, JSVAL, STRING, SYMBOL_T, SrcLoc, UNDEFINED_T, VOID, arrayOf, canBoxFuncIntoDyn, canConvertToDyn, canDynCheckTo, canMarshalTypedFuncIntoIsland, ffiClassType, ffiSourceParamTypes, funcOf, isFfiCallbackParam, isFfiContextParam, isFfiReleaseParam, isUnitType, shapeHasAccessorSlots, typeEquals } from "../../ir/ir.js";
import type { IrFfiCallbackParam, IrFfiCallbackParamClass, IrFfiImport, IrFfiReleaseParam } from "../../ir/ir.js"; import type { IrFfiCallbackParam, IrFfiCallbackParamClass, IrFfiImport, IrFfiReleaseParam } from "../../ir/ir.js";
import { isJsSourceFile, isNodeEsmFile, locOf, requireSpecOf } from "../program.js"; import { isJsSourceFile, isNodeEsmFile, locOf, requireSpecOf } from "../program.js";
import { genResultRecord, isGenericCallableMemberType, jsOpenObjectType, typeKey } from "../type-mapper.js"; import { genResultRecord, isGenericCallableMemberType, jsOpenObjectType, typeKey } from "../type-mapper.js";
@@ -5269,7 +5269,7 @@ function objectToStringTag(lowerer: Lowerer, type: IrType): string | null {
case "set": return "Set"; case "set": return "Set";
case "promise": return "Promise"; case "promise": return "Promise";
case "bytes": case "bytes":
return ({ u8: "Uint8Array", u32: "Uint32Array", i32: "Int32Array", f32: "Float32Array", f64: "Float64Array" })[type.elem]; return BYTES_ELEMENT_NAME[type.elem];
case "object": case "object":
return type.className === "%Error" || lowerer.isSubclassOf(type.className, "%Error") ? "Error" : null; return type.className === "%Error" || lowerer.isSubclassOf(type.className, "%Error") ? "Error" : null;
default: return null; default: return null;
@@ -3148,6 +3148,9 @@ export function lowerArrayOfCall(lowerer: Lowerer, call: ts.CallExpression,
} }
return { kind: "call", callee: helper, args: [src, fnArg], type: arrayOf(outElem), loc }; return { kind: "call", callee: helper, args: [src, fnArg], type: arrayOf(outElem), loc };
} }
if (src.type.kind === "bytes" && args.length === 1) {
return { kind: "bytesIntrinsic", method: "toArray", receiver: src, args: [], type: arrayOf(F64), loc };
}
if (src.type.kind === "set" && args.length === 1) { if (src.type.kind === "set" && args.length === 1) {
return { kind: "setIntrinsic", method: "toArray", receiver: src, args: [], type: arrayOf(src.type.elem), loc }; return { kind: "setIntrinsic", method: "toArray", receiver: src, args: [], type: arrayOf(src.type.elem), loc };
} }
@@ -11,7 +11,7 @@ import { pathToFileURL } from "node:url";
import type { Lowerer } from "./lowerer.js"; import type { Lowerer } from "./lowerer.js";
import { OBJECT_CALLABLE_VALUES } from "./surfaces.js"; import { OBJECT_CALLABLE_VALUES } from "./surfaces.js";
import { wasiGuestPath } from "../../wasi-paths.js"; import { wasiGuestPath } from "../../wasi-paths.js";
import { BIGINT_T, BOOL, CAUGHT, DYN, DYN_HANDLE_KINDS, F64, IrExpr, IrFunction, IrJsOp, IrLocal, IrRecordShape, IrStmt, IrType, JSVAL, NULL_T, REF_TRUTHY_KINDS, REGEX, RUNTIME_ERROR_CLASSES, SEARCH_PARAMS_T, STRING, SrcLoc, UNDEFINED_T, VOID, arrayOf, canAdaptDynFuncTo, canDynCheckTo, canBoxFuncIntoDyn, funcOf, isDynTypedRefType, isSupportedArrayElem, isUnitType, jsOpResultKind, shapeHasAccessorSlots, typeEquals, typeKey, unionContainerArmsOk } from "../../ir/ir.js"; import { BIGINT_T, BYTES_ELEMENT_NAME, BOOL, CAUGHT, DYN, DYN_HANDLE_KINDS, F64, IrBytesElem, IrExpr, IrFunction, IrJsOp, IrLocal, IrRecordShape, IrStmt, IrType, JSVAL, NULL_T, REF_TRUTHY_KINDS, REGEX, RUNTIME_ERROR_CLASSES, SEARCH_PARAMS_T, STRING, SrcLoc, UNDEFINED_T, VOID, arrayOf, canAdaptDynFuncTo, canDynCheckTo, canBoxFuncIntoDyn, funcOf, isDynTypedRefType, isSupportedArrayElem, isUnitType, jsOpResultKind, shapeHasAccessorSlots, typeEquals, typeKey, unionContainerArmsOk } from "../../ir/ir.js";
import { cjsClassExprWholeExportOf, cjsExportAssignmentOf, cjsExportDiscardReason, isCjsExportTableLiteral, isCjsJsFile, isJsSourceFile, isModuleExportsAccess, isNodeEsmFile, locOf } from "../program.js"; import { cjsClassExprWholeExportOf, cjsExportAssignmentOf, cjsExportDiscardReason, isCjsExportTableLiteral, isCjsJsFile, isJsSourceFile, isModuleExportsAccess, isNodeEsmFile, locOf } from "../program.js";
import { ARRAY_METHODS, builtinConstLit, builtinFenceHintOf, builtinModuleConstOf, builtinModulesArrayLit, builtinModuleFnOf, COMPOUND_ASSIGN_OPS, CompoundOp, ISLAND_SURFACE, isChildSurfaceMember, MAP_METHODS, NARROW_FIRST, SET_METHODS, STRING_INDEX_METHODS, STR_METHODS, UNSUPPORTED_EXPR, sideEffectFreeOptionValue, stdlibGlobalNameOf } from "./surfaces.js"; import { ARRAY_METHODS, builtinConstLit, builtinFenceHintOf, builtinModuleConstOf, builtinModulesArrayLit, builtinModuleFnOf, COMPOUND_ASSIGN_OPS, CompoundOp, ISLAND_SURFACE, isChildSurfaceMember, MAP_METHODS, NARROW_FIRST, SET_METHODS, STRING_INDEX_METHODS, STR_METHODS, UNSUPPORTED_EXPR, sideEffectFreeOptionValue, stdlibGlobalNameOf } from "./surfaces.js";
import { UNSUPPORTED, blockedBindingUseDiag, requiresDynamicPackageDiag, unsupportedDiag } from "../../diagnostics/diagnostic.js"; import { UNSUPPORTED, blockedBindingUseDiag, requiresDynamicPackageDiag, unsupportedDiag } from "../../diagnostics/diagnostic.js";
@@ -2505,10 +2505,9 @@ function lowerExprInner(lowerer: Lowerer, expr: ts.Expression): IrExpr {
) { ) {
return { kind: "dynCheck", value: expr, type: narrowed, loc: expr.loc }; return { kind: "dynCheck", value: expr, type: narrowed, loc: expr.loc };
} }
// An `instanceof Uint8Array` narrow: the checked-dynamic tree's bytes kind, extracted // An instanceof narrow retains the native view after validating its
// with the same validated copy the checked cast uses (a Buffer that // exact element kind. Buffer also matches Uint8Array.
// crossed in rides the kind too — it IS a Uint8Array in Node). if (narrowed?.kind === "bytes") {
if (narrowed?.kind === "bytes" && narrowed.elem === "u8") {
return { kind: "dynCheck", value: expr, type: narrowed, loc: expr.loc }; return { kind: "dynCheck", value: expr, type: narrowed, loc: expr.loc };
} }
// An `instanceof Error` narrow: the checked-dynamic tree's error encoding rebuilds a // An `instanceof Error` narrow: the checked-dynamic tree's error encoding rebuilds a
@@ -3963,7 +3962,7 @@ function lowerPromiseThenPresence(
// A native checked iterable can supply scalar elements even when // A native checked iterable can supply scalar elements even when
// the checker inferred a typed array from its producer. Drain once // the checker inferred a typed array from its producer. Drain once
// and validate the elements before the ordinary spread copy. // and validate the elements before the ordinary spread copy.
if (src.type.kind === "dyn" && (type.elem.kind === "f64" || type.elem.kind === "string" || type.elem.kind === "bool")) { if (src.type.kind === "dyn" && (type.elem.kind === "dyn" || type.elem.kind === "f64" || type.elem.kind === "string" || type.elem.kind === "bool")) {
src = lowerer.coerceInto(el.expression, { src = lowerer.coerceInto(el.expression, {
kind: "libCall", fn: "dyn.iterPack", args: [src, kind: "libCall", fn: "dyn.iterPack", args: [src,
{ kind: "strLit", value: el.expression.getText(), type: STRING, loc: locOf(el) }], { kind: "strLit", value: el.expression.getText(), type: STRING, loc: locOf(el) }],
@@ -5548,6 +5547,12 @@ export function ensureString(lowerer: Lowerer, e: IrExpr, node: ts.Node): IrExpr
// strings verbatim, arrays via join, objects as "[object Object]"). // strings verbatim, arrays via join, objects as "[object Object]").
return { kind: "toString", operand: e, type: STRING, loc: e.loc }; return { kind: "toString", operand: e, type: STRING, loc: e.loc };
} }
if (e.type.kind === "bytes") {
// Preserve the runtime brand: typed arrays join numeric elements,
// while a Buffer stored in a Uint8Array slot decodes its bytes.
const boxed: IrExpr = { kind: "dynFrom", value: e, type: DYN, loc: e.loc };
return { kind: "toString", operand: boxed, type: STRING, loc: e.loc };
}
if (e.type.kind === "jsval") { if (e.type.kind === "jsval") {
// String(v) in the engine — JS-exact (and Node-exact in templates). // String(v) in the engine — JS-exact (and Node-exact in templates).
return { kind: "jsOp", op: "toStr", args: [e], type: STRING, loc: e.loc }; return { kind: "jsOp", op: "toStr", args: [e], type: STRING, loc: e.loc };
@@ -7942,22 +7947,22 @@ export function lowerBinary(lowerer: Lowerer, expr: ts.BinaryExpression): IrExpr
lowerer.builtinStreamInfoOf(rhsMemberSymbol) ?? lowerer.builtinStreamInfoOf(rhsMemberSymbol) ??
undefined; undefined;
if (!target) { if (!target) {
// `u instanceof Uint8Array` on an `unknown` value: the checked-dynamic tree carries a // Unknown storage retains each numeric typed array's exact native brand.
// bytes kind — one runtime tag test, and tsc's narrowing types the if (ts.isIdentifier(expr.right) && !lowerer.caughtLocalOf(expr.left)) {
// true branch (reads bridge through maybeNarrow's validated const elem = (Object.keys(BYTES_ELEMENT_NAME) as IrBytesElem[]).find(
// extraction, like the typeof tests). Node's Buffer IS a Uint8Array (kind) => lowerer.isStdlibGlobal(expr.right, BYTES_ELEMENT_NAME[kind]),
// subclass and rides the same bytes kind, so both worlds answer true );
// for Buffer payloads — Node-exact (the bytes kind's other if (elem !== undefined) {
// divergences are SEMANTICS.md 45). Catch bindings stay out (their const left = lowerer.lowerExpr(expr.left);
// payload is a typed snapshot, not a dyn). if (left.type.kind === "dyn") {
if ( return { kind: "dynTest", test: "bytes", bytesElem: elem, value: left, type: BOOL, loc };
ts.isIdentifier(expr.right) && }
lowerer.isStdlibGlobal(expr.right, "Uint8Array") && if (left.type.kind === "bytes") {
!lowerer.caughtLocalOf(expr.left) return {
) { kind: "seqExpr", stmts: [{ kind: "exprStmt", expr: left, loc }],
const left = lowerer.lowerExpr(expr.left); result: { kind: "boolLit", value: left.type.elem === elem, type: BOOL, loc }, type: BOOL, loc,
if (left.type.kind === "dyn") { };
return { kind: "dynTest", test: "bytes", value: left, type: BOOL, loc }; }
} }
} }
// `x instanceof RegExp` over a union with a regex arm (the // `x instanceof RegExp` over a union with a regex arm (the
@@ -875,14 +875,6 @@ export function lowerStaticResponseCall(lowerer: Lowerer, call: ts.CallExpressio
const recvType = lowerer.checker.getBaseTypeOfLiteralType(lowerer.typeOf(access.expression)); const recvType = lowerer.checker.getBaseTypeOfLiteralType(lowerer.typeOf(access.expression));
const sym = recvType.getAliasSymbol() ?? recvType.getSymbol(); const sym = recvType.getAliasSymbol() ?? recvType.getSymbol();
if (!sym || sym.name !== "Response" || !lowerer.isStdlibSymbol(sym)) return null; if (!sym || sym.name !== "Response" || !lowerer.isStdlibSymbol(sym)) return null;
if (member === "arrayBuffer") {
lowerer.noLowering(
"Response.arrayBuffer() in a static build",
call,
"use Response.bytes() for the native Uint8Array body; free-standing ArrayBuffer values have no static representation",
sym,
);
}
const recv = lowerer.lowerExpr(access.expression); const recv = lowerer.lowerExpr(access.expression);
if (recv.type.kind !== "dyn") return null; if (recv.type.kind !== "dyn") return null;
return lowerStaticFixedFetchMethodCall( return lowerStaticFixedFetchMethodCall(
@@ -906,7 +898,7 @@ export function lowerStaticResponseCall(lowerer: Lowerer, call: ts.CallExpressio
} }
: { : {
kind: "libCall", kind: "libCall",
fn: "fetch.responseJson", fn: member === "arrayBuffer" ? "fetch.responseArrayBuffer" : "fetch.responseJson",
args: [receiver], args: [receiver],
type: { kind: "promise", inner: DYN }, type: { kind: "promise", inner: DYN },
loc: locOf(call), loc: locOf(call),
@@ -1212,7 +1204,7 @@ export function fenceStaticResponseMember(
lowerer.noLowering( lowerer.noLowering(
`Response.${member} in a static build`, `Response.${member} in a static build`,
access, access,
"the native static Response surface is status/ok/statusText/url/redirected/headers/body/bodyUsed plus json(), text(), and bytes(); use --dynamic for the wider Web API", "the native static Response surface is status/ok/statusText/url/redirected/headers/body/bodyUsed plus json(), text(), bytes(), and arrayBuffer(); use --dynamic for the wider Web API",
sym, sym,
); );
} }
@@ -1008,6 +1008,7 @@ export const BUILTIN_MODULE_FN_ALIASES: Record<string, Record<string, readonly I
readFileSync: ["fs.readFileSyncBuf", "fs.readFileSyncBytes", "fs.readFileSyncDyn", "fs.readFdSync", "fs.readFdSyncBytes"], readFileSync: ["fs.readFileSyncBuf", "fs.readFileSyncBytes", "fs.readFileSyncDyn", "fs.readFdSync", "fs.readFdSyncBytes"],
// The bytes-data form and the { mode } options form. // The bytes-data form and the { mode } options form.
writeFileSync: ["fs.writeFileSyncBytes", "fs.writeFileModeSync"], writeFileSync: ["fs.writeFileSyncBytes", "fs.writeFileModeSync"],
appendFileSync: ["fs.appendFileSyncBytes"],
// The utf8 string overload; the table row is the Buffer-window form. // The utf8 string overload; the table row is the Buffer-window form.
writeSync: ["fs.writeStrSync"], writeSync: ["fs.writeStrSync"],
// The { recursive, mode } option lowerings. // The { recursive, mode } option lowerings.
@@ -2,7 +2,7 @@ import { InternalCompilerError } from "../errors.js";
import * as ts from "./ts7/adapter.js"; import * as ts from "./ts7/adapter.js";
import { bodyReadsArguments } from "./arguments-usage.js"; import { bodyReadsArguments } from "./arguments-usage.js";
import type { IrRecordShape, IrType, IrUnionDef, IrUnionDiscriminant } from "../ir/ir.js"; import type { IrRecordShape, IrType, IrUnionDef, IrUnionDiscriminant } from "../ir/ir.js";
import { arrayOf, BOOL, bytesOf, canConvertToDyn, CHILD_T, CRYPTOHASH_T, CRYPTOHMAC_T, DATE_T, DYN, F64, funcOf, isSupportedArrayElem, isSupportedIndexValue, isSupportedMapKey, isSupportedMapValue, isSupportedSetElem, isUnitType, JSVAL, mapOf, NULL_T, PROCSTREAM_T, RUNTIME_EMITTER_CLASS, RUNTIME_ERROR_CLASSES, RUNTIME_STREAM_CLASSES, setOf, STRING, SYMBOL_T, typeEquals, typeKey, unionContainerArmsOk, UNDEFINED_T, VOID } from "../ir/ir.js"; import { BYTES_ELEMENT_NAME, arrayOf, BOOL, bytesOf, canConvertToDyn, CHILD_T, CRYPTOHASH_T, CRYPTOHMAC_T, DATE_T, DYN, F64, funcOf, isSupportedArrayElem, isSupportedIndexValue, isSupportedMapKey, isSupportedMapValue, isSupportedSetElem, isUnitType, JSVAL, mapOf, NULL_T, PROCSTREAM_T, RUNTIME_EMITTER_CLASS, RUNTIME_ERROR_CLASSES, RUNTIME_STREAM_CLASSES, setOf, STRING, SYMBOL_T, typeEquals, typeKey, unionContainerArmsOk, UNDEFINED_T, VOID } from "../ir/ir.js";
import { BIGINT_T } from "../ir/ir.js"; import { BIGINT_T } from "../ir/ir.js";
import { isJsSourceFile, isNodeTypesPath } from "./program.js"; import { isJsSourceFile, isNodeTypesPath } from "./program.js";
@@ -485,7 +485,7 @@ export function formatIrType(t: IrType, shapes: ShapeRegistry, unions: UnionRegi
case "bytes": case "bytes":
// The u8 kind reads as Uint8Array (Buffer maps here too — one // The u8 kind reads as Uint8Array (Buffer maps here too — one
// runtime representation; the message stays honest either way). // runtime representation; the message stays honest either way).
return append(t.elem === "u8" ? "Uint8Array" : t.elem === "u32" ? "Uint32Array" : t.elem === "i32" ? "Int32Array" : t.elem === "f32" ? "Float32Array" : "Float64Array"); return append(BYTES_ELEMENT_NAME[t.elem]);
case "map": case "map":
append("Map<"); append("Map<");
child(t.key); child(t.key);
@@ -1859,6 +1859,10 @@ function mapTypeInner(type: ts.Type, ctx: TypeMapperCtx): IrType | null {
if (isStdlibInterface("ArrayBuffer") || isStdlibInterface("PropertyDescriptor") || isStdlibInterface("ProcessVersions")) return DYN; if (isStdlibInterface("ArrayBuffer") || isStdlibInterface("PropertyDescriptor") || isStdlibInterface("ProcessVersions")) return DYN;
if (isStdlibInterface("Uint8Array")) return bytesOf("u8"); if (isStdlibInterface("Uint8Array")) return bytesOf("u8");
if (isStdlibInterface("Uint32Array")) return bytesOf("u32"); if (isStdlibInterface("Uint32Array")) return bytesOf("u32");
if (isStdlibInterface("Uint8ClampedArray")) return bytesOf("u8c");
if (isStdlibInterface("Int8Array")) return bytesOf("i8");
if (isStdlibInterface("Uint16Array")) return bytesOf("u16");
if (isStdlibInterface("Int16Array")) return bytesOf("i16");
// Int32Array: the signed 32-bit kind (element reads sign-extend, writes // Int32Array: the signed 32-bit kind (element reads sign-extend, writes
// ToInt32-wrap) — the Atomics.wait sleep idiom constructs one over a // ToInt32-wrap) — the Atomics.wait sleep idiom constructs one over a
// SharedArrayBuffer, and the i32 semantics hold for every other use. // SharedArrayBuffer, and the i32 semantics hold for every other use.
+2 -1
View File
@@ -1,5 +1,6 @@
import { InternalCompilerError } from "../errors.js"; import { InternalCompilerError } from "../errors.js";
import { import {
BYTES_ELEMENT_NAME,
DYN_HANDLE_KINDS, DYN_HANDLE_KINDS,
RUNTIME_STREAM_CLASSES, RUNTIME_STREAM_CLASSES,
typeEquals, typeEquals,
@@ -72,7 +73,7 @@ export function dynDesc(
case "nullT": return "null"; case "nullT": return "null";
case "undefinedT": return "undefined"; case "undefinedT": return "undefined";
case "dyn": return "unknown"; case "dyn": return "unknown";
case "bytes": return "Uint8Array"; case "bytes": return BYTES_ELEMENT_NAME[t.elem];
case "object": return t.className.replace(/^%/, ""); case "object": return t.className.replace(/^%/, "");
case "union": { case "union": {
const def = unionsById.get(t.unionId); const def = unionsById.get(t.unionId);
+16 -1
View File
@@ -1,5 +1,5 @@
import { describe, expect, test } from "vitest"; import { describe, expect, test } from "vitest";
import { DYN, F64, HANDLE_KINDS, canDynCheckTo, isIslandCallbackParamType, isJsonSafeType, isJsonStringifySafeType, isJsonStringifyDynamicType, type IrRecordShape, type IrType, type IrUnionDef, POINTER_KINDS, STRING, arrayOf, typeEquals, typeKey } from "./ir.js"; import { DYN, F64, HANDLE_KINDS, BYTES_ELEMENT_NAME, bytesOf, canConvertToDyn, canDynCheckTo, type IrBytesElem, isIslandCallbackParamType, isJsonSafeType, isJsonStringifySafeType, isJsonStringifyDynamicType, type IrRecordShape, type IrType, type IrUnionDef, POINTER_KINDS, STRING, arrayOf, typeEquals, typeKey } from "./ir.js";
describe("IR kind sets", () => { describe("IR kind sets", () => {
test("keeps procStream as the scalar handle exception", () => { test("keeps procStream as the scalar handle exception", () => {
@@ -88,3 +88,18 @@ describe("checked records with opaque payloads", () => {
expect(isJsonStringifyDynamicType(arrayOf({ kind: "union", unionId: "optional" }), record, union)).toBe(true); expect(isJsonStringifyDynamicType(arrayOf({ kind: "union", unionId: "optional" }), record, union)).toBe(true);
}); });
}); });
describe("native typed-array boundaries", () => {
test.each(Object.keys(BYTES_ELEMENT_NAME) as IrBytesElem[])("preserves %s in checked records without a JSON/island claim", (elem) => {
const view = bytesOf(elem);
const record = (id: string): IrRecordShape | undefined => id === "views"
? { id, fields: [{ name: "values", type: arrayOf(view) }] } : undefined;
const union = () => undefined;
const shape: IrType = { kind: "record", shapeId: "views" };
expect(canConvertToDyn(shape, record, union)).toBe(true);
expect(canDynCheckTo(shape, record, union)).toBe(true);
expect(isJsonSafeType(shape, record, union)).toBe(false);
expect(isIslandCallbackParamType(shape, record, union)).toBe(false);
});
});
+40 -25
View File
@@ -24,10 +24,18 @@ export interface SrcLoc {
/* ── types ─────────────────────────────────────────────────────────────── */ /* ── types ─────────────────────────────────────────────────────────────── */
/** Typed-array element kinds with a runtime representation: Uint8Array/Buffer, /** Numeric typed-array element kinds with a native representation. Float16
* Uint32Array, Int32Array, Float32Array, and Float64Array. Other flavors stay * and BigInt typed arrays remain frontend-fenced. */
* frontend-fenced. */ export type IrBytesElem = "u8" | "u8c" | "i8" | "u16" | "i16" | "u32" | "i32" | "f32" | "f64";
export type IrBytesElem = "u8" | "u32" | "i32" | "f32" | "f64";
export const BYTES_ELEMENT_SIZE: Record<IrBytesElem, number> = {
u8: 1, u8c: 1, i8: 1, u16: 2, i16: 2, u32: 4, i32: 4, f32: 4, f64: 8,
};
export const BYTES_ELEMENT_NAME: Record<IrBytesElem, string> = {
u8: "Uint8Array", u8c: "Uint8ClampedArray", i8: "Int8Array", u16: "Uint16Array", i16: "Int16Array",
u32: "Uint32Array", i32: "Int32Array", f32: "Float32Array", f64: "Float64Array",
};
export type IrType = export type IrType =
| { kind: "f64" } | { kind: "f64" }
@@ -1992,6 +2000,10 @@ export type IrLibFn =
| "arrayBuffer.isView" | "arrayBuffer.isView"
| "arrayBuffer.byteLengthGetter" | "arrayBuffer.byteLengthGetter"
| "arrayBuffer.byteLengthDescriptor" | "arrayBuffer.byteLengthDescriptor"
| "arrayBuffer.viewU8C"
| "arrayBuffer.viewI8"
| "arrayBuffer.viewU16"
| "arrayBuffer.viewI16"
| "arrayBuffer.viewU8" | "arrayBuffer.viewU8"
| "arrayBuffer.viewU32" | "arrayBuffer.viewU32"
| "arrayBuffer.viewI32" | "arrayBuffer.viewI32"
@@ -2004,6 +2016,7 @@ export type IrLibFn =
* are opaque checked-dynamic handles. */ * are opaque checked-dynamic handles. */
| "fetch.start" | "fetch.start"
| "fetch.responseNew" | "fetch.responseNew"
| "fetch.responseArrayBuffer"
| "fetch.responseJson" | "fetch.responseJson"
| "fetch.responseText" | "fetch.responseText"
| "fetch.responseBytes" | "fetch.responseBytes"
@@ -5359,9 +5372,9 @@ export type IrExpr =
* control flow understands on `unknown`: `typeof v === "string" | * control flow understands on `unknown`: `typeof v === "string" |
* "number" | "boolean" | "undefined"` and the unit comparisons `v === * "number" | "boolean" | "undefined"` and the unit comparisons `v ===
* undefined` / `v === null` (`"nullish"` is the LOOSE `v == null` pair — * undefined` / `v === null` (`"nullish"` is the LOOSE `v == null` pair —
* undefined or null in one test), and `v instanceof Uint8Array` * undefined or null in one test), and `v instanceof TypedArray`
* (`"bytes"` — the checked-dynamic tree's bytes kind; Node's Buffer IS a Uint8Array * (`"bytes"` plus bytesElem, default u8; Node's Buffer is a Uint8Array
* subclass and both worlds answer true for it, SEMANTICS.md 45), plus * subclass), plus
* the two object-family tests: `"object"` is `typeof v === "object"` * the two object-family tests: `"object"` is `typeof v === "object"`
* exactly (true for the checked-dynamic tree's object, array, bytes, AND null kinds — * exactly (true for the checked-dynamic tree's object, array, bytes, AND null kinds —
* JS's oldest wart preserved), `"array"` is `Array.isArray(v)` (the * JS's oldest wart preserved), `"array"` is `Array.isArray(v)` (the
@@ -5383,7 +5396,7 @@ export type IrExpr =
* "function"` — true exactly for the checked-dynamic tree's function kind (boxed * "function"` — true exactly for the checked-dynamic tree's function kind (boxed
* closures); function values are truthy and answer FALSE to the * closures); function values are truthy and answer FALSE to the
* `"object"` test, JS-exact. */ * `"object"` test, JS-exact. */
| { kind: "dynTest"; test: "string" | "number" | "boolean" | "undefined" | "null" | "nullish" | "bytes" | "buffer" | "object" | "array" | "truthy" | "error" | "function"; negated?: true; value: IrExpr; type: IrType; loc: SrcLoc } | { kind: "dynTest"; test: "string" | "number" | "boolean" | "undefined" | "null" | "nullish" | "bytes" | "buffer" | "object" | "array" | "truthy" | "error" | "function"; bytesElem?: IrBytesElem; negated?: true; value: IrExpr; type: IrType; loc: SrcLoc }
/** Keyed read on a dyn value — `pkg.name` / `pkg["k"]` / the /** Keyed read on a dyn value — `pkg.name` / `pkg["k"]` / the
* `pkg?.scripts` chain step on a JSON.parse result. `key` is * `pkg?.scripts` chain step on a JSON.parse result. `key` is
* string-typed (a strLit for the dot form); `type` is always dyn. An * string-typed (a strLit for the dot form); `type` is always dyn. An
@@ -5889,18 +5902,18 @@ function isJsonSafeAt(
stringify: boolean, stringify: boolean,
undefinedAllowed: boolean, undefinedAllowed: boolean,
visiting: Set<string>, visiting: Set<string>,
dynFields = false, nativeFields = false,
): boolean { ): boolean {
if (HANDLE_KINDS.has(t.kind)) return false; if (HANDLE_KINDS.has(t.kind)) return false;
switch (t.kind) { switch (t.kind) {
case "dyn": case "dyn":
return dynFields; return nativeFields;
case "f64": case "f64":
case "string": case "string":
case "bool": case "bool":
return true; return true;
case "array": case "array":
return isJsonSafeAt(t.elem, getRecord, getUnion, stringify, stringify, visiting, dynFields); return isJsonSafeAt(t.elem, getRecord, getUnion, stringify, stringify, visiting, nativeFields);
case "record": { case "record": {
const shape = getRecord(t.shapeId); const shape = getRecord(t.shapeId);
if (!shape) return false; if (!shape) return false;
@@ -5910,7 +5923,7 @@ function isJsonSafeAt(
// short-circuits every `every` up the walk). // short-circuits every `every` up the walk).
if (visiting.has(t.shapeId)) return true; if (visiting.has(t.shapeId)) return true;
visiting.add(t.shapeId); visiting.add(t.shapeId);
if (!shape.fields.every((f) => isJsonSafeAt(f.type, getRecord, getUnion, stringify, !shape.tuple || stringify, visiting, dynFields))) { if (!shape.fields.every((f) => isJsonSafeAt(f.type, getRecord, getUnion, stringify, !shape.tuple || stringify, visiting, nativeFields))) {
return false; return false;
} }
// Overflow values sit in record-key position too: dyn is JSON-safe // Overflow values sit in record-key position too: dyn is JSON-safe
@@ -5918,7 +5931,7 @@ function isJsonSafeAt(
// like any undefined-valued key), everything else follows the // like any undefined-valued key), everything else follows the
// record-field rule. // record-field rule.
if (shape.indexValue && shape.indexValue.kind !== "dyn") { if (shape.indexValue && shape.indexValue.kind !== "dyn") {
return isJsonSafeAt(shape.indexValue, getRecord, getUnion, stringify, true, visiting, dynFields); return isJsonSafeAt(shape.indexValue, getRecord, getUnion, stringify, true, visiting, nativeFields);
} }
return true; return true;
} }
@@ -5928,8 +5941,10 @@ function isJsonSafeAt(
const key = `${t.unionId}:${stringify}:${undefinedAllowed}`; const key = `${t.unionId}:${stringify}:${undefinedAllowed}`;
if (visiting.has(key)) return true; // the recursive knot, union-flavored if (visiting.has(key)) return true; // the recursive knot, union-flavored
visiting.add(key); visiting.add(key);
return def.arms.every((a) => a.kind === "undefinedT" ? undefinedAllowed : isJsonSafeAt(a, getRecord, getUnion, stringify, undefinedAllowed, visiting, dynFields)); return def.arms.every((a) => a.kind === "undefinedT" ? undefinedAllowed : isJsonSafeAt(a, getRecord, getUnion, stringify, undefinedAllowed, visiting, nativeFields));
} }
case "bytes":
return nativeFields;
case "func": case "func":
case "object": case "object":
// Class values stringify as "{}" husks in Node (own enumerable statics // Class values stringify as "{}" husks in Node (own enumerable statics
@@ -5954,10 +5969,6 @@ function isJsonSafeAt(
// omitted as object values) — silent divergence banned; rejected. // omitted as object values) — silent divergence banned; rejected.
case "symbol": case "symbol":
case "bigint": case "bigint":
// Typed arrays stringify as index-keyed objects ({"0":1,...}) and
// Buffers as {type:"Buffer",data:[...]} in Node — neither shape is
// representable type-directedly; rejected like Maps.
case "bytes":
case "jsval": case "jsval":
case "caught": case "caught":
case "promise": case "promise":
@@ -6195,7 +6206,7 @@ export function classDynViewSupported(
} }
/** A static type that CONVERTS into a dyn value — the dynFrom domain: /** A static type that CONVERTS into a dyn value — the dynFrom domain:
* JSON-safe data, bytes<u8> (retained views), identity-preserving class * JSON-safe data, numeric typed arrays (retained views), identity-preserving class
* references, undefined-armed unions of those arms, boxable function types, * references, undefined-armed unions of those arms, boxable function types,
* and the runtime HANDLE kinds (boxed by reference — DYN_HANDLE_KINDS). */ * and the runtime HANDLE kinds (boxed by reference — DYN_HANDLE_KINDS). */
export function canConvertToDyn( export function canConvertToDyn(
@@ -6204,13 +6215,13 @@ export function canConvertToDyn(
getUnion: (unionId: string) => IrUnionDef | undefined, getUnion: (unionId: string) => IrUnionDef | undefined,
): boolean { ): boolean {
if (isJsonSafeType(t, getRecord, getUnion)) return true; if (isJsonSafeType(t, getRecord, getUnion)) return true;
// bytes<u8> and boxable functions are dyn kinds the walker boxes // numeric typed arrays and boxable functions are dyn kinds the walker boxes
// ANYWHERE (bytes and functions held by identity), including nested // ANYWHERE (bytes and functions held by identity), including nested
// in records/arrays/unions. isJsonSafeType rejects them, but dynFrom // in records/arrays/unions. isJsonSafeType rejects them, but dynFrom
// needs only that the walker can build the dyn value, so this composite // needs only that the walker can build the dyn value, so this composite
// fold extends the JSON-safe core. // fold extends the JSON-safe core.
if (canBoxDynComposite(t, getRecord, getUnion)) return true; if (canBoxDynComposite(t, getRecord, getUnion)) return true;
if (t.kind === "bytes" && t.elem === "u8") return true; if (t.kind === "bytes") return true;
// %Error converts as the checked-dynamic tree's error encoding ({%error, name, message, // %Error converts as the checked-dynamic tree's error encoding ({%error, name, message,
// code?} — the caughtToDyn shape, scr_dyn_from_error): the dyn 'error' // code?} — the caughtToDyn shape, scr_dyn_from_error): the dyn 'error'
// listener boundary (a mustCall-wrapped handler receiving the payload). // listener boundary (a mustCall-wrapped handler receiving the payload).
@@ -6249,7 +6260,7 @@ export function canConvertToDyn(
} }
/** The composite extension of the dynFrom domain: JSON-safe scalars plus /** The composite extension of the dynFrom domain: JSON-safe scalars plus
* bytes<u8> and boxable functions anywhere, recursing through records * numeric typed arrays and boxable functions anywhere, recursing through records
* (fields + index value), arrays, and unit-armed unions — exactly the * (fields + index value), arrays, and unit-armed unions — exactly the
* sc_td_* walker's capability. Returns false for a composite carrying a * sc_td_* walker's capability. Returns false for a composite carrying a
* kind the walker cannot box (Maps or handles nested in a record); those * kind the walker cannot box (Maps or handles nested in a record); those
@@ -6270,7 +6281,7 @@ function canBoxDynComposite(
case "classval": case "classval":
return true; return true;
case "bytes": case "bytes":
return t.elem === "u8"; return true;
case "func": case "func":
return canBoxFuncIntoDyn(t, getRecord, getUnion); return canBoxFuncIntoDyn(t, getRecord, getUnion);
case "array": case "array":
@@ -6298,7 +6309,7 @@ function canBoxDynComposite(
} }
/** A type a dyn value can be VALIDATED into — the dynCheck domain: /** A type a dyn value can be VALIDATED into — the dynCheck domain:
* JSON-safe data, bytes<u8> (retained views), the %Error extraction, * JSON-safe data, numeric typed arrays (retained views), the %Error extraction,
* undefined-armed unions of JSON-safe arms, adaptable function types, * undefined-armed unions of JSON-safe arms, adaptable function types,
* and the runtime HANDLE kinds (a tag-checked reference unwrap — * and the runtime HANDLE kinds (a tag-checked reference unwrap —
* DYN_HANDLE_KINDS). */ * DYN_HANDLE_KINDS). */
@@ -6313,7 +6324,7 @@ export function canDynCheckTo(
// serializable. Backends already retain dyn fields and fill missing // serializable. Backends already retain dyn fields and fill missing
// unknown record fields with the undefined value. // unknown record fields with the undefined value.
if (isJsonSafeAt(t, getRecord, getUnion, false, false, new Set(), true)) return true; if (isJsonSafeAt(t, getRecord, getUnion, false, false, new Set(), true)) return true;
if (t.kind === "bytes" && t.elem === "u8") return true; if (t.kind === "bytes") return true;
if (t.kind === "classval") return true; if (t.kind === "classval") return true;
if (t.kind === "object" && t.className === "%Error") return true; if (t.kind === "object" && t.className === "%Error") return true;
if (t.kind === "func") return canAdaptDynFuncTo(t, getRecord, getUnion); if (t.kind === "func") return canAdaptDynFuncTo(t, getRecord, getUnion);
@@ -7578,6 +7589,10 @@ export function moduleLibNondeterministicSurface(mod: IrModule): string | null {
export const MAY_THROW_LIB_FNS: ReadonlySet<IrLibFn> = new Set([ export const MAY_THROW_LIB_FNS: ReadonlySet<IrLibFn> = new Set([
"arrayBuffer.new", "arrayBuffer.new",
"arrayBuffer.byteLengthGetter", "arrayBuffer.byteLengthGetter",
"arrayBuffer.viewU8C",
"arrayBuffer.viewI8",
"arrayBuffer.viewU16",
"arrayBuffer.viewI16",
"arrayBuffer.viewU8", "arrayBuffer.viewU8",
"arrayBuffer.viewU32", "arrayBuffer.viewU32",
"arrayBuffer.viewI32", "arrayBuffer.viewI32",
+10
View File
@@ -422,3 +422,13 @@ test("mixed literal discriminators resolve field unions declared later", () => {
mod.unions![0]!.discriminant!.cases[1]!.values.push(false); mod.unions![0]!.discriminant!.cases[1]!.values.push(false);
expect(validateModule(mod).some((error) => error.message.includes("invalid or repeated"))).toBe(true); expect(validateModule(mod).some((error) => error.message.includes("invalid or repeated"))).toBe(true);
}); });
test("typed-array brand tests serialize their element kind and reject misplaced brands", () => {
const expr: IrExpr = { kind: "dynTest", test: "bytes", bytesElem: "u16", value: { kind: "dynObjLit", type: DYN, loc }, type: BOOL, loc };
const mod = expressionModule(expr, []);
expect(validateModule(mod)).toEqual([]);
expect(deserializeModule(serializeModule(mod))).toEqual(mod);
expr.test = "array";
expect(validateModule(mod).map((d) => d.message)).toContain("in main: dynTest bytesElem requires a valid bytes test");
});
+9 -1
View File
@@ -18,7 +18,7 @@ import type {
IrUnionDef, IrUnionDef,
SrcLoc, SrcLoc,
} from "./ir.js"; } from "./ir.js";
import { arrayOf, BOOL, BYTES_U8, bytesOf, canAdaptDynFuncTo, canDynCheckTo, canConvertToDyn, canExitIslandToType, canMarshalIntoIsland, canMarshalTypedFuncIntoIsland, CHILD_T, CHILDSTREAM_T, CHILDWRITER_T, CRYPTOHASH_T, CRYPTOHMAC_T, DATE_T, DGRAMSOCK_T, DYN, DYN_HANDLE_KINDS, F64, ffiClassType, ffiSourceParamTypes, FILEHANDLE_T, FSWATCHER_T, HTTP2SESSION_T, HTTP2STREAM_T, HTTPCLIENTREQ_T, HTTPREQ_T, HTTPRES_T, islandPromisePayloadTag, isDynTypedRefType, isFfiCallbackParam, isFfiContextParam, isFfiReleaseParam, isJsonSafeType, isJsonStringifySafeType, isRefCounted, isSupportedArrayElem, isSupportedIndexValue, isSupportedMapKey, isSupportedMapValue, isSupportedSetElem, isUnitType, jsOpResultKind, JSVAL, NETSERVER_T, NETSOCKET_T, PROCSTREAM_T, REF_TRUTHY_KINDS, REGEX, RUNTIME_EMITTER_CLASS, RUNTIME_ERROR_CLASSES, RUNTIME_STREAM_CLASSES, SEARCH_PARAMS_T, SECURECTX_T, shapeHasAccessorSlots, SPAWNRES_T, STATS_T, STRING, SYMBOL_T, TESTCTX_T, typeEquals, typeKey, unionContainerArmsOk, URL_T, VOID } from "./ir.js"; import { arrayOf, BYTES_ELEMENT_NAME, BOOL, BYTES_U8, bytesOf, canAdaptDynFuncTo, canDynCheckTo, canConvertToDyn, canExitIslandToType, canMarshalIntoIsland, canMarshalTypedFuncIntoIsland, CHILD_T, CHILDSTREAM_T, CHILDWRITER_T, CRYPTOHASH_T, CRYPTOHMAC_T, DATE_T, DGRAMSOCK_T, DYN, DYN_HANDLE_KINDS, F64, ffiClassType, ffiSourceParamTypes, FILEHANDLE_T, FSWATCHER_T, HTTP2SESSION_T, HTTP2STREAM_T, HTTPCLIENTREQ_T, HTTPREQ_T, HTTPRES_T, islandPromisePayloadTag, isDynTypedRefType, isFfiCallbackParam, isFfiContextParam, isFfiReleaseParam, isJsonSafeType, isJsonStringifySafeType, isRefCounted, isSupportedArrayElem, isSupportedIndexValue, isSupportedMapKey, isSupportedMapValue, isSupportedSetElem, isUnitType, jsOpResultKind, JSVAL, NETSERVER_T, NETSOCKET_T, PROCSTREAM_T, REF_TRUTHY_KINDS, REGEX, RUNTIME_EMITTER_CLASS, RUNTIME_ERROR_CLASSES, RUNTIME_STREAM_CLASSES, SEARCH_PARAMS_T, SECURECTX_T, shapeHasAccessorSlots, SPAWNRES_T, STATS_T, STRING, SYMBOL_T, TESTCTX_T, typeEquals, typeKey, unionContainerArmsOk, URL_T, VOID } from "./ir.js";
import { BIGINT_T } from "./ir.js"; import { BIGINT_T } from "./ir.js";
import { unionWideningTags } from "./analysis.js"; import { unionWideningTags } from "./analysis.js";
import { alwaysReturns } from "./control-flow.js"; import { alwaysReturns } from "./control-flow.js";
@@ -88,6 +88,7 @@ export const REGEX_INTRINSIC_SIGS: Record<
export const LIB_FN_SIGS: Record<IrLibFn, { argTypes: (IrType | null)[]; result: IrType }> = { export const LIB_FN_SIGS: Record<IrLibFn, { argTypes: (IrType | null)[]; result: IrType }> = {
"fetch.start": { argTypes: [STRING, DYN], result: { kind: "promise", inner: DYN } }, "fetch.start": { argTypes: [STRING, DYN], result: { kind: "promise", inner: DYN } },
"fetch.responseNew": { argTypes: [DYN, DYN], result: DYN }, "fetch.responseNew": { argTypes: [DYN, DYN], result: DYN },
"fetch.responseArrayBuffer": { argTypes: [DYN], result: { kind: "promise", inner: DYN } },
"fetch.responseJson": { argTypes: [DYN], result: { kind: "promise", inner: DYN } }, "fetch.responseJson": { argTypes: [DYN], result: { kind: "promise", inner: DYN } },
"fetch.responseText": { argTypes: [DYN], result: { kind: "promise", inner: STRING } }, "fetch.responseText": { argTypes: [DYN], result: { kind: "promise", inner: STRING } },
"fetch.responseBytes": { argTypes: [DYN], result: { kind: "promise", inner: BYTES_U8 } }, "fetch.responseBytes": { argTypes: [DYN], result: { kind: "promise", inner: BYTES_U8 } },
@@ -1112,6 +1113,10 @@ export const LIB_FN_SIGS: Record<IrLibFn, { argTypes: (IrType | null)[]; result:
"arrayBuffer.isView": { argTypes: [DYN], result: BOOL }, "arrayBuffer.isView": { argTypes: [DYN], result: BOOL },
"arrayBuffer.byteLengthGetter": { argTypes: [], result: F64 }, "arrayBuffer.byteLengthGetter": { argTypes: [], result: F64 },
"arrayBuffer.byteLengthDescriptor": { argTypes: [DYN], result: DYN }, "arrayBuffer.byteLengthDescriptor": { argTypes: [DYN], result: DYN },
"arrayBuffer.viewU8C": { argTypes: [DYN, DYN, DYN], result: bytesOf("u8c") },
"arrayBuffer.viewI8": { argTypes: [DYN, DYN, DYN], result: bytesOf("i8") },
"arrayBuffer.viewU16": { argTypes: [DYN, DYN, DYN], result: bytesOf("u16") },
"arrayBuffer.viewI16": { argTypes: [DYN, DYN, DYN], result: bytesOf("i16") },
"arrayBuffer.viewU8": { argTypes: [DYN, DYN, DYN], result: BYTES_U8 }, "arrayBuffer.viewU8": { argTypes: [DYN, DYN, DYN], result: BYTES_U8 },
"arrayBuffer.viewU32": { argTypes: [DYN, DYN, DYN], result: bytesOf("u32") }, "arrayBuffer.viewU32": { argTypes: [DYN, DYN, DYN], result: bytesOf("u32") },
"arrayBuffer.viewI32": { argTypes: [DYN, DYN, DYN], result: bytesOf("i32") }, "arrayBuffer.viewI32": { argTypes: [DYN, DYN, DYN], result: bytesOf("i32") },
@@ -3791,6 +3796,9 @@ function validateFunction(
checkExpr(e.value); checkExpr(e.value);
expectType(e.value, { kind: "dyn" }, "dynTest operand"); expectType(e.value, { kind: "dyn" }, "dynTest operand");
if (e.type.kind !== "bool") err("dynTest must be bool", e.loc); if (e.type.kind !== "bool") err("dynTest must be bool", e.loc);
if (e.bytesElem !== undefined && (e.test !== "bytes" || !Object.hasOwn(BYTES_ELEMENT_NAME, e.bytesElem))) {
err("dynTest bytesElem requires a valid bytes test", e.loc);
}
break; break;
} }
case "dynKeyGet": { case "dynKeyGet": {
+2 -3
View File
@@ -3441,9 +3441,8 @@
"id": "stdlib.response.arrayBuffer", "id": "stdlib.response.arrayBuffer",
"kind": "stdlib", "kind": "stdlib",
"name": "Response.arrayBuffer", "name": "Response.arrayBuffer",
"status": "dynamic-only", "status": "static",
"code": "SC2020", "note": "Node 24.15.0 / Undici 7.24.4; facets: body-consumption, promise-settlement, state-machine, error-shape; differential evidence: fixture:static"
"note": "Node 24.15.0 / Undici 7.24.4; free-standing ArrayBuffer values have no static representation; use Response.bytes()"
}, },
{ {
"id": "stdlib.response.blob", "id": "stdlib.response.blob",
@@ -11157,6 +11157,36 @@
"<repo>/tests/corpus/union-discriminant-width.ts" "<repo>/tests/corpus/union-discriminant-width.ts"
], ],
"diags": [] "diags": []
},
"<repo>/tests/corpus/number-predicates-unknown.ts": {
"order": [
"<repo>/tests/corpus/number-predicates-unknown.ts"
],
"diags": []
},
"<repo>/tests/corpus/typed-array-byte-io.js": {
"order": [
"<repo>/tests/corpus/typed-array-byte-io.js"
],
"diags": []
},
"<repo>/tests/corpus/typed-array-dynamic-methods.js": {
"order": [
"<repo>/tests/corpus/typed-array-dynamic-methods.js"
],
"diags": []
},
"<repo>/tests/corpus/typed-array-integer-widths.ts": {
"order": [
"<repo>/tests/corpus/typed-array-integer-widths.ts"
],
"diags": []
},
"<repo>/tests/corpus/typed-array-unknown.ts": {
"order": [
"<repo>/tests/corpus/typed-array-unknown.ts"
],
"diags": []
} }
} }
} }
+78 -30
View File
@@ -26,7 +26,36 @@ static void scr_bytes_oom(void) {
} }
size_t scr_bytes_elem_size(ScrBytesElem elem) { size_t scr_bytes_elem_size(ScrBytesElem elem) {
return elem == SCR_BYTES_U8 ? 1 : elem == SCR_BYTES_F64 ? 8 : 4; switch (elem) {
case SCR_BYTES_U8: case SCR_BYTES_U8C: case SCR_BYTES_I8: return 1;
case SCR_BYTES_U16: case SCR_BYTES_I16: return 2;
case SCR_BYTES_U32: case SCR_BYTES_I32: case SCR_BYTES_F32: return 4;
case SCR_BYTES_F64: return 8;
}
scr_trap("scriptc: invalid typed array element kind\n");
}
const char *scr_bytes_elem_name(ScrBytesElem elem) {
switch (elem) {
case SCR_BYTES_U8: return "Uint8Array";
case SCR_BYTES_U8C: return "Uint8ClampedArray";
case SCR_BYTES_I8: return "Int8Array";
case SCR_BYTES_U16: return "Uint16Array";
case SCR_BYTES_I16: return "Int16Array";
case SCR_BYTES_U32: return "Uint32Array";
case SCR_BYTES_I32: return "Int32Array";
case SCR_BYTES_F32: return "Float32Array";
case SCR_BYTES_F64: return "Float64Array";
}
scr_trap("scriptc: invalid typed array element kind\n");
}
double scr_bytes_to_u8_clamp(double value) {
if (!(value > 0)) return 0;
if (value >= 255) return 255;
double lower = floor(value);
double fraction = value - lower;
return fraction > 0.5 || (fraction == 0.5 && ((unsigned)lower & 1)) ? lower + 1 : lower;
} }
/* ── lifecycle ─────────────────────────────────────────────────────────── */ /* ── lifecycle ─────────────────────────────────────────────────────────── */
@@ -79,6 +108,11 @@ ScrBytes *scr_bytes_copy(const ScrBytes *src) {
return b; return b;
} }
ScrBytes *scr_bytes_raw_view(ScrBytes *bytes) {
if (bytes->elem == SCR_BYTES_U8) return scr_bytes_retain(bytes);
return scr_dataview_new(bytes, scr_bytes_byte_offset(bytes), true, scr_bytes_byte_len(bytes));
}
ScrBytes *scr_bytes_as_buffer(ScrBytes *bytes) { ScrBytes *scr_bytes_as_buffer(ScrBytes *bytes) {
bytes->is_buffer = true; bytes->is_buffer = true;
return scr_bytes_retain(bytes); return scr_bytes_retain(bytes);
@@ -150,8 +184,23 @@ static uint32_t scr_bytes_to_u32(double v) {
double scr_bytes_get(const ScrBytes *b, double i) { double scr_bytes_get(const ScrBytes *b, double i) {
size_t idx = scr_bytes_check_index(b, i); size_t idx = scr_bytes_check_index(b, i);
switch (b->elem) { switch (b->elem) {
case SCR_BYTES_U8: case SCR_BYTES_U8: case SCR_BYTES_U8C:
return (double)b->data[idx]; return (double)b->data[idx];
case SCR_BYTES_I8: {
int8_t v;
memcpy(&v, b->data + idx, 1);
return (double)v;
}
case SCR_BYTES_U16: {
uint16_t v;
memcpy(&v, b->data + idx * 2, 2);
return (double)v;
}
case SCR_BYTES_I16: {
int16_t v;
memcpy(&v, b->data + idx * 2, 2);
return (double)v;
}
case SCR_BYTES_U32: { case SCR_BYTES_U32: {
uint32_t v; uint32_t v;
memcpy(&v, b->data + idx * 4, 4); memcpy(&v, b->data + idx * 4, 4);
@@ -179,9 +228,17 @@ double scr_bytes_get(const ScrBytes *b, double i) {
void scr_bytes_set(ScrBytes *b, double i, double v) { void scr_bytes_set(ScrBytes *b, double i, double v) {
size_t idx = scr_bytes_check_index(b, i); size_t idx = scr_bytes_check_index(b, i);
switch (b->elem) { switch (b->elem) {
case SCR_BYTES_U8: case SCR_BYTES_U8: case SCR_BYTES_I8:
b->data[idx] = (uint8_t)scr_bytes_to_u32(v); b->data[idx] = (uint8_t)scr_bytes_to_u32(v);
break; break;
case SCR_BYTES_U8C:
b->data[idx] = (uint8_t)scr_bytes_to_u8_clamp(v);
break;
case SCR_BYTES_U16: case SCR_BYTES_I16: {
uint16_t u = (uint16_t)scr_bytes_to_u32(v);
memcpy(b->data + idx * 2, &u, 2);
break;
}
case SCR_BYTES_U32: { case SCR_BYTES_U32: {
uint32_t u = scr_bytes_to_u32(v); uint32_t u = scr_bytes_to_u32(v);
memcpy(b->data + idx * 4, &u, 4); memcpy(b->data + idx * 4, &u, 4);
@@ -1796,33 +1853,9 @@ ScrBytes *scr_bytes_from_str(const ScrStr *s, const ScrStr *enc) {
ScrBytes *scr_bytes_from_arr(ScrBytesElem elem, const ScrArr *arr) { ScrBytes *scr_bytes_from_arr(ScrBytesElem elem, const ScrArr *arr) {
ScrBytes *b = scr_bytes_alloc(elem, arr->len); ScrBytes *b = scr_bytes_alloc(elem, arr->len);
for (size_t i = 0; i < arr->len; i++) { for (size_t i = 0; i < arr->len; i++) {
/* Buffer.from(Array) applies Number/ToUint* to each value. A hole is // Iteration reads holes as undefined: floating arrays store NaN and
* read as undefined and therefore contributes zero; it must never read // integer arrays store zero through their normal element conversion.
* an uninitialized dense slot or assume sparse storage is allocated. */ scr_bytes_set(b, (double)i, scr_arr_get_number(arr, (double)i));
double v = scr_arr_has(arr, (double)i) ? scr_arr_get_f64((ScrArr *)arr, (double)i) : 0;
switch (elem) {
case SCR_BYTES_U8:
b->data[i] = (uint8_t)scr_bytes_to_u32(v);
break;
case SCR_BYTES_U32: {
uint32_t u = scr_bytes_to_u32(v);
memcpy(b->data + i * 4, &u, 4);
break;
}
case SCR_BYTES_F32: {
float f = (float)v;
memcpy(b->data + i * 4, &f, 4);
break;
}
case SCR_BYTES_F64:
memcpy(b->data + i * 8, &v, 8);
break;
case SCR_BYTES_I32: {
uint32_t u = scr_bytes_to_u32(v);
memcpy(b->data + i * 4, &u, 4); /* same residue reinterpreted */
break;
}
}
} }
return b; return b;
} }
@@ -2451,3 +2484,18 @@ bool scr_dataview_write_u64_raw(ScrBytes *b, double offset, bool le, uint64_t va
for (size_t i = 0; i < 8; i++) p[le ? i : 7 - i] = (uint8_t)(value >> (8 * i)); for (size_t i = 0; i < 8; i++) p[le ? i : 7 - i] = (uint8_t)(value >> (8 * i));
return true; return true;
} }
ScrArr *scr_bytes_to_arr(const ScrBytes *b) {
ScrArr *out = scr_arr_new(SCR_ELEM_F64, b->len ? b->len : 1);
for (size_t i = 0; i < b->len; i++) {
scr_arr_push_f64(out, scr_bytes_get(b, (double)i));
}
return out;
}
ScrStr *scr_bytes_join(const ScrBytes *b, const ScrStr *separator) {
ScrArr *values = scr_bytes_to_arr(b);
ScrStr *out = scr_arr_join(values, (ScrStr *)separator);
scr_arr_release(values);
return out;
}
+6 -6
View File
@@ -218,7 +218,7 @@ static ScrBytes *scr_buffer_from_array_like(const ScrDyn *value, double length)
ScrBytes *scr_buffer_from_dyn(const ScrDyn *value, const ScrStr *encoding) { ScrBytes *scr_buffer_from_dyn(const ScrDyn *value, const ScrStr *encoding) {
if (value->kind == SCR_DYN_STR) return scr_bytes_from_str(value->v.str, encoding); if (value->kind == SCR_DYN_STR) return scr_bytes_from_str(value->v.str, encoding);
if (value->kind == SCR_DYN_BYTES) return scr_bytes_copy(value->v.bytes); if (value->kind == SCR_DYN_BYTES) return scr_bytes_convert(SCR_BYTES_U8, value->v.bytes);
if (value->kind == SCR_DYN_ARR) { if (value->kind == SCR_DYN_ARR) {
return scr_buffer_from_array_like(value, (double)value->v.arr.len); return scr_buffer_from_array_like(value, (double)value->v.arr.len);
} }
@@ -263,7 +263,7 @@ ScrBytes *scr_buffer_from_dyn(const ScrDyn *value, const ScrStr *encoding) {
/* A bytes payload or the API's own ERR_INVALID_ARG_TYPE (borrowed). */ /* A bytes payload or the API's own ERR_INVALID_ARG_TYPE (borrowed). */
static ScrBytes *scr_bytes_chk_u8(const ScrDyn *d, const char *argname) { static ScrBytes *scr_bytes_chk_u8(const ScrDyn *d, const char *argname) {
if (d->kind != SCR_DYN_BYTES) { if (!scr_dyn_bytes_is(d, SCR_BYTES_U8)) {
scr_dyn_arg_type_fail(argname, "an instance of Buffer or Uint8Array", d); scr_dyn_arg_type_fail(argname, "an instance of Buffer or Uint8Array", d);
return NULL; return NULL;
} }
@@ -376,7 +376,7 @@ static bool scr_fs_cb_chk(const ScrDyn *cb, const char *name) {
* these ladders — the checked-dynamic tree has no URL kind here, and Node would accept * these ladders — the checked-dynamic tree has no URL kind here, and Node would accept
* only file: URLs anyway). */ * only file: URLs anyway). */
static bool scr_fs_path_chk(const ScrDyn *p, const char *name) { static bool scr_fs_path_chk(const ScrDyn *p, const char *name) {
if (p->kind == SCR_DYN_STR || p->kind == SCR_DYN_BYTES) return true; if (p->kind == SCR_DYN_STR || scr_dyn_bytes_is(p, SCR_BYTES_U8)) return true;
scr_dyn_arg_type_fail(name, "of type string or an instance of Buffer or URL", p); scr_dyn_arg_type_fail(name, "of type string or an instance of Buffer or URL", p);
return false; return false;
} }
@@ -456,7 +456,7 @@ static void scr_fs_exists_fire(ScrClosure *self) {
ScrStr *p = scr_str_retain(path->v.str); ScrStr *p = scr_str_retain(path->v.str);
ans = scr_fs_exists(p); ans = scr_fs_exists(p);
scr_str_release(p); scr_str_release(p);
} else if (path->kind == SCR_DYN_BYTES) { } else if (scr_dyn_bytes_is(path, SCR_BYTES_U8)) {
ScrStr *p = scr_str_new((const char *)path->v.bytes->data, path->v.bytes->len); ScrStr *p = scr_str_new((const char *)path->v.bytes->data, path->v.bytes->len);
ans = scr_fs_exists(p); ans = scr_fs_exists(p);
scr_str_release(p); scr_str_release(p);
@@ -472,7 +472,7 @@ static void scr_fs_exists_fire(ScrClosure *self) {
ScrDyn *scr_fs_exists_async(const ScrDyn *path, const ScrDyn *cb) { ScrDyn *scr_fs_exists_async(const ScrDyn *path, const ScrDyn *cb) {
if (!scr_fs_cb_chk(cb, "cb")) return NULL; if (!scr_fs_cb_chk(cb, "cb")) return NULL;
if (path->kind != SCR_DYN_STR && path->kind != SCR_DYN_BYTES) { if (path->kind != SCR_DYN_STR && !scr_dyn_bytes_is(path, SCR_BYTES_U8)) {
/* Node's wart, kept exactly: a path getValidatedPath rejects answers /* Node's wart, kept exactly: a path getValidatedPath rejects answers
* false through the callback SYNCHRONOUSLY (`return callback(false)` * false through the callback SYNCHRONOUSLY (`return callback(false)`
* in lib/fs.js exists). */ * in lib/fs.js exists). */
@@ -682,7 +682,7 @@ void scr_fs_read_chk(const ScrDyn *fd, const ScrDyn *buffer, const ScrDyn *offse
scr_dyn_arg_type_fail("fd", "of type number", fd); scr_dyn_arg_type_fail("fd", "of type number", fd);
return; return;
} }
double buflen = (double)buffer->v.bytes->len; double buflen = scr_bytes_byte_len(buffer->v.bytes);
if (!scr_fs_dyn_absent(offset)) { if (!scr_fs_dyn_absent(offset)) {
/* validateInteger's MAX_SAFE range first, the buffer bound second — /* validateInteger's MAX_SAFE range first, the buffer bound second —
* Node renders each with its own max. */ * Node renders each with its own max. */
-15
View File
@@ -144,18 +144,3 @@ ScrBytes *scr_bytes_with(const ScrBytes *b, double index, double value) {
scr_bytes_set(out, actual, value); scr_bytes_set(out, actual, value);
return out; return out;
} }
ScrArr *scr_bytes_to_arr(const ScrBytes *b) {
ScrArr *out = scr_arr_new(SCR_ELEM_F64, b->len ? b->len : 1);
for (size_t i = 0; i < b->len; i++) {
scr_arr_push_f64(out, scr_bytes_get(b, (double)i));
}
return out;
}
ScrStr *scr_bytes_join(const ScrBytes *b, const ScrStr *separator) {
ScrArr *values = scr_bytes_to_arr(b);
ScrStr *out = scr_arr_join(values, (ScrStr *)separator);
scr_arr_release(values);
return out;
}
+4 -1
View File
@@ -115,7 +115,10 @@ static void dc_throw_bad_fn_arg(const ScrDyn *cb, const char *arg_name) {
case SCR_DYN_NULL: received = "null"; break; case SCR_DYN_NULL: received = "null"; break;
case SCR_DYN_UNDEF: received = "undefined"; break; case SCR_DYN_UNDEF: received = "undefined"; break;
case SCR_DYN_ARR: received = "an instance of Array"; break; case SCR_DYN_ARR: received = "an instance of Array"; break;
case SCR_DYN_BYTES: received = "an instance of Uint8Array"; break; case SCR_DYN_BYTES:
snprintf(detail, sizeof detail, "an instance of %s", scr_bytes_elem_name(cb->v.bytes->elem));
received = detail;
break;
case SCR_DYN_BOOL: case SCR_DYN_BOOL:
snprintf(detail, sizeof detail, "type boolean (%s)", cb->v.b ? "true" : "false"); snprintf(detail, sizeof detail, "type boolean (%s)", cb->v.b ? "true" : "false");
received = detail; received = detail;
+51 -8
View File
@@ -94,9 +94,9 @@ static void scr_dyn_display_buf(ScrJsonBuf *b, const ScrDyn *d) {
} }
for (size_t i = 0; i < d->v.bytes->len; i++) { for (size_t i = 0; i < d->v.bytes->len; i++) {
if (i > 0) scr_jb_putc(b, ','); if (i > 0) scr_jb_putc(b, ',');
char n[16]; ScrStr *n = scr_f64_to_scrstr(scr_bytes_get(d->v.bytes, (double)i));
snprintf(n, sizeof n, "%u", (unsigned)d->v.bytes->data[i]); for (size_t j = 0; j < n->len; j++) scr_jb_putc(b, n->data[j]);
scr_jb_puts(b, n); scr_str_release(n);
} }
return; return;
case SCR_DYN_FUNC: case SCR_DYN_FUNC:
@@ -176,6 +176,13 @@ static double dyn_index_arg(ScrDyn *const *args, size_t argc, size_t i, double d
return 0; return 0;
} }
static double dyn_bytes_index_arg(ScrDyn *const *args, size_t argc, size_t index, double fallback) {
if (index >= argc || args[index]->kind == SCR_DYN_UNDEF) return fallback;
double value;
if (!scr_dyn_number_coerce_js(args[index], &value)) return 0;
return isnan(value) ? 0 : trunc(value);
}
/* JS relative-index normalization (slice's rule). */ /* JS relative-index normalization (slice's rule). */
static size_t dyn_rel_index(double rel, size_t len) { static size_t dyn_rel_index(double rel, size_t len) {
if (rel < 0) { if (rel < 0) {
@@ -1023,18 +1030,18 @@ static ScrDyn *scr_dyn_invoke_impl(
ScrBytes *bytes = recv->v.bytes; ScrBytes *bytes = recv->v.bytes;
size_t blen = bytes->len; size_t blen = bytes->len;
if (dyn_name_is(method, "at")) { if (dyn_name_is(method, "at")) {
double iD = dyn_index_arg(args, argc, 0, 0, what); double iD = dyn_bytes_index_arg(args, argc, 0, 0);
if (scr_exc_pending()) return NULL; if (scr_exc_pending()) return NULL;
double idx = iD < 0 ? (double)blen + iD : iD; double idx = iD < 0 ? (double)blen + iD : iD;
if (idx < 0 || idx >= (double)blen) return scr_dyn_retain(scr_dyn_undefined()); if (idx < 0 || idx >= (double)blen) return scr_dyn_retain(scr_dyn_undefined());
return scr_dyn_new_num((double)bytes->data[(size_t)idx]); return scr_dyn_new_num(scr_bytes_get(bytes, idx));
} }
if (dyn_name_is(method, "slice") || dyn_name_is(method, "subarray")) { if (dyn_name_is(method, "slice") || dyn_name_is(method, "subarray")) {
/* subarray and Buffer.slice alias their source. TypedArray.slice /* subarray and Buffer.slice alias their source. TypedArray.slice
* owns an independent copy; all keep the receiver's Buffer flavor. */ * owns an independent copy; all keep the receiver's Buffer flavor. */
double startD = dyn_index_arg(args, argc, 0, 0, what); double startD = dyn_bytes_index_arg(args, argc, 0, 0);
if (scr_exc_pending()) return NULL; if (scr_exc_pending()) return NULL;
double endD = dyn_index_arg(args, argc, 1, (double)blen, what); double endD = dyn_bytes_index_arg(args, argc, 1, (double)blen);
if (scr_exc_pending()) return NULL; if (scr_exc_pending()) return NULL;
ScrBytes *out = recv->buffer || dyn_name_is(method, "subarray") ScrBytes *out = recv->buffer || dyn_name_is(method, "subarray")
? scr_bytes_subarray(bytes, startD, endD) : scr_bytes_slice(bytes, startD, endD); ? scr_bytes_subarray(bytes, startD, endD) : scr_bytes_slice(bytes, startD, endD);
@@ -1042,8 +1049,44 @@ static ScrDyn *scr_dyn_invoke_impl(
scr_bytes_release(out); scr_bytes_release(out);
return d; return d;
} }
if (dyn_name_is(method, "set")) {
double offset = dyn_bytes_index_arg(args, argc, 1, 0);
if (scr_exc_pending()) return NULL;
scr_bytes_set_from_dyn(bytes, argc ? args[0] : scr_dyn_undefined(), offset);
return scr_exc_pending() ? NULL : scr_dyn_retain(scr_dyn_undefined());
}
if (dyn_name_is(method, "copyWithin")) {
double target = dyn_bytes_index_arg(args, argc, 0, 0);
if (scr_exc_pending()) return NULL;
double start = dyn_bytes_index_arg(args, argc, 1, 0);
if (scr_exc_pending()) return NULL;
double end = dyn_bytes_index_arg(args, argc, 2, (double)blen);
if (scr_exc_pending()) return NULL;
scr_bytes_release(scr_bytes_copy_within(bytes, target, start, end));
return scr_dyn_retain(recv);
}
if (dyn_name_is(method, "fill") && !recv->buffer) {
double value;
if (!scr_dyn_number_coerce_js(argc ? args[0] : scr_dyn_undefined(), &value)) return NULL;
double start = dyn_bytes_index_arg(args, argc, 1, 0);
if (scr_exc_pending()) return NULL;
double end = dyn_bytes_index_arg(args, argc, 2, (double)blen);
if (scr_exc_pending()) return NULL;
scr_bytes_release(scr_bytes_fill_elem(bytes, value, start, end));
return scr_dyn_retain(recv);
}
if (dyn_name_is(method, "join") || (dyn_name_is(method, "toString") && !recv->buffer)) {
ScrStr *separator = dyn_name_is(method, "join") && argc && args[0]->kind != SCR_DYN_UNDEF
? scr_dyn_string_coerce_js(args[0]) : scr_str_new(",", 1);
if (scr_exc_pending()) { scr_str_release(separator); return NULL; }
ScrStr *joined = scr_bytes_join(bytes, separator);
scr_str_release(separator);
ScrDyn *out = scr_dyn_new_str(joined);
scr_str_release(joined);
return out;
}
if (dyn_bytes_proto_real(method)) { if (dyn_bytes_proto_real(method)) {
dyn_throw_unsupported("Uint8Array", method); dyn_throw_unsupported(scr_bytes_elem_name(bytes->elem), method);
return NULL; return NULL;
} }
} }
+11 -3
View File
@@ -195,6 +195,7 @@ enum {
SF_COLLECT_JSON, SF_COLLECT_JSON,
SF_COLLECT_TEXT, SF_COLLECT_TEXT,
SF_COLLECT_BYTES, SF_COLLECT_BYTES,
SF_COLLECT_ARRAY_BUFFER,
}; };
enum { enum {
@@ -1545,7 +1546,7 @@ static ScrBytes *sf_chunk_bytes(const ScrDyn *chunk) {
scr_dyn_release(materialized); scr_dyn_release(materialized);
return bytes; return bytes;
} }
if (chunk && chunk->kind == SCR_DYN_BYTES) { if (chunk && scr_dyn_bytes_is(chunk, SCR_BYTES_U8)) {
return scr_bytes_copy(chunk->v.bytes); return scr_bytes_copy(chunk->v.bytes);
} }
if (chunk && chunk->kind == SCR_DYN_STR) { if (chunk && chunk->kind == SCR_DYN_STR) {
@@ -1639,6 +1640,8 @@ static void sf_collector_finish(SfStream *s) {
ScrStr *text = scr_text_decode(bytes); ScrStr *text = scr_text_decode(bytes);
value = scr_dyn_new_str(text); value = scr_dyn_new_str(text);
scr_str_release(text); scr_str_release(text);
} else if (c->mode == SF_COLLECT_ARRAY_BUFFER) {
value = scr_array_buffer_from_bytes(bytes);
} else { } else {
value = scr_dyn_new_bytes(bytes); value = scr_dyn_new_bytes(bytes);
} }
@@ -3207,7 +3210,7 @@ static ScrPromise *sf_response_collect(SfResponse *r, int mode) {
scr_str_release(empty); scr_str_release(empty);
} else { } else {
ScrBytes *empty = scr_bytes_new(SCR_BYTES_U8, 0); ScrBytes *empty = scr_bytes_new(SCR_BYTES_U8, 0);
value = scr_dyn_new_bytes(empty); value = mode == SF_COLLECT_ARRAY_BUFFER ? scr_array_buffer_from_bytes(empty) : scr_dyn_new_bytes(empty);
scr_bytes_release(empty); scr_bytes_release(empty);
} }
if (scr_exc_pending()) { if (scr_exc_pending()) {
@@ -3229,6 +3232,7 @@ static ScrDyn *sf_response_invoke(void *ptr, ScrDyn *self,
if (strcmp(method, "json") == 0) mode = SF_COLLECT_JSON; if (strcmp(method, "json") == 0) mode = SF_COLLECT_JSON;
else if (strcmp(method, "text") == 0) mode = SF_COLLECT_TEXT; else if (strcmp(method, "text") == 0) mode = SF_COLLECT_TEXT;
else if (strcmp(method, "bytes") == 0) mode = SF_COLLECT_BYTES; else if (strcmp(method, "bytes") == 0) mode = SF_COLLECT_BYTES;
else if (strcmp(method, "arrayBuffer") == 0) mode = SF_COLLECT_ARRAY_BUFFER;
if (mode >= 0) { if (mode >= 0) {
ScrPromise *p = sf_response_collect(r, mode); ScrPromise *p = sf_response_collect(r, mode);
ScrDyn *out = scr_dyn_new_promise(p); ScrDyn *out = scr_dyn_new_promise(p);
@@ -3287,6 +3291,10 @@ ScrPromise *scr_fetch_response_text(ScrDyn *response) {
return sf_response_collect_dyn(response, SF_COLLECT_TEXT); return sf_response_collect_dyn(response, SF_COLLECT_TEXT);
} }
ScrPromise *scr_fetch_response_array_buffer(ScrDyn *response) {
return sf_response_collect_dyn(response, SF_COLLECT_ARRAY_BUFFER);
}
ScrPromise *scr_fetch_response_bytes(ScrDyn *response) { ScrPromise *scr_fetch_response_bytes(ScrDyn *response) {
return sf_response_collect_dyn(response, SF_COLLECT_BYTES); return sf_response_collect_dyn(response, SF_COLLECT_BYTES);
} }
@@ -5017,7 +5025,7 @@ ScrDyn *scr_fetch_response_new(ScrDyn *body, ScrDyn *init) {
!(body->kind == SCR_DYN_HANDLE && !(body->kind == SCR_DYN_HANDLE &&
body->v.handle.tag == SCR_DYNH_WEB_STREAM)) { body->v.handle.tag == SCR_DYNH_WEB_STREAM)) {
if (body->kind == SCR_DYN_BYTES) { if (body->kind == SCR_DYN_BYTES) {
body_bytes = scr_bytes_copy(body->v.bytes); body_bytes = scr_bytes_from_data(body->v.bytes->data, body->v.bytes->len * scr_bytes_elem_size(body->v.bytes->elem));
} else { } else {
ScrStr *text = ScrStr *text =
body->kind == SCR_DYN_STR body->kind == SCR_DYN_STR
+8 -8
View File
@@ -1512,14 +1512,14 @@ void scr_http_res_write_str(ScrHttpRes *r, ScrStr *data /*borrowed*/) {
void scr_http_res_write_bytes(ScrHttpRes *r, ScrBytes *data /*borrowed*/) { void scr_http_res_write_bytes(ScrHttpRes *r, ScrBytes *data /*borrowed*/) {
double expected; double expected;
if (r->head_sent && scr_http_res_expected_length(r, &expected) && if (r->head_sent && scr_http_res_expected_length(r, &expected) &&
(double)(r->strict_bytes_written + data->len) > expected) { (double)(r->strict_bytes_written + (data->len * scr_bytes_elem_size(data->elem))) > expected) {
r->strict_bytes_written += data->len; r->strict_bytes_written += (data->len * scr_bytes_elem_size(data->elem));
scr_http_res_length_mismatch(r, expected); scr_http_res_length_mismatch(r, expected);
r->strict_bytes_written -= data->len; r->strict_bytes_written -= (data->len * scr_bytes_elem_size(data->elem));
return; return;
} }
if (r->strict_content_length) r->strict_bytes_written += data->len; if (r->strict_content_length) r->strict_bytes_written += (data->len * scr_bytes_elem_size(data->elem));
scr_http_res_write_raw(r, (const char *)data->data, data->len); scr_http_res_write_raw(r, (const char *)data->data, (data->len * scr_bytes_elem_size(data->elem)));
} }
/* res.flushHeaders(): the head goes out NOW (streaming framing — chunked /* res.flushHeaders(): the head goes out NOW (streaming framing — chunked
@@ -1642,7 +1642,7 @@ void scr_http_res_end_str(ScrHttpRes *r, ScrStr *data /*borrowed*/) {
} }
void scr_http_res_end_bytes(ScrHttpRes *r, ScrBytes *data /*borrowed*/) { void scr_http_res_end_bytes(ScrHttpRes *r, ScrBytes *data /*borrowed*/) {
scr_http_res_end_raw(r, (const char *)data->data, data->len); scr_http_res_end_raw(r, (const char *)data->data, (data->len * scr_bytes_elem_size(data->elem)));
} }
void scr_http_res_write_head(ScrHttpRes *r, double status) { void scr_http_res_write_head(ScrHttpRes *r, double status) {
@@ -3552,7 +3552,7 @@ void scr_http_client_write_str(ScrHttpClientReq *c, ScrStr *data /*borrowed*/) {
} }
void scr_http_client_write_bytes(ScrHttpClientReq *c, ScrBytes *data /*borrowed*/) { void scr_http_client_write_bytes(ScrHttpClientReq *c, ScrBytes *data /*borrowed*/) {
scr_http_client_write_raw(c, (const char *)data->data, data->len); scr_http_client_write_raw(c, (const char *)data->data, (data->len * scr_bytes_elem_size(data->elem)));
} }
static void scr_http_client_cork_flush(ScrHttpClientReq *c) { static void scr_http_client_cork_flush(ScrHttpClientReq *c) {
@@ -3608,7 +3608,7 @@ void scr_http_client_end_str(ScrHttpClientReq *c, ScrStr *data /*borrowed*/) {
} }
void scr_http_client_end_bytes(ScrHttpClientReq *c, ScrBytes *data /*borrowed*/) { void scr_http_client_end_bytes(ScrHttpClientReq *c, ScrBytes *data /*borrowed*/) {
scr_http_client_end_raw(c, (const char *)data->data, data->len); scr_http_client_end_raw(c, (const char *)data->data, (data->len * scr_bytes_elem_size(data->elem)));
} }
void scr_http_client_flush_headers(ScrHttpClientReq *c) { void scr_http_client_flush_headers(ScrHttpClientReq *c) {
+2 -2
View File
@@ -1887,7 +1887,7 @@ void scr_http2_stream_write_str(ScrH2Stream *st, ScrStr *data /*borrowed*/) {
} }
void scr_http2_stream_write_bytes(ScrH2Stream *st, ScrBytes *data /*borrowed*/) { void scr_http2_stream_write_bytes(ScrH2Stream *st, ScrBytes *data /*borrowed*/) {
scr_h2_stream_send(st, (const char *)data->data, (size_t)data->len); scr_h2_stream_send(st, (const char *)data->data, (size_t)(data->len * scr_bytes_elem_size(data->elem)));
} }
void scr_http2_stream_end(ScrH2Stream *st) { void scr_http2_stream_end(ScrH2Stream *st) {
@@ -1905,7 +1905,7 @@ void scr_http2_stream_end_str(ScrH2Stream *st, ScrStr *data /*borrowed*/) {
void scr_http2_stream_end_bytes(ScrH2Stream *st, ScrBytes *data /*borrowed*/) { void scr_http2_stream_end_bytes(ScrH2Stream *st, ScrBytes *data /*borrowed*/) {
if (st->destroyed || st->want_end || scr_h2_stream_local_closed(st)) return; if (st->destroyed || st->want_end || scr_h2_stream_local_closed(st)) return;
scr_h2_buf_put(&st->wbuf, (const char *)data->data, (size_t)data->len); scr_h2_buf_put(&st->wbuf, (const char *)data->data, (size_t)(data->len * scr_bytes_elem_size(data->elem)));
st->want_end = true; st->want_end = true;
scr_h2_stream_flush(st); scr_h2_stream_flush(st);
} }
+7 -3
View File
@@ -872,18 +872,22 @@ ScrStr *scr_insp_dyn(ScrDyn *d, double recurse, double depth) {
return scr_insp_buffer(b); return scr_insp_buffer(b);
} }
char prefix[48]; char prefix[48];
int pn = snprintf(prefix, sizeof prefix, "Uint8Array(%zu) [", b->len); int pn = snprintf(prefix, sizeof prefix, "%s(%zu) [", scr_bytes_elem_name(b->elem), b->len);
if (b->len == 0) { if (b->len == 0) {
InspBuf out = {0}; InspBuf out = {0};
ib_bytes(&out, prefix, (size_t)pn); ib_bytes(&out, prefix, (size_t)pn);
ib_char(&out, ']'); ib_char(&out, ']');
return ib_take(&out); return ib_take(&out);
} }
if (recurse > depth) return scr_str_new("[Uint8Array]", 12); if (recurse > depth) {
char tag[32];
int n = snprintf(tag, sizeof tag, "[%s]", scr_bytes_elem_name(b->elem));
return scr_str_new(tag, (size_t)n);
}
scr_insp_begin(recurse + 1); scr_insp_begin(recurse + 1);
size_t shown = b->len < 100 ? b->len : 100; size_t shown = b->len < 100 ? b->len : 100;
for (size_t i = 0; i < shown; i++) { for (size_t i = 0; i < shown; i++) {
ScrStr *s = scr_insp_f64((double)((const unsigned char *)b->data)[i]); ScrStr *s = scr_insp_f64(scr_bytes_get(b, (double)i));
scr_insp_entry(s, true); scr_insp_entry(s, true);
scr_str_release(s); scr_str_release(s);
} }
+19 -25
View File
@@ -1019,6 +1019,23 @@ static const char *isl_dyn_unmarshalable(const ScrDyn *d) {
static JSValue isl_dynfn_new(const ScrDyn *d); /* the checked-dynamic tree-function shim, below */ static JSValue isl_dynfn_new(const ScrDyn *d); /* the checked-dynamic tree-function shim, below */
static JSValue isl_from_bytes(const ScrBytes *b) {
if (b->elem == SCR_BYTES_U8) return JS_NewUint8ArrayCopy(isl_ctx, b->data, b->len);
JSValue buf = JS_NewArrayBufferCopy(isl_ctx, b->data, b->len * scr_bytes_elem_size(b->elem));
if (JS_IsException(buf)) return buf;
JSValueConst argv[3] = {buf, JS_UNDEFINED, JS_UNDEFINED};
JSValue v = JS_NewTypedArray(isl_ctx, 3, argv,
b->elem == SCR_BYTES_U8C ? JS_TYPED_ARRAY_UINT8C
: b->elem == SCR_BYTES_I8 ? JS_TYPED_ARRAY_INT8
: b->elem == SCR_BYTES_U16 ? JS_TYPED_ARRAY_UINT16
: b->elem == SCR_BYTES_I16 ? JS_TYPED_ARRAY_INT16
: b->elem == SCR_BYTES_U32 ? JS_TYPED_ARRAY_UINT32
: b->elem == SCR_BYTES_I32 ? JS_TYPED_ARRAY_INT32
: b->elem == SCR_BYTES_F64 ? JS_TYPED_ARRAY_FLOAT64 : JS_TYPED_ARRAY_FLOAT32);
JS_FreeValue(isl_ctx, buf);
return v;
}
static JSValue isl_from_dyn(const ScrDyn *d) { static JSValue isl_from_dyn(const ScrDyn *d) {
switch (d->kind) { switch (d->kind) {
case SCR_DYN_FUNC: case SCR_DYN_FUNC:
@@ -1038,8 +1055,7 @@ static JSValue isl_from_dyn(const ScrDyn *d) {
case SCR_DYN_STR: case SCR_DYN_STR:
return JS_NewStringLen(isl_ctx, d->v.str->data, d->v.str->len); return JS_NewStringLen(isl_ctx, d->v.str->data, d->v.str->len);
case SCR_DYN_BYTES: case SCR_DYN_BYTES:
/* Only u8 payloads reach the checked-dynamic tree today (scr_json.c's stringify note). */ return isl_from_bytes(d->v.bytes);
return JS_NewUint8ArrayCopy(isl_ctx, d->v.bytes->data, d->v.bytes->len);
case SCR_DYN_ARR: { case SCR_DYN_ARR: {
JSValue arr = JS_NewArray(isl_ctx); JSValue arr = JS_NewArray(isl_ctx);
if (JS_IsException(arr)) return arr; if (JS_IsException(arr)) return arr;
@@ -9732,29 +9748,7 @@ ScrJsval *scr_jsval_null(void) {
ScrJsval *scr_jsval_from_bytes(const ScrBytes *b) { ScrJsval *scr_jsval_from_bytes(const ScrBytes *b) {
isl_entry(); isl_entry();
if (b->elem == SCR_BYTES_U8) { JSValue v = isl_from_bytes(b);
JSValue v = JS_NewUint8ArrayCopy(isl_ctx, b->data, b->len);
if (JS_IsException(v)) {
isl_bridge_exception();
return NULL;
}
return isl_cell_new(v);
}
JSValue buf = JS_NewArrayBufferCopy(isl_ctx, b->data,
b->len * scr_bytes_elem_size(b->elem));
if (JS_IsException(buf)) {
isl_bridge_exception();
return NULL;
}
/* The engine's constructor reads the offset/length slots unconditionally
* — pad them with undefined like a real JS call would. */
JSValueConst argv[3] = {buf, JS_UNDEFINED, JS_UNDEFINED};
JSValue v = JS_NewTypedArray(isl_ctx, 3, argv,
b->elem == SCR_BYTES_U32 ? JS_TYPED_ARRAY_UINT32
: b->elem == SCR_BYTES_I32 ? JS_TYPED_ARRAY_INT32
: b->elem == SCR_BYTES_F64 ? JS_TYPED_ARRAY_FLOAT64
: JS_TYPED_ARRAY_FLOAT32);
JS_FreeValue(isl_ctx, buf);
if (JS_IsException(v)) { if (JS_IsException(v)) {
isl_bridge_exception(); isl_bridge_exception();
return NULL; return NULL;
+28 -18
View File
@@ -545,7 +545,7 @@ void scr_dyn_arr_push_spread(ScrDyn *arr, const ScrDyn *src, const char *what) {
} }
if (src->kind == SCR_DYN_BYTES) { if (src->kind == SCR_DYN_BYTES) {
for (size_t i = 0; i < src->v.bytes->len; i++) { for (size_t i = 0; i < src->v.bytes->len; i++) {
scr_dyn_arr_push(arr, scr_dyn_new_num((double)src->v.bytes->data[i])); scr_dyn_arr_push(arr, scr_dyn_new_num(scr_bytes_get(src->v.bytes, (double)i)));
} }
return; return;
} }
@@ -1165,6 +1165,10 @@ void scr_dyn_typed_ref_cache_cast(
d->v.typed_ref.casts = cast; d->v.typed_ref.casts = cast;
} }
bool scr_dyn_bytes_is(const ScrDyn *d, int elem) {
return d && d->kind == SCR_DYN_BYTES && d->v.bytes->elem == (ScrBytesElem)elem;
}
ScrBytes *scr_dyn_bytes_unbox(const ScrDyn *d) { ScrBytes *scr_dyn_bytes_unbox(const ScrDyn *d) {
return scr_bytes_retain(d->v.bytes); return scr_bytes_retain(d->v.bytes);
} }
@@ -1377,7 +1381,9 @@ const char *scr_dyn_specific_type(const ScrDyn *cb, char *detail, size_t cap) {
case SCR_DYN_UNDEF: d = "undefined"; break; case SCR_DYN_UNDEF: d = "undefined"; break;
case SCR_DYN_OBJ: d = "an instance of Object"; break; case SCR_DYN_OBJ: d = "an instance of Object"; break;
case SCR_DYN_ARR: d = "an instance of Array"; break; case SCR_DYN_ARR: d = "an instance of Array"; break;
case SCR_DYN_BYTES: d = "an instance of Uint8Array"; break; case SCR_DYN_BYTES:
snprintf(detail, cap, "an instance of %s", cb->buffer ? "Buffer" : scr_bytes_elem_name(cb->v.bytes->elem));
break;
case SCR_DYN_FUNC: case SCR_DYN_FUNC:
/* determineSpecificType: `function ${value.name}` — anonymous /* determineSpecificType: `function ${value.name}` — anonymous
* functions keep Node's trailing space. */ * functions keep Node's trailing space. */
@@ -2052,7 +2058,14 @@ ScrStr *scr_dyn_object_tag(const ScrDyn *d) {
} }
tag = "[object Object]"; tag = "[object Object]";
break; break;
case SCR_DYN_BYTES: tag = "[object Uint8Array]"; break; case SCR_DYN_BYTES: {
ScrJsonBuf b;
scr_jb_init(&b);
scr_jb_puts(&b, "[object ");
scr_jb_puts(&b, scr_bytes_elem_name(d->v.bytes->elem));
scr_jb_putc(&b, ']');
return scr_jb_finish(&b);
}
case SCR_DYN_FUNC: tag = "[object Function]"; break; case SCR_DYN_FUNC: tag = "[object Function]"; break;
case SCR_DYN_PROMISE: tag = "[object Promise]"; break; case SCR_DYN_PROMISE: tag = "[object Promise]"; break;
default: { default: {
@@ -2377,18 +2390,10 @@ ScrStr *scr_dyn_to_string(const ScrDyn *d, const ScrStr *enc) {
switch (d->kind) { switch (d->kind) {
case SCR_DYN_BYTES: case SCR_DYN_BYTES:
if (d->buffer) return scr_bytes_to_str(d->v.bytes, enc); if (d->buffer) return scr_bytes_to_str(d->v.bytes, enc);
/* Uint8Array.prototype.toString is Array's: elements joined */
{ {
ScrStr *out = scr_str_new("", 0); ScrStr *separator = scr_str_new(",", 1);
for (size_t i = 0; i < d->v.bytes->len; i++) { ScrStr *out = scr_bytes_join(d->v.bytes, separator);
char n[16]; scr_str_release(separator);
int w = snprintf(n, sizeof n, i > 0 ? ",%u" : "%u", (unsigned)d->v.bytes->data[i]);
ScrStr *piece = scr_str_new(n, (size_t)w);
ScrStr *joined = scr_str_concat(out, piece);
scr_str_release(out);
scr_str_release(piece);
out = joined;
}
return out; return out;
} }
case SCR_DYN_STR: case SCR_DYN_STR:
@@ -2920,13 +2925,14 @@ void scr_jb_put_dyn(ScrJsonBuf *b, const ScrDyn *d) {
return; return;
case SCR_DYN_BYTES: { case SCR_DYN_BYTES: {
/* Node's JSON.stringify over a typed array: the index-keyed object /* Node's JSON.stringify over a typed array: the index-keyed object
* form — {"0":1,"1":2}. u8 payloads only reach the checked-dynamic tree today. */ * form — {"0":1,"1":2}, using each view's numeric element kind. */
scr_jb_putc(b, '{'); scr_jb_putc(b, '{');
for (size_t i = 0; i < d->v.bytes->len; i++) { for (size_t i = 0; i < d->v.bytes->len; i++) {
if (i > 0) scr_jb_putc(b, ','); if (i > 0) scr_jb_putc(b, ',');
char idx[32]; char idx[32];
snprintf(idx, sizeof idx, "\"%zu\":%u", i, (unsigned)d->v.bytes->data[i]); snprintf(idx, sizeof idx, "\"%zu\":", i);
scr_jb_puts(b, idx); scr_jb_puts(b, idx);
scr_jb_put_f64(b, scr_bytes_get(d->v.bytes, (double)i));
} }
scr_jb_putc(b, '}'); scr_jb_putc(b, '}');
return; return;
@@ -3029,7 +3035,7 @@ static const char *scr_dyn_kind_name(const ScrDyn *d) {
case SCR_DYN_ARR: return "array"; case SCR_DYN_ARR: return "array";
case SCR_DYN_OBJ: return "object"; case SCR_DYN_OBJ: return "object";
case SCR_DYN_UNDEF: return "undefined"; case SCR_DYN_UNDEF: return "undefined";
case SCR_DYN_BYTES: return "Uint8Array"; case SCR_DYN_BYTES: return scr_bytes_elem_name(d->v.bytes->elem);
case SCR_DYN_FUNC: return "function"; case SCR_DYN_FUNC: return "function";
case SCR_DYN_HANDLE: return scr_dyn_handle_cls(d); /* "got IncomingMessage" */ case SCR_DYN_HANDLE: return scr_dyn_handle_cls(d); /* "got IncomingMessage" */
case SCR_DYN_PROMISE: return "Promise"; /* "got Promise" */ case SCR_DYN_PROMISE: return "Promise"; /* "got Promise" */
@@ -4374,7 +4380,7 @@ static ScrDyn *scr_dyn_objwalk(const ScrDyn *v, ScrObjWalk mode) {
ScrDyn *val = NULL; ScrDyn *val = NULL;
if (mode != SCR_OBJWALK_KEYS) { if (mode != SCR_OBJWALK_KEYS) {
val = v->kind == SCR_DYN_ARR ? scr_dyn_retain(v->v.arr.items[i]) val = v->kind == SCR_DYN_ARR ? scr_dyn_retain(v->v.arr.items[i])
: scr_dyn_new_num((double)v->v.bytes->data[i]); : scr_dyn_new_num(scr_bytes_get(v->v.bytes, (double)i));
} }
if (mode == SCR_OBJWALK_KEYS) { if (mode == SCR_OBJWALK_KEYS) {
scr_dyn_arr_push(out, scr_dyn_objwalk_key(key, (size_t)klen)); scr_dyn_arr_push(out, scr_dyn_objwalk_key(key, (size_t)klen));
@@ -5093,6 +5099,10 @@ ScrBytes *scr_array_buffer_view(ScrBytesElem elem, const ScrDyn *buffer,
ScrBytes *scr_array_buffer_view_##name(ScrDyn *buffer, ScrDyn *offset, ScrDyn *length) { \ ScrBytes *scr_array_buffer_view_##name(ScrDyn *buffer, ScrDyn *offset, ScrDyn *length) { \
return scr_array_buffer_view(elem, buffer, offset, length); \ return scr_array_buffer_view(elem, buffer, offset, length); \
} }
SCR_ARRAY_BUFFER_VIEW(u8c, SCR_BYTES_U8C)
SCR_ARRAY_BUFFER_VIEW(i8, SCR_BYTES_I8)
SCR_ARRAY_BUFFER_VIEW(u16, SCR_BYTES_U16)
SCR_ARRAY_BUFFER_VIEW(i16, SCR_BYTES_I16)
SCR_ARRAY_BUFFER_VIEW(u8, SCR_BYTES_U8) SCR_ARRAY_BUFFER_VIEW(u8, SCR_BYTES_U8)
SCR_ARRAY_BUFFER_VIEW(u32, SCR_BYTES_U32) SCR_ARRAY_BUFFER_VIEW(u32, SCR_BYTES_U32)
SCR_ARRAY_BUFFER_VIEW(i32, SCR_BYTES_I32) SCR_ARRAY_BUFFER_VIEW(i32, SCR_BYTES_I32)
+1 -1
View File
@@ -2222,7 +2222,7 @@ void scr_net_sock_write_str(ScrNetSocket *s, ScrStr *data /*borrowed*/) {
} }
void scr_net_sock_write_bytes(ScrNetSocket *s, ScrBytes *data /*borrowed*/) { void scr_net_sock_write_bytes(ScrNetSocket *s, ScrBytes *data /*borrowed*/) {
scr_net_sock_write_raw(s, (const char *)data->data, data->len); scr_net_sock_write_raw(s, (const char *)data->data, (data->len * scr_bytes_elem_size(data->elem)));
} }
void scr_net_sock_end(ScrNetSocket *s) { void scr_net_sock_end(ScrNetSocket *s) {
+22 -11
View File
@@ -3252,15 +3252,11 @@ typedef enum {
* drops object members holding it and prints null for array slots, * drops object members holding it and prints null for array slots,
* exactly Node. dynCheck matches it against exactly the undefined arm. */ * exactly Node. dynCheck matches it against exactly the undefined arm. */
SCR_DYN_UNDEF, SCR_DYN_UNDEF,
/* A Uint8Array/Buffer VALUE. Never produced by the parser — it enters /* A numeric typed-array/Buffer value retained by reference. Boxing and
* the checked-dynamic tree through the compiler's static→dyn converters (a bytes<u8> * checked extraction preserve the view, exact element kind, and backing
* value flowing into an `unknown` slot: stdin chunks passed to * allocation. typeof answers "object"; String() joins numeric elements
* unknown-typed helpers). Owns a ScrBytes payload (a COPY of the static * (Buffer keeps its encoding behavior); JSON writes an index-keyed object.
* source — the boundary's aliasing stance). typeof answers "object" * Exact-brand tests accept Buffer as Uint8Array. */
* (kind tests all miss), String() joins the elements ("1,2,3" —
* Uint8Array.prototype.toString), JSON serializes the index-keyed
* object form ({"0":1}), and dynCheck extracts a fresh copy against a
* Uint8Array target. */
SCR_DYN_BYTES, SCR_DYN_BYTES,
/* A FUNCTION value. Never produced by the parser — it enters the checked-dynamic tree /* A FUNCTION value. Never produced by the parser — it enters the checked-dynamic tree
* through the compiler's static→dyn converters (a typed closure flowing * through the compiler's static→dyn converters (a typed closure flowing
@@ -3625,6 +3621,8 @@ void scr_dyn_typed_ref_cache_cast(
void *(*retain)(void *), void (*release)(void *)); void *(*retain)(void *), void (*release)(void *));
/* Retains a SCR_DYN_BYTES view (+1) for checked extraction. */ /* Retains a SCR_DYN_BYTES view (+1) for checked extraction. */
ScrBytes *scr_dyn_bytes_unbox(const ScrDyn *d); ScrBytes *scr_dyn_bytes_unbox(const ScrDyn *d);
/* Exact numeric typed-array brand; elem is a ScrBytesElem tag. */
bool scr_dyn_bytes_is(const ScrDyn *d, int elem);
void scr_dyn_arr_push(ScrDyn *arr, ScrDyn *item); void scr_dyn_arr_push(ScrDyn *arr, ScrDyn *item);
/* Spread completion for a runtime-arity argument list (`f(...xs)` in the /* Spread completion for a runtime-arity argument list (`f(...xs)` in the
* checked-dynamic tier): flattens `src` into `arr` per JS's spread over the * checked-dynamic tier): flattens `src` into `arr` per JS's spread over the
@@ -4687,6 +4685,7 @@ ScrPromise *scr_fetch_static(ScrStr *url, ScrDyn *init); /* +1 promise<Response
ScrDyn *scr_fetch_response_new(ScrDyn *body, ScrDyn *init); /* borrowed args; +1 Response handle or NULL pending */ ScrDyn *scr_fetch_response_new(ScrDyn *body, ScrDyn *init); /* borrowed args; +1 Response handle or NULL pending */
ScrPromise *scr_fetch_response_json(ScrDyn *response); /* +1 promise<dyn> */ ScrPromise *scr_fetch_response_json(ScrDyn *response); /* +1 promise<dyn> */
ScrPromise *scr_fetch_response_text(ScrDyn *response); /* +1 promise<dyn> */ ScrPromise *scr_fetch_response_text(ScrDyn *response); /* +1 promise<dyn> */
ScrPromise *scr_fetch_response_array_buffer(ScrDyn *response); /* +1 promise<dyn> */
ScrPromise *scr_fetch_response_bytes(ScrDyn *response); /* +1 promise<dyn> */ ScrPromise *scr_fetch_response_bytes(ScrDyn *response); /* +1 promise<dyn> */
ScrDyn *scr_fetch_abort_controller_new(void); /* +1 AbortController handle */ ScrDyn *scr_fetch_abort_controller_new(void); /* +1 AbortController handle */
/* Borrowed number; +1 AbortSignal handle or NULL pending. */ /* Borrowed number; +1 AbortSignal handle or NULL pending. */
@@ -5245,7 +5244,7 @@ double scr_bit_ushr(double a, double b);
double scr_bit_not(double a); double scr_bit_not(double a);
/* ── typed arrays / Buffer (scr_bytes.c) ────────────────────────────── /* ── typed arrays / Buffer (scr_bytes.c) ──────────────────────────────
* ONE runtime representation for Uint8Array/Uint32Array/Float32Array/Float64Array, * ONE runtime representation for the supported numeric typed arrays,
* Node's Buffer (a Uint8Array subclass), and DataView: a refcounted, * Node's Buffer (a Uint8Array subclass), and DataView: a refcounted,
* MUTABLE, fixed-length element buffer. An ScrBytes either OWNS its * MUTABLE, fixed-length element buffer. An ScrBytes either OWNS its
* storage (backing == NULL, byteOffset 0) or is a VIEW: its `data` points * storage (backing == NULL, byteOffset 0) or is a VIEW: its `data` points
@@ -5272,6 +5271,10 @@ typedef enum ScrBytesElem {
SCR_BYTES_F32, /* Float32Array */ SCR_BYTES_F32, /* Float32Array */
SCR_BYTES_I32, /* Int32Array (reads sign-extend; writes ToInt32-wrap) */ SCR_BYTES_I32, /* Int32Array (reads sign-extend; writes ToInt32-wrap) */
SCR_BYTES_F64, /* Float64Array */ SCR_BYTES_F64, /* Float64Array */
SCR_BYTES_I8, /* Int8Array */
SCR_BYTES_U16, /* Uint16Array */
SCR_BYTES_I16, /* Int16Array */
SCR_BYTES_U8C, /* Uint8ClampedArray */
} ScrBytesElem; } ScrBytesElem;
typedef struct ScrBytes { typedef struct ScrBytes {
@@ -5287,7 +5290,9 @@ typedef struct ScrBytes {
bool is_buffer; /* Buffer brand belongs to the view, not its backing. */ bool is_buffer; /* Buffer brand belongs to the view, not its backing. */
} ScrBytes; } ScrBytes;
size_t scr_bytes_elem_size(ScrBytesElem elem); /* 1, 4, or 8 */ size_t scr_bytes_elem_size(ScrBytesElem elem); /* 1, 2, 4, or 8 */
const char *scr_bytes_elem_name(ScrBytesElem elem);
double scr_bytes_to_u8_clamp(double value);
/* node:string_decoder's StringDecoder (scr_bytes.c, beside the decoders /* node:string_decoder's StringDecoder (scr_bytes.c, beside the decoders
* it shares): the decoder value is a record holding the CANONICAL * it shares): the decoder value is a record holding the CANONICAL
@@ -5316,6 +5321,8 @@ ScrJsval *scr_jsval_from_bytes(const ScrBytes *b);
* THROWS Node's "Invalid typed array length" RangeError catchably and * THROWS Node's "Invalid typed array length" RangeError catchably and
* returns NULL with the exception pending). */ * returns NULL with the exception pending). */
ScrBytes *scr_bytes_new(ScrBytesElem elem, double n); /* +1 */ ScrBytes *scr_bytes_new(ScrBytesElem elem, double n); /* +1 */
/* Shared u8 view over exactly the source view's raw bytes. Borrows; +1. */
ScrBytes *scr_bytes_raw_view(ScrBytes *bytes);
ScrBytes *scr_bytes_as_buffer(ScrBytes *bytes); /* borrows; +1, Buffer factory only */ ScrBytes *scr_bytes_as_buffer(ScrBytes *bytes); /* borrows; +1, Buffer factory only */
/* Views retain the root allocation; offsets are relative to its complete /* Views retain the root allocation; offsets are relative to its complete
* byte storage, including when src is itself a subarray. */ * byte storage, including when src is itself a subarray. */
@@ -5329,6 +5336,10 @@ double scr_array_buffer_byte_length_getter(void);
ScrDyn *scr_array_buffer_byte_length_descriptor(ScrDyn *getter); ScrDyn *scr_array_buffer_byte_length_descriptor(ScrDyn *getter);
ScrBytes *scr_array_buffer_view(ScrBytesElem elem, const ScrDyn *buffer, ScrBytes *scr_array_buffer_view(ScrBytesElem elem, const ScrDyn *buffer,
const ScrDyn *offset, const ScrDyn *length); const ScrDyn *offset, const ScrDyn *length);
ScrBytes *scr_array_buffer_view_u8c(ScrDyn *, ScrDyn *, ScrDyn *);
ScrBytes *scr_array_buffer_view_i8(ScrDyn *, ScrDyn *, ScrDyn *);
ScrBytes *scr_array_buffer_view_u16(ScrDyn *, ScrDyn *, ScrDyn *);
ScrBytes *scr_array_buffer_view_i16(ScrDyn *, ScrDyn *, ScrDyn *);
ScrBytes *scr_array_buffer_view_u8(ScrDyn *, ScrDyn *, ScrDyn *); ScrBytes *scr_array_buffer_view_u8(ScrDyn *, ScrDyn *, ScrDyn *);
ScrBytes *scr_array_buffer_view_u32(ScrDyn *, ScrDyn *, ScrDyn *); ScrBytes *scr_array_buffer_view_u32(ScrDyn *, ScrDyn *, ScrDyn *);
ScrBytes *scr_array_buffer_view_i32(ScrDyn *, ScrDyn *, ScrDyn *); ScrBytes *scr_array_buffer_view_i32(ScrDyn *, ScrDyn *, ScrDyn *);
+3 -3
View File
@@ -845,7 +845,7 @@ static void scr_stream_dyn_data(ScrDyn *const *args, size_t argc,
*data_str = NULL; *data_str = NULL;
if (argc < 2) return; if (argc < 2) return;
const ScrDyn *a = args[1]; const ScrDyn *a = args[1];
if (a->kind == SCR_DYN_BYTES) *data = scr_dyn_bytes_unbox(a); if (a->kind == SCR_DYN_BYTES) *data = scr_bytes_raw_view(a->v.bytes);
else if (a->kind == SCR_DYN_STR) *data_str = scr_str_retain(a->v.str); else if (a->kind == SCR_DYN_STR) *data_str = scr_str_retain(a->v.str);
} }
@@ -877,7 +877,7 @@ ScrDyn *scr_stream_done_dyn_l(ScrClosure *clo, ScrDyn *const *args, size_t argc)
bool scr_stream_push_dyn(ScrStream *s, const ScrDyn *d) { bool scr_stream_push_dyn(ScrStream *s, const ScrDyn *d) {
switch (d->kind) { switch (d->kind) {
case SCR_DYN_BYTES: { case SCR_DYN_BYTES: {
ScrBytes *b = scr_dyn_bytes_unbox(d); ScrBytes *b = scr_bytes_raw_view(d->v.bytes);
bool r = scr_stream_push(s, b); bool r = scr_stream_push(s, b);
scr_bytes_release(b); scr_bytes_release(b);
return r; return r;
@@ -897,7 +897,7 @@ bool scr_stream_push_dyn(ScrStream *s, const ScrDyn *d) {
bool scr_stream_write_dyn(ScrStream *s, const ScrDyn *d, ScrClosure *cb) { bool scr_stream_write_dyn(ScrStream *s, const ScrDyn *d, ScrClosure *cb) {
switch (d->kind) { switch (d->kind) {
case SCR_DYN_BYTES: { case SCR_DYN_BYTES: {
ScrBytes *b = scr_dyn_bytes_unbox(d); ScrBytes *b = scr_bytes_raw_view(d->v.bytes);
bool r = scr_stream_write(s, b, cb); bool r = scr_stream_write(s, b, cb);
scr_bytes_release(b); scr_bytes_release(b);
return r; return r;
+2 -2
View File
@@ -1105,7 +1105,7 @@ static ScrBytes *scr_tls_pem_dyn(const ScrDyn *v, const char *what, bool concat)
memcpy(b->data, v->v.str->data, v->v.str->len); memcpy(b->data, v->v.str->data, v->v.str->len);
return b; return b;
} }
if (v->kind == SCR_DYN_BYTES) return scr_dyn_bytes_unbox(v); if (scr_dyn_bytes_is(v, SCR_BYTES_U8)) return scr_dyn_bytes_unbox(v);
if (v->kind == SCR_DYN_ARR) { if (v->kind == SCR_DYN_ARR) {
if (v->v.arr.len == 0) { if (v->v.arr.len == 0) {
ScrJsonBuf b; ScrJsonBuf b;
@@ -1128,7 +1128,7 @@ static ScrBytes *scr_tls_pem_dyn(const ScrDyn *v, const char *what, bool concat)
for (size_t i = 0; i < v->v.arr.len; i++) { for (size_t i = 0; i < v->v.arr.len; i++) {
const ScrDyn *e = v->v.arr.items[i]; const ScrDyn *e = v->v.arr.items[i];
if (e->kind == SCR_DYN_STR) total += e->v.str->len + 1; if (e->kind == SCR_DYN_STR) total += e->v.str->len + 1;
else if (e->kind == SCR_DYN_BYTES) total += e->v.bytes->len + 1; else if (scr_dyn_bytes_is(e, SCR_BYTES_U8)) total += e->v.bytes->len + 1;
else { else {
scr_dyn_arg_type_fail(what, "of type string or an instance of Buffer or Uint8Array", e); scr_dyn_arg_type_fail(what, "of type string or an instance of Buffer or Uint8Array", e);
return NULL; return NULL;
+10
View File
@@ -0,0 +1,10 @@
let calls = 0;
function input(value: unknown): unknown { calls++; return value; }
function check(value: unknown): void {
console.log(Number.isFinite(input(value)), Number.isNaN(input(value)), Number.isInteger(input(value)), Number.isSafeInteger(input(value)));
}
check(0); check(-0); check(1.25); check(NaN); check(Infinity); check(-Infinity);
check(9007199254740991); check(9007199254740992);
check("1"); check(null); check(undefined); check(true); check(new Uint16Array([1]));
check({ valueOf(): number { console.log("should not coerce"); return 1; } });
console.log(calls);
+18
View File
@@ -0,0 +1,18 @@
import { PassThrough } from "node:stream";
async function check(value) {
console.log(Buffer.from(value).toString("hex"));
const stream = new PassThrough();
stream.on("data", chunk => console.log(chunk.toString("hex")));
stream.end(value);
const response = new Response(value);
const body = new Uint8Array(await response.arrayBuffer());
console.log(body.join(","));
try { Buffer.compare(Buffer.from([1]), value); } catch (e) { console.log(e.name, e.message); }
}
const buffer = new ArrayBuffer(8);
const raw = new Uint8Array(buffer);
raw.set([1, 2, 3, 4, 5, 6, 7, 8]);
await check(new Uint16Array(buffer, 2, 2));
await check(new Int16Array([-1, -32768]));
await check(new Float32Array([1.5, -2]));
@@ -0,0 +1,33 @@
function exercise(value) {
console.log(value.length, value.byteLength, value.byteOffset, value.at(-1), value.at("1"));
const view = value.subarray(1);
const copy = value.slice(1);
console.log(view.buffer === value.buffer, copy.buffer === value.buffer);
value.set(new Int8Array([-1, 127]), 1);
console.log(String(value), String(view), String(copy));
console.log(value.fill(3.5, "0", 1) === value);
console.log(value.copyWithin(2, 0, 2) === value);
console.log(value.join("|"), value.toString());
console.log(value.join({ toString() { return ":"; } }));
console.log(JSON.stringify(Object.values(value)), JSON.stringify(Object.entries(value)));
console.log([...value].join(","));
const [first, second] = value;
console.log(first, second);
try { value.set([1, 2], 4); } catch (e) { console.log(e.name, e.message); }
}
exercise(new Int8Array([1, 2, 3, 4]));
exercise(new Uint16Array([1, 2, 3, 4]));
exercise(new Int16Array([1, 2, 3, 4]));
exercise(new Uint8ClampedArray([1, 2, 3, 4]));
exercise(new Float32Array([1.25, 2.5, 3.75, 4]));
// OpenTUI's color constructor copies a narrow view held in an untyped field.
class Color {
buffer;
constructor(buffer) {
this.buffer = new Uint16Array(4);
this.buffer.set(buffer.subarray(0, 4));
}
}
const color = new Color(new Uint16Array([65535, 32768, 255, 1, 99]));
console.log(String(color.buffer), color.buffer instanceof Uint16Array);
@@ -0,0 +1,42 @@
const input = [-65537, -32769, -129, -1, 0, 1, 127, 128, 255, 256, 32767, 32768, 65535, 65536, NaN, Infinity, -Infinity, 1.5, 2.5];
const signed8 = new Int8Array(input);
const unsigned16 = new Uint16Array(input);
const signed16 = new Int16Array(input);
const clamped = new Uint8ClampedArray(input);
console.log(Array.from(signed8).join(","));
console.log(Array.from(unsigned16).join(","));
console.log(Array.from(signed16).join(","));
console.log(Array.from(clamped).join(","));
console.log(signed8.byteLength, unsigned16.byteLength, signed16.byteLength, clamped.byteLength);
signed8[0] = 255;
unsigned16[0] = -2;
signed16[0] = 65535;
clamped[0] = 2.5;
clamped[1] = 3.5;
console.log(signed8[0], unsigned16[0], signed16[0], clamped[0], clamped[1]);
const storage = new ArrayBuffer(12);
const words = new Uint16Array(storage, 2, 4);
const signed = new Int16Array(storage, 2, 4);
words[0] = 65535;
signed[1] = -32768;
console.log(words.byteOffset, words.byteLength, signed[0], words[1]);
const view = words.subarray(1, 3);
view[0] = 1234;
const copied = words.slice(0, 2);
copied[0] = 9;
console.log(words[0], words[1], view.byteOffset, copied[0]);
words.set(new Int8Array([-1, 127]), 2);
console.log(Array.from(words).join(","));
console.log(Array.from(new Uint8ClampedArray(new Float64Array([0.5, 1.5, 254.5, 255.5]))).join(","));
const holes: number[] = [1];
holes.length = 3;
holes[2] = 5;
console.log(Array.from(new Int16Array(holes)).join(","));
console.log(Array.from(new Float32Array(holes)).join(","));
console.log(String(signed8), `${unsigned16}`, "" + signed16, String(clamped));
console.log(String(new Int16Array(0)), String(new Uint8Array([65, 66])));
console.log(String(new Float32Array([1.5, NaN, Infinity])));
console.log(String(new Float64Array([1.5, NaN, Infinity])));
console.log(String(new Int32Array([-1, 2147483647])), String(new Uint32Array([4294967295])));
+34
View File
@@ -0,0 +1,34 @@
import { inspect } from "node:util";
function describe(value: unknown): void {
console.log(value instanceof Int8Array, value instanceof Uint8Array, value instanceof Uint8ClampedArray,
value instanceof Uint16Array, value instanceof Int16Array, value instanceof Uint32Array,
value instanceof Int32Array, value instanceof Float32Array, value instanceof Float64Array);
console.log(String(value), JSON.stringify(value), inspect(value));
console.log(Object.prototype.toString.call(value));
}
describe(new Int8Array([-129, 255]));
describe(new Uint8Array([255, 256]));
describe(new Uint8ClampedArray([2.5, 3.5]));
describe(new Uint16Array([65535, 65536]));
describe(new Int16Array([32768, 65535]));
describe(new Uint32Array([4294967295, 4294967296]));
describe(new Int32Array([2147483648, 4294967295]));
describe(new Float32Array([1.25, NaN, Infinity, -0]));
describe(new Float64Array([1.25, NaN, -Infinity, -0]));
describe(Buffer.from([1, 2]));
describe(null);
const source = new Uint16Array([65535, 32768]);
const unknownView: unknown = source;
const restored = unknownView as Uint16Array;
restored[0] = 1234;
console.log(source === restored, source[0]);
if (unknownView instanceof Uint16Array) console.log(unknownView[1], unknownView.byteLength);
const nested: Record<string, unknown> = { words: source, floats: new Float64Array([1.5]) };
console.log((nested.words as Uint16Array) === source, String(nested.floats));
const shape = { words: new Int16Array([-1, 2]), nested: [new Float64Array([1.5, 2.5])] };
const opaque: unknown = shape;
const recovered = opaque as { words: Int16Array; nested: Float64Array[] };
console.log(recovered.words === shape.words, recovered.nested[0] === shape.nested[0]);
console.log(recovered.words[0], recovered.nested[0][1]);
+2 -3
View File
@@ -1,7 +1,6 @@
// fetch(url), RequestInit, AbortSignal, readable bodies, and the // fetch(url), RequestInit, AbortSignal, readable bodies, and the
// Response json/text/bytes readers are native static surface. // Response json/text/bytes/arrayBuffer readers are native static surface.
// arrayBuffer() and constructing Headers remain in the broader dynamic // Reading a body method as a value and constructing Headers remain fenced.
// web tier and diagnose cleanly at their use sites.
async function probe(url: string): Promise<number> { async function probe(url: string): Promise<number> {
const r = await fetch(url); const r = await fetch(url);
return r.status; return r.status;
+9
View File
@@ -896,3 +896,12 @@ try {
const caught = error as Error; const caught = error as Error;
console.log("fixed content-length mismatch:", caught.name, caught.message); console.log("fixed content-length mismatch:", caught.name, caught.message);
} }
const bufferResponse = await fetch(`${process.argv[2]}/json`);
const rawBody = await bufferResponse.arrayBuffer();
console.log("arrayBuffer body:", rawBody instanceof ArrayBuffer, bufferResponse.bodyUsed, new TextDecoder().decode(new Uint8Array(rawBody)));
try { await bufferResponse.arrayBuffer(); } catch (error) { console.log("arrayBuffer consumed:", (error as Error).name); }
const nullBufferResponse = new Response(null);
const emptyBuffer1 = await nullBufferResponse.arrayBuffer();
const emptyBuffer2 = await nullBufferResponse.arrayBuffer();
console.log("arrayBuffer null body:", emptyBuffer1.byteLength, emptyBuffer2.byteLength, emptyBuffer1 === emptyBuffer2, nullBufferResponse.bodyUsed);
@@ -1,35 +1,17 @@
fetch-static.ts:20:9 - error SC2020: 'Response.arrayBuffer() in a static build' is part of the standard library types but has no scriptc lowering yet fetch-static.ts:27:23 - error SC2020: 'Response.arrayBuffer in a static build' is part of the standard library types but has no scriptc lowering yet
19 | async function arrayBufferBody(url: string): Promise<void> { 26 | async function bracketArrayBufferRead(url: string): Promise<void> {
20 | await (await fetch(url)).arrayBuffer(); 27 | const unsupported = (await fetch(url))["arrayBuffer"];
| ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
21 | }
hint: use Response.bytes() for the native Uint8Array body; free-standing ArrayBuffer values have no static representation
fetch-static.ts:24:9 - error SC2020: 'Response.arrayBuffer() in a static build' is part of the standard library types but has no scriptc lowering yet
23 | async function bracketArrayBufferBody(url: string): Promise<void> {
24 | await (await fetch(url))["arrayBuffer"]();
| ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
25 | }
hint: use Response.bytes() for the native Uint8Array body; free-standing ArrayBuffer values have no static representation
fetch-static.ts:28:23 - error SC2020: 'Response.arrayBuffer in a static build' is part of the standard library types but has no scriptc lowering yet
27 | async function bracketArrayBufferRead(url: string): Promise<void> {
28 | const unsupported = (await fetch(url))["arrayBuffer"];
| ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ | ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
29 | } 28 | }
hint: the native static Response surface is status/ok/statusText/url/redirected/headers/body/bodyUsed plus json(), text(), and bytes(); use --dynamic for the wider Web API hint: the native static Response surface is status/ok/statusText/url/redirected/headers/body/bodyUsed plus json(), text(), bytes(), and arrayBuffer(); use --dynamic for the wider Web API
fetch-static.ts:31:17 - error SC2020: 'new Headers' is part of the standard library types but has no scriptc lowering yet fetch-static.ts:30:17 - error SC2020: 'new Headers' is part of the standard library types but has no scriptc lowering yet
30 | bracketArrayBufferRead("http://localhost/e"); 29 | bracketArrayBufferRead("http://localhost/e");
31 | const headers = new Headers(); 30 | const headers = new Headers();
| ^~~~~~~~~~~~~ | ^~~~~~~~~~~~~
32 | // The Headers constructor remains fenced even though response.headers is native. 31 | // The Headers constructor remains fenced even though response.headers is native.
hint: the type checker sees the full standard library, but only the supported surface compiles (https://scriptc.dev/limitations) hint: the type checker sees the full standard library, but only the supported surface compiles (https://scriptc.dev/limitations)