From bc9ef21b90a6919fca14a62f5cc67b4b4147b7ea Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Mon, 19 Oct 2026 23:28:21 -0500 Subject: [PATCH] Keep DataView's own behavior through unknown A DataView reached as a checked-dynamic value behaved like a Uint8Array over its bytes: it printed as `Uint8Array(2) [ 0, 0 ]`, its string and tag were the typed array's, `.length` and index reads answered from the bytes, and typed array methods ran on it. It now inspects as Node's `DataView { [byteLength], [byteOffset], [buffer] }`, answers "[object DataView]", has no length or indexed elements, and throws "is not a function" for typed array methods. --- packages/compiler/src/backend/llvm/dyn.ts | 10 ++--- .../corpus/6346-inspect-dyn-dataview.ts.json | 6 +++ .../corpus/6347-dataview-methods-js.cjs.json | 6 +++ packages/runtime/src/scr_dyn_invoke.c | 11 +++++ packages/runtime/src/scr_inspect.c | 36 ++++++++++++++++ packages/runtime/src/scr_json.c | 17 ++++++-- tests/corpus/6346-inspect-dyn-dataview.ts | 41 +++++++++++++++++++ tests/corpus/6347-dataview-methods-js.cjs | 25 +++++++++++ .../6346-inspect-dyn-dataview.ts.json | 1 + .../6347-dataview-methods-js.cjs.json | 1 + 10 files changed, 146 insertions(+), 8 deletions(-) create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6346-inspect-dyn-dataview.ts.json create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6347-dataview-methods-js.cjs.json create mode 100644 tests/corpus/6346-inspect-dyn-dataview.ts create mode 100644 tests/corpus/6347-dataview-methods-js.cjs create mode 100644 tests/harness/corpus-costs/6346-inspect-dyn-dataview.ts.json create mode 100644 tests/harness/corpus-costs/6347-dataview-methods-js.cjs.json diff --git a/packages/compiler/src/backend/llvm/dyn.ts b/packages/compiler/src/backend/llvm/dyn.ts index ae4f12fb8..86b4f5be8 100644 --- a/packages/compiler/src/backend/llvm/dyn.ts +++ b/packages/compiler/src/backend/llvm/dyn.ts @@ -3265,8 +3265,9 @@ export class LlDyn { } B.startBlock(labels.get(DYN_KIND.BYTES)!); { - // Buffer-flavored values coerce utf8; plain Uint8Array joins - // its elements. + // Buffer-flavored values coerce utf8; typed arrays join their + // elements and a DataView answers "[object DataView]" (the + // runtime's conversion). const bufp = B.tmp(); const bufRaw = B.tmp(); const isBuf = B.tmp(); @@ -3288,10 +3289,9 @@ export class LlDyn { B.line(`call void @scr_str_release(ptr ${txt})`); B.br(done); B.startBlock(lJoin); - const bts = this.payloadOf(B, "%d", "ptr"); - host.declare(`declare ptr @scr_bytes_join(ptr, ptr)`); + host.declare(`declare ptr @scr_dyn_to_string(ptr, ptr)`); const joined = B.tmp(); - B.line(`${joined} = call ptr @scr_bytes_join(ptr ${bts}, ptr ${host.internLiteral(",")})`); + B.line(`${joined} = call ptr @scr_dyn_to_string(ptr %d, ptr null)`); this.putScrStr(B, "%b", joined); B.line(`call void @scr_str_release(ptr ${joined})`); B.br(done); diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6346-inspect-dyn-dataview.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6346-inspect-dyn-dataview.ts.json new file mode 100644 index 000000000..3e144e748 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6346-inspect-dyn-dataview.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/6346-inspect-dyn-dataview.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6347-dataview-methods-js.cjs.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6347-dataview-methods-js.cjs.json new file mode 100644 index 000000000..91c408e7a --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6347-dataview-methods-js.cjs.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/6347-dataview-methods-js.cjs" + ], + "diags": [] +} diff --git a/packages/runtime/src/scr_dyn_invoke.c b/packages/runtime/src/scr_dyn_invoke.c index 8a1bfa728..90e8d0363 100644 --- a/packages/runtime/src/scr_dyn_invoke.c +++ b/packages/runtime/src/scr_dyn_invoke.c @@ -1558,6 +1558,17 @@ static ScrDyn *scr_dyn_invoke_impl( } return scr_exc_pending() ? NULL : scr_dyn_retain(scr_dyn_undefined()); } + /* The typed array methods below are not DataView's; it inherits + * Object.prototype's. */ + if (dyn_name_is(method, "toString") || dyn_name_is(method, "toLocaleString")) { + ScrStr *tag = scr_str_new("[object DataView]", 17); + ScrDyn *out = scr_dyn_new_str(tag); + scr_str_release(tag); + return out; + } + if (dyn_name_is(method, "valueOf")) return scr_dyn_retain(recv); + dyn_throw_not_fn(what); + return NULL; } size_t blen = bytes->len; if (dyn_name_is(method, "at")) { diff --git a/packages/runtime/src/scr_inspect.c b/packages/runtime/src/scr_inspect.c index e38c7eb9b..3903cabeb 100644 --- a/packages/runtime/src/scr_inspect.c +++ b/packages/runtime/src/scr_inspect.c @@ -823,6 +823,41 @@ static ScrStr *scr_insp_array_buffer(const ScrBytes *h, const char *cls, double return out; } +/* DataView: formatValue's `DataView { [byteLength], [byteOffset], + * [buffer] }`, the buffer rendered one level deeper. `[DataView]` beyond + * depth. */ +static ScrStr *scr_insp_data_view(const ScrBytes *b, double recurse, double depth) { + if (recurse > depth) return scr_str_new("[DataView]", 10); + const ScrBytes *owner = b->backing ? b->backing : b; + char line[64]; + scr_insp_begin(recurse + 1); + int n = snprintf(line, sizeof line, "[byteLength]: %.0f", scr_bytes_byte_len(b)); + ScrStr *entry = scr_str_new(line, (size_t)n); + scr_insp_entry(entry, false); + scr_str_release(entry); + n = snprintf(line, sizeof line, "[byteOffset]: %.0f", scr_bytes_byte_offset(b)); + entry = scr_str_new(line, (size_t)n); + scr_insp_entry(entry, false); + scr_str_release(entry); + ScrStr *buffer = scr_insp_array_buffer(owner, owner->shared ? "SharedArrayBuffer" : "ArrayBuffer", + recurse + 1, depth); + InspBuf eb = {0}; + ib_cstr(&eb, "[buffer]: "); + ib_bytes(&eb, buffer->data, buffer->len); + scr_str_release(buffer); + entry = ib_take(&eb); + scr_insp_entry(entry, false); + scr_str_release(entry); + ScrStr *base = scr_str_new("", 0); + ScrStr *b0 = scr_str_new("DataView {", 10); + ScrStr *b1 = scr_str_new("}", 1); + ScrStr *out = scr_insp_end(base, b0, b1, recurse + 1, false, false); + scr_str_release(base); + scr_str_release(b0); + scr_str_release(b1); + return out; +} + static ScrStr *scr_insp_dyn_error(ScrDyn *d, double recurse, double depth); /* A runtime error through its node: the cached one, or one built from its @@ -1542,6 +1577,7 @@ ScrStr *scr_insp_dyn(ScrDyn *d, double recurse, double depth) { } case SCR_DYN_BYTES: { ScrBytes *b = d->v.bytes; + if (b->is_data_view) return scr_insp_data_view(b, recurse, depth); if (d->buffer) { /* Buffer-flavored (a stream chunk): Node's form. */ return scr_insp_buffer(b); diff --git a/packages/runtime/src/scr_json.c b/packages/runtime/src/scr_json.c index 83b56bea4..db290a23e 100644 --- a/packages/runtime/src/scr_json.c +++ b/packages/runtime/src/scr_json.c @@ -3456,10 +3456,19 @@ ScrDyn *scr_bytes_construct(const ScrDyn *callee, const ScrDyn *args, const ScrS /* Shared view metadata and indexed reads for both compiler backends. */ ScrDyn *scr_dyn_bytes_key_get(const ScrDyn *value, const ScrStr *key) { ScrBytes *bytes = value->v.bytes; - if (key->len == 6 && memcmp(key->data, "length", 6) == 0) return scr_dyn_new_num(scr_bytes_len(bytes)); if (key->len == 10 && memcmp(key->data, "byteLength", 10) == 0) return scr_dyn_new_num(scr_bytes_byte_len(bytes)); if (key->len == 10 && memcmp(key->data, "byteOffset", 10) == 0) return scr_dyn_new_num(scr_bytes_byte_offset(bytes)); if (key->len == 6 && memcmp(key->data, "buffer", 6) == 0) return scr_array_buffer_from_bytes(bytes); + if (bytes->is_data_view) { + /* A DataView has no length or indexed elements. */ + if (key->len == 11 && memcmp(key->data, "constructor", 11) == 0) { + static const char message[] = "DataView's constructor through unknown is not supported yet"; + scr_throw_error_msg(SCR_ERR_ERROR, message, sizeof message - 1); + return NULL; + } + return scr_dyn_retain(scr_dyn_undefined()); + } + if (key->len == 6 && memcmp(key->data, "length", 6) == 0) return scr_dyn_new_num(scr_bytes_len(bytes)); if (key->len == 11 && memcmp(key->data, "constructor", 11) == 0) { if (!value->buffer) return scr_bytes_constructor_for(bytes->elem); /* Buffer retains its separate builtin identity. */ @@ -6364,7 +6373,7 @@ ScrStr *scr_dyn_object_tag(const ScrDyn *d) { 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_puts(&b, d->v.bytes->is_data_view ? "DataView" : scr_bytes_elem_name(d->v.bytes->elem)); scr_jb_putc(&b, ']'); return scr_jb_finish(&b); } @@ -7059,6 +7068,8 @@ ScrStr *scr_dyn_to_string(const ScrDyn *d, const ScrStr *enc) { return d->v.symbol.render(d->v.symbol.value); case SCR_DYN_BYTES: if (d->buffer) return scr_bytes_to_str(d->v.bytes, enc); + /* DataView has no toString of its own: Object.prototype.toString. */ + if (d->v.bytes->is_data_view) return scr_str_new("[object DataView]", 17); { ScrStr *separator = scr_str_new(",", 1); ScrStr *out = scr_bytes_join(d->v.bytes, separator); @@ -8211,7 +8222,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 scr_bytes_elem_name(d->v.bytes->elem); + case SCR_DYN_BYTES: return d->v.bytes->is_data_view ? "DataView" : 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" */ diff --git a/tests/corpus/6346-inspect-dyn-dataview.ts b/tests/corpus/6346-inspect-dyn-dataview.ts new file mode 100644 index 000000000..ff4acfafd --- /dev/null +++ b/tests/corpus/6346-inspect-dyn-dataview.ts @@ -0,0 +1,41 @@ +// A DataView reached through `unknown` keeps its own behavior: Node's +// `DataView { [byteLength], [byteOffset], [buffer] }` inspection, the +// "[object DataView]" string and tag, no length or indexed elements, and +// no typed array methods. +import { inspect } from "node:util"; + +function show(label: string, value: unknown): void { + console.log(label, value); +} + +const buf = new ArrayBuffer(8); +const whole = new DataView(buf); +whole.setUint16(0, 0x1234); +show("whole", whole); +show("offset", new DataView(buf, 2, 4)); +show("empty", new DataView(new ArrayBuffer(0))); +show("big", new DataView(new ArrayBuffer(120))); +show("nested", { view: new DataView(buf, 6) }); +show("shared", new DataView(new SharedArrayBuffer(2))); + +const u: unknown = whole; +console.log(inspect({ a: { u } }, { depth: 1 }), inspect([u], { depth: 0 })); +console.log(String(u), Object.prototype.toString.call(u), `${u}`); + +const view: unknown = new DataView(buf, 1); +const o = view as { [k: string]: unknown }; +console.log(o.length, o[0], o.byteLength, o.byteOffset); +// eslint-disable-next-line @typescript-eslint/no-explicit-any +const loose = view as any; +console.log(loose.toString(), loose.valueOf() === view); +for (const method of ["at", "join", "fill", "slice"]) { + try { + loose[method](0); + } catch (e) { + console.log(method, (e as Error).name); + } +} +console.log(view instanceof DataView, ArrayBuffer.isView(view)); +// A typed array through `unknown` is unaffected. +const bytes: unknown = new Uint8Array([1, 2]); +console.log(bytes, String(bytes), `${bytes}`, (bytes as Uint8Array).length); diff --git a/tests/corpus/6347-dataview-methods-js.cjs b/tests/corpus/6347-dataview-methods-js.cjs new file mode 100644 index 000000000..680aaa011 --- /dev/null +++ b/tests/corpus/6347-dataview-methods-js.cjs @@ -0,0 +1,25 @@ +'use strict'; +// A DataView held in an untyped binding answers its own methods and +// Object.prototype's, and none of the typed array methods. +const view = [new DataView(new ArrayBuffer(4))][0]; +function attempt(label, f) { + try { + console.log(label, f()); + } catch (e) { + console.log(label, `${e.name}: ${e.message}`); + } +} +function use(v) { + v.setUint16(0, 0x1234); + attempt('get', () => [v.getUint16(0), v.getUint8(1)].join(' ')); + attempt('toString', () => v.toString()); + attempt('toLocaleString', () => v.toLocaleString()); + attempt('valueOf', () => v.valueOf() === v); + attempt('join', () => v.join('-')); + attempt('at', () => v.at(0)); + attempt('fill', () => v.fill(1)); + attempt('slice', () => v.slice(0, 1)); + attempt('indexOf', () => v.indexOf(0)); + attempt('string', () => `${String(v)} ${v}`); +} +use(view); diff --git a/tests/harness/corpus-costs/6346-inspect-dyn-dataview.ts.json b/tests/harness/corpus-costs/6346-inspect-dyn-dataview.ts.json new file mode 100644 index 000000000..2b1382076 --- /dev/null +++ b/tests/harness/corpus-costs/6346-inspect-dyn-dataview.ts.json @@ -0,0 +1 @@ +{ "plain": 1700, "san": 2200 } diff --git a/tests/harness/corpus-costs/6347-dataview-methods-js.cjs.json b/tests/harness/corpus-costs/6347-dataview-methods-js.cjs.json new file mode 100644 index 000000000..e053739a0 --- /dev/null +++ b/tests/harness/corpus-costs/6347-dataview-methods-js.cjs.json @@ -0,0 +1 @@ +{ "plain": 1500, "san": 2000 }