From 9a7e42fd1bab5742b4a3ab56410c9c0dc071c481 Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Thu, 23 Jul 2026 19:45:15 -0500 Subject: [PATCH 1/4] Fresh-ArrayBuffer erasure, DataView setters, and named buffer fences MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - new Uint8Array/Uint32Array/Int32Array/Float32Array(new ArrayBuffer(n)) erases the buffer into a zero-filled view exactly like the SharedArrayBuffer sleep idiom (literal byte length divisible by the element size), and new DataView(new ArrayBuffer(n), off?, len?) views a fresh zeroed allocation — nothing else can reference the buffer, so the erasure is exact. - The DataView setter family (setUint8/Int8/Uint16/Int16/Uint32/Int32/Float32/Float64) lowers end to end: one scr_dataview_set runtime mirror of the getter (ToIndex offsets, Node's constant RangeError, modular integer truncation, double→float rounding, both endiannesses) wired through both backends; the bigint and Float16 setters keep the member fence. - The resizable surface fences by NAME everywhere it can appear: new ArrayBuffer/resize/transfer/maxByteLength and SharedArrayBuffer.grow now state the reason (no free-standing ArrayBuffer value exists; grow has no threads to share with) at the constructor, identifier, member, and declared-type chokepoints, including the maxByteLength options bag inside an otherwise-erasable position. - Differential fixture 2560 pins the erasure forms, every setter width/endianness/coercion, aliasing through x.buffer views, and the RangeError edges byte-identically vs Node on both backends; es2025-fences/dataview/stdlib-fence snapshots pin the new named hints. --- .../src/backend/emission/emit-exprs.ts | 20 ++- .../src/backend/emission/emit-types.ts | 13 ++ packages/compiler/src/backend/llvm/emitter.ts | 31 ++++ .../src/frontend/lowering/lower-classes.ts | 1 + .../src/frontend/lowering/lower-containers.ts | 110 +++++++++++-- .../src/frontend/lowering/lower-exprs.ts | 1 + .../compiler/src/frontend/lowering/lowerer.ts | 14 +- .../src/frontend/lowering/surfaces.ts | 9 ++ packages/compiler/src/ir/nodes.ts | 25 ++- packages/compiler/src/ir/validate.ts | 24 ++- packages/runtime/src/scr_bytes.c | 43 +++++ packages/runtime/src/scr_runtime.h | 8 + tests/corpus/2560-arraybuffer-dataview-set.ts | 100 ++++++++++++ tests/diagnostics/es2025-fences.ts | 4 + tests/harness/__snapshots__/dataview.ts.txt | 8 +- .../__snapshots__/es2025-fences.ts.txt | 149 ++++++++++-------- .../harness/__snapshots__/stdlib-fence.ts.txt | 4 +- 17 files changed, 463 insertions(+), 101 deletions(-) create mode 100644 tests/corpus/2560-arraybuffer-dataview-set.ts diff --git a/packages/compiler/src/backend/emission/emit-exprs.ts b/packages/compiler/src/backend/emission/emit-exprs.ts index 411cb2b99..20bb8fb31 100644 --- a/packages/compiler/src/backend/emission/emit-exprs.ts +++ b/packages/compiler/src/backend/emission/emit-exprs.ts @@ -3,7 +3,7 @@ * emitter's frames (see the discipline comment in emitter core). */ import type { CEmitter, Temp } from "./emitter.js"; import { arrayOf, BOOL, BYTES_U8, bytesOf, canMarshalFuncIntoIsland, CHILDSTREAM_T, DYN, F64, IrExpr, IrRecordShape, IrType, isRefCounted, isUnitType, MAY_THROW_LIB_FNS, RUNTIME_ERROR_CLASSES, STRING, typeEquals } from "../../ir/nodes.js"; -import { boxAccess, BYTES_NUM_KIND_C, BYTES_NUM_VAR_C, bytesElemKindC, cDecl, cFnPtrCast, cNumberLiteral, cStringLiteral, cType, DV_GET_KIND_C, elemAccess, mapKeyAccess, mapKeyKindC, mapValKindC, retainCallC, vAdapters } from "./emit-types.js"; +import { boxAccess, 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, retainCallC, vAdapters } from "./emit-types.js"; import { mangleClassNew, mangleClassRetain, mangleClassStruct, mangleField, mangleFnClosure, mangleFunction, mangleGlobal, mangleLocal, mangleRecordNew, mangleRecordStruct, mangleVtStruct } from "../mangle.js"; import { OVERFLOW_MEMBER } from "./emit-shapes.js"; import { dynDestrCheckHelper, dynIterNHelper, dynKeyGetHelper } from "./emit-walkers.js"; @@ -1137,6 +1137,24 @@ export function emitExpr(E: CEmitter, e: IrExpr): Temp { E.emitPendingCheck(); return t; } + case "dvSetUint8": + case "dvSetInt8": + case "dvSetUint16": + case "dvSetInt16": + case "dvSetUint32": + case "dvSetInt32": + case "dvSetFloat32": + case "dvSetFloat64": { + // DataView setters: [offset, value, littleEndian?] — void; + // throw the getters' constant RangeError on a bad offset. + const kind = DV_SET_KIND_C[method]; + E.line( + `scr_dataview_set(${r.name}, ${args[0]!.name}, ${args[1]!.name}, ${kind}, ` + + `${args[2]?.name ?? "false"});${E.srcComment(e.loc)}`, + ); + E.emitPendingCheck(); + return { name: "", type: e.type }; + } default: { const _exhaustive: never = method; void _exhaustive; diff --git a/packages/compiler/src/backend/emission/emit-types.ts b/packages/compiler/src/backend/emission/emit-types.ts index 0ca4e902d..f70230a29 100644 --- a/packages/compiler/src/backend/emission/emit-types.ts +++ b/packages/compiler/src/backend/emission/emit-types.ts @@ -585,6 +585,19 @@ export const DV_GET_KIND_C: Record = { dvGetBigInt64Number: "SCR_DV_BIGI64", }; +/** The runtime's ScrDataViewGet tag per dvSet* bytesIntrinsic method (the + * setters reuse the getter kinds; no BIG setters exist). */ +export const DV_SET_KIND_C: Record = { + dvSetUint8: "SCR_DV_U8", + dvSetInt8: "SCR_DV_I8", + dvSetUint16: "SCR_DV_U16", + dvSetInt16: "SCR_DV_I16", + dvSetUint32: "SCR_DV_U32", + dvSetInt32: "SCR_DV_I32", + dvSetFloat32: "SCR_DV_F32", + dvSetFloat64: "SCR_DV_F64", +}; + /** Runtime accessor suffix for an element type: arrays store f64 and bool * unboxed and everything refcounted as a pointer (`_ref`). */ export function elemAccess(elem: IrType): "f64" | "bool" | "ref" { diff --git a/packages/compiler/src/backend/llvm/emitter.ts b/packages/compiler/src/backend/llvm/emitter.ts index bab93f194..88ed7ff35 100644 --- a/packages/compiler/src/backend/llvm/emitter.ts +++ b/packages/compiler/src/backend/llvm/emitter.ts @@ -772,6 +772,19 @@ const DV_GET_KIND: Record = { dvGetBigInt64Number: 9, }; +/** ScrDataViewGet per dvSet* method (the setters reuse the getter kinds; + * no BIG setters exist). */ +const DV_SET_KIND: Record = { + dvSetUint8: 0, + dvSetInt8: 1, + dvSetUint16: 2, + dvSetInt16: 3, + dvSetUint32: 4, + dvSetInt32: 5, + dvSetFloat32: 6, + dvSetFloat64: 7, +}; + class LlEmitter { /** Interned string literals: UTF-8 text → { symbol, byte length } — * first-use order, the C emitter's determinism discipline. */ @@ -8849,6 +8862,24 @@ class LlEmitter { false, true, ); + case "dvSetUint8": + case "dvSetInt8": + case "dvSetUint16": + case "dvSetInt16": + case "dvSetUint32": + case "dvSetInt32": + case "dvSetFloat32": + case "dvSetFloat64": + // DataView setters: [offset, value, littleEndian?] — void; throw + // the getters' constant RangeError on a bad offset. + return call( + "scr_dataview_set", + "void (ptr, double, double, i32, i1 zeroext)", + `ptr ${r.name}, double ${args[0]!.name}, double ${args[1]!.name}, ` + + `i32 ${DV_SET_KIND[method]}, i1 ${args[2]?.name ?? "false"}`, + false, + true, + ); default: { const _exhaustive: never = method; void _exhaustive; diff --git a/packages/compiler/src/frontend/lowering/lower-classes.ts b/packages/compiler/src/frontend/lowering/lower-classes.ts index df5475731..cff654d4d 100644 --- a/packages/compiler/src/frontend/lowering/lower-classes.ts +++ b/packages/compiler/src/frontend/lowering/lower-classes.ts @@ -5330,6 +5330,7 @@ export function lowerNew(L: Lowerer, expr: ts.NewExpression): IrExpr { WeakRef: "deref()-after-collect exposes GC timing — genuinely dynamic; hold a strong reference instead", FinalizationRegistry: "finalization callbacks expose GC timing — genuinely dynamic; release resources explicitly instead", SharedArrayBuffer: "no shared-memory threads exist in a compiled program — Uint8Array is the byte storage", + ArrayBuffer: "no free-standing ArrayBuffer value exists — typed arrays own their storage: allocate the view directly (new Uint8Array(n)), or erase a fresh buffer into one (new Uint8Array(new ArrayBuffer(n)), new DataView(new ArrayBuffer(n), ...))", Proxy: "property-access metaprogramming has no static lowering (every property read must resolve at compile time)", Function: "runtime code generation cannot be compiled ahead of time (the eval stance) — write the function", }; diff --git a/packages/compiler/src/frontend/lowering/lower-containers.ts b/packages/compiler/src/frontend/lowering/lower-containers.ts index bd3162bab..230cfc129 100644 --- a/packages/compiler/src/frontend/lowering/lower-containers.ts +++ b/packages/compiler/src/frontend/lowering/lower-containers.ts @@ -4400,30 +4400,44 @@ const BYTES_CTORS: Record = { const seed: IrExpr = { kind: "arrayLit", elems, type: arrayOf(F64), loc }; return { kind: "bytesNew", source: seed, type, loc }; } - // The SYNTACTIC `new T(new SharedArrayBuffer(n))` form — the - // Atomics.wait sleep idiom's construction. The buffer never exists - // as a value: n must be a byte-length LITERAL divisible by the - // element size (tsc would admit any number; a bad one is Node's - // RangeError — rejected at compile time instead of half-lowering), - // and the whole expression is a zero-filled typed array of - // n/elemSize elements. Sharing is unobservable without threads - // (scriptc has none), so erasing the SharedArrayBuffer is exact — - // SEMANTICS.md documents the stance. + // The SYNTACTIC `new T(new ArrayBuffer(n))` and `new T(new + // SharedArrayBuffer(n))` forms — fresh-buffer construction (the + // shared spelling is the Atomics.wait sleep idiom's). The buffer + // never exists as a value: n must be a byte-length LITERAL divisible + // by the element size (tsc would admit any number; a bad one is + // Node's RangeError — rejected at compile time instead of + // half-lowering), and the whole expression is a zero-filled typed + // array of n/elemSize elements. Erasing the buffer is exact: nothing + // else can ever reference it, so neither sharing (scriptc has no + // threads) nor aliasing is observable — SEMANTICS.md documents the + // stance. The RESIZABLE form (a maxByteLength options bag) fences by + // name: resize needs the buffer to exist as a value, and none does. if ( ts.isNewExpression(argNode) && ts.isIdentifier(argNode.expression) && - argNode.expression.text === "SharedArrayBuffer" && + (argNode.expression.text === "ArrayBuffer" || + argNode.expression.text === "SharedArrayBuffer") && L.isStdlibSymbol(L.resolveValueSymbol(argNode.expression) ?? undefined) ) { + const bufCtor = argNode.expression.text; + if ((argNode.arguments?.length ?? 0) > 1) { + L.noLowering( + `new ${name} over a resizable ${bufCtor}`, + argNode, + "a maxByteLength options bag makes the buffer resizable, and resizing needs the buffer " + + "to exist as a runtime value — no free-standing ArrayBuffer value exists (the buffer here " + + "erases into the view): drop the options bag", + ); + } const elemSize = elem === "u8" ? 1 : 4; const lenArg = argNode.arguments?.length === 1 ? argNode.arguments[0] : undefined; const lenT = lenArg ? L.typeOf(lenArg) : null; const byteLen = lenT?.isNumberLiteralType() ? lenT.value : null; if (byteLen === null || byteLen % elemSize !== 0 || byteLen < 0) { L.noLowering( - `new ${name} over this SharedArrayBuffer`, + `new ${name} over this ${bufCtor}`, argNode, - `the byte length must be a number literal divisible by ${elemSize} — new ${name}(new SharedArrayBuffer(${elemSize}))`, + `the byte length must be a number literal divisible by ${elemSize} — new ${name}(new ${bufCtor}(${elemSize}))`, ); } const count: IrExpr = { kind: "numLit", value: byteLen / elemSize, type: F64, loc }; @@ -4477,9 +4491,48 @@ const BYTES_CTORS: Record = { "supported: new DataView(x.buffer), (x.buffer, byteOffset), or (x.buffer, byteOffset, byteLength)", ); } + // The FRESH-BUFFER form: `new DataView(new ArrayBuffer(n), ...)` — the + // buffer erases into the view (nothing else can ever reference it, so + // the aliasing a shared buffer would exhibit is unobservable): the + // view's storage is a fresh zero-filled n-byte allocation. n must be a + // non-negative integer LITERAL (tsc admits any number; a fractional + // one is ToIndex truncation nothing here implements). byteOffset and + // byteLength keep their runtime Node-RangeError story. The RESIZABLE + // form (a maxByteLength options bag) fences by name. + if ( + ts.isNewExpression(bufNode) && + ts.isIdentifier(bufNode.expression) && + bufNode.expression.text === "ArrayBuffer" && + L.isStdlibSymbol(L.resolveValueSymbol(bufNode.expression) ?? undefined) + ) { + if ((bufNode.arguments?.length ?? 0) > 1) { + L.noLowering( + "new DataView over a resizable ArrayBuffer", + bufNode, + "a maxByteLength options bag makes the buffer resizable, and resizing needs the buffer " + + "to exist as a runtime value — no free-standing ArrayBuffer value exists (the buffer here " + + "erases into the view): drop the options bag", + ); + } + const lenArg = bufNode.arguments?.length === 1 ? bufNode.arguments[0] : undefined; + const lenT = lenArg ? L.typeOf(lenArg) : null; + const byteLen = lenT?.isNumberLiteralType() ? lenT.value : null; + if (byteLen === null || !Number.isInteger(byteLen) || byteLen < 0) { + L.noLowering( + "new DataView over this ArrayBuffer", + bufNode, + "the byte length must be a non-negative integer literal — new DataView(new ArrayBuffer(8))", + ); + } + const count: IrExpr = { kind: "numLit", value: byteLen, type: F64, loc }; + const receiver: IrExpr = { kind: "bytesNew", source: count, type: BYTES_U8, loc }; + const idxArgs = args.slice(1).map((a) => L.lowerExprExpecting(a, F64)); + return { kind: "bytesIntrinsic", method: "dataViewNew", receiver, args: idxArgs, type: BYTES_U8, loc }; + } const hint = "views compile over a typed array's own storage — new DataView(x.buffer, byteOffset?, byteLength?) " + - "where x is a Uint8Array/Uint32Array/Float32Array/Buffer value; free-standing ArrayBuffers have no representation"; + "where x is a Uint8Array/Uint32Array/Float32Array/Buffer value — or a fresh buffer erased into " + + "the view: new DataView(new ArrayBuffer(n), ...); free-standing ArrayBuffers have no representation"; if (!ts.isPropertyAccessExpression(bufNode) || bufNode.name.text !== "buffer" || bufNode.questionDotToken) { L.noLowering("new DataView over this buffer expression", bufNode, hint); } @@ -4667,6 +4720,26 @@ const BYTES_CTORS: Record = { if (nArgs === 2) args.push(L.lowerExprExpecting(call.arguments[1]!, BOOL)); return { kind: "bytesIntrinsic", method: dvGetter.method, receiver, args, type: F64, loc }; } + // DataView setters — the getters' mirror: (byteOffset, value) plus the + // optional littleEndian bool on the multi-byte kinds. Void results; + // the same constant Node RangeError on a bad offset (may-throw seeds). + // setBigUint64/setBigInt64 never lower (bigint ARGUMENTS have no + // representation and no composed form exists) — they keep the member + // fence, as does setFloat16. + const dvSetter = own(DV_SETTERS, name); + if (dvSetter !== undefined && receiverIr.elem === "u8") { + const maxArgs = dvSetter.le ? 3 : 2; + if (nArgs < 2 || nArgs > maxArgs) { + L.noLowering(`.${name} with ${nArgs} arguments`, call); + } + const receiver = L.lowerExpr(access.expression); + const args = [ + L.lowerExprExpecting(call.arguments[0]!, F64), + L.lowerExprExpecting(call.arguments[1]!, F64), + ]; + if (nArgs === 3) args.push(L.lowerExprExpecting(call.arguments[2]!, BOOL)); + return { kind: "bytesIntrinsic", method: dvSetter.method, receiver, args, type: VOID, loc }; + } return null; } @@ -4909,6 +4982,17 @@ const DV_GETTERS: Record = { + setUint8: { method: "dvSetUint8", le: false }, + setInt8: { method: "dvSetInt8", le: false }, + setUint16: { method: "dvSetUint16", le: true }, + setInt16: { method: "dvSetInt16", le: true }, + setUint32: { method: "dvSetUint32", le: true }, + setInt32: { method: "dvSetInt32", le: true }, + setFloat32: { method: "dvSetFloat32", le: true }, + setFloat64: { method: "dvSetFloat64", le: true }, +}; + /** The Buffer statics — `Buffer.from(...)`, `Buffer.alloc(n)`, * `Buffer.concat(list)`, `Buffer.isBuffer(x)` — on THE stdlib Buffer * global (name + provenance; fallback and @types/node alike). Null when diff --git a/packages/compiler/src/frontend/lowering/lower-exprs.ts b/packages/compiler/src/frontend/lowering/lower-exprs.ts index a31548339..a43836ef1 100644 --- a/packages/compiler/src/frontend/lowering/lower-exprs.ts +++ b/packages/compiler/src/frontend/lowering/lower-exprs.ts @@ -849,6 +849,7 @@ function lowerExprInner(L: Lowerer, expr: ts.Expression): IrExpr { Reflect: "reflective property access has no static lowering — read and call members directly", Intl: "locale- and ICU-backed behavior lives outside the static runtime (the localeCompare stance: code-unit order, no collation/locale data)", SharedArrayBuffer: "no shared-memory threads exist in a compiled program — Uint8Array is the byte storage", + ArrayBuffer: "no free-standing ArrayBuffer value exists — typed arrays own their storage (new Uint8Array(n) allocates; new Uint8Array(new ArrayBuffer(n)) erases the buffer into the view)", WeakRef: "deref()-after-collect exposes GC timing — genuinely dynamic; hold a strong reference instead", FinalizationRegistry: "finalization callbacks expose GC timing — genuinely dynamic; release resources explicitly instead", eval: "runtime code evaluation cannot be compiled ahead of time", diff --git a/packages/compiler/src/frontend/lowering/lowerer.ts b/packages/compiler/src/frontend/lowering/lowerer.ts index 22e99164a..f433ad80a 100644 --- a/packages/compiler/src/frontend/lowering/lowerer.ts +++ b/packages/compiler/src/frontend/lowering/lowerer.ts @@ -2198,9 +2198,19 @@ export class Lowerer { } } // STANDARD-LIBRARY nominal types with no lowering at all: the SC2020 - // story, naming the type. + // story, naming the type — and for the families with a WHY, the same + // pointed reason the identifier/constructor chokepoints teach. if (stdlibNominal) { - this.pushDiag(noLoweringDiag(this.checker.typeToString(type), locOf(node))); + const typeHints: Record = { + ArrayBuffer: + "no free-standing ArrayBuffer value exists — typed arrays own their storage " + + "(new Uint8Array(n) allocates; new Uint8Array(new ArrayBuffer(n)) erases the buffer into the view)", + SharedArrayBuffer: + "no shared-memory threads exist in a compiled program — Uint8Array is the byte storage", + }; + this.pushDiag( + noLoweringDiag(this.checker.typeToString(type), locOf(node), typeHints[stdlibOwnSym?.name ?? ""]), + ); throw new PoisonError(); } // USER record shapes blocked by ONE member (SC2009's record arm): diff --git a/packages/compiler/src/frontend/lowering/surfaces.ts b/packages/compiler/src/frontend/lowering/surfaces.ts index 7ece457d9..55c15b6e9 100644 --- a/packages/compiler/src/frontend/lowering/surfaces.ts +++ b/packages/compiler/src/frontend/lowering/surfaces.ts @@ -1038,6 +1038,15 @@ export const BUILTIN_MODULE_FENCE_HINTS: Record = new "dvGetFloat64", "dvGetBigUint64Number", "dvGetBigInt64Number", + "dvSetUint8", + "dvSetInt8", + "dvSetUint16", + "dvSetInt16", + "dvSetUint32", + "dvSetInt32", + "dvSetFloat32", + "dvSetFloat64", ]); /** The regex operation surface. Receiver/arg conventions (validated): diff --git a/packages/compiler/src/ir/validate.ts b/packages/compiler/src/ir/validate.ts index 766739aed..8708d06ad 100644 --- a/packages/compiler/src/ir/validate.ts +++ b/packages/compiler/src/ir/validate.ts @@ -2082,13 +2082,23 @@ function validateFunction( ? { argTypes: [STRING, F64, F64, F64], minArgs: 4, result: F64 } : e.method === "dataViewNew" ? { argTypes: [F64, F64], minArgs: 0, result: BYTES_U8 } - : { - // dvGet*: the 8-bit getters take no littleEndian. - argTypes: - e.method === "dvGetUint8" || e.method === "dvGetInt8" ? [F64] : [F64, BOOL], - minArgs: 1, - result: F64, - }); + : e.method.startsWith("dvSet") + ? { + // dvSet*: [offset, value], the 8-bit setters take no littleEndian. + argTypes: + e.method === "dvSetUint8" || e.method === "dvSetInt8" + ? [F64, F64] + : [F64, F64, BOOL], + minArgs: 2, + result: VOID, + } + : { + // dvGet*: the 8-bit getters take no littleEndian. + argTypes: + e.method === "dvGetUint8" || e.method === "dvGetInt8" ? [F64] : [F64, BOOL], + minArgs: 1, + result: F64, + }); if (e.args.length < sig.minArgs || e.args.length > sig.argTypes.length) { err(`bytesIntrinsic ${e.method}: ${e.args.length} args`, e.loc); } diff --git a/packages/runtime/src/scr_bytes.c b/packages/runtime/src/scr_bytes.c index a24e24135..8d3f84475 100644 --- a/packages/runtime/src/scr_bytes.c +++ b/packages/runtime/src/scr_bytes.c @@ -312,6 +312,49 @@ double scr_dataview_get(const ScrBytes *b, double byte_off, ScrDataViewGet kind, return 0; /* unreachable */ } +void scr_dataview_set(ScrBytes *b, double byte_off, double value, ScrDataViewGet kind, bool le) { + size_t width = scr_dataview_get_size(kind); + /* ToIndex + the view-relative bounds check — the getters' ONE message. */ + double off = (byte_off != byte_off) ? 0 : trunc(byte_off); + if (!(off >= 0) || off > 9007199254740991.0 || off + (double)width > (double)b->len) { + static const char msg[] = "Offset is outside the bounds of the DataView"; + scr_throw_error_msg(SCR_ERR_RANGE, msg, sizeof msg - 1); + return; + } + /* Coerce to the stored bit pattern. The integer kinds share ToUint32's + * 2^32 residue (signed/unsigned store the same bits; narrower widths + * take the low bytes — 2^width divides 2^32, so the residues agree); + * F32 rounds double→float to nearest even, exactly the spec. */ + uint64_t u = 0; + switch (kind) { + case SCR_DV_U8: + case SCR_DV_I8: + case SCR_DV_U16: + case SCR_DV_I16: + case SCR_DV_U32: + case SCR_DV_I32: + u = (uint64_t)scr_bytes_to_u32(value); + break; + case SCR_DV_F32: { + float f = (float)value; + uint32_t bits; + memcpy(&bits, &f, 4); + u = bits; + break; + } + case SCR_DV_F64: + memcpy(&u, &value, 8); + break; + default: + return; /* BIG kinds never lower (bigint arguments) */ + } + /* Scatter host-independently; le false is the JS big-endian default. */ + uint8_t *p = b->data + (size_t)off; + for (size_t i = 0; i < width; i++) { + p[le ? i : width - 1 - i] = (uint8_t)(u >> (8 * i)); + } +} + /* ── encodings (u8 only — the compiler routes only u8 receivers here) ──── */ static const char scr_hex_digits[] = "0123456789abcdef"; diff --git a/packages/runtime/src/scr_runtime.h b/packages/runtime/src/scr_runtime.h index f5bc949cf..f4c9de3e4 100644 --- a/packages/runtime/src/scr_runtime.h +++ b/packages/runtime/src/scr_runtime.h @@ -4052,6 +4052,14 @@ typedef enum ScrDataViewGet { } ScrDataViewGet; double scr_dataview_get(const ScrBytes *b, double byte_off, ScrDataViewGet kind, bool le); +/* DataView setters (the integer/float kinds only — the BIG kinds never + * lower: bigint arguments have no representation). Values coerce + * JS-exactly: the integer kinds by modular truncation (ToUint32's residue + * — the narrower widths store its low bytes, the same 2^width residue), + * F32 by double→float round-to-nearest-even. Offsets go through ToIndex + * with the getters' one RangeError. */ +void scr_dataview_set(ScrBytes *b, double byte_off, double value, ScrDataViewGet kind, bool le); + /* Element read/write. Any invalid index — negative, fractional, NaN, or * out of bounds — TRAPS like the array runtime (SEMANTICS.md documents * the divergence from JS's undefined-read/ignored-write). Writes coerce diff --git a/tests/corpus/2560-arraybuffer-dataview-set.ts b/tests/corpus/2560-arraybuffer-dataview-set.ts new file mode 100644 index 000000000..8602c1e9b --- /dev/null +++ b/tests/corpus/2560-arraybuffer-dataview-set.ts @@ -0,0 +1,100 @@ +// Fresh-ArrayBuffer erasure and DataView setters. `new T(new +// ArrayBuffer(n))` and `new DataView(new ArrayBuffer(n), ...)` erase the +// buffer into the view (nothing else can reference it, so aliasing is +// unobservable); the setter family mirrors the getters: JS-exact value +// coercions (modular truncation, double→float rounding), the optional +// littleEndian flag, and Node's one constant RangeError on bad offsets. + +// Erasure into every typed-array kind: zero-filled, correct element count. +const u8 = new Uint8Array(new ArrayBuffer(8)); +u8[0] = 255; +console.log(u8.length, u8.byteLength, u8[0], u8[7]); +const u32 = new Uint32Array(new ArrayBuffer(16)); +u32[3] = 0xdeadbeef; +console.log(u32.length, u32.byteLength, u32[3]); +const i32 = new Int32Array(new ArrayBuffer(8)); +i32[1] = -2; +console.log(i32.length, i32[1]); +const f32 = new Float32Array(new ArrayBuffer(12)); +f32[2] = 1.5; +console.log(f32.length, f32[2]); +const empty = new Uint8Array(new ArrayBuffer(0)); +console.log(empty.length, empty.byteLength); + +// DataView over a fresh buffer: zero bytes until written. +const dv = new DataView(new ArrayBuffer(8)); +console.log(dv.byteLength, dv.byteOffset, dv.getFloat64(0)); + +// Setter/getter roundtrips on every width, both endiannesses. +dv.setUint8(0, 0xab); +dv.setInt8(1, -1); +console.log(dv.getUint8(0), dv.getInt8(1), dv.getUint8(1)); +dv.setUint16(2, 0xbeef); +console.log(dv.getUint16(2), dv.getUint8(2), dv.getUint8(3)); +dv.setUint16(2, 0xbeef, true); +console.log(dv.getUint16(2, true), dv.getUint8(2), dv.getUint8(3)); +dv.setInt16(4, -2, false); +console.log(dv.getInt16(4), dv.getUint16(4)); +dv.setUint32(4, 0x01020304); +console.log(dv.getUint32(4), dv.getInt32(4), dv.getUint8(7)); +dv.setInt32(4, -16, true); +console.log(dv.getInt32(4, true), dv.getUint32(4, true), dv.getUint32(4)); +dv.setFloat64(0, 1.5); +console.log(dv.getFloat64(0), dv.getUint8(0), dv.getUint8(1)); +dv.setFloat64(0, 1.5, true); +console.log(dv.getFloat64(0, true), dv.getUint8(6), dv.getUint8(7)); +dv.setFloat32(4, 1.1); +console.log(dv.getFloat32(4), dv.getFloat64(0, true)); + +// Value coercions: modular truncation on the integer kinds (NaN and +// ±Infinity store 0), double→float rounding on Float32. +dv.setUint8(0, 257.9); +dv.setUint8(1, -1); +dv.setUint8(2, 0 / 0); +dv.setUint8(3, 1 / 0); +console.log(dv.getUint8(0), dv.getUint8(1), dv.getUint8(2), dv.getUint8(3)); +dv.setUint16(0, 65536 + 7); +console.log(dv.getUint16(0)); +dv.setInt16(0, -32769); +console.log(dv.getInt16(0)); +dv.setUint32(0, 4294967296 + 41); +console.log(dv.getUint32(0)); +dv.setFloat32(0, 0.1); +console.log(dv.getFloat32(0)); + +// Fractional and NaN offsets go through ToIndex like the getters. +dv.setUint8(5.7, 9); +console.log(dv.getUint8(5)); +dv.setUint8(0 / 0, 42); +console.log(dv.getUint8(0)); + +// The offset/length constructor args keep their runtime story, and a +// view over x.buffer aliases x — setters write through. +const backing = new Uint8Array([1, 2, 3, 4, 5, 6, 7, 8]); +const win = new DataView(backing.buffer, 2, 4); +win.setUint16(0, 0x0910); +console.log(win.byteLength, win.byteOffset, backing[2], backing[3], backing[4]); +const dvo = new DataView(new ArrayBuffer(8), 6); +console.log(dvo.byteLength, dvo.byteOffset); + +// Bad setter offsets: Node's one constant RangeError, catchably. +function caught(label: string, fn: () => void): void { + try { + fn(); + console.log(label, "ok"); + } catch (e) { + if (e instanceof RangeError) console.log(label, "RangeError:", e.message); + else console.log(label, "unexpected"); + } +} +caught("set-end", () => dv.setUint8(8, 1)); +caught("set-neg", () => dv.setUint8(-1, 1)); +caught("set-wide", () => dv.setFloat64(1, 1)); +caught("set-edge", () => dv.setFloat64(0, 2.5)); +caught("set-u32", () => dv.setUint32(5, 1)); +caught("set-inf", () => dv.setUint8(1 / 0, 1)); +console.log(dv.getFloat64(0)); + +// Constructor bounds on the fresh-buffer form throw like Node too. +caught("ctor-off", () => { new DataView(new ArrayBuffer(4), 5); }); +caught("ctor-len", () => { new DataView(new ArrayBuffer(4), 1, 4); }); diff --git a/tests/diagnostics/es2025-fences.ts b/tests/diagnostics/es2025-fences.ts index 17ddc7970..f3f8e21fc 100644 --- a/tests/diagnostics/es2025-fences.ts +++ b/tests/diagnostics/es2025-fences.ts @@ -10,6 +10,10 @@ const ab = new ArrayBuffer(8, { maxByteLength: 16 }); ab.resize(16); const moved = ab.transfer(); +// The resizable form fences by name even in an erasure position (the +// fixed-length new Uint8Array(new ArrayBuffer(8)) lowers). +const rz = new Uint8Array(new ArrayBuffer(8, { maxByteLength: 16 })); + // SharedArrayBuffer growth. const sab = new SharedArrayBuffer(8, { maxByteLength: 16 }); sab.grow(16); diff --git a/tests/harness/__snapshots__/dataview.ts.txt b/tests/harness/__snapshots__/dataview.ts.txt index bed07845f..0ac028853 100644 --- a/tests/harness/__snapshots__/dataview.ts.txt +++ b/tests/harness/__snapshots__/dataview.ts.txt @@ -5,7 +5,7 @@ dataview.ts:9:7 - error SC2020: 'ArrayBuffer' is part of the standard library ty | ^~~~~~~ 10 | - hint: the type checker sees the full standard library, but only the supported surface compiles (https://scriptc.dev/limitations) + hint: no free-standing ArrayBuffer value exists — typed arrays own their storage (new Uint8Array(n) allocates; new Uint8Array(new ArrayBuffer(n)) erases the buffer into the view) dataview.ts:9:17 - error SC1090: '.buffer' outside new DataView(x.buffer, ...) — scriptc typed arrays own their storage: no free-standing ArrayBuffer value exists, and slice()/subarray() copy is not supported yet @@ -37,7 +37,7 @@ dataview.ts:15:7 - error SC2020: 'ArrayBuffer' is part of the standard library t | ^~ 16 | const direct = new DataView(ab); - hint: the type checker sees the full standard library, but only the supported surface compiles (https://scriptc.dev/limitations) + hint: no free-standing ArrayBuffer value exists — typed arrays own their storage (new Uint8Array(n) allocates; new Uint8Array(new ArrayBuffer(n)) erases the buffer into the view) dataview.ts:15:12 - error SC2020: 'new ArrayBuffer' is part of the standard library types but has no scriptc lowering yet @@ -46,7 +46,7 @@ dataview.ts:15:12 - error SC2020: 'new ArrayBuffer' is part of the standard libr | ^~~~~~~~~~~~~~~~~~ 16 | const direct = new DataView(ab); - hint: the type checker sees the full standard library, but only the supported surface compiles (https://scriptc.dev/limitations) + hint: no free-standing ArrayBuffer value exists — typed arrays own their storage: allocate the view directly (new Uint8Array(n)), or erase a fresh buffer into one (new Uint8Array(new ArrayBuffer(n)), new DataView(new ArrayBuffer(n), ...)) dataview.ts:16:29 - error SC2020: 'new DataView over this buffer expression' is part of the standard library types but has no scriptc lowering yet @@ -55,4 +55,4 @@ dataview.ts:16:29 - error SC2020: 'new DataView over this buffer expression' is | ^~ 17 | - hint: views compile over a typed array's own storage — new DataView(x.buffer, byteOffset?, byteLength?) where x is a Uint8Array/Uint32Array/Float32Array/Buffer value; free-standing ArrayBuffers have no representation \ No newline at end of file + hint: views compile over a typed array's own storage — new DataView(x.buffer, byteOffset?, byteLength?) where x is a Uint8Array/Uint32Array/Float32Array/Buffer value — or a fresh buffer erased into the view: new DataView(new ArrayBuffer(n), ...); free-standing ArrayBuffers have no representation \ No newline at end of file diff --git a/tests/harness/__snapshots__/es2025-fences.ts.txt b/tests/harness/__snapshots__/es2025-fences.ts.txt index 14169623d..e4fd60850 100644 --- a/tests/harness/__snapshots__/es2025-fences.ts.txt +++ b/tests/harness/__snapshots__/es2025-fences.ts.txt @@ -5,7 +5,7 @@ es2025-fences.ts:9:7 - error SC2020: 'ArrayBuffer' is part of the standard libra | ^~ 10 | ab.resize(16); - hint: the type checker sees the full standard library, but only the supported surface compiles (https://scriptc.dev/limitations) + hint: no free-standing ArrayBuffer value exists — typed arrays own their storage (new Uint8Array(n) allocates; new Uint8Array(new ArrayBuffer(n)) erases the buffer into the view) es2025-fences.ts:9:12 - error SC2020: 'new ArrayBuffer' is part of the standard library types but has no scriptc lowering yet @@ -14,7 +14,7 @@ es2025-fences.ts:9:12 - error SC2020: 'new ArrayBuffer' is part of the standard | ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 10 | ab.resize(16); - hint: the type checker sees the full standard library, but only the supported surface compiles (https://scriptc.dev/limitations) + hint: no free-standing ArrayBuffer value exists — typed arrays own their storage: allocate the view directly (new Uint8Array(n)), or erase a fresh buffer into one (new Uint8Array(new ArrayBuffer(n)), new DataView(new ArrayBuffer(n), ...)) es2025-fences.ts:10:1 - error SC2020: 'ArrayBuffer.resize' is part of the standard library types but has no scriptc lowering yet @@ -23,7 +23,7 @@ es2025-fences.ts:10:1 - error SC2020: 'ArrayBuffer.resize' is part of the standa | ^~~~~~~~~ 11 | const moved = ab.transfer(); - hint: the type checker sees the full standard library, but only the supported surface compiles (https://scriptc.dev/limitations) + hint: resize/transfer/maxByteLength need the buffer to exist as a runtime value, and no free-standing ArrayBuffer value does — typed arrays own fixed-length storage (new Uint8Array(n)); allocate a new view and copy instead es2025-fences.ts:11:7 - error SC2020: 'ArrayBuffer' is part of the standard library types but has no scriptc lowering yet @@ -32,7 +32,7 @@ es2025-fences.ts:11:7 - error SC2020: 'ArrayBuffer' is part of the standard libr | ^~~~~ 12 | - hint: the type checker sees the full standard library, but only the supported surface compiles (https://scriptc.dev/limitations) + hint: no free-standing ArrayBuffer value exists — typed arrays own their storage (new Uint8Array(n) allocates; new Uint8Array(new ArrayBuffer(n)) erases the buffer into the view) es2025-fences.ts:11:15 - error SC2020: 'ArrayBuffer.transfer' is part of the standard library types but has no scriptc lowering yet @@ -41,130 +41,139 @@ es2025-fences.ts:11:15 - error SC2020: 'ArrayBuffer.transfer' is part of the sta | ^~~~~~~~~~~ 12 | - hint: the type checker sees the full standard library, but only the supported surface compiles (https://scriptc.dev/limitations) + hint: resize/transfer/maxByteLength need the buffer to exist as a runtime value, and no free-standing ArrayBuffer value does — typed arrays own fixed-length storage (new Uint8Array(n)); allocate a new view and copy instead -es2025-fences.ts:14:7 - error SC2020: 'SharedArrayBuffer' is part of the standard library types but has no scriptc lowering yet +es2025-fences.ts:15:27 - error SC2020: 'new Uint8Array over a resizable ArrayBuffer' is part of the standard library types but has no scriptc lowering yet - 13 | // SharedArrayBuffer growth. - 14 | const sab = new SharedArrayBuffer(8, { maxByteLength: 16 }); + 14 | // fixed-length new Uint8Array(new ArrayBuffer(8)) lowers). + 15 | const rz = new Uint8Array(new ArrayBuffer(8, { maxByteLength: 16 })); + | ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + 16 | + + hint: a maxByteLength options bag makes the buffer resizable, and resizing needs the buffer to exist as a runtime value — no free-standing ArrayBuffer value exists (the buffer here erases into the view): drop the options bag + +es2025-fences.ts:18:7 - error SC2020: 'SharedArrayBuffer' is part of the standard library types but has no scriptc lowering yet + + 17 | // SharedArrayBuffer growth. + 18 | const sab = new SharedArrayBuffer(8, { maxByteLength: 16 }); | ^~~ - 15 | sab.grow(16); - - hint: the type checker sees the full standard library, but only the supported surface compiles (https://scriptc.dev/limitations) - -es2025-fences.ts:14:13 - error SC2020: 'new SharedArrayBuffer' is part of the standard library types but has no scriptc lowering yet - - 13 | // SharedArrayBuffer growth. - 14 | const sab = new SharedArrayBuffer(8, { maxByteLength: 16 }); - | ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - 15 | sab.grow(16); + 19 | sab.grow(16); hint: no shared-memory threads exist in a compiled program — Uint8Array is the byte storage -es2025-fences.ts:15:1 - error SC2020: 'SharedArrayBuffer.grow' is part of the standard library types but has no scriptc lowering yet +es2025-fences.ts:18:13 - error SC2020: 'new SharedArrayBuffer' is part of the standard library types but has no scriptc lowering yet - 14 | const sab = new SharedArrayBuffer(8, { maxByteLength: 16 }); - 15 | sab.grow(16); + 17 | // SharedArrayBuffer growth. + 18 | const sab = new SharedArrayBuffer(8, { maxByteLength: 16 }); + | ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + 19 | sab.grow(16); + + hint: no shared-memory threads exist in a compiled program — Uint8Array is the byte storage + +es2025-fences.ts:19:1 - error SC2020: 'SharedArrayBuffer.grow' is part of the standard library types but has no scriptc lowering yet + + 18 | const sab = new SharedArrayBuffer(8, { maxByteLength: 16 }); + 19 | sab.grow(16); | ^~~~~~~~ - 16 | - - hint: the type checker sees the full standard library, but only the supported surface compiles (https://scriptc.dev/limitations) - -es2025-fences.ts:19:7 - error SC2009: values of type '{ async: false; value: "not-equal" | "timed-out"; } | { async: true; value: Promise<"ok" | "timed-out">; }' cannot be compiled: the union shape is supported, but its arm '{ async: false; value: "not-equal" | "timed-out"; }' does not compile - - 18 | const ia = new Int32Array(4); - 19 | const waited = Atomics.waitAsync(ia, 0, 0, 1); - | ^~~~~~ 20 | + hint: no shared-memory threads exist in a compiled program — Uint8Array is the byte storage (a fixed-length allocation: grow has nothing to share it with) + +es2025-fences.ts:23:7 - error SC2009: values of type '{ async: false; value: "not-equal" | "timed-out"; } | { async: true; value: Promise<"ok" | "timed-out">; }' cannot be compiled: the union shape is supported, but its arm '{ async: false; value: "not-equal" | "timed-out"; }' does not compile + + 22 | const ia = new Int32Array(4); + 23 | const waited = Atomics.waitAsync(ia, 0, 0, 1); + | ^~~~~~ + 24 | + hint: the outer shape is supported — the named component is the blocker; restate it inside the compilable set (number, string, boolean, arrays, Maps, Sets, RegExp, functions, classes, records, unions of those, and 'unknown') -es2025-fences.ts:19:16 - error SC2020: 'Atomics.waitAsync' is part of the standard library types but has no scriptc lowering yet +es2025-fences.ts:23:16 - error SC2020: 'Atomics.waitAsync' is part of the standard library types but has no scriptc lowering yet - 18 | const ia = new Int32Array(4); - 19 | const waited = Atomics.waitAsync(ia, 0, 0, 1); + 22 | const ia = new Int32Array(4); + 23 | const waited = Atomics.waitAsync(ia, 0, 0, 1); | ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - 20 | + 24 | hint: Atomics.wait(int32Array, idx, expected, timeoutMs) is the supported Atomics surface (scriptc has no threads — wait is the synchronous-sleep idiom, and nothing else has anyone to race) -es2025-fences.ts:22:7 - error SC2020: 'Float16Array' is part of the standard library types but has no scriptc lowering yet +es2025-fences.ts:26:7 - error SC2020: 'Float16Array' is part of the standard library types but has no scriptc lowering yet - 21 | // Float16 (ES2025): the typed array, Math.f16round, DataView accessors. - 22 | const f16 = new Float16Array(4); + 25 | // Float16 (ES2025): the typed array, Math.f16round, DataView accessors. + 26 | const f16 = new Float16Array(4); | ^~~ - 23 | console.log(Math.f16round(1.337)); + 27 | console.log(Math.f16round(1.337)); hint: the type checker sees the full standard library, but only the supported surface compiles (https://scriptc.dev/limitations) -es2025-fences.ts:22:13 - error SC2020: 'new Float16Array' is part of the standard library types but has no scriptc lowering yet +es2025-fences.ts:26:13 - error SC2020: 'new Float16Array' is part of the standard library types but has no scriptc lowering yet - 21 | // Float16 (ES2025): the typed array, Math.f16round, DataView accessors. - 22 | const f16 = new Float16Array(4); + 25 | // Float16 (ES2025): the typed array, Math.f16round, DataView accessors. + 26 | const f16 = new Float16Array(4); | ^~~~~~~~~~~~~~~~~~~ - 23 | console.log(Math.f16round(1.337)); + 27 | console.log(Math.f16round(1.337)); hint: the type checker sees the full standard library, but only the supported surface compiles (https://scriptc.dev/limitations) -es2025-fences.ts:23:13 - error SC2020: 'Math.f16round' is part of the standard library types but has no scriptc lowering yet +es2025-fences.ts:27:13 - error SC2020: 'Math.f16round' is part of the standard library types but has no scriptc lowering yet - 22 | const f16 = new Float16Array(4); - 23 | console.log(Math.f16round(1.337)); + 26 | const f16 = new Float16Array(4); + 27 | console.log(Math.f16round(1.337)); | ^~~~~~~~~~~~~ - 24 | const dv = new DataView(new Uint8Array(8).buffer); + 28 | const dv = new DataView(new Uint8Array(8).buffer); hint: the type checker sees the full standard library, but only the supported surface compiles (https://scriptc.dev/limitations) -es2025-fences.ts:25:1 - error SC2020: 'DataView.setFloat16' is part of the standard library types but has no scriptc lowering yet +es2025-fences.ts:29:1 - error SC2020: 'DataView.setFloat16' is part of the standard library types but has no scriptc lowering yet - 24 | const dv = new DataView(new Uint8Array(8).buffer); - 25 | dv.setFloat16(0, 1.5); + 28 | const dv = new DataView(new Uint8Array(8).buffer); + 29 | dv.setFloat16(0, 1.5); | ^~~~~~~~~~~~~ - 26 | + 30 | hint: the type checker sees the full standard library, but only the supported surface compiles (https://scriptc.dev/limitations) -es2025-fences.ts:28:7 - error SC2020: 'IteratorObject' is part of the standard library types but has no scriptc lowering yet +es2025-fences.ts:32:7 - error SC2020: 'IteratorObject' is part of the standard library types but has no scriptc lowering yet - 27 | // First-class iterator objects. - 28 | const it = Iterator.from([1, 2, 3].values()); + 31 | // First-class iterator objects. + 32 | const it = Iterator.from([1, 2, 3].values()); | ^~ - 29 | + 33 | hint: the type checker sees the full standard library, but only the supported surface compiles (https://scriptc.dev/limitations) -es2025-fences.ts:28:12 - error SC2020: 'Iterator.from' is part of the standard library types but has no scriptc lowering yet +es2025-fences.ts:32:12 - error SC2020: 'Iterator.from' is part of the standard library types but has no scriptc lowering yet - 27 | // First-class iterator objects. - 28 | const it = Iterator.from([1, 2, 3].values()); + 31 | // First-class iterator objects. + 32 | const it = Iterator.from([1, 2, 3].values()); | ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - 29 | + 33 | hint: first-class iterator objects have no lowering — iterator helpers compile as one chain on an array iterator, consumed in place: arr.values().map(f).take(n).toArray() -es2025-fences.ts:31:15 - error SC2020: 'Promise.try with 3 arguments' is part of the standard library types but has no scriptc lowering yet +es2025-fences.ts:35:15 - error SC2020: 'Promise.try with 3 arguments' is part of the standard library types but has no scriptc lowering yet - 30 | // Promise.try's ...args form (close over the values instead). - 31 | const tried = Promise.try((a: number, b: number) => a + b, 1, 2); + 34 | // Promise.try's ...args form (close over the values instead). + 35 | const tried = Promise.try((a: number, b: number) => a + b, 1, 2); | ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - 32 | + 36 | hint: the ...args form has no lowering — close over the values: Promise.try(() => f(a, b)) -es2025-fences.ts:34:17 - error SC2020: 'Object.groupBy at this result type' is part of the standard library types but has no scriptc lowering yet +es2025-fences.ts:38:17 - error SC2020: 'Object.groupBy at this result type' is part of the standard library types but has no scriptc lowering yet - 33 | // Object.groupBy at a literal-union key type (a fixed-field record). - 34 | const grouped = Object.groupBy([1, 2, 3], (n) => (n % 2 === 0 ? "even" : "odd")); + 37 | // Object.groupBy at a literal-union key type (a fixed-field record). + 38 | const grouped = Object.groupBy([1, 2, 3], (n) => (n % 2 === 0 ? "even" : "odd")); | ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - 35 | + 39 | hint: a string-keyed result (Partial>) is the supported form — a literal-union key type makes a fixed-field record: widen the callback's key type to string (numbers stringify like JS property keys) -es2025-fences.ts:39:19 - error SC2020: 'Set.prototype.union over a set-like argument of type 'Map'' is part of the standard library types but has no scriptc lowering yet +es2025-fences.ts:43:19 - error SC2020: 'Set.prototype.union over a set-like argument of type 'Map'' is part of the standard library types but has no scriptc lowering yet - 38 | const m = new Map([[1, "x"]]); - 39 | const u = s.union(m); + 42 | const m = new Map([[1, "x"]]); + 43 | const u = s.union(m); | ^ - 40 | + 44 | hint: only a real Set lowers (the argument's has/size must be the builtins) — build one first: new Set(...) \ No newline at end of file diff --git a/tests/harness/__snapshots__/stdlib-fence.ts.txt b/tests/harness/__snapshots__/stdlib-fence.ts.txt index 51e168e06..edba8f247 100644 --- a/tests/harness/__snapshots__/stdlib-fence.ts.txt +++ b/tests/harness/__snapshots__/stdlib-fence.ts.txt @@ -109,7 +109,7 @@ stdlib-fence.ts:26:7 - error SC2020: 'ArrayBuffer' is part of the standard libra | ^~~ 27 | const st = new Set(new Set([1, 2])); // array seeds lower; Set/iterable seeds stay fenced - hint: the type checker sees the full standard library, but only the supported surface compiles (https://scriptc.dev/limitations) + hint: no free-standing ArrayBuffer value exists — typed arrays own their storage (new Uint8Array(n) allocates; new Uint8Array(new ArrayBuffer(n)) erases the buffer into the view) stdlib-fence.ts:26:13 - error SC2020: 'new ArrayBuffer' is part of the standard library types but has no scriptc lowering yet @@ -118,7 +118,7 @@ stdlib-fence.ts:26:13 - error SC2020: 'new ArrayBuffer' is part of the standard | ^~~~~~~~~~~~~~~~~~ 27 | const st = new Set(new Set([1, 2])); // array seeds lower; Set/iterable seeds stay fenced - hint: the type checker sees the full standard library, but only the supported surface compiles (https://scriptc.dev/limitations) + hint: no free-standing ArrayBuffer value exists — typed arrays own their storage: allocate the view directly (new Uint8Array(n)), or erase a fresh buffer into one (new Uint8Array(new ArrayBuffer(n)), new DataView(new ArrayBuffer(n), ...)) stdlib-fence.ts:27:12 - error SC2020: 'new Set(values)' is part of the standard library types but has no scriptc lowering yet From b31d160fbddfd602236c2b70c728f2a8a6b50b2e Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Thu, 23 Jul 2026 20:15:31 -0500 Subject: [PATCH 2/4] Object.is goes static with the spec's SameValue MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Number pairs take a new scr_num_same_value runtime primitive (NaN equals NaN, +0 differs from -0 — the two divergences from ===); strings, booleans, unit literals, and every reference kind (arrays, records, class instances with hierarchy widening, functions, class values, Maps/Sets, symbols, typed arrays, promises) ride exactly the strict-equality machinery, whose answers SameValue shares. - Union operands lower through unionEq, which grows a sameValue flag: the f64 arm's payload compare upgrades to SameValue (a separate interned C helper per union; the LLVM lane calls the runtime primitive inline), every other arm is shared between the two semantics. - Statically disjoint kind pairs answer the constant false with the operands still evaluated for their effects (Object.is is (any, any) — tsc admits every pair); dyn/jsval operands and partially-overlapping unions fence by name with strict equality's validate/narrow-first stance. - Differential fixture 2561 pins the NaN/±0 edges, mixed-kind calls with effect counting, reference identity across kinds, and union payloads (including NaN/±0 inside a union and union-vs-plain wraps) byte-identically vs Node on both backends; stdlib-fence pins the named fences. - Found pre-existing (reproduced at the base commit): the C backend SIGSEGVs on console.log(asBase === d, asBase === new Base()) — an upcast identity compare plus a constructing compare in one call; the fixture avoids the shape and the bug is reported for a follow-up lane. --- .../src/backend/emission/emit-exprs.ts | 11 +- .../src/backend/emission/emit-walkers.ts | 28 ++-- .../compiler/src/backend/emission/emitter.ts | 4 +- packages/compiler/src/backend/llvm/emitter.ts | 10 +- .../src/frontend/lowering/lower-calls.ts | 135 +++++++++++++++++- .../src/frontend/lowering/lower-exprs.ts | 1 + .../src/frontend/lowering/lower-stmts.ts | 1 + packages/compiler/src/ir/nodes.ts | 11 +- packages/compiler/src/ir/validate.ts | 1 + packages/runtime/src/scr_lib.c | 8 ++ packages/runtime/src/scr_runtime.h | 4 + tests/corpus/2561-object-is.ts | 68 +++++++++ tests/diagnostics/stdlib-fence.ts | 9 ++ .../harness/__snapshots__/stdlib-fence.ts.txt | 34 +++-- 14 files changed, 298 insertions(+), 27 deletions(-) create mode 100644 tests/corpus/2561-object-is.ts diff --git a/packages/compiler/src/backend/emission/emit-exprs.ts b/packages/compiler/src/backend/emission/emit-exprs.ts index 20bb8fb31..b36055a46 100644 --- a/packages/compiler/src/backend/emission/emit-exprs.ts +++ b/packages/compiler/src/backend/emission/emit-exprs.ts @@ -1945,11 +1945,13 @@ export function emitExpr(E: CEmitter, e: IrExpr): Temp { return E.newTemp(e.type, e.negated ? `!(${test})` : test); } case "unionEq": { - // Strict equality of the ARM values via the per-union helper (tag - // compare + per-arm payload compare). Both boxes are borrowed. + // Strict equality (or Object.is's SameValue — the f64 arm's + // compare is the one difference) of the ARM values via the + // per-union helper (tag compare + per-arm payload compare). Both + // boxes are borrowed. const l = E.emitExpr(e.left); const r = E.emitExpr(e.right); - const call = `${E.unionEqHelper(e.unionId)}(${l.name}, ${r.name})`; + const call = `${E.unionEqHelper(e.unionId, e.sameValue)}(${l.name}, ${r.name})`; return E.newTemp(e.type, e.negated ? `!${call}` : call); } case "caughtTest": { @@ -2498,6 +2500,9 @@ export function emitExpr(E: CEmitter, e: IrExpr): Temp { return finish(`scr_num_to_exponential(${arg(0)})`); case "num.toFixed0": return finish(`scr_num_to_fixed0(${arg(0)})`); + // Object.is over two numbers — SameValue on doubles. No throw. + case "num.sameValue": + return finish(`scr_num_same_value(${arg(0)}, ${arg(1)})`); // The URL surface (scr_url.c): construction and the two // fileURLToPath receiver forms throw catchable TypeErrors // (may-throw seed set); the getters are pure reads. Receivers diff --git a/packages/compiler/src/backend/emission/emit-walkers.ts b/packages/compiler/src/backend/emission/emit-walkers.ts index 31146fb72..c84bf1c0c 100644 --- a/packages/compiler/src/backend/emission/emit-walkers.ts +++ b/packages/compiler/src/backend/emission/emit-walkers.ts @@ -113,22 +113,26 @@ import { OVERFLOW_MEMBER } from "./emit-shapes.js"; return name; } -/** The per-union strict-equality helper (interned per unionId): +/** The per-union strict-equality helper (interned per unionId, with a + * separate SameValue variant per unionId when Object.is reaches it): * different tags are never equal; equal tags compare the arm values — - * unit arms equal, f64 with C == (NaN !== NaN, +0 === -0), strings by - * bytes, bools by value, ref arms by POINTER identity (JS object - * equality). Borrows both operands. */ - export function unionEqHelper(E: CEmitter, unionId: string): string { - const existing = E.unionEqFns.get(unionId); + * unit arms equal, f64 with C == (NaN !== NaN, +0 === -0) or SameValue + * (NaN equals NaN, +0 differs from -0) under the sameValue flag, + * strings by bytes, bools by value, ref arms by POINTER identity (JS + * object equality). Borrows both operands. */ + export function unionEqHelper(E: CEmitter, unionId: string, sameValue: boolean): string { + const key = sameValue ? `sv:${unionId}` : unionId; + const existing = E.unionEqFns.get(key); if (existing) return existing; const def = E.unionsById.get(unionId); if (!def) throw new Error(`emitter bug: equality of unknown union ${unionId}`); const name = `sc_ue_${E.unionEqFns.size}`; - E.unionEqFns.set(unionId, name); + E.unionEqFns.set(key, name); + const what = sameValue ? "SameValue" : "strict equality"; const sig = `static bool ${name}(ScrUnion *a, ScrUnion *b)`; - E.walkerProtos.push(`${sig}; /* strict equality ${unionId} */`); + E.walkerProtos.push(`${sig}; /* ${what} ${unionId} */`); const d: string[] = [ - `${sig} { /* strict equality ${unionId} */`, + `${sig} { /* ${what} ${unionId} */`, ` if (a->tag != b->tag) return false;`, ` switch (a->tag) {`, ]; @@ -139,7 +143,11 @@ import { OVERFLOW_MEMBER } from "./emit-shapes.js"; d.push(` case ${i}: return true;`); break; case "f64": - d.push(` case ${i}: return scr_union_get_f64(a) == scr_union_get_f64(b);`); + d.push( + sameValue + ? ` case ${i}: return scr_num_same_value(scr_union_get_f64(a), scr_union_get_f64(b));` + : ` case ${i}: return scr_union_get_f64(a) == scr_union_get_f64(b);`, + ); break; case "bool": d.push(` case ${i}: return scr_union_get_bool(a) == scr_union_get_bool(b);`); diff --git a/packages/compiler/src/backend/emission/emitter.ts b/packages/compiler/src/backend/emission/emitter.ts index d77e7e798..582e8b516 100644 --- a/packages/compiler/src/backend/emission/emitter.ts +++ b/packages/compiler/src/backend/emission/emitter.ts @@ -1055,8 +1055,8 @@ export class CEmitter { return unionTruthyHelper(this, unionId); } - unionEqHelper(unionId: string): string { - return unionEqHelper(this, unionId); + unionEqHelper(unionId: string, sameValue: boolean): string { + return unionEqHelper(this, unionId, sameValue); } unionToStrHelper(unionId: string): string { diff --git a/packages/compiler/src/backend/llvm/emitter.ts b/packages/compiler/src/backend/llvm/emitter.ts index 88ed7ff35..f096be5d8 100644 --- a/packages/compiler/src/backend/llvm/emitter.ts +++ b/packages/compiler/src/backend/llvm/emitter.ts @@ -185,6 +185,7 @@ const LIB_FN_SYMS: Record = { "dyn.errInstanceof": "scr_dyn_err_instanceof", "num.toExponential": "scr_num_to_exponential", "num.toFixed0": "scr_num_to_fixed0", + "num.sameValue": "scr_num_same_value", "number.isFinite": "scr_num_is_finite", "number.isNaN": "scr_num_is_nan", "number.isInteger": "scr_num_is_integer", @@ -4336,7 +4337,14 @@ class LlEmitter { const t = B.tmp(); B.line(`${a} = call double @scr_union_get_f64(ptr ${l.name})`); B.line(`${b} = call double @scr_union_get_f64(ptr ${r.name})`); - B.line(`${t} = fcmp oeq double ${a}, ${b}`); + if (e.sameValue) { + // Object.is's f64 compare: NaN equals NaN, +0 differs + // from -0 — the runtime SameValue. + this.declare(`declare zeroext i1 @scr_num_same_value(double, double)`); + B.line(`${t} = call zeroext i1 @scr_num_same_value(double ${a}, double ${b})`); + } else { + B.line(`${t} = fcmp oeq double ${a}, ${b}`); + } B.line(`store i1 ${t}, ptr ${slot}`); break; } diff --git a/packages/compiler/src/frontend/lowering/lower-calls.ts b/packages/compiler/src/frontend/lowering/lower-calls.ts index 201fe2ad8..71a32db61 100644 --- a/packages/compiler/src/frontend/lowering/lower-calls.ts +++ b/packages/compiler/src/frontend/lowering/lower-calls.ts @@ -10,12 +10,12 @@ import { isJsSourceFile, locOf } from "../program.js"; import { isGenericCallableMemberType, typeKey } from "../types.js"; import { PoisonError, dynFallbackType, dynUndefinedExpr, importCallHandleType, jsFuncNameOf, newFnCtx } from "./lowerer.js"; import { enforceLibBoundary } from "./lib-boundary.js"; -import { builtinFenceHintOf, builtinModuleFnOf } from "./surfaces.js"; +import { NARROW_FIRST, builtinFenceHintOf, builtinModuleFnOf } from "./surfaces.js"; import { requiresDynamicDiag } from "../../diagnostics/diagnostic.js"; import { mixinFnShapeOf } from "./lower-mixins.js"; import { bufEncoding, dynStringReceiver, lowerArrayFromCall, lowerDynArrayFilterCall, lowerDynArrayFlatMapCall, lowerGroupByStaticCall, lowerIteratorHelperCall, lowerObjectAssignIndexShape, lowerObjectFromEntriesCall, lowerObjectIterOverIndexShape, lowerRegexMethodCall, lowerStringMethodCall, lowerTupleReadMethodCall } from "./lower-containers.js"; import { lowerChildStreamMethodCall, lowerDirentMethodCall, lowerPerfHooksCall, lowerProcStreamMethodCall, lowerReflectApplyCall, lowerWatcherMethodCall } from "./lower-builtins.js"; -import { lowerPromiseAllTupleCall, lowerPromiseRejectCall, probeLower, templateRawTextOf } from "./lower-exprs.js"; +import { droppableStatic, lowerPromiseAllTupleCall, lowerPromiseRejectCall, probeLower, templateRawTextOf } from "./lower-exprs.js"; import { httpClientFnBindingOf, isStreamUndefCallExpr, lowerHttpClientFnCall } from "./lower-server.js"; import { EMITTER_API_MEMBERS, exactInstanceClassOf, findGenericMethodOn, lowerClassGenericMethodCall, lowerStaticMethodCall, type ClassInfo } from "./lower-classes.js"; import { emitterRooted, lowerEmitterMethodCall } from "./lower-emitter.js"; @@ -6147,11 +6147,142 @@ export function lowerPromiseMethodCall(L: Lowerer, call: ts.CallExpression, return { kind: "libCall", fn: "regexp.escape", args: [arg], type: STRING, loc: locOf(call) }; } + /** Object.is over statically disjoint kinds: the constant false, with + * both operands still evaluated for their effects (droppable statics + * fold away — JS evaluates arguments, but nothing observes a pure one). */ + function objectIsDisjointFalse(left: IrExpr, right: IrExpr, loc: SrcLoc): IrExpr { + const stmts: IrStmt[] = []; + for (const e of [left, right]) { + if (!droppableStatic(e)) stmts.push({ kind: "exprStmt", expr: e, loc }); + } + const answer: IrExpr = { kind: "boolLit", value: false, type: BOOL, loc }; + if (stmts.length === 0) return answer; + return { kind: "seqExpr", stmts, result: answer, type: BOOL, loc }; + } + function lowerObjectStaticCall(L: Lowerer, call: ts.CallExpression, access: ts.PropertyAccessExpression,): IrExpr | null { if (call.questionDotToken || access.questionDotToken) return null; if (!L.isStdlibGlobal(access.expression, "Object")) return null; const member = access.name.text; + // Object.is — the spec's SameValue over the static kinds. Number + // pairs take the runtime SameValue (NaN equals NaN, +0 differs from + // -0 — the two divergences from ===); every other supported pair + // rides exactly the strict-equality machinery, whose answers + // SameValue shares: strings by bytes, bools by value, unit literals + // by tag, unions per arm (a number arm's payload compare upgrades to + // SameValue via unionEq's flag), and the reference kinds by pointer + // identity. Statically DISJOINT kind pairs answer the constant false + // with the operands still evaluated (tsc admits any pair — Object.is + // is (any, any) — and JS evaluates the arguments either way). + // dyn/jsval operands keep strict equality's stance: validate first. + if (member === "is") { + if (call.arguments.length !== 2 || call.arguments.some((a) => ts.isSpreadElement(a))) { + L.noLowering( + `Object.is with ${call.arguments.length} arguments`, + call, + "exactly two arguments are the lowered form (JS treats a missing one as undefined — pass it explicitly)", + ); + } + const loc = locOf(call); + const leftNode = call.arguments[0]!; + const rightNode = call.arguments[1]!; + const left = L.lowerExpr(leftNode); + const right = L.lowerExpr(rightNode); + const lk = left.type.kind; + const rk = right.type.kind; + if (lk === "f64" && rk === "f64") { + return { kind: "libCall", fn: "num.sameValue", args: [left, right], type: BOOL, loc }; + } + if (left.type.kind === "string" && right.type.kind === "string") { + return { kind: "strEq", negated: false, left, right, type: BOOL, loc }; + } + if (lk === "bool" && rk === "bool") { + return { kind: "bin", op: "===", left, right, type: BOOL, loc }; + } + const unitTest = L.lowerUnitComparison(left, right, false, loc); + if (unitTest) return unitTest; + if (lk === "dyn" || rk === "dyn" || lk === "jsval" || rk === "jsval") { + L.noLowering( + "Object.is over a dynamic operand", + call, + "validate/narrow the value first (strict equality's rule) — SameValue only differs from === on numbers (NaN, ±0)", + ); + } + if (left.type.kind === "union" || right.type.kind === "union") { + const ut = left.type.kind === "union" ? left.type : (right.type as IrType & { kind: "union" }); + const bothUnion = left.type.kind === "union" && right.type.kind === "union"; + const sameUnion = bothUnion && typeEquals(left.type, right.type); + if ((sameUnion || !bothUnion) && L.eqComparableUnion(ut.unionId)) { + const plain = left.type.kind === "union" ? right : left; + const arms = L.unions.get(ut.unionId)?.arms ?? []; + // The plain side wraps into the union exactly like === when the + // union holds its type; a plain PRIMITIVE the union has no arm + // for is the disjoint constant false (coercing it would strand). + if (bothUnion || arms.some((a) => typeEquals(a, plain.type))) { + const sameValue = arms.some((a) => a.kind === "f64"); + return { + kind: "unionEq", + unionId: ut.unionId, + negated: false, + sameValue, + left: L.coerceInto(leftNode, left, ut), + right: L.coerceInto(rightNode, right, ut), + type: BOOL, + loc, + }; + } + if ( + plain.type.kind === "f64" || plain.type.kind === "string" || + plain.type.kind === "bool" || isUnitType(plain.type) + ) { + return objectIsDisjointFalse(left, right, loc); + } + } + L.noLowering( + "Object.is over these union operands", + call, + `union-typed comparisons need one comparable shape (${NARROW_FIRST})`, + ); + } + // Reference kinds: pointer identity — exactly strict equality + // (hierarchy-related classes widen the derived side first). + let idLeft = left; + let idRight = right; + if (left.type.kind === "object" && right.type.kind === "object") { + if (L.isSubclassOf(left.type.className, right.type.className)) { + idLeft = L.upcastTo(left, right.type.className); + } else if (L.isSubclassOf(right.type.className, left.type.className)) { + idRight = L.upcastTo(right, left.type.className); + } + } + if ( + (idLeft.type.kind === "func" && idRight.type.kind === "func") || + (idLeft.type.kind === "classval" && idRight.type.kind === "classval") + ) { + return { kind: "bin", op: "===", left: idLeft, right: idRight, type: BOOL, loc }; + } + if ( + (idLeft.type.kind === "array" || idLeft.type.kind === "map" || + idLeft.type.kind === "set" || idLeft.type.kind === "object" || + idLeft.type.kind === "record" || idLeft.type.kind === "symbol" || + idLeft.type.kind === "bytes" || idLeft.type.kind === "promise") && + typeEquals(idLeft.type, idRight.type) + ) { + return { kind: "bin", op: "===", left: idLeft, right: idRight, type: BOOL, loc }; + } + // Statically disjoint pairs with a primitive/unit side: SameValue + // never crosses kinds, so the answer is the constant false. + const disjoint = new Set(["f64", "string", "bool", "undefinedT", "nullT"]); + if (lk !== rk && (disjoint.has(lk) || disjoint.has(rk))) { + return objectIsDisjointFalse(left, right, loc); + } + L.noLowering( + `Object.is over '${L.fmt(left.type)}' and '${L.fmt(right.type)}' operands`, + call, + "the operands must share one comparable kind (numbers, strings, booleans, units, one union shape, or one reference type)", + ); + } // `Object.assign(fn, { props })` whose RESULT type maps to the hybrid // (function-with-properties) record: the chalk-shape CONSTRUCTOR. if (member === "assign") { diff --git a/packages/compiler/src/frontend/lowering/lower-exprs.ts b/packages/compiler/src/frontend/lowering/lower-exprs.ts index a43836ef1..15445f5fc 100644 --- a/packages/compiler/src/frontend/lowering/lower-exprs.ts +++ b/packages/compiler/src/frontend/lowering/lower-exprs.ts @@ -7131,6 +7131,7 @@ export function lowerBinary(L: Lowerer, expr: ts.BinaryExpression): IrExpr { kind: "unionEq", unionId: ut.unionId, negated, + sameValue: false, left: L.coerceInto(expr.left, left, ut), right: L.coerceInto(expr.right, right, ut), type: BOOL, diff --git a/packages/compiler/src/frontend/lowering/lower-stmts.ts b/packages/compiler/src/frontend/lowering/lower-stmts.ts index 9ab27e461..312bd54dd 100644 --- a/packages/compiler/src/frontend/lowering/lower-stmts.ts +++ b/packages/compiler/src/frontend/lowering/lower-stmts.ts @@ -2822,6 +2822,7 @@ export function lowerVarDecl(L: Lowerer, decl: ts.VariableDeclaration, isLet: bo kind: "unionEq", unionId: unionType.unionId, negated: false, + sameValue: false, left: disc, right: L.coerceInto(clause.expression, test, unionType), type: BOOL, diff --git a/packages/compiler/src/ir/nodes.ts b/packages/compiler/src/ir/nodes.ts index fd8129177..bf0040d72 100644 --- a/packages/compiler/src/ir/nodes.ts +++ b/packages/compiler/src/ir/nodes.ts @@ -1736,6 +1736,11 @@ export type IrLibFn = * forms keep their island lowering. Results +1; never throw. */ | "num.toExponential" | "num.toFixed0" + /** Object.is over two numbers — the spec's SameValue on doubles: NaN + * equals NaN, +0 differs from -0, everything else is `===`. Plain bool + * result; never throws. (Union-armed operands take unionEq's sameValue + * flag instead — this is the both-f64 fast path.) */ + | "num.sameValue" /** `delete process.env[NAME]` — unsetenv(3): the mutation is visible to * every later read (process.envGet asks getenv fresh) and inherited by * spawned children, exactly Node. Statement position only (JS's boolean @@ -4293,11 +4298,15 @@ export type IrExpr = * comparison (`u === "text"`) arrives here after the frontend wraps the * plain side (payload identity is preserved by the wrap, so ref-arm * semantics stay JS-exact). Operands are borrowed; result is a plain - * bool. `negated` is the `!==` spelling. */ + * bool. `negated` is the `!==` spelling. `sameValue` upgrades the f64 + * arm's compare from `===` to SameValue (NaN equals NaN, +0 differs + * from -0) — the Object.is lowering; every other arm's compare is + * shared between the two semantics. */ | { kind: "unionEq"; unionId: string; negated: boolean; + sameValue: boolean; left: IrExpr; right: IrExpr; type: IrType; diff --git a/packages/compiler/src/ir/validate.ts b/packages/compiler/src/ir/validate.ts index 8708d06ad..c74d258dc 100644 --- a/packages/compiler/src/ir/validate.ts +++ b/packages/compiler/src/ir/validate.ts @@ -209,6 +209,7 @@ export const LIB_FN_SIGS: Record 1; +const g = (): number => 2; +console.log(Object.is(f, f), Object.is(f, g)); +const m = new Map(); +console.log(Object.is(m, m), Object.is(m, new Map())); +const st = new Set([1]); +console.log(Object.is(st, st), Object.is(st, new Set([1]))); +class Base { tag = "b"; } +class Derived extends Base { extra = 1; } +const d = new Derived(); +const asBase: Base = d; +const otherBase = new Base(); +console.log(Object.is(d, d), Object.is(asBase, d), Object.is(asBase, otherBase)); +console.log(Object.is(Base, Base), Object.is(Base, Derived)); + +// Unions: SameValue per arm — NaN and ±0 payloads answer the spec, ref +// arms compare identity, and a plain side wraps like ===. +function u(v: number | string | null): number | string | null { return v; } +const a1 = u(NaN); +const b1 = u(NaN); +console.log(Object.is(a1, b1), a1 === b1); +const z1 = u(0); +const z2 = u(-0); +console.log(Object.is(z1, z2), z1 === z2, Object.is(z2, u(-0))); +console.log(Object.is(u("hi"), u("hi")), Object.is(u("hi"), u(7)), Object.is(u(null), u(null)), Object.is(u(null), u(0))); +console.log(Object.is(u(3), 3), Object.is(3, u(3)), Object.is(u("x"), "x"), Object.is(u(null), null), Object.is(u(3), 4)); +console.log(Object.is(u(NaN), NaN), Object.is(u(0), -0), Object.is(u(-0), -0)); +// A primitive the union cannot hold: constant false. +console.log(Object.is(u(3), true)); +const ru: number[] | null = arr; +console.log(Object.is(ru, arr), Object.is(ru, null)); + +// Object.is drives control flow like any boolean. +const vals = [0, -0, NaN, 1]; +let negZeros = 0; +let nans = 0; +for (const v of vals) { + if (Object.is(v, -0)) negZeros++; + if (Object.is(v, NaN)) nans++; +} +console.log(negZeros, nans); diff --git a/tests/diagnostics/stdlib-fence.ts b/tests/diagnostics/stdlib-fence.ts index 5133513f1..578d3bc75 100644 --- a/tests/diagnostics/stdlib-fence.ts +++ b/tests/diagnostics/stdlib-fence.ts @@ -67,4 +67,13 @@ const thened = work().then((v) => v + 1, () => 0); const caught = work().catch(() => 0); const resolved = Promise.resolve(1); +// Object.is lowers over one comparable kind; the unlowered forms fence +// by name — a validate-first dynamic operand and partially-overlapping +// unions needing narrowing. +const dynIs = Object.is(JSON.parse("1") as unknown, 1); +function pick(a: string | number, b: number | boolean): boolean { + return Object.is(a, b); // partially-overlapping unions: narrow first +} +const picked = pick(1, 2); + const big = 10n; diff --git a/tests/harness/__snapshots__/stdlib-fence.ts.txt b/tests/harness/__snapshots__/stdlib-fence.ts.txt index edba8f247..ca16920a4 100644 --- a/tests/harness/__snapshots__/stdlib-fence.ts.txt +++ b/tests/harness/__snapshots__/stdlib-fence.ts.txt @@ -282,16 +282,34 @@ stdlib-fence.ts:66:16 - error SC2020: 'then with 2 arguments' is part of the sta hint: the supported form takes exactly one fulfillment handler — chain .catch(...) for the rejection half -stdlib-fence.ts:70:7 - error SC2001: values of type '10n' cannot be compiled yet (supported: number, string, boolean, arrays, Maps, Sets, RegExp, functions, classes, records, unions of those, and 'unknown') +stdlib-fence.ts:73:15 - error SC2020: 'Object.is over a dynamic operand' is part of the standard library types but has no scriptc lowering yet - 69 | - 70 | const big = 10n; + 72 | // unions needing narrowing. + 73 | const dynIs = Object.is(JSON.parse("1") as unknown, 1); + | ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + 74 | function pick(a: string | number, b: number | boolean): boolean { + + hint: validate/narrow the value first (strict equality's rule) — SameValue only differs from === on numbers (NaN, ±0) + +stdlib-fence.ts:75:10 - error SC2020: 'Object.is over these union operands' is part of the standard library types but has no scriptc lowering yet + + 74 | function pick(a: string | number, b: number | boolean): boolean { + 75 | return Object.is(a, b); // partially-overlapping unions: narrow first + | ^~~~~~~~~~~~~~~ + 76 | } + + hint: union-typed comparisons need one comparable shape (narrow first: check a discriminant field, or compare with '!== undefined'/'!== null' for unit arms) + +stdlib-fence.ts:79:7 - error SC2001: values of type '10n' cannot be compiled yet (supported: number, string, boolean, arrays, Maps, Sets, RegExp, functions, classes, records, unions of those, and 'unknown') + + 78 | + 79 | const big = 10n; | ^~~ - 71 | + 80 | -stdlib-fence.ts:70:13 - error SC1090: syntax 'BigIntLiteral' is not supported yet +stdlib-fence.ts:79:13 - error SC1090: syntax 'BigIntLiteral' is not supported yet - 69 | - 70 | const big = 10n; + 78 | + 79 | const big = 10n; | ^~~ - 71 | \ No newline at end of file + 80 | \ No newline at end of file From fa7cc089a9e260de911a59fb46c05214b38b817c Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Thu, 23 Jul 2026 20:30:47 -0500 Subject: [PATCH 3/4] The en-US Intl slice: composed NumberFormat and toLocaleString go static, the rest fences by name MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - new Intl.NumberFormat("en-US").format(x), the callable spelling, and x.toLocaleString("en-US") lower to one runtime formatter with default options: decimal notation, 0-3 fraction digits rounded half-up on the SHORTEST round-tripping decimal (ICU's rounding input, probed against Node — format(1.0005) is "1.001" though toFixed(3) answers "1.000"; 1e23 prints the shortest form's zeros, not the exact expansion), "," grouping, "∞"/"NaN" texts, and "-0" for negatives rounding to zero. The en-US/latn symbols are the whole embedded locale surface — no ICU is linked. - The Ryū digit core factors out of scr_f64_to_str as scr_f64_digits (same bytes; the 1M-double oracle gate stays green) and feeds the formatter; a 30,000-double ad-hoc fuzz across magnitude regimes matched Node byte-for-byte on both backends. - Every unlowered Intl form fences by NAME with its own reason: the environment-default locale a compiled binary cannot carry (no-locale forms), other locales and the rest of the namespace (DurationFormat, DateTimeFormat, PluralRules, getCanonicalLocales — ICU data the binary does not embed; Node 24's DurationFormat additionally throws V8-internal Temporal errors with embedded source paths, so byte parity there is a version-dependent landmine), options bags, formatter values (the composed form is the lowered shape), and non-number format arguments. - Differential fixture 2562 pins the rounding discriminators, grouping, ±0/NaN/∞, tiny and huge edges, and both spellings byte-identically vs Node on both backends; the new intl diagnostics fixture pins every named fence, and the Intl hints across the identifier/member/type chokepoints now teach what lowers. --- .../src/backend/emission/emit-exprs.ts | 4 + packages/compiler/src/backend/llvm/emitter.ts | 1 + .../src/frontend/lowering/lower-calls.ts | 115 ++++++++++++++++++ .../src/frontend/lowering/lower-exprs.ts | 2 +- .../compiler/src/frontend/lowering/lowerer.ts | 13 ++ .../src/frontend/lowering/surfaces.ts | 7 +- packages/compiler/src/ir/nodes.ts | 9 ++ packages/compiler/src/ir/validate.ts | 1 + packages/runtime/src/scr_lib.c | 90 ++++++++++++++ packages/runtime/src/scr_number.c | 48 +++++--- packages/runtime/src/scr_runtime.h | 14 +++ tests/corpus/2562-intl-numberformat-en-us.ts | 41 +++++++ tests/diagnostics/intl.ts | 25 ++++ tests/harness/__snapshots__/intl.ts.txt | 114 +++++++++++++++++ .../harness/__snapshots__/stdlib-fence.ts.txt | 8 +- 15 files changed, 465 insertions(+), 27 deletions(-) create mode 100644 tests/corpus/2562-intl-numberformat-en-us.ts create mode 100644 tests/diagnostics/intl.ts create mode 100644 tests/harness/__snapshots__/intl.ts.txt diff --git a/packages/compiler/src/backend/emission/emit-exprs.ts b/packages/compiler/src/backend/emission/emit-exprs.ts index b36055a46..2c47181b2 100644 --- a/packages/compiler/src/backend/emission/emit-exprs.ts +++ b/packages/compiler/src/backend/emission/emit-exprs.ts @@ -2503,6 +2503,10 @@ export function emitExpr(E: CEmitter, e: IrExpr): Temp { // Object.is over two numbers — SameValue on doubles. No throw. case "num.sameValue": return finish(`scr_num_same_value(${arg(0)}, ${arg(1)})`); + // Intl.NumberFormat("en-US").format / toLocaleString("en-US") + // with default options (scr_lib.c). +1 string; no throw. + case "intl.numFormatEnUs": + return finish(`scr_intl_num_format_en_us(${arg(0)})`); // The URL surface (scr_url.c): construction and the two // fileURLToPath receiver forms throw catchable TypeErrors // (may-throw seed set); the getters are pure reads. Receivers diff --git a/packages/compiler/src/backend/llvm/emitter.ts b/packages/compiler/src/backend/llvm/emitter.ts index f096be5d8..8a169932c 100644 --- a/packages/compiler/src/backend/llvm/emitter.ts +++ b/packages/compiler/src/backend/llvm/emitter.ts @@ -186,6 +186,7 @@ const LIB_FN_SYMS: Record = { "num.toExponential": "scr_num_to_exponential", "num.toFixed0": "scr_num_to_fixed0", "num.sameValue": "scr_num_same_value", + "intl.numFormatEnUs": "scr_intl_num_format_en_us", "number.isFinite": "scr_num_is_finite", "number.isNaN": "scr_num_is_nan", "number.isInteger": "scr_num_is_integer", diff --git a/packages/compiler/src/frontend/lowering/lower-calls.ts b/packages/compiler/src/frontend/lowering/lower-calls.ts index 71a32db61..81561fbf7 100644 --- a/packages/compiler/src/frontend/lowering/lower-calls.ts +++ b/packages/compiler/src/frontend/lowering/lower-calls.ts @@ -3589,6 +3589,9 @@ export function lowerCall(L: Lowerer, expr: ts.CallExpression): IrExpr { lowerSymbolStaticCall(L, expr, expr.expression) ?? lowerSymbolMethodCall(L, expr, expr.expression) ?? lowerRegExpStaticCall(L, expr, expr.expression) ?? + // The composed en-US Intl.NumberFormat form — before the member + // fences (the receiver's Intl.NumberFormat type has no mapping). + lowerIntlNumberFormatCall(L, expr, expr.expression) ?? lowerGroupByStaticCall(L, expr, expr.expression) ?? // Iterator-helper chains rooted at arr.values() — before the // array method paths (the terminal names collide with array @@ -4286,6 +4289,43 @@ const inliningPredicates = new Set(); const fn = name === "toExponential" ? "num.toExponential" : "num.toFixed0"; return { kind: "libCall", fn, args: [operand], type: STRING, loc }; } + // Number.prototype.toLocaleString("en-US") — the spec makes it + // NumberFormat(locale).format(this), so the en-US embedded formatter + // answers exactly. The unlowered forms fence by NAME: no locale (the + // host environment's default, which a compiled binary cannot carry), + // other locales (ICU data the binary does not embed), options bags. + if (name === "toLocaleString" && recvKind === "f64") { + if (call.arguments.length === 0) { + L.noLowering( + "Number.prototype.toLocaleString without a locale", + call, + "the default locale is the host environment's, which a compiled binary cannot carry — " + + 'pass it explicitly: x.toLocaleString("en-US")', + ); + } + if (call.arguments.length > 1) { + L.noLowering( + "Number.prototype.toLocaleString with an options bag", + call, + "the embedded data covers DEFAULT options only (decimal notation, up to 3 fraction " + + 'digits, grouping) — x.toLocaleString("en-US")', + ); + } + const locNode = call.arguments[0]!; + if (ts.isSpreadElement(locNode) || !ts.isStringLiteralLike(locNode) || locNode.text !== "en-US") { + L.noLowering( + !ts.isSpreadElement(locNode) && ts.isStringLiteralLike(locNode) + ? `Number.prototype.toLocaleString at locale "${locNode.text}"` + : "Number.prototype.toLocaleString with a non-literal locale", + locNode, + '"en-US" (Node\'s default-build locale) is the one locale whose data the runtime embeds — ' + + "everything else is ICU data the binary does not carry", + ); + } + const operand = L.lowerExpr(recv); + if (operand.type.kind !== "f64") return null; + return { kind: "libCall", fn: "intl.numFormatEnUs", args: [operand], type: STRING, loc }; + } if (name === "charAt" && recvKind === "string" && call.arguments.length === 1 && !ts.isSpreadElement(call.arguments[0]!)) { const receiver = L.lowerExpr(recv); @@ -6147,6 +6187,81 @@ export function lowerPromiseMethodCall(L: Lowerer, call: ts.CallExpression, return { kind: "libCall", fn: "regexp.escape", args: [arg], type: STRING, loc: locOf(call) }; } + /** The composed en-US Intl.NumberFormat form: `new Intl.NumberFormat( + * "en-US").format(x)` (and the callable spelling without `new` — the + * spec makes them the same formatter). Only the COMPOSED form lowers — + * formatter values have no representation — and only the one locale + * whose data the runtime embeds, with default options: decimal + * notation, 0–3 fraction digits rounded half-up on the shortest + * round-tripping decimal (ICU's rounding input — format(1.0005) is + * "1.001" though toFixed(3) answers "1.000"), "," grouping. The + * unlowered forms fence by NAME (no locale — the host environment's + * default, which a compiled binary cannot carry; other locales — ICU + * data the binary does not embed; options bags; non-number arguments). + * Null when the callee isn't a NumberFormat-construction .format. */ + function lowerIntlNumberFormatCall(L: Lowerer, call: ts.CallExpression, + access: ts.PropertyAccessExpression,): IrExpr | null { + if (call.questionDotToken || access.questionDotToken) return null; + if (access.name.text !== "format") return null; + let recv: ts.Expression = access.expression; + while (ts.isParenthesizedExpression(recv)) recv = recv.expression; + let ctorArgs: readonly ts.Expression[]; + if (ts.isNewExpression(recv) || (ts.isCallExpression(recv) && !recv.questionDotToken)) { + const ctor = recv.expression; + if ( + !ts.isPropertyAccessExpression(ctor) || ctor.questionDotToken || + ctor.name.text !== "NumberFormat" || !L.isStdlibGlobal(ctor.expression, "Intl") + ) { + return null; + } + ctorArgs = recv.arguments ?? []; + } else { + return null; + } + const loc = locOf(call); + if (ctorArgs.length === 0) { + L.noLowering( + "Intl.NumberFormat without a locale", + recv, + "the default locale is the host environment's, which a compiled binary cannot carry — " + + 'pass it explicitly: new Intl.NumberFormat("en-US").format(x)', + ); + } + if (ctorArgs.length > 1) { + L.noLowering( + "Intl.NumberFormat with an options bag", + ctorArgs[1]!, + "the embedded data covers DEFAULT options only (decimal notation, up to 3 fraction digits, " + + 'grouping) — new Intl.NumberFormat("en-US").format(x)', + ); + } + const locArg = ctorArgs[0]!; + if (ts.isSpreadElement(locArg) || !ts.isStringLiteralLike(locArg) || locArg.text !== "en-US") { + L.noLowering( + !ts.isSpreadElement(locArg) && ts.isStringLiteralLike(locArg) + ? `Intl.NumberFormat at locale "${locArg.text}"` + : "Intl.NumberFormat with a non-literal locale", + locArg, + '"en-US" (Node\'s default-build locale) is the one locale whose data the runtime embeds — ' + + "everything else is ICU data the binary does not carry", + ); + } + if (call.arguments.length !== 1 || ts.isSpreadElement(call.arguments[0]!)) { + L.noLowering(`Intl.NumberFormat("en-US").format with ${call.arguments.length} arguments`, call); + } + const argNode = call.arguments[0]!; + if (L.mapTypeOf(L.typeOf(argNode))?.kind !== "f64") { + L.noLowering( + `Intl.NumberFormat("en-US").format over a '${L.checker.typeToString(L.typeOf(argNode))}'`, + argNode, + "a number argument is the lowered form (bigint and numeric-string inputs have no representation)", + ); + } + const arg = L.lowerExprExpecting(argNode, F64); + if (arg.type.kind !== "f64") L.badType(argNode, L.typeOf(argNode)); + return { kind: "libCall", fn: "intl.numFormatEnUs", args: [arg], type: STRING, loc }; + } + /** Object.is over statically disjoint kinds: the constant false, with * both operands still evaluated for their effects (droppable statics * fold away — JS evaluates arguments, but nothing observes a pure one). */ diff --git a/packages/compiler/src/frontend/lowering/lower-exprs.ts b/packages/compiler/src/frontend/lowering/lower-exprs.ts index 15445f5fc..81e3fd3a7 100644 --- a/packages/compiler/src/frontend/lowering/lower-exprs.ts +++ b/packages/compiler/src/frontend/lowering/lower-exprs.ts @@ -847,7 +847,7 @@ function lowerExprInner(L: Lowerer, expr: ts.Expression): IrExpr { BigInt: "arbitrary-precision integers have no static representation — f64 is the one number type (Number.MAX_SAFE_INTEGER bounds exact integers)", Proxy: "property-access metaprogramming has no static lowering (every property read must resolve at compile time)", Reflect: "reflective property access has no static lowering — read and call members directly", - Intl: "locale- and ICU-backed behavior lives outside the static runtime (the localeCompare stance: code-unit order, no collation/locale data)", + Intl: "locale- and ICU-backed behavior lives outside the static runtime (the localeCompare stance: code-unit order, no collation/locale data) — what lowers: the composed new Intl.NumberFormat(\"en-US\").format(x) and x.toLocaleString(\"en-US\") with default options", SharedArrayBuffer: "no shared-memory threads exist in a compiled program — Uint8Array is the byte storage", ArrayBuffer: "no free-standing ArrayBuffer value exists — typed arrays own their storage (new Uint8Array(n) allocates; new Uint8Array(new ArrayBuffer(n)) erases the buffer into the view)", WeakRef: "deref()-after-collect exposes GC timing — genuinely dynamic; hold a strong reference instead", diff --git a/packages/compiler/src/frontend/lowering/lowerer.ts b/packages/compiler/src/frontend/lowering/lowerer.ts index f433ad80a..f21204c33 100644 --- a/packages/compiler/src/frontend/lowering/lowerer.ts +++ b/packages/compiler/src/frontend/lowering/lowerer.ts @@ -2201,12 +2201,25 @@ export class Lowerer { // story, naming the type — and for the families with a WHY, the same // pointed reason the identifier/constructor chokepoints teach. if (stdlibNominal) { + const intlHint = + "Intl formatter values have no representation — the COMPOSED en-US forms lower: " + + 'new Intl.NumberFormat("en-US").format(x) and x.toLocaleString("en-US") with default options; ' + + "the rest is ICU locale data the binary does not carry"; const typeHints: Record = { ArrayBuffer: "no free-standing ArrayBuffer value exists — typed arrays own their storage " + "(new Uint8Array(n) allocates; new Uint8Array(new ArrayBuffer(n)) erases the buffer into the view)", SharedArrayBuffer: "no shared-memory threads exist in a compiled program — Uint8Array is the byte storage", + NumberFormat: intlHint, + DateTimeFormat: intlHint, + DurationFormat: intlHint, + PluralRules: intlHint, + Collator: intlHint, + ListFormat: intlHint, + RelativeTimeFormat: intlHint, + Segmenter: intlHint, + DisplayNames: intlHint, }; this.pushDiag( noLoweringDiag(this.checker.typeToString(type), locOf(node), typeHints[stdlibOwnSym?.name ?? ""]), diff --git a/packages/compiler/src/frontend/lowering/surfaces.ts b/packages/compiler/src/frontend/lowering/surfaces.ts index 55c15b6e9..4074d7c30 100644 --- a/packages/compiler/src/frontend/lowering/surfaces.ts +++ b/packages/compiler/src/frontend/lowering/surfaces.ts @@ -1049,7 +1049,7 @@ export const BUILTIN_MODULE_FENCE_HINTS: Record= 0; i--) { + if (d[i] != '9') { + d[i]++; + return false; + } + d[i] = '0'; + } + d[0] = '1'; + return true; +} + +/* Intl.NumberFormat("en-US").format(x) / x.toLocaleString("en-US") with + * DEFAULT options: decimal notation, minimum 0 / maximum 3 fraction + * digits, "," grouping every three integer digits, "∞"/"NaN" texts, and + * "-0" whenever the input is negative or negative zero even after + * rounding to zero. Rounding is half-up ON THE SHORTEST ROUND-TRIPPING + * DECIMAL — ICU's rounding input, probed against Node: format(1.0005) is + * "1.001" although the double is 1.000499... and toFixed(3) answers + * "1.000"; format(1e23) prints the shortest form's trailing zeros, not + * the double's exact expansion. The en-US/latn symbols (",", ".", "∞", + * "NaN", group size 3) are the whole embedded locale surface. Verified + * differentially against Node. Result +1; never throws. */ +ScrStr *scr_intl_num_format_en_us(double x) { + if (isnan(x)) return scr_str_new("NaN", 3); + if (isinf(x)) { + return x < 0 ? scr_str_new("-\xE2\x88\x9E", 4) : scr_str_new("\xE2\x88\x9E", 3); + } + bool neg = signbit(x) != 0; + if (x == 0) return neg ? scr_str_new("-0", 2) : scr_str_new("0", 1); + double a = neg ? -x : x; + + /* Shortest digits: value = 0.d × 10^n (no trailing zeros, k ≤ 17). */ + char d[18]; + int n; + int k = scr_f64_digits(a, d, &n); + + /* Round at 3 fraction digits: fraction position p is digit index + * n+p-1, so index n+3 is the first DROPPED digit. Half-up on the + * decimal digits — the shortest string ends right after them, so + * "first dropped digit ≥ 5" IS the whole decision. */ + int keep = n + 3; + if (keep < k) { + bool up = keep >= 0 && d[keep] >= '5'; + k = keep < 0 ? 0 : keep; + if (up && scr_dec_inc(d, k)) { + /* Carried out (all nines, or the round-up-from-nothing 0.0005 + * case): one leading 1, the dropped nines fold into the scale. */ + k = 1; + n += 1; + } else if (k == 0) { + /* Everything rounded away: ±0 with the sign preserved. */ + return neg ? scr_str_new("-0", 2) : scr_str_new("0", 1); + } + } + + /* Assemble: integer digits (indices [0, n)), zero-padded past k, then + * the ≤ 3 fraction digits (indices n..n+2, '0' outside [0, k)) with + * trailing zeros trimmed, then commas every three integer digits. */ + char frac[3]; + int flen = 0; + for (int p = 1; p <= 3; p++) { + int idx = n + p - 1; + frac[flen++] = (idx >= 0 && idx < k) ? d[idx] : '0'; + } + while (flen > 0 && frac[flen - 1] == '0') flen--; + + char out[512]; + int o = 0; + if (neg) out[o++] = '-'; + if (n <= 0) { + out[o++] = '0'; + } else { + for (int i = 0; i < n; i++) { + if (i > 0 && (n - i) % 3 == 0) out[o++] = ','; + out[o++] = (i < k) ? d[i] : '0'; + } + } + if (flen > 0) { + out[o++] = '.'; + memcpy(out + o, frac, (size_t)flen); + o += flen; + } + return scr_str_new(out, (size_t)o); +} + /* Object.is over two numbers — the spec's SameValue on doubles: NaN * equals NaN, +0 differs from -0, everything else is ==. */ bool scr_num_same_value(double a, double b) { diff --git a/packages/runtime/src/scr_number.c b/packages/runtime/src/scr_number.c index 6e13c87c8..ace235b29 100644 --- a/packages/runtime/src/scr_number.c +++ b/packages/runtime/src/scr_number.c @@ -24,27 +24,19 @@ * unchanged. Provides d2d(), d2d_small_int(), decimalLength17(), div10(). */ #include "../vendor/ryu/d2s.c" -size_t scr_f64_to_str(double x, char *buf) { - if (isnan(x)) return (size_t)(stpcpy(buf, "NaN") - buf); - if (x == 0) return (size_t)(stpcpy(buf, "0") - buf); /* covers -0 */ - if (isinf(x)) { - return (size_t)(stpcpy(buf, x < 0 ? "-Infinity" : "Infinity") - buf); - } - - char *out = buf; - if (x < 0) { - *out++ = '-'; - x = -x; - } - - /* Shortest round-tripping digits via Ryū. Mirrors d2s_buffered_n's - * dispatch: the exact small-integer fast path first (trailing decimal - * zeros folded into the exponent), the full algorithm otherwise. */ +/* The Ryū digit core, shared by the ECMA placement below and the Intl + * en-US number formatter (scr_lib.c): the shortest round-tripping digit + * string for a positive finite double — value = 0.digits × 10^n with no + * trailing zeros. Returns k (the digit count, ≤ 17); digits is + * NUL-terminated. Mirrors d2s_buffered_n's dispatch: the exact + * small-integer fast path first (trailing decimal zeros folded into the + * exponent), the full algorithm otherwise. */ +int scr_f64_digits(double x, char digits[18], int *n_out) { uint64_t bits; memcpy(&bits, &x, sizeof bits); const uint64_t ieeeMantissa = bits & ((1ull << DOUBLE_MANTISSA_BITS) - 1); const uint32_t ieeeExponent = - (uint32_t)(bits >> DOUBLE_MANTISSA_BITS); /* sign already stripped */ + (uint32_t)(bits >> DOUBLE_MANTISSA_BITS); /* sign must be stripped */ floating_decimal_64 v; if (d2d_small_int(ieeeMantissa, ieeeExponent, &v)) { for (;;) { @@ -60,9 +52,8 @@ size_t scr_f64_to_str(double x, char *buf) { /* Ryū's (mantissa, exponent) → ECMA's (digits, k, n): the k mantissa * digits have no trailing zeros, and value = 0.digits * 10^n. */ - char digits[18]; int k = (int)decimalLength17(v.mantissa); - int n = v.exponent + k; + *n_out = v.exponent + k; digits[k] = '\0'; uint64_t m = v.mantissa; for (int i = k - 1; i >= 0; i--) { @@ -70,6 +61,25 @@ size_t scr_f64_to_str(double x, char *buf) { digits[i] = (char)('0' + (uint32_t)m - 10 * (uint32_t)q); m = q; } + return k; +} + +size_t scr_f64_to_str(double x, char *buf) { + if (isnan(x)) return (size_t)(stpcpy(buf, "NaN") - buf); + if (x == 0) return (size_t)(stpcpy(buf, "0") - buf); /* covers -0 */ + if (isinf(x)) { + return (size_t)(stpcpy(buf, x < 0 ? "-Infinity" : "Infinity") - buf); + } + + char *out = buf; + if (x < 0) { + *out++ = '-'; + x = -x; + } + + char digits[18]; + int n; + int k = scr_f64_digits(x, digits, &n); if (k <= n && n <= 21) { /* Integer: digits followed by n-k zeros. */ diff --git a/packages/runtime/src/scr_runtime.h b/packages/runtime/src/scr_runtime.h index 940b00dba..f57437221 100644 --- a/packages/runtime/src/scr_runtime.h +++ b/packages/runtime/src/scr_runtime.h @@ -3867,6 +3867,12 @@ void scr_jsval_cast_fail(ScrJsval *v, const ScrStr *target); */ size_t scr_f64_to_str(double x, char *buf); +/* The Ryū digit core (scr_number.c), shared with the Intl en-US number + * formatter: the shortest round-tripping digits of a POSITIVE finite + * double — value = 0.digits × 10^n, no trailing zeros, NUL-terminated. + * Returns k, the digit count (≤ 17). */ +int scr_f64_digits(double x, char digits[18], int *n_out); + /* ToString for template literals / string coercion. Returns +1. */ ScrStr *scr_f64_to_scrstr(double x); ScrStr *scr_bool_to_scrstr(bool b); /* interned "true"/"false" */ @@ -3902,6 +3908,14 @@ ScrStr *scr_num_to_fixed0(double x); * equals NaN, +0 differs from -0, everything else is ==. Never throws. */ bool scr_num_same_value(double a, double b); +/* Intl.NumberFormat("en-US").format(x) / x.toLocaleString("en-US") with + * default options: decimal notation, 0–3 fraction digits rounded half-up + * on the shortest round-tripping decimal (ICU's rounding input — NOT + * toFixed's exact-value rounding), "," grouping every three integer + * digits, "∞"/"NaN" texts, "-0" for negative inputs rounding to zero. + * en-US is the one embedded locale. Result +1; never throws. */ +ScrStr *scr_intl_num_format_en_us(double x); + /* ── Date, the composed slice (scr_lib.c) ───────────────────────────── * Date values have no representation; the runtime surface is exactly * Date.now() and toISOString over a millisecond time value. */ diff --git a/tests/corpus/2562-intl-numberformat-en-us.ts b/tests/corpus/2562-intl-numberformat-en-us.ts new file mode 100644 index 000000000..340a9b526 --- /dev/null +++ b/tests/corpus/2562-intl-numberformat-en-us.ts @@ -0,0 +1,41 @@ +// The composed en-US Intl.NumberFormat forms with default options — +// new Intl.NumberFormat("en-US").format(x), the callable spelling, and +// x.toLocaleString("en-US"). Rounding is half-up on the SHORTEST +// round-tripping decimal (ICU's rounding input): format(1.0005) is +// "1.001" although toFixed(3) answers "1.000" on the same double, and +// 1e23 prints the shortest form's zeros, not the double's exact +// expansion. Grouping, ±0, NaN/∞ texts, and the tiny/huge edges pin +// byte-identically against Node. +const nf = (x: number): string => new Intl.NumberFormat("en-US").format(x); + +// Grouping and plain decimals. +console.log(nf(0), nf(1), nf(100), nf(1000), nf(10000), nf(999999), nf(-1000000)); +console.log(nf(1234567.891), nf(-1234567.891), nf(123456789.9999), nf(0.5), nf(-0.4)); + +// The shortest-decimal rounding discriminators — (1.0005).toFixed(3) +// would answer "1.000" (exact-value rounding); format answers "1.001". +console.log(nf(1.0005), nf(1.0015), nf(123.4565), nf(7.995)); +console.log(nf(0.0625), nf(999.9995), nf(0.1 + 0.2), nf(1.100000023841858)); + +// Signs, zeros, and the non-finite texts. +console.log(nf(-0), nf(NaN), nf(1 / 0), nf(-1 / 0), nf(-2.5)); + +// Tiny values collapse to ±0 or the smallest kept fraction digit. +console.log(nf(5e-4), nf(-5e-4), nf(0.00049), nf(1e-7), nf(-1e-7), nf(5e-324)); + +// Huge values keep full integer digits, grouped — the shortest form's +// zeros for 1e23 (the exact expansion would end ...611,392). +console.log(nf(1e21), nf(1e23), nf(9007199254740993)); +console.log(nf(1.7976931348623157e308)); + +// The callable spelling constructs the same formatter. +console.log(Intl.NumberFormat("en-US").format(12345.678)); + +// Number.prototype.toLocaleString("en-US") is the same operation. +console.log((1234567.891).toLocaleString("en-US"), (0.0625).toLocaleString("en-US"), (-0).toLocaleString("en-US")); +const xs = [1234.5, -9876543.21, 0.5, 1e6]; +console.log(xs.map((x) => x.toLocaleString("en-US")).join("|")); + +// Composed results are ordinary strings. +const label = `total: ${nf(1234.5)} units`; +console.log(label, label.length, nf(42).padStart(8, ".")); diff --git a/tests/diagnostics/intl.ts b/tests/diagnostics/intl.ts new file mode 100644 index 000000000..b4adad8a6 --- /dev/null +++ b/tests/diagnostics/intl.ts @@ -0,0 +1,25 @@ +// The Intl surface. What lowers: the COMPOSED en-US NumberFormat forms +// with default options — new Intl.NumberFormat("en-US").format(x) and +// x.toLocaleString("en-US"). Everything else fences by NAME: the +// environment-default locale a compiled binary cannot carry, other +// locales (ICU data the binary does not embed), options bags, formatter +// VALUES, and the rest of the Intl namespace (DurationFormat, +// DateTimeFormat, PluralRules, getCanonicalLocales). + +// The unlowered NumberFormat variants, each with its own reason. +const noLocale = new Intl.NumberFormat().format(1234.5); +const otherLocale = new Intl.NumberFormat("de-DE").format(1234.5); +const withOptions = new Intl.NumberFormat("en-US", { maximumFractionDigits: 2 }).format(1234.5); +const hoisted = new Intl.NumberFormat("en-US"); +const viaValue = hoisted.format(1234.5); + +// toLocaleString's unlowered variants. +const envDefault = (1234.5).toLocaleString(); +const german = (1234.5).toLocaleString("de-DE"); +const grouped = (1234.5).toLocaleString("en-US", { useGrouping: false }); + +// The rest of Intl keeps the ICU story. +const when = new Intl.DateTimeFormat("en-US").format(); +const dur = new Intl.DurationFormat("en-US").format({ hours: 1, minutes: 23 }); +const rule = new Intl.PluralRules("en-US").select(1); +const canon = Intl.getCanonicalLocales("en-us"); diff --git a/tests/harness/__snapshots__/intl.ts.txt b/tests/harness/__snapshots__/intl.ts.txt new file mode 100644 index 000000000..72799c66e --- /dev/null +++ b/tests/harness/__snapshots__/intl.ts.txt @@ -0,0 +1,114 @@ +intl.ts:10:18 - error SC2020: 'Intl.NumberFormat without a locale' is part of the standard library types but has no scriptc lowering yet + + 9 | // The unlowered NumberFormat variants, each with its own reason. + 10 | const noLocale = new Intl.NumberFormat().format(1234.5); + | ^~~~~~~~~~~~~~~~~~~~~~~ + 11 | const otherLocale = new Intl.NumberFormat("de-DE").format(1234.5); + + hint: the default locale is the host environment's, which a compiled binary cannot carry — pass it explicitly: new Intl.NumberFormat("en-US").format(x) + +intl.ts:11:43 - error SC2020: 'Intl.NumberFormat at locale "de-DE"' is part of the standard library types but has no scriptc lowering yet + + 10 | const noLocale = new Intl.NumberFormat().format(1234.5); + 11 | const otherLocale = new Intl.NumberFormat("de-DE").format(1234.5); + | ^~~~~~~ + 12 | const withOptions = new Intl.NumberFormat("en-US", { maximumFractionDigits: 2 }).format(1234.5); + + hint: "en-US" (Node's default-build locale) is the one locale whose data the runtime embeds — everything else is ICU data the binary does not carry + +intl.ts:12:52 - error SC2020: 'Intl.NumberFormat with an options bag' is part of the standard library types but has no scriptc lowering yet + + 11 | const otherLocale = new Intl.NumberFormat("de-DE").format(1234.5); + 12 | const withOptions = new Intl.NumberFormat("en-US", { maximumFractionDigits: 2 }).format(1234.5); + | ^~~~~~~~~~~~~~~~~~~~~~~~~~~~ + 13 | const hoisted = new Intl.NumberFormat("en-US"); + + hint: the embedded data covers DEFAULT options only (decimal notation, up to 3 fraction digits, grouping) — new Intl.NumberFormat("en-US").format(x) + +intl.ts:13:7 - error SC2020: 'NumberFormat' is part of the standard library types but has no scriptc lowering yet + + 12 | const withOptions = new Intl.NumberFormat("en-US", { maximumFractionDigits: 2 }).format(1234.5); + 13 | const hoisted = new Intl.NumberFormat("en-US"); + | ^~~~~~~ + 14 | const viaValue = hoisted.format(1234.5); + + hint: Intl formatter values have no representation — the COMPOSED en-US forms lower: new Intl.NumberFormat("en-US").format(x) and x.toLocaleString("en-US") with default options; the rest is ICU locale data the binary does not carry + +intl.ts:13:17 - error SC1090: constructing values other than classes declared in the program is not supported yet + + 12 | const withOptions = new Intl.NumberFormat("en-US", { maximumFractionDigits: 2 }).format(1234.5); + 13 | const hoisted = new Intl.NumberFormat("en-US"); + | ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + 14 | const viaValue = hoisted.format(1234.5); + +intl.ts:14:18 - error SC2020: 'NumberFormat.format' is part of the standard library types but has no scriptc lowering yet + + 13 | const hoisted = new Intl.NumberFormat("en-US"); + 14 | const viaValue = hoisted.format(1234.5); + | ^~~~~~~~~~~~~~ + 15 | + + hint: locale- and ICU-backed behavior lives outside the static runtime (the localeCompare stance: code-unit order, no collation/locale data) — what lowers: the composed new Intl.NumberFormat("en-US").format(x) and x.toLocaleString("en-US") with default options; format with template literals, toFixed, and toString otherwise + +intl.ts:17:20 - error SC2020: 'Number.prototype.toLocaleString without a locale' is part of the standard library types but has no scriptc lowering yet + + 16 | // toLocaleString's unlowered variants. + 17 | const envDefault = (1234.5).toLocaleString(); + | ^~~~~~~~~~~~~~~~~~~~~~~~~ + 18 | const german = (1234.5).toLocaleString("de-DE"); + + hint: the default locale is the host environment's, which a compiled binary cannot carry — pass it explicitly: x.toLocaleString("en-US") + +intl.ts:18:40 - error SC2020: 'Number.prototype.toLocaleString at locale "de-DE"' is part of the standard library types but has no scriptc lowering yet + + 17 | const envDefault = (1234.5).toLocaleString(); + 18 | const german = (1234.5).toLocaleString("de-DE"); + | ^~~~~~~ + 19 | const grouped = (1234.5).toLocaleString("en-US", { useGrouping: false }); + + hint: "en-US" (Node's default-build locale) is the one locale whose data the runtime embeds — everything else is ICU data the binary does not carry + +intl.ts:19:17 - error SC2020: 'Number.prototype.toLocaleString with an options bag' is part of the standard library types but has no scriptc lowering yet + + 18 | const german = (1234.5).toLocaleString("de-DE"); + 19 | const grouped = (1234.5).toLocaleString("en-US", { useGrouping: false }); + | ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + 20 | + + hint: the embedded data covers DEFAULT options only (decimal notation, up to 3 fraction digits, grouping) — x.toLocaleString("en-US") + +intl.ts:22:14 - error SC2020: 'DateTimeFormat.format' is part of the standard library types but has no scriptc lowering yet + + 21 | // The rest of Intl keeps the ICU story. + 22 | const when = new Intl.DateTimeFormat("en-US").format(); + | ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + 23 | const dur = new Intl.DurationFormat("en-US").format({ hours: 1, minutes: 23 }); + + hint: locale- and ICU-backed behavior lives outside the static runtime (the localeCompare stance: code-unit order, no collation/locale data) — what lowers: the composed new Intl.NumberFormat("en-US").format(x) and x.toLocaleString("en-US") with default options; format with template literals, toFixed, and toString otherwise + +intl.ts:23:13 - error SC2020: 'DurationFormat.format' is part of the standard library types but has no scriptc lowering yet + + 22 | const when = new Intl.DateTimeFormat("en-US").format(); + 23 | const dur = new Intl.DurationFormat("en-US").format({ hours: 1, minutes: 23 }); + | ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + 24 | const rule = new Intl.PluralRules("en-US").select(1); + + hint: locale- and ICU-backed behavior lives outside the static runtime (the localeCompare stance: code-unit order, no collation/locale data) — what lowers: the composed new Intl.NumberFormat("en-US").format(x) and x.toLocaleString("en-US") with default options; format with template literals, toFixed, and toString otherwise + +intl.ts:24:14 - error SC2020: 'PluralRules.select' is part of the standard library types but has no scriptc lowering yet + + 23 | const dur = new Intl.DurationFormat("en-US").format({ hours: 1, minutes: 23 }); + 24 | const rule = new Intl.PluralRules("en-US").select(1); + | ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + 25 | const canon = Intl.getCanonicalLocales("en-us"); + + hint: locale- and ICU-backed behavior lives outside the static runtime (the localeCompare stance: code-unit order, no collation/locale data) — what lowers: the composed new Intl.NumberFormat("en-US").format(x) and x.toLocaleString("en-US") with default options; format with template literals, toFixed, and toString otherwise + +intl.ts:25:15 - error SC2020: 'Intl.getCanonicalLocales' is part of the standard library types but has no scriptc lowering yet + + 24 | const rule = new Intl.PluralRules("en-US").select(1); + 25 | const canon = Intl.getCanonicalLocales("en-us"); + | ^~~~~~~~~~~~~~~~~~~~~~~~ + 26 | + + hint: locale- and ICU-backed behavior lives outside the static runtime (the localeCompare stance: code-unit order, no collation/locale data) — what lowers: the composed new Intl.NumberFormat("en-US").format(x) and x.toLocaleString("en-US") with default options; format with template literals, toFixed, and toString otherwise \ No newline at end of file diff --git a/tests/harness/__snapshots__/stdlib-fence.ts.txt b/tests/harness/__snapshots__/stdlib-fence.ts.txt index ca16920a4..0ae99da92 100644 --- a/tests/harness/__snapshots__/stdlib-fence.ts.txt +++ b/tests/harness/__snapshots__/stdlib-fence.ts.txt @@ -48,7 +48,7 @@ stdlib-fence.ts:20:7 - error SC2020: 'NumberFormat' is part of the standard libr | ^~~ 21 | - hint: the type checker sees the full standard library, but only the supported surface compiles (https://scriptc.dev/limitations) + hint: Intl formatter values have no representation — the COMPOSED en-US forms lower: new Intl.NumberFormat("en-US").format(x) and x.toLocaleString("en-US") with default options; the rest is ICU locale data the binary does not carry stdlib-fence.ts:20:13 - error SC1090: constructing values other than classes declared in the program is not supported yet @@ -210,14 +210,14 @@ stdlib-fence.ts:42:17 - error SC2020: '.split with 2 arguments on strings' is pa hint: the type checker sees the full standard library, but only the supported surface compiles (https://scriptc.dev/limitations) -stdlib-fence.ts:44:19 - error SC2020: 'number.toLocaleString' is part of the standard library types but has no scriptc lowering yet +stdlib-fence.ts:44:19 - error SC2020: 'Number.prototype.toLocaleString without a locale' is part of the standard library types but has no scriptc lowering yet 43 | const fixed = (1.5).toFixed(); // digit-free toFixed lowers now (the static ties-up integer) 44 | const localized = (1234.5).toLocaleString(); - | ^~~~~~~~~~~~~~~~~~~~~~~ + | ^~~~~~~~~~~~~~~~~~~~~~~~~ 45 | - hint: locale- and ICU-backed behavior lives outside the static runtime (the localeCompare stance: code-unit order, no collation/locale data) — format with template literals, toFixed, and toString instead + hint: the default locale is the host environment's, which a compiled binary cannot carry — pass it explicitly: x.toLocaleString("en-US") stdlib-fence.ts:49:15 - error SC2020: '.indexOf with 2 arguments' is part of the standard library types but has no scriptc lowering yet From 8752ed311721bbbc3822da2c80ad19fe8d4e93ff Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Thu, 23 Jul 2026 20:52:10 -0500 Subject: [PATCH 4/4] Record order-parity baselines for the new stdlib fixtures MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Adds the ts7 canary entries for 2560-2562 and the intl diagnostics fixture, plus the missing baselines for the pre-existing 2510-2513 and 2520-2523 fixtures the re-record picked up — purely additive, nothing removed or changed. --- .../test/ts7/baselines/order-parity.json | 156 +++++++++++++----- 1 file changed, 114 insertions(+), 42 deletions(-) diff --git a/packages/compiler/test/ts7/baselines/order-parity.json b/packages/compiler/test/ts7/baselines/order-parity.json index 6f8bd2cb8..d513df035 100644 --- a/packages/compiler/test/ts7/baselines/order-parity.json +++ b/packages/compiler/test/ts7/baselines/order-parity.json @@ -4574,6 +4574,54 @@ ], "diags": [] }, + "/tests/corpus/2510-dyn-evolving-array-map.ts": { + "order": [ + "/tests/corpus/2510-dyn-evolving-array-map.ts" + ], + "diags": [] + }, + "/tests/corpus/2511-dyn-evolving-array-filter-foreach.ts": { + "order": [ + "/tests/corpus/2511-dyn-evolving-array-filter-foreach.ts" + ], + "diags": [] + }, + "/tests/corpus/2512-dyn-evolving-array-mixed-push.ts": { + "order": [ + "/tests/corpus/2512-dyn-evolving-array-mixed-push.ts" + ], + "diags": [] + }, + "/tests/corpus/2513-dyn-evolving-array-derived.ts": { + "order": [ + "/tests/corpus/2513-dyn-evolving-array-derived.ts" + ], + "diags": [] + }, + "/tests/corpus/2520-private-statics-class-name-calls.ts": { + "order": [ + "/tests/corpus/2520-private-statics-class-name-calls.ts" + ], + "diags": [] + }, + "/tests/corpus/2521-private-statics-class-expression.ts": { + "order": [ + "/tests/corpus/2521-private-statics-class-expression.ts" + ], + "diags": [] + }, + "/tests/corpus/2522-private-statics-decorated.ts": { + "order": [ + "/tests/corpus/2522-private-statics-decorated.ts" + ], + "diags": [] + }, + "/tests/corpus/2523-private-statics-aliased-class.ts": { + "order": [ + "/tests/corpus/2523-private-statics-aliased-class.ts" + ], + "diags": [] + }, "/tests/corpus/2530-object-destructuring-decl.ts": { "order": [ "/tests/corpus/2530-object-destructuring-decl.ts" @@ -4682,6 +4730,24 @@ ], "diags": [] }, + "/tests/corpus/2560-arraybuffer-dataview-set.ts": { + "order": [ + "/tests/corpus/2560-arraybuffer-dataview-set.ts" + ], + "diags": [] + }, + "/tests/corpus/2561-object-is.ts": { + "order": [ + "/tests/corpus/2561-object-is.ts" + ], + "diags": [] + }, + "/tests/corpus/2562-intl-numberformat-en-us.ts": { + "order": [ + "/tests/corpus/2562-intl-numberformat-en-us.ts" + ], + "diags": [] + }, "/tests/corpus/300-if-else.ts": { "order": [ "/tests/corpus/300-if-else.ts" @@ -6210,6 +6276,12 @@ ], "diags": [] }, + "/tests/diagnostics/intl.ts": { + "order": [ + "/tests/diagnostics/intl.ts" + ], + "diags": [] + }, "/tests/diagnostics/island-boundary-slots.ts": { "order": [ "/tests/diagnostics/island-boundary-slots.ts" @@ -6554,48 +6626,6 @@ ], "diags": [] }, - "/tests/fixtures/node-types/argv-env.ts": { - "order": [ - "/tests/fixtures/node-types/argv-env.ts" - ], - "diags": [] - }, - "/tests/fixtures/node-types/fenced.ts": { - "order": [ - "/tests/fixtures/node-types/fenced.ts" - ], - "diags": [] - }, - "/tests/fixtures/node-types/import-fences.ts": { - "order": [ - "/tests/fixtures/node-types/import-fences.ts" - ], - "diags": [ - { - "code": "SC1010", - "message": "the 'v8' module is not supported yet", - "loc": { - "file": "/tests/fixtures/node-types/import-fences.ts", - "start": 422, - "end": 461 - }, - "milestone": "M4", - "hint": "relative imports (./file, ../dir/file), package.json-mediated project imports (#alias via the imports field, self-name references via exports), installed npm packages (their code runs under --dynamic), and the built-in fs, fs/promises, path, os, url, crypto, zlib, child_process, net, http, tls, https, http2, dgram, dns, util, util/types, string_decoder, readline, events, stream, buffer, assert, assert/strict, worker_threads, cluster, tty, async_hooks, timers, timers/promises, diagnostics_channel, perf_hooks, and node:test and node:module modules (bare or node:-prefixed) are supported" - } - ] - }, - "/tests/fixtures/node-types/path-os.ts": { - "order": [ - "/tests/fixtures/node-types/path-os.ts" - ], - "diags": [] - }, - "/tests/fixtures/node-types/stream-capture.ts": { - "order": [ - "/tests/fixtures/node-types/stream-capture.ts" - ], - "diags": [] - }, "/tests/fixtures/npm/cases/2465-getter-table/main.ts": { "order": [ "/tests/fixtures/npm/cases/2465-getter-table/main.ts" @@ -6993,6 +7023,48 @@ "hint": "set \"strictNullChecks\": true (or \"strict\": true) in the project tsconfig" } ] + }, + "/tests/fixtures/node-types/argv-env.ts": { + "order": [ + "/tests/fixtures/node-types/argv-env.ts" + ], + "diags": [] + }, + "/tests/fixtures/node-types/fenced.ts": { + "order": [ + "/tests/fixtures/node-types/fenced.ts" + ], + "diags": [] + }, + "/tests/fixtures/node-types/import-fences.ts": { + "order": [ + "/tests/fixtures/node-types/import-fences.ts" + ], + "diags": [ + { + "code": "SC1010", + "message": "the 'v8' module is not supported yet", + "loc": { + "file": "/tests/fixtures/node-types/import-fences.ts", + "start": 422, + "end": 461 + }, + "milestone": "M4", + "hint": "relative imports (./file, ../dir/file), package.json-mediated project imports (#alias via the imports field, self-name references via exports), installed npm packages (their code runs under --dynamic), and the built-in fs, fs/promises, path, os, url, crypto, zlib, child_process, net, http, tls, https, http2, dgram, dns, util, util/types, string_decoder, readline, events, stream, buffer, assert, assert/strict, worker_threads, cluster, tty, async_hooks, timers, timers/promises, diagnostics_channel, perf_hooks, and node:test and node:module modules (bare or node:-prefixed) are supported" + } + ] + }, + "/tests/fixtures/node-types/path-os.ts": { + "order": [ + "/tests/fixtures/node-types/path-os.ts" + ], + "diags": [] + }, + "/tests/fixtures/node-types/stream-capture.ts": { + "order": [ + "/tests/fixtures/node-types/stream-capture.ts" + ], + "diags": [] } } }