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.
- `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.
## 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.
- 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.
- 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.
- 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.
@@ -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.
**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.
+7 -7
View File
@@ -41,7 +41,7 @@ import type {
IrUnionDef,
SrcLoc,
} 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 { scalarizeNumericRecords } from "../../ir/scalar-records.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"];
for (const elem of elements) {
for (const mode of modes) {
@@ -1439,8 +1439,8 @@ export class CEmitter {
`}`,
);
} else {
const valueType = elem === "f32" ? "float" : elem === "f64" ? "double" : elem === "i32" ? "int32_t" : "uint32_t";
const size = elem === "f64" ? 8 : 4;
const size = BYTES_ELEMENT_SIZE[elem];
const valueType = elem === "f32" ? "float" : elem === "f64" ? "double" : `${elem.startsWith("i") ? "int" : "uint"}${size * 8}_t`;
out.push(
`static inline double sc_bytes_get_${elem}${suffix}(const ScrBytes *b, ${indexType} i) {`,
` ${valueType} v;`,
@@ -1458,9 +1458,9 @@ export class CEmitter {
`}`,
);
} else {
const valueType = elem === "f32" ? "float" : elem === "f64" ? "double" : "uint32_t";
const init = elem === "f32" ? `(float)v` : elem === "f64" ? "v" : `sc_bytes_coerce_u32(v)`;
const size = elem === "f64" ? 8 : 4;
const size = BYTES_ELEMENT_SIZE[elem];
const valueType = elem === "f32" ? "float" : elem === "f64" ? "double" : `uint${size * 8}_t`;
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(
`static inline void sc_bytes_set_${elem}${suffix}(ScrBytes *b, ${indexType} i, double v) {`,
` 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
* emitter's frames (see the discipline comment in emitter core). */
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 { mangleClassNew, mangleClassRetain, mangleClassStruct, mangleField, mangleFnClosure, mangleFunction, mangleGlobal, mangleLocal, mangleRecordClone, mangleRecordNew, mangleRecordStruct, mangleVtStruct } from "../mangle.js";
import { OVERFLOW_MEMBER } from "./shapes.js";
@@ -1993,7 +1993,7 @@ function emitContainerExpr(
}
return emitter.newTemp(
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":
// Any invalid index traps (the array runtime's discipline).
@@ -3253,6 +3253,10 @@ function emitDynamicExpr(
// kind, so the calls stay unconditional); narrowing never changes
// representation (SEMANTICS.md).
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") {
const test = `(${d.name}->kind == SCR_DYN_BYTES && ${d.name}->buffer)`;
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)})`);
case "fetch.responseNew":
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":
return finish(`scr_fetch_response_json(${arg(0)})`);
case "fetch.responseText":
@@ -4533,6 +4539,14 @@ function emitDynamicLibCall(state: LibCallState): Temp {
return finish(`scr_array_buffer_byte_length_getter()`);
case "arrayBuffer.byteLengthDescriptor":
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":
return finish(`scr_array_buffer_view_u8(${arg(0)}, ${arg(1)}, ${arg(2)})`);
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. */
const BYTES_ELEM_KIND_C: Record<IrBytesElem, string> = {
u8: "SCR_BYTES_U8",
u8c: "SCR_BYTES_U8C",
i8: "SCR_BYTES_I8",
u16: "SCR_BYTES_U16",
i16: "SCR_BYTES_I16",
u32: "SCR_BYTES_U32",
i32: "SCR_BYTES_I32",
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 { DYN_HANDLE_KINDS, type IrType, type IrUnionDef, isDynTypedRefType, isRefCounted, typeEquals, typeKey } from "../../ir/ir.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 { jsonObjectKeyLabel } from "../json-literal.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++) {`,
` if (i > 0) scr_jb_putc(b, ',');`,
` char n[16];`,
` snprintf(n, sizeof n, "%u", (unsigned)d->v.bytes->data[i]);`,
` scr_jb_puts(b, n);`,
` ScrStr *n = scr_f64_to_scrstr(scr_bytes_get(d->v.bytes, (double)i));`,
` for (size_t j = 0; j < n->len; j++) scr_jb_putc(b, n->data[j]);`,
` scr_str_release(n);`,
` }`,
` break;`,
` }`,
@@ -800,9 +800,7 @@ export function jsonWriteHelper(emitter: CEmitter, t: IrType): string {
d.push(` return true;`);
break;
case "bytes":
// A Uint8Array target (the checked-dynamic tree carries u8 payloads only).
if (t.elem !== "u8") throw new InternalCompilerError(`emitter bug: dynMatch of bytes<${t.elem}>`);
d.push(` return d->kind == SCR_DYN_BYTES;`);
d.push(` return scr_dyn_bytes_is(d, ${bytesElemKindC(t.elem)});`);
break;
case "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) {`,
` 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) {`,
` 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 {`,
` /* String iteration: whole code POINTS (astral chars arrive`,
` * 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);`);
break;
case "bytes":
// `u as Uint8Array`: kind check, then retain the shared view.
if (t.elem !== "u8") throw new InternalCompilerError(`emitter bug: dynCheck of bytes<${t.elem}>`);
d.push(` if (d->kind != SCR_DYN_BYTES) { scr_dyn_check_fail(path, ${want}, d); return NULL; }`);
// Check the exact element brand before retaining the shared view.
d.push(` if (!scr_dyn_bytes_is(d, ${bytesElemKindC(t.elem)})) { scr_dyn_check_fail(path, ${want}, d); return NULL; }`);
d.push(` return scr_dyn_bytes_unbox(d);`);
break;
case "object":
@@ -1572,9 +1569,7 @@ export function jsonWriteHelper(emitter: CEmitter, t: IrType): string {
d.push(` return ${dynFuncBoxHelper(emitter, t)}(v, NULL);`);
break;
case "bytes":
// bytes<u8> → a retained native view (the boundary
// stance; stdin chunks into unknown-typed helpers).
if (t.elem !== "u8") throw new InternalCompilerError(`emitter bug: to-dyn of bytes<${t.elem}>`);
// Preserve the view, its element brand, and its backing allocation.
d.push(` return scr_dyn_new_bytes(v);`);
break;
case "record": {
+14 -1
View File
@@ -1,6 +1,6 @@
import { commentText, unsignedHex } from "../literals.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
* 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);
/** 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";
/* The dyn (ScrDyn dyn) helper EMITTERS for the LLVM backend — the .ll
* 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.
B.terminate(`ret i1 true`);
break;
case "bytes":
if (t.elem !== "u8") throw new InternalCompilerError(`llvm emitter bug: dynMatch of bytes<${t.elem}>`);
kindIs(DYN_KIND.BYTES);
case "bytes": {
this.host.declare(`declare zeroext i1 @scr_dyn_bytes_is(ptr, i32)`);
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;
}
case "func":
kindIs(DYN_KIND.FUNC);
break;
@@ -798,9 +801,16 @@ export class LlDyn {
break;
}
case "bytes": {
// `u as Uint8Array`: kind check, then retain the shared view.
if (t.elem !== "u8") throw new InternalCompilerError(`llvm emitter bug: dynCheck of bytes<${t.elem}>`);
requireKind(DYN_KIND.BYTES, "dc");
host.declare(`declare zeroext i1 @scr_dyn_bytes_is(ptr, i32)`);
const matched = B.tmp();
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)`);
const r = B.tmp();
B.line(`${r} = call ptr @scr_dyn_bytes_unbox(ptr %d)`);
@@ -1360,7 +1370,6 @@ export class LlDyn {
break;
}
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)`);
const r = B.tmp();
B.line(`${r} = call ptr @scr_dyn_new_bytes(ptr %v)`);
@@ -2017,35 +2026,11 @@ export class LlDyn {
B.br(done);
B.startBlock(lJoin);
const bts = this.payloadOf(B, "%d", "ptr");
const blenp = B.tmp();
const blen = B.tmp();
const bdatap = B.tmp();
const bdata = B.tmp();
B.line(`${blenp} = getelementptr inbounds i8, ptr ${bts}, i64 ${this.abiOffset(8, 4)} ; ->len`);
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})`);
});
host.declare(`declare ptr @scr_bytes_join(ptr, ptr)`);
const joined = B.tmp();
B.line(`${joined} = call ptr @scr_bytes_join(ptr ${bts}, ptr ${host.internLiteral(",")})`);
this.putScrStr(B, "%b", joined);
B.line(`call void @scr_str_release(ptr ${joined})`);
B.br(done);
}
B.startBlock(labels.get(DYN_KIND.FUNC)!);
@@ -2814,7 +2799,7 @@ export class LlDyn {
B.line(`store ptr ${r2}, ptr ${itemSlot}`);
B.br(lPush);
}
// BYTES: by byte.
// BYTES: by numeric element.
B.startBlock(lBy);
{
const bts = this.payloadOf(B, "%d", "ptr");
@@ -2828,17 +2813,12 @@ export class LlDyn {
const lMiss = B.newLabel("din.bm");
B.condBr(inR, lHit, lMiss);
B.startBlock(lHit);
const bdatap = B.tmp();
const bdata = 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();
host.declare(`declare double @scr_bytes_get(ptr, double)`);
const index = B.tmp();
const bd = B.tmp();
const r = B.tmp();
B.line(`${bp} = getelementptr inbounds i8, ptr ${bdata}, ${host.sizeType} ${i}`);
B.line(`${bv} = load i8, ptr ${bp}`);
B.line(`${bd} = uitofp i8 ${bv} to double`);
B.line(`${index} = uitofp ${host.sizeType} ${i} to double`);
B.line(`${bd} = call double @scr_bytes_get(ptr ${bts}, double ${index})`);
B.line(`${r} = call ptr @scr_dyn_new_num(double ${bd})`);
B.line(`store ptr ${r}, ptr ${itemSlot}`);
B.br(lPush);
@@ -1,6 +1,6 @@
/* Focused LLVM expression emission extracted from emitter.ts. */
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 { F64_INF, f64Lit } from "./common.js";
@@ -128,8 +128,9 @@ export function emitBytesLength(host: LlvmEmitterContext, elem: IrBytesElem, rec
const len = B.tmp();
B.line(`${p} = getelementptr inbounds %ScrBytes, ptr ${receiver}, i64 0, i32 1`);
B.line(`${len} = load ${host.sizeType}, ptr ${p}`);
const count = bytes && elem !== "u8" ? B.tmp() : len;
if (count !== len) B.line(`${count} = shl ${host.sizeType} ${len}, ${elem === "f64" ? 3 : 2}`);
const size = BYTES_ELEMENT_SIZE[elem];
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();
B.line(`${out} = uitofp ${host.sizeType} ${count} to double`);
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 data = host.emitBytesData(receiver);
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 wide = B.tmp();
const out = B.tmp();
B.line(`${p} = getelementptr inbounds i8, ptr ${data}, ${host.sizeType} ${idx}`);
B.line(`${raw} = load i8, ptr ${p}, align 1`);
B.line(`${wide} = zext i8 ${raw} to i32`);
B.line(`${out} = uitofp i32 ${wide} to double`);
B.line(`${p} = getelementptr inbounds i${bits}, ptr ${data}, ${host.sizeType} ${idx}`);
B.line(`${raw} = load i${bits}, ptr ${p}, align 1`);
B.line(`${wide} = ${signed ? "sext" : "zext"} i${bits} ${raw} to i32`);
B.line(`${out} = ${signed ? "sitofp" : "uitofp"} i32 ${wide} to double`);
return { name: out, type: F64 };
}
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 {
const B = host.B;
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 p = B.tmp();
if (elem === "u8") {
if (BYTES_ELEMENT_SIZE[elem] < 4) {
const bits = BYTES_ELEMENT_SIZE[elem] * 8;
const byte = B.tmp();
B.line(`${byte} = trunc i32 ${stored!} to i8`);
B.line(`${p} = getelementptr inbounds i8, ptr ${data}, ${host.sizeType} ${idx}`);
B.line(`store i8 ${byte}, ptr ${p}, align 1`);
B.line(`${byte} = trunc i32 ${stored!} to i${bits}`);
B.line(`${p} = getelementptr inbounds i${bits}, ptr ${data}, ${host.sizeType} ${idx}`);
B.line(`store i${bits} ${byte}, ptr ${p}, align 1`);
return;
}
if (elem === "f32") {
@@ -2,6 +2,7 @@
import { InternalCompilerError } from "../../errors.js";
import { streamTypedRefEligible } from "../../ir/analysis.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 { elemAccess, vAdapters } from "./shapes.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).
const d = host.emitExpr(e.value);
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)`);
test = B.tmp();
B.line(`${test} = call zeroext i1 @scr_dyn_truthy(ptr ${d.name})`);
@@ -1,21 +1,13 @@
/* Focused LLVM expression emission extracted from emitter.ts. */
import { InternalCompilerError } from "../../errors.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 { arrNewCall, elemAccess } from "./shapes.js";
import { LlvmUnsupportedError } from "./unsupported.js";
import type { LlvmEmitterContext, ExprOf, LlValue } from "./expr-context.js";
/** ScrBytesElem (scr_runtime.h): U8, U32, F32, I32, F64. */
const BYTES_ELEM_NUM: Record<IrBytesElem, number> = {
u8: 0,
u32: 1,
f32: 2,
i32: 3,
f64: 4,
};
import { f64Lit } from "./common.js";
import { BYTES_ELEM_NUM, f64Lit } from "./common.js";
export function emitLiteralExpr(host: LlvmEmitterContext, e: ExprOf<"numLit" | "boolLit" | "strLit" | "moduleNsRef" | "unitLit" | "varRef">): LlValue {
const B = host.B;
@@ -30,6 +30,10 @@ export const LIB_FN_SYMS: Record<string, string> = {
"arrayBuffer.isView": "scr_array_buffer_is_view",
"arrayBuffer.byteLengthGetter": "scr_array_buffer_byte_length_getter",
"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.viewU32": "scr_array_buffer_view_u32",
"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.
"fetch.start": "scr_fetch_static",
"fetch.responseNew": "scr_fetch_response_new",
"fetch.responseArrayBuffer": "scr_fetch_response_array_buffer",
"fetch.responseJson": "scr_fetch_response_json",
"fetch.responseText": "scr_fetch_response_text",
"fetch.responseBytes": "scr_fetch_response_bytes",
@@ -500,12 +500,12 @@ export const NODE24_FETCH_COMPAT_PROFILE = {
facets: ["property-read"],
evidence: [fixture("static")],
})),
...["json", "text", "bytes"].map((member): FetchCompatOperation => ({
...["json", "text", "bytes", "arrayBuffer"].map((member): FetchCompatOperation => ({
id: `stdlib.response.${member}`,
name: `Response.${member}`,
kind: "method",
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: {
@@ -711,13 +711,7 @@ export const NODE24_FETCH_COMPAT_PROFILE = {
...["url", "redirected", "status", "ok", "statusText", "headers", "body", "bodyUsed"].map(
(member) => staticEntry(`stdlib.response.${member}`, "Response", member, "prototype"),
),
dynamicEntry(
"stdlib.response.arrayBuffer",
"Response",
"arrayBuffer",
"prototype",
"free-standing ArrayBuffer values have no static representation; use Response.bytes()",
),
staticEntry("stdlib.response.arrayBuffer", "Response", "arrayBuffer", "prototype"),
...["clone", "blob", "formData"].map((member) =>
unsupportedEntry(
`stdlib.response.${member}`,
@@ -1,6 +1,6 @@
import { BUF_NUM_METHODS } from "./buffer-numeric-methods.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 type { Lowerer } from "../lowerer.js";
import { dynUndefinedExpr, own } from "../lowerer.js";
@@ -97,17 +97,21 @@ function lowerBytesToSortedCall(
/* ── typed arrays / Buffer ─────────────────────────────────────────────── */
/** 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. */
const BYTES_CTORS: Record<string, IrBytesElem | undefined> = {
Uint8Array: "u8",
Uint8ClampedArray: "u8c",
Int8Array: "i8",
Uint16Array: "u16",
Int16Array: "i16",
Uint32Array: "u32",
Int32Array: "i32",
Float32Array: "f32",
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
* classBySymbol). Lowered argument shapes: none (empty), a length
* (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
* ArrayLike/Iterable union, which cannot map — the Set-seed pattern), or
* 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
* typed-array construction. */
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",
);
}
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 lenT = lenArg ? lowerer.typeOf(lenArg) : 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`,
argNode,
`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(
@@ -260,7 +264,7 @@ function lowerArrayBufferView(lowerer: Lowerer, args: readonly ts.Expression[],
const node = args[index];
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 };
}
@@ -318,9 +322,9 @@ export function lowerBytesMethodCall(lowerer: Lowerer, call: ts.CallExpression,
loc,
};
}
if (receiverIr.elem === "u8" && name === "join") {
if (name === "join") {
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 = {
kind: "strLit",
@@ -158,10 +158,10 @@ function lowerOptionalNumberPredicate(
// arm runs the predicate; every other arm answers false after evaluating
// the argument once. This includes the undefined added by array reads.
const widened = value.type;
if (widened.kind !== "union") return null;
const numberTag = lowerer.armTag(widened.unionId, F64);
if (numberTag < 0) return null;
const key = `number.optionalPredicate:${fn}:${widened.unionId}`;
if (widened.kind !== "union" && widened.kind !== "dyn") return null;
const numberTag = widened.kind === "union" ? lowerer.armTag(widened.unionId, F64) : -1;
if (widened.kind === "union" && numberTag < 0) return null;
const key = `number.optionalPredicate:${fn}:${typeKey(widened)}`;
let helper = lowerer.widthHelpers.get(key);
if (!helper) {
helper = `%number.optionalPredicate.${lowerer.widthHelpers.size}`;
@@ -175,7 +175,9 @@ function lowerOptionalNumberPredicate(
body: [
{
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 }],
else_: null,
loc,
@@ -185,7 +187,9 @@ function lowerOptionalNumberPredicate(
value: {
kind: "libCall",
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,
loc,
},
@@ -7,7 +7,7 @@ import * as ts from "../ts7/adapter.js";
import { bodyReadsArguments } from "../arguments-usage.js";
import type { Lowerer } from "./lowerer.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 { isJsSourceFile, isNodeEsmFile, locOf, requireSpecOf } from "../program.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 "promise": return "Promise";
case "bytes":
return ({ u8: "Uint8Array", u32: "Uint32Array", i32: "Int32Array", f32: "Float32Array", f64: "Float64Array" })[type.elem];
return BYTES_ELEMENT_NAME[type.elem];
case "object":
return type.className === "%Error" || lowerer.isSubclassOf(type.className, "%Error") ? "Error" : 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 };
}
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) {
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 { OBJECT_CALLABLE_VALUES } from "./surfaces.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 { 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";
@@ -2505,10 +2505,9 @@ function lowerExprInner(lowerer: Lowerer, expr: ts.Expression): IrExpr {
) {
return { kind: "dynCheck", value: expr, type: narrowed, loc: expr.loc };
}
// An `instanceof Uint8Array` narrow: the checked-dynamic tree's bytes kind, extracted
// with the same validated copy the checked cast uses (a Buffer that
// crossed in rides the kind too — it IS a Uint8Array in Node).
if (narrowed?.kind === "bytes" && narrowed.elem === "u8") {
// An instanceof narrow retains the native view after validating its
// exact element kind. Buffer also matches Uint8Array.
if (narrowed?.kind === "bytes") {
return { kind: "dynCheck", value: expr, type: narrowed, loc: expr.loc };
}
// 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
// the checker inferred a typed array from its producer. Drain once
// 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, {
kind: "libCall", fn: "dyn.iterPack", args: [src,
{ 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]").
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") {
// String(v) in the engine — JS-exact (and Node-exact in templates).
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) ??
undefined;
if (!target) {
// `u instanceof Uint8Array` on an `unknown` value: the checked-dynamic tree carries a
// bytes kind — one runtime tag test, and tsc's narrowing types the
// true branch (reads bridge through maybeNarrow's validated
// extraction, like the typeof tests). Node's Buffer IS a Uint8Array
// subclass and rides the same bytes kind, so both worlds answer true
// for Buffer payloads — Node-exact (the bytes kind's other
// divergences are SEMANTICS.md 45). Catch bindings stay out (their
// payload is a typed snapshot, not a dyn).
if (
ts.isIdentifier(expr.right) &&
lowerer.isStdlibGlobal(expr.right, "Uint8Array") &&
!lowerer.caughtLocalOf(expr.left)
) {
const left = lowerer.lowerExpr(expr.left);
if (left.type.kind === "dyn") {
return { kind: "dynTest", test: "bytes", value: left, type: BOOL, loc };
// Unknown storage retains each numeric typed array's exact native brand.
if (ts.isIdentifier(expr.right) && !lowerer.caughtLocalOf(expr.left)) {
const elem = (Object.keys(BYTES_ELEMENT_NAME) as IrBytesElem[]).find(
(kind) => lowerer.isStdlibGlobal(expr.right, BYTES_ELEMENT_NAME[kind]),
);
if (elem !== undefined) {
const left = lowerer.lowerExpr(expr.left);
if (left.type.kind === "dyn") {
return { kind: "dynTest", test: "bytes", bytesElem: elem, value: left, type: BOOL, loc };
}
if (left.type.kind === "bytes") {
return {
kind: "seqExpr", stmts: [{ kind: "exprStmt", expr: left, loc }],
result: { kind: "boolLit", value: left.type.elem === elem, type: BOOL, loc }, type: BOOL, loc,
};
}
}
}
// `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 sym = recvType.getAliasSymbol() ?? recvType.getSymbol();
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);
if (recv.type.kind !== "dyn") return null;
return lowerStaticFixedFetchMethodCall(
@@ -906,7 +898,7 @@ export function lowerStaticResponseCall(lowerer: Lowerer, call: ts.CallExpressio
}
: {
kind: "libCall",
fn: "fetch.responseJson",
fn: member === "arrayBuffer" ? "fetch.responseArrayBuffer" : "fetch.responseJson",
args: [receiver],
type: { kind: "promise", inner: DYN },
loc: locOf(call),
@@ -1212,7 +1204,7 @@ export function fenceStaticResponseMember(
lowerer.noLowering(
`Response.${member} in a static build`,
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,
);
}
@@ -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"],
// The bytes-data form and the { mode } options form.
writeFileSync: ["fs.writeFileSyncBytes", "fs.writeFileModeSync"],
appendFileSync: ["fs.appendFileSyncBytes"],
// The utf8 string overload; the table row is the Buffer-window form.
writeSync: ["fs.writeStrSync"],
// The { recursive, mode } option lowerings.
@@ -2,7 +2,7 @@ import { InternalCompilerError } from "../errors.js";
import * as ts from "./ts7/adapter.js";
import { bodyReadsArguments } from "./arguments-usage.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 { isJsSourceFile, isNodeTypesPath } from "./program.js";
@@ -485,7 +485,7 @@ export function formatIrType(t: IrType, shapes: ShapeRegistry, unions: UnionRegi
case "bytes":
// The u8 kind reads as Uint8Array (Buffer maps here too — one
// 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":
append("Map<");
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("Uint8Array")) return bytesOf("u8");
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
// ToInt32-wrap) — the Atomics.wait sleep idiom constructs one over a
// SharedArrayBuffer, and the i32 semantics hold for every other use.
+2 -1
View File
@@ -1,5 +1,6 @@
import { InternalCompilerError } from "../errors.js";
import {
BYTES_ELEMENT_NAME,
DYN_HANDLE_KINDS,
RUNTIME_STREAM_CLASSES,
typeEquals,
@@ -72,7 +73,7 @@ export function dynDesc(
case "nullT": return "null";
case "undefinedT": return "undefined";
case "dyn": return "unknown";
case "bytes": return "Uint8Array";
case "bytes": return BYTES_ELEMENT_NAME[t.elem];
case "object": return t.className.replace(/^%/, "");
case "union": {
const def = unionsById.get(t.unionId);
+16 -1
View File
@@ -1,5 +1,5 @@
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", () => {
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);
});
});
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 ─────────────────────────────────────────────────────────────── */
/** Typed-array element kinds with a runtime representation: Uint8Array/Buffer,
* Uint32Array, Int32Array, Float32Array, and Float64Array. Other flavors stay
* frontend-fenced. */
export type IrBytesElem = "u8" | "u32" | "i32" | "f32" | "f64";
/** Numeric typed-array element kinds with a native representation. Float16
* and BigInt typed arrays remain frontend-fenced. */
export type IrBytesElem = "u8" | "u8c" | "i8" | "u16" | "i16" | "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 =
| { kind: "f64" }
@@ -1992,6 +2000,10 @@ export type IrLibFn =
| "arrayBuffer.isView"
| "arrayBuffer.byteLengthGetter"
| "arrayBuffer.byteLengthDescriptor"
| "arrayBuffer.viewU8C"
| "arrayBuffer.viewI8"
| "arrayBuffer.viewU16"
| "arrayBuffer.viewI16"
| "arrayBuffer.viewU8"
| "arrayBuffer.viewU32"
| "arrayBuffer.viewI32"
@@ -2004,6 +2016,7 @@ export type IrLibFn =
* are opaque checked-dynamic handles. */
| "fetch.start"
| "fetch.responseNew"
| "fetch.responseArrayBuffer"
| "fetch.responseJson"
| "fetch.responseText"
| "fetch.responseBytes"
@@ -5359,9 +5372,9 @@ export type IrExpr =
* control flow understands on `unknown`: `typeof v === "string" |
* "number" | "boolean" | "undefined"` and the unit comparisons `v ===
* undefined` / `v === null` (`"nullish"` is the LOOSE `v == null` pair —
* undefined or null in one test), and `v instanceof Uint8Array`
* (`"bytes"` — the checked-dynamic tree's bytes kind; Node's Buffer IS a Uint8Array
* subclass and both worlds answer true for it, SEMANTICS.md 45), plus
* undefined or null in one test), and `v instanceof TypedArray`
* (`"bytes"` plus bytesElem, default u8; Node's Buffer is a Uint8Array
* subclass), plus
* the two object-family tests: `"object"` is `typeof v === "object"`
* 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
@@ -5383,7 +5396,7 @@ export type IrExpr =
* "function"` — true exactly for the checked-dynamic tree's function kind (boxed
* closures); function values are truthy and answer FALSE to the
* `"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
* `pkg?.scripts` chain step on a JSON.parse result. `key` is
* string-typed (a strLit for the dot form); `type` is always dyn. An
@@ -5889,18 +5902,18 @@ function isJsonSafeAt(
stringify: boolean,
undefinedAllowed: boolean,
visiting: Set<string>,
dynFields = false,
nativeFields = false,
): boolean {
if (HANDLE_KINDS.has(t.kind)) return false;
switch (t.kind) {
case "dyn":
return dynFields;
return nativeFields;
case "f64":
case "string":
case "bool":
return true;
case "array":
return isJsonSafeAt(t.elem, getRecord, getUnion, stringify, stringify, visiting, dynFields);
return isJsonSafeAt(t.elem, getRecord, getUnion, stringify, stringify, visiting, nativeFields);
case "record": {
const shape = getRecord(t.shapeId);
if (!shape) return false;
@@ -5910,7 +5923,7 @@ function isJsonSafeAt(
// short-circuits every `every` up the walk).
if (visiting.has(t.shapeId)) return true;
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;
}
// 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
// record-field rule.
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;
}
@@ -5928,8 +5941,10 @@ function isJsonSafeAt(
const key = `${t.unionId}:${stringify}:${undefinedAllowed}`;
if (visiting.has(key)) return true; // the recursive knot, union-flavored
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 "object":
// Class values stringify as "{}" husks in Node (own enumerable statics
@@ -5954,10 +5969,6 @@ function isJsonSafeAt(
// omitted as object values) — silent divergence banned; rejected.
case "symbol":
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 "caught":
case "promise":
@@ -6195,7 +6206,7 @@ export function classDynViewSupported(
}
/** 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,
* and the runtime HANDLE kinds (boxed by reference — DYN_HANDLE_KINDS). */
export function canConvertToDyn(
@@ -6204,13 +6215,13 @@ export function canConvertToDyn(
getUnion: (unionId: string) => IrUnionDef | undefined,
): boolean {
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
// in records/arrays/unions. isJsonSafeType rejects them, but dynFrom
// needs only that the walker can build the dyn value, so this composite
// fold extends the JSON-safe core.
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,
// code?} — the caughtToDyn shape, scr_dyn_from_error): the dyn 'error'
// 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
* 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
* 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
@@ -6270,7 +6281,7 @@ function canBoxDynComposite(
case "classval":
return true;
case "bytes":
return t.elem === "u8";
return true;
case "func":
return canBoxFuncIntoDyn(t, getRecord, getUnion);
case "array":
@@ -6298,7 +6309,7 @@ function canBoxDynComposite(
}
/** 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,
* and the runtime HANDLE kinds (a tag-checked reference unwrap —
* DYN_HANDLE_KINDS). */
@@ -6313,7 +6324,7 @@ export function canDynCheckTo(
// serializable. Backends already retain dyn fields and fill missing
// unknown record fields with the undefined value.
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 === "object" && t.className === "%Error") return true;
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([
"arrayBuffer.new",
"arrayBuffer.byteLengthGetter",
"arrayBuffer.viewU8C",
"arrayBuffer.viewI8",
"arrayBuffer.viewU16",
"arrayBuffer.viewI16",
"arrayBuffer.viewU8",
"arrayBuffer.viewU32",
"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);
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,
SrcLoc,
} 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 { unionWideningTags } from "./analysis.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 }> = {
"fetch.start": { argTypes: [STRING, DYN], result: { kind: "promise", inner: 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.responseText": { argTypes: [DYN], result: { kind: "promise", inner: STRING } },
"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.byteLengthGetter": { argTypes: [], result: F64 },
"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.viewU32": { argTypes: [DYN, DYN, DYN], result: bytesOf("u32") },
"arrayBuffer.viewI32": { argTypes: [DYN, DYN, DYN], result: bytesOf("i32") },
@@ -3791,6 +3796,9 @@ function validateFunction(
checkExpr(e.value);
expectType(e.value, { kind: "dyn" }, "dynTest operand");
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;
}
case "dynKeyGet": {
+2 -3
View File
@@ -3441,9 +3441,8 @@
"id": "stdlib.response.arrayBuffer",
"kind": "stdlib",
"name": "Response.arrayBuffer",
"status": "dynamic-only",
"code": "SC2020",
"note": "Node 24.15.0 / Undici 7.24.4; free-standing ArrayBuffer values have no static representation; use Response.bytes()"
"status": "static",
"note": "Node 24.15.0 / Undici 7.24.4; facets: body-consumption, promise-settlement, state-machine, error-shape; differential evidence: fixture:static"
},
{
"id": "stdlib.response.blob",
@@ -11157,6 +11157,36 @@
"<repo>/tests/corpus/union-discriminant-width.ts"
],
"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) {
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 ─────────────────────────────────────────────────────────── */
@@ -79,6 +108,11 @@ ScrBytes *scr_bytes_copy(const ScrBytes *src) {
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) {
bytes->is_buffer = true;
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) {
size_t idx = scr_bytes_check_index(b, i);
switch (b->elem) {
case SCR_BYTES_U8:
case SCR_BYTES_U8: case SCR_BYTES_U8C:
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: {
uint32_t v;
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) {
size_t idx = scr_bytes_check_index(b, i);
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);
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: {
uint32_t u = scr_bytes_to_u32(v);
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 *b = scr_bytes_alloc(elem, arr->len);
for (size_t i = 0; i < arr->len; i++) {
/* Buffer.from(Array) applies Number/ToUint* to each value. A hole is
* read as undefined and therefore contributes zero; it must never read
* an uninitialized dense slot or assume sparse storage is allocated. */
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;
}
}
// Iteration reads holes as undefined: floating arrays store NaN and
// integer arrays store zero through their normal element conversion.
scr_bytes_set(b, (double)i, scr_arr_get_number(arr, (double)i));
}
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));
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) {
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) {
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). */
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);
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
* only file: URLs anyway). */
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);
return false;
}
@@ -456,7 +456,7 @@ static void scr_fs_exists_fire(ScrClosure *self) {
ScrStr *p = scr_str_retain(path->v.str);
ans = scr_fs_exists(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);
ans = scr_fs_exists(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) {
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
* false through the callback SYNCHRONOUSLY (`return callback(false)`
* 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);
return;
}
double buflen = (double)buffer->v.bytes->len;
double buflen = scr_bytes_byte_len(buffer->v.bytes);
if (!scr_fs_dyn_absent(offset)) {
/* validateInteger's MAX_SAFE range first, the buffer bound second —
* 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);
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_UNDEF: received = "undefined"; 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:
snprintf(detail, sizeof detail, "type boolean (%s)", cb->v.b ? "true" : "false");
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++) {
if (i > 0) scr_jb_putc(b, ',');
char n[16];
snprintf(n, sizeof n, "%u", (unsigned)d->v.bytes->data[i]);
scr_jb_puts(b, n);
ScrStr *n = scr_f64_to_scrstr(scr_bytes_get(d->v.bytes, (double)i));
for (size_t j = 0; j < n->len; j++) scr_jb_putc(b, n->data[j]);
scr_str_release(n);
}
return;
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;
}
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). */
static size_t dyn_rel_index(double rel, size_t len) {
if (rel < 0) {
@@ -1023,18 +1030,18 @@ static ScrDyn *scr_dyn_invoke_impl(
ScrBytes *bytes = recv->v.bytes;
size_t blen = bytes->len;
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;
double idx = iD < 0 ? (double)blen + iD : iD;
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")) {
/* subarray and Buffer.slice alias their source. TypedArray.slice
* 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;
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;
ScrBytes *out = recv->buffer || dyn_name_is(method, "subarray")
? 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);
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)) {
dyn_throw_unsupported("Uint8Array", method);
dyn_throw_unsupported(scr_bytes_elem_name(bytes->elem), method);
return NULL;
}
}
+11 -3
View File
@@ -195,6 +195,7 @@ enum {
SF_COLLECT_JSON,
SF_COLLECT_TEXT,
SF_COLLECT_BYTES,
SF_COLLECT_ARRAY_BUFFER,
};
enum {
@@ -1545,7 +1546,7 @@ static ScrBytes *sf_chunk_bytes(const ScrDyn *chunk) {
scr_dyn_release(materialized);
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);
}
if (chunk && chunk->kind == SCR_DYN_STR) {
@@ -1639,6 +1640,8 @@ static void sf_collector_finish(SfStream *s) {
ScrStr *text = scr_text_decode(bytes);
value = scr_dyn_new_str(text);
scr_str_release(text);
} else if (c->mode == SF_COLLECT_ARRAY_BUFFER) {
value = scr_array_buffer_from_bytes(bytes);
} else {
value = scr_dyn_new_bytes(bytes);
}
@@ -3207,7 +3210,7 @@ static ScrPromise *sf_response_collect(SfResponse *r, int mode) {
scr_str_release(empty);
} else {
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);
}
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;
else if (strcmp(method, "text") == 0) mode = SF_COLLECT_TEXT;
else if (strcmp(method, "bytes") == 0) mode = SF_COLLECT_BYTES;
else if (strcmp(method, "arrayBuffer") == 0) mode = SF_COLLECT_ARRAY_BUFFER;
if (mode >= 0) {
ScrPromise *p = sf_response_collect(r, mode);
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);
}
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) {
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->v.handle.tag == SCR_DYNH_WEB_STREAM)) {
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 {
ScrStr *text =
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*/) {
double expected;
if (r->head_sent && scr_http_res_expected_length(r, &expected) &&
(double)(r->strict_bytes_written + data->len) > expected) {
r->strict_bytes_written += data->len;
(double)(r->strict_bytes_written + (data->len * scr_bytes_elem_size(data->elem))) > expected) {
r->strict_bytes_written += (data->len * scr_bytes_elem_size(data->elem));
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;
}
if (r->strict_content_length) r->strict_bytes_written += data->len;
scr_http_res_write_raw(r, (const char *)data->data, 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_bytes_elem_size(data->elem)));
}
/* 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*/) {
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) {
@@ -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*/) {
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) {
@@ -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*/) {
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) {
+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*/) {
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) {
@@ -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*/) {
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;
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);
}
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) {
InspBuf out = {0};
ib_bytes(&out, prefix, (size_t)pn);
ib_char(&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);
size_t shown = b->len < 100 ? b->len : 100;
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_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_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) {
switch (d->kind) {
case SCR_DYN_FUNC:
@@ -1038,8 +1055,7 @@ static JSValue isl_from_dyn(const ScrDyn *d) {
case SCR_DYN_STR:
return JS_NewStringLen(isl_ctx, d->v.str->data, d->v.str->len);
case SCR_DYN_BYTES:
/* Only u8 payloads reach the checked-dynamic tree today (scr_json.c's stringify note). */
return JS_NewUint8ArrayCopy(isl_ctx, d->v.bytes->data, d->v.bytes->len);
return isl_from_bytes(d->v.bytes);
case SCR_DYN_ARR: {
JSValue arr = JS_NewArray(isl_ctx);
if (JS_IsException(arr)) return arr;
@@ -9732,29 +9748,7 @@ ScrJsval *scr_jsval_null(void) {
ScrJsval *scr_jsval_from_bytes(const ScrBytes *b) {
isl_entry();
if (b->elem == SCR_BYTES_U8) {
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);
JSValue v = isl_from_bytes(b);
if (JS_IsException(v)) {
isl_bridge_exception();
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) {
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;
}
@@ -1165,6 +1165,10 @@ void scr_dyn_typed_ref_cache_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) {
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_OBJ: d = "an instance of Object"; 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:
/* determineSpecificType: `function ${value.name}` — anonymous
* functions keep Node's trailing space. */
@@ -2052,7 +2058,14 @@ ScrStr *scr_dyn_object_tag(const ScrDyn *d) {
}
tag = "[object Object]";
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_PROMISE: tag = "[object Promise]"; break;
default: {
@@ -2377,18 +2390,10 @@ ScrStr *scr_dyn_to_string(const ScrDyn *d, const ScrStr *enc) {
switch (d->kind) {
case SCR_DYN_BYTES:
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);
for (size_t i = 0; i < d->v.bytes->len; i++) {
char n[16];
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;
}
ScrStr *separator = scr_str_new(",", 1);
ScrStr *out = scr_bytes_join(d->v.bytes, separator);
scr_str_release(separator);
return out;
}
case SCR_DYN_STR:
@@ -2920,13 +2925,14 @@ void scr_jb_put_dyn(ScrJsonBuf *b, const ScrDyn *d) {
return;
case SCR_DYN_BYTES: {
/* 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, '{');
for (size_t i = 0; i < d->v.bytes->len; i++) {
if (i > 0) scr_jb_putc(b, ',');
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_put_f64(b, scr_bytes_get(d->v.bytes, (double)i));
}
scr_jb_putc(b, '}');
return;
@@ -3029,7 +3035,7 @@ static const char *scr_dyn_kind_name(const ScrDyn *d) {
case SCR_DYN_ARR: return "array";
case SCR_DYN_OBJ: return "object";
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_HANDLE: return scr_dyn_handle_cls(d); /* "got IncomingMessage" */
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;
if (mode != SCR_OBJWALK_KEYS) {
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) {
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) { \
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(u32, SCR_BYTES_U32)
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*/) {
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) {
+22 -11
View File
@@ -3252,15 +3252,11 @@ typedef enum {
* drops object members holding it and prints null for array slots,
* exactly Node. dynCheck matches it against exactly the undefined arm. */
SCR_DYN_UNDEF,
/* A Uint8Array/Buffer VALUE. Never produced by the parser — it enters
* the checked-dynamic tree through the compiler's static→dyn converters (a bytes<u8>
* value flowing into an `unknown` slot: stdin chunks passed to
* unknown-typed helpers). Owns a ScrBytes payload (a COPY of the static
* source — the boundary's aliasing stance). typeof answers "object"
* (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. */
/* A numeric typed-array/Buffer value retained by reference. Boxing and
* checked extraction preserve the view, exact element kind, and backing
* allocation. typeof answers "object"; String() joins numeric elements
* (Buffer keeps its encoding behavior); JSON writes an index-keyed object.
* Exact-brand tests accept Buffer as Uint8Array. */
SCR_DYN_BYTES,
/* 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
@@ -3625,6 +3621,8 @@ void scr_dyn_typed_ref_cache_cast(
void *(*retain)(void *), void (*release)(void *));
/* Retains a SCR_DYN_BYTES view (+1) for checked extraction. */
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);
/* 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
@@ -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 */
ScrPromise *scr_fetch_response_json(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> */
ScrDyn *scr_fetch_abort_controller_new(void); /* +1 AbortController handle */
/* 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);
/* ── 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,
* MUTABLE, fixed-length element buffer. An ScrBytes either OWNS its
* 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_I32, /* Int32Array (reads sign-extend; writes ToInt32-wrap) */
SCR_BYTES_F64, /* Float64Array */
SCR_BYTES_I8, /* Int8Array */
SCR_BYTES_U16, /* Uint16Array */
SCR_BYTES_I16, /* Int16Array */
SCR_BYTES_U8C, /* Uint8ClampedArray */
} ScrBytesElem;
typedef struct ScrBytes {
@@ -5287,7 +5290,9 @@ typedef struct ScrBytes {
bool is_buffer; /* Buffer brand belongs to the view, not its backing. */
} 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
* 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
* returns NULL with the exception pending). */
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 */
/* Views retain the root allocation; offsets are relative to its complete
* 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);
ScrBytes *scr_array_buffer_view(ScrBytesElem elem, const ScrDyn *buffer,
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_u32(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;
if (argc < 2) return;
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);
}
@@ -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) {
switch (d->kind) {
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);
scr_bytes_release(b);
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) {
switch (d->kind) {
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);
scr_bytes_release(b);
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);
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->v.arr.len == 0) {
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++) {
const ScrDyn *e = v->v.arr.items[i];
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 {
scr_dyn_arg_type_fail(what, "of type string or an instance of Buffer or Uint8Array", e);
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
// Response json/text/bytes readers are native static surface.
// arrayBuffer() and constructing Headers remain in the broader dynamic
// web tier and diagnose cleanly at their use sites.
// Response json/text/bytes/arrayBuffer readers are native static surface.
// Reading a body method as a value and constructing Headers remain fenced.
async function probe(url: string): Promise<number> {
const r = await fetch(url);
return r.status;
+9
View File
@@ -896,3 +896,12 @@ try {
const caught = error as Error;
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> {
20 | await (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"];
26 | async function bracketArrayBufferRead(url: string): Promise<void> {
27 | 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");
31 | const headers = new Headers();
29 | bracketArrayBufferRead("http://localhost/e");
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)