From 4610fe32be43fdcfdaec9bafb880994813d01a57 Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Fri, 30 Oct 2026 06:30:40 -0500 Subject: [PATCH] Read SharedArrayBuffer, DataView, BigInt64Array, BigUint64Array, Proxy and Intl as values SharedArrayBuffer, DataView, BigInt64Array, BigUint64Array and Proxy read as values are interned constructor values with Node's name and length. Calling one throws Node's TypeError "Constructor X requires 'new'". instanceof through a stored value answers as Node does: SharedArrayBuffer and DataView test the value's kind, BigInt64Array and BigUint64Array answer false (neither is constructed), and Proxy answers false for a primitive and throws Node's TypeError for an object. Intl read as a value is a namespace object with Node's 12 members in Node's order. Its members are function values that throw the SC1090 fence when called; direct calls such as new Intl.NumberFormat("en-US") keep their lowerings. TypeScript sources read the same values into any and unknown slots, so the diagnostics program uses Atomics for a builtin without a value. --- .../frontend/lowering/builtin-namespaces.ts | 21 ++++++++++-- .../lowering/expressions/property-access.ts | 10 +++++- .../builtin-values-proxy-intl-views.cjs.json | 6 ++++ packages/runtime/src/scr_dyn_invoke.c | 32 ++++++++++++++---- packages/runtime/src/scr_json.c | 16 +++++++++ .../builtin-values-proxy-intl-views.cjs | 33 +++++++++++++++++++ tests/corpus/builtin-values-untyped-slots.ts | 11 +++++++ .../builtin-values-without-lowering.ts | 2 +- .../builtin-values-without-lowering.ts.txt | 14 ++++---- .../builtin-values-proxy-intl-views.cjs.json | 1 + 10 files changed, 128 insertions(+), 18 deletions(-) create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/builtin-values-proxy-intl-views.cjs.json create mode 100644 tests/corpus/builtin-values-proxy-intl-views.cjs create mode 100644 tests/harness/corpus-costs/builtin-values-proxy-intl-views.cjs.json diff --git a/packages/compiler/src/frontend/lowering/builtin-namespaces.ts b/packages/compiler/src/frontend/lowering/builtin-namespaces.ts index 07c6bda5b..991e40657 100644 --- a/packages/compiler/src/frontend/lowering/builtin-namespaces.ts +++ b/packages/compiler/src/frontend/lowering/builtin-namespaces.ts @@ -1,4 +1,4 @@ -/** The standard namespace objects (`Math`, `JSON`, `Reflect`) read as +/** The standard namespace objects (`Math`, `JSON`, `Reflect`, `Intl`) read as * JavaScript values. Each is one object per program, built on first use, * with Node's own properties in Node's order: the functions are writable, * configurable and non-enumerable, the Math constants are read-only and @@ -106,6 +106,21 @@ function namespaceMembers(name: string): readonly NamespaceMember[] | null { { key: "set", length: 3 }, { key: "setPrototypeOf", length: 2 }, ]; + case "Intl": + return [ + { key: "getCanonicalLocales", length: 1 }, + { key: "supportedValuesOf", length: 1 }, + { key: "DateTimeFormat", length: 0 }, + { key: "NumberFormat", length: 0 }, + { key: "Collator", length: 0 }, + { key: "PluralRules", length: 0 }, + { key: "RelativeTimeFormat", length: 0 }, + { key: "ListFormat", length: 0 }, + { key: "Locale", length: 1 }, + { key: "DisplayNames", length: 2 }, + { key: "Segmenter", length: 0 }, + { key: "DurationFormat", length: 0 }, + ]; default: return null; } @@ -270,7 +285,9 @@ function functionValue(lowerer: Lowerer, canonical: string, key: string, loc: Sr ? mathMemberValue(lowerer, key, loc) : canonical === "JSON" ? jsonMemberValue(lowerer, key, loc) - : reflectMemberValue(lowerer, key, loc); + : canonical === "Reflect" + ? reflectMemberValue(lowerer, key, loc) + : null; if (value !== null) return value.type.kind === "dyn" ? value diff --git a/packages/compiler/src/frontend/lowering/expressions/property-access.ts b/packages/compiler/src/frontend/lowering/expressions/property-access.ts index 3b617c9fe..9daeaf83a 100644 --- a/packages/compiler/src/frontend/lowering/expressions/property-access.ts +++ b/packages/compiler/src/frontend/lowering/expressions/property-access.ts @@ -4993,7 +4993,10 @@ const ERROR_CONSTRUCTOR_VALUES: ReadonlySet = new Set([ * constructor, and dynamic values answer `.constructor` with the same one, * so `o.constructor === Object` and `err.constructor === TypeError` are * exact identity tests, and `err.constructor.name` is the constructor's - * name. */ + * name. SharedArrayBuffer, DataView, BigInt64Array, BigUint64Array and + * Proxy have values with Node's name, length and instanceof answers; + * calling one throws Node's TypeError, and constructing through the value + * is fenced. */ const GLOBAL_CONSTRUCTOR_VALUES: ReadonlySet = new Set([ "Object", "Function", @@ -5005,6 +5008,11 @@ const GLOBAL_CONSTRUCTOR_VALUES: ReadonlySet = new Set([ "WeakMap", "WeakSet", "FinalizationRegistry", + "SharedArrayBuffer", + "DataView", + "BigInt64Array", + "BigUint64Array", + "Proxy", ...ERROR_CONSTRUCTOR_VALUES, ]); diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/builtin-values-proxy-intl-views.cjs.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/builtin-values-proxy-intl-views.cjs.json new file mode 100644 index 000000000..c4a1cf629 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/builtin-values-proxy-intl-views.cjs.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/builtin-values-proxy-intl-views.cjs" + ], + "diags": [] +} diff --git a/packages/runtime/src/scr_dyn_invoke.c b/packages/runtime/src/scr_dyn_invoke.c index 0279ccab1..8ac5bfc2d 100644 --- a/packages/runtime/src/scr_dyn_invoke.c +++ b/packages/runtime/src/scr_dyn_invoke.c @@ -1959,7 +1959,9 @@ ScrDyn *scr_dyn_builtin_method(const ScrStr *prototype, const ScrStr *method) { * answer `.constructor` on the matching dynamic values, so * `o.constructor === Object` and `a.constructor === b.constructor` are * exact identity tests. `Object(v)` works through the stored value; calling - * the others through it is fenced. */ + * the others through it is fenced. SharedArrayBuffer, DataView, + * BigInt64Array, BigUint64Array and Proxy are interned the same way; + * calling one throws Node's "Constructor X requires 'new'" TypeError. */ /* String constants live in named arrays in their own section rather than * in the unit's merged string pool (the explicit section also keeps the * compiler from folding the unit's existing literals into them), so @@ -2012,11 +2014,23 @@ static const char dyn_s38[] SCR_DYN_COMPAT_STR = "structuredClone"; static const char dyn_s39[] SCR_DYN_COMPAT_STR = "The \"input\" argument must be specified"; static const char dyn_s40[] SCR_DYN_COMPAT_STR = "The \"The value argument must be specified\" argument must be specified"; static const char dyn_s41[] SCR_DYN_COMPAT_STR = "ERR_MISSING_ARGS"; +static const char dyn_s42[] SCR_DYN_COMPAT_STR = "SharedArrayBuffer"; +static const char dyn_s43[] SCR_DYN_COMPAT_STR = "DataView"; +static const char dyn_s44[] SCR_DYN_COMPAT_STR = "BigInt64Array"; +static const char dyn_s45[] SCR_DYN_COMPAT_STR = "BigUint64Array"; +static const char dyn_s46[] SCR_DYN_COMPAT_STR = "Proxy"; +static const char dyn_s47[] SCR_DYN_COMPAT_STR = "Constructor %s requires 'new'"; static const char *const dyn_global_ctor_names[] = { dyn_s26, dyn_s29, dyn_s25, dyn_s30, dyn_s31, dyn_s21, dyn_s22, dyn_s24, dyn_s35, + dyn_s42, dyn_s43, dyn_s44, dyn_s45, dyn_s46, }; #define DYN_GLOBAL_CTORS (sizeof dyn_global_ctor_names / sizeof dyn_global_ctor_names[0]) +/* Each constructor's own length, indexed like dyn_global_ctor_names. */ +static const uint8_t dyn_global_ctor_lengths[DYN_GLOBAL_CTORS] = {1, 7, 2, 0, 0, 1, 0, 0, 1, 1, 1, 3, 3, 2}; +/* The first of the constructors that only `new` builds + * (SharedArrayBuffer onward): calling one throws Node's TypeError. */ +#define DYN_GLOBAL_NEW_ONLY 9 static SCR_TL ScrDyn *dyn_global_ctors[DYN_GLOBAL_CTORS]; static SCR_TL bool dyn_global_ctors_registered; @@ -2029,6 +2043,13 @@ static void dyn_global_ctors_cleanup(void) { static ScrDyn *dyn_global_ctor_call(ScrClosure *closure, ScrDyn *const *args, size_t argc) { const char *name = closure->fn; + for (size_t i = DYN_GLOBAL_NEW_ONLY; i < DYN_GLOBAL_CTORS; i++) { + if (name != dyn_global_ctor_names[i]) continue; + char message[64]; + int length = snprintf(message, sizeof message, dyn_s47, name); + scr_throw_error_msg(SCR_ERR_TYPE, message, (size_t)length); + return NULL; + } bool object = strcmp(name, dyn_s26) == 0; if (object) { const ScrDyn *value = argc ? args[0] : scr_dyn_undefined(); @@ -2046,8 +2067,8 @@ static ScrDyn *dyn_global_ctor_call(ScrClosure *closure, ScrDyn *const *args, si } /* `new C(...args)` through a stored Object, Date, Map, Set, WeakMap or - * WeakSet builds what the `new` expression builds. RegExp, Promise, and - * Object over a primitive (a wrapper object) are fenced. */ + * WeakSet builds what the `new` expression builds. The other constructors, + * and Object over a primitive (a wrapper object), are fenced. */ static bool dyn_global_construct(const ScrDyn *callee, const ScrDyn *args, ScrDyn **result) { if (callee->kind != SCR_DYN_FUNC || callee->v.fn.thunk != dyn_global_ctor_call) return false; const char *name = callee->v.fn.clo->fn; @@ -2083,11 +2104,8 @@ static ScrDyn *dyn_global_ctor(const char *name) { scr_dyn_global_construct_fn = dyn_global_construct; } if (!dyn_global_ctors[i]) { - /* Each constructor's own length: Object 1, Date 7, RegExp 2, Promise 1, - * the collections 0, FinalizationRegistry 1. */ - uint32_t length = i == 0 || i == 5 || i == 8 ? 1 : i == 1 ? 7 : i == 2 ? 2 : 0; dyn_global_ctors[i] = scr_dyn_new_func(scr_closure_new((void *)dyn_global_ctor_names[i], 0), - dyn_global_ctor_call, length, dyn_s6, + dyn_global_ctor_call, dyn_global_ctor_lengths[i], dyn_s6, dyn_global_ctor_names[i]); /* `o.constructor.prototype` of a plain object is Object.prototype * (zod's isPlainObject). The other prototypes are not modeled. */ diff --git a/packages/runtime/src/scr_json.c b/packages/runtime/src/scr_json.c index 311596597..2668278c5 100644 --- a/packages/runtime/src/scr_json.c +++ b/packages/runtime/src/scr_json.c @@ -5538,10 +5538,26 @@ static int scr_dyn_global_constructor_instanceof(const ScrDyn *value, const ScrD return -1; /* A native Date, or an instance of a class declared `extends Date`. */ if (strcmp(callee->v.fn.name, "Date") == 0) return scr_dyn_date_instance_is(value); + if (strcmp(callee->v.fn.name, "DataView") == 0) return scr_dyn_data_view_is(value); + /* No value is a BigInt64Array or BigUint64Array: neither is constructed. */ + if (strcmp(callee->v.fn.name, "BigInt64Array") == 0 || strcmp(callee->v.fn.name, "BigUint64Array") == 0) + return 0; + /* Proxy has no prototype: false for a primitive, Node's TypeError for an + * object. */ + if (strcmp(callee->v.fn.name, "Proxy") == 0) { + if (value->kind == SCR_DYN_UNDEF || value->kind == SCR_DYN_NULL || value->kind == SCR_DYN_BOOL || + value->kind == SCR_DYN_NUM || value->kind == SCR_DYN_STR || value->kind == SCR_DYN_BIGINT || + value->kind == SCR_DYN_SYMBOL) + return 0; + static const char message[] = "Function has non-object prototype 'undefined' in instanceof check"; + scr_throw_error_msg(SCR_ERR_TYPE, message, sizeof message - 1); + return 0; + } static const struct { const char *name; int tag; } kinds[] = { {"RegExp", SCR_DYNH_REGEXP}, {"Map", SCR_DYNH_MAP}, {"Set", SCR_DYNH_SET}, {"WeakMap", SCR_DYNH_WEAK_MAP}, {"WeakSet", SCR_DYNH_WEAK_SET}, {"FinalizationRegistry", SCR_DYNH_FINALIZATION_REGISTRY}, + {"SharedArrayBuffer", SCR_DYNH_SHARED_ARRAY_BUFFER}, }; bool promise = strcmp(callee->v.fn.name, "Promise") == 0; int tag = -1; diff --git a/tests/corpus/builtin-values-proxy-intl-views.cjs b/tests/corpus/builtin-values-proxy-intl-views.cjs new file mode 100644 index 000000000..2cde73bb4 --- /dev/null +++ b/tests/corpus/builtin-values-proxy-intl-views.cjs @@ -0,0 +1,33 @@ +// SharedArrayBuffer, DataView, BigInt64Array, BigUint64Array, Proxy and Intl +// read as values hold Node's functions and namespace object. +function attempt(label, run) { + try { + console.log(label, run()); + } catch (error) { + console.log(label, error.name, error.message); + } +} + +const constructors = { SharedArrayBuffer, DataView, BigInt64Array, BigUint64Array, Proxy }; +for (const key of Object.keys(constructors)) { + const value = constructors[key]; + console.log(key, typeof value, value.name, value.length, value === globalThis[key]); + attempt(`call ${key}`, () => value()); +} +console.log(constructors); +console.log(String(DataView), Object.prototype.toString.call(constructors.Proxy)); + +const shared = new SharedArrayBuffer(8); +const view = new DataView(new ArrayBuffer(4)); +const stored = { shared: SharedArrayBuffer, view: DataView, proxy: Proxy, big: BigInt64Array }; +console.log(shared instanceof stored.shared, new ArrayBuffer(1) instanceof stored.shared); +console.log(view instanceof stored.view, new Uint8Array(4) instanceof stored.view); +console.log(view instanceof stored.big, JSON.parse("1") instanceof stored.proxy); +attempt("instanceof Proxy", () => ({}) instanceof stored.proxy); + +const intl = Intl; +console.log(typeof intl, intl === globalThis.Intl, Object.keys(intl).length, "NumberFormat" in intl); +console.log(Object.getOwnPropertyNames(intl).join(",")); +console.log(Object.prototype.toString.call(intl), intl[Symbol.toStringTag]); +console.log(typeof intl.DateTimeFormat, intl.Locale.length, intl.DisplayNames.length); +console.log(new Intl.NumberFormat("en-US").format(1234.5)); diff --git a/tests/corpus/builtin-values-untyped-slots.ts b/tests/corpus/builtin-values-untyped-slots.ts index 4185a524e..c32857ef6 100644 --- a/tests/corpus/builtin-values-untyped-slots.ts +++ b/tests/corpus/builtin-values-untyped-slots.ts @@ -51,3 +51,14 @@ console.log( Object.keys(math).length, Object.getOwnPropertyNames(json).join(","), ); + +const views: any = { intl: Intl, proxy: Proxy, view: DataView, shared: SharedArrayBuffer }; +console.log(typeof views.intl, Object.getOwnPropertyNames(views.intl).length); +console.log(Object.prototype.toString.call(views.intl), views.intl.Locale.length); +console.log(views.proxy.name, views.proxy.length, views.view.name, views.shared.length); +try { + views.proxy(); +} catch (error) { + console.log((error as Error).message); +} +console.log(views); diff --git a/tests/diagnostics/builtin-values-without-lowering.ts b/tests/diagnostics/builtin-values-without-lowering.ts index b8b5c90a1..6ea93c949 100644 --- a/tests/diagnostics/builtin-values-without-lowering.ts +++ b/tests/diagnostics/builtin-values-without-lowering.ts @@ -5,7 +5,7 @@ function take(x: unknown): unknown { } const inferred = { json: JSON }; -const loose: any = { intl: Intl, proxy: Proxy }; +const loose: any = { atomics: Atomics }; const timers: Record = { setTimeout }; const list: any[] = [Array, parseInt]; console.log(inferred, loose, timers, list, take(process)); diff --git a/tests/harness/__snapshots__/builtin-values-without-lowering.ts.txt b/tests/harness/__snapshots__/builtin-values-without-lowering.ts.txt index 418eaedef..466015d88 100644 --- a/tests/harness/__snapshots__/builtin-values-without-lowering.ts.txt +++ b/tests/harness/__snapshots__/builtin-values-without-lowering.ts.txt @@ -3,7 +3,7 @@ builtin-values-without-lowering.ts:7:7 - error SC2009: values of type '{ json: J 6 | 7 | const inferred = { json: JSON }; | ^~~~~~~~ - 8 | const loose: any = { intl: Intl, proxy: Proxy }; + 8 | const loose: any = { atomics: Atomics }; 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') @@ -12,22 +12,22 @@ builtin-values-without-lowering.ts:7:18 - error SC2009: values of type '{ json: 6 | 7 | const inferred = { json: JSON }; | ^~~~~~~~~~~~~~ - 8 | const loose: any = { intl: Intl, proxy: Proxy }; + 8 | const loose: any = { atomics: Atomics }; 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') -builtin-values-without-lowering.ts:8:28 - error SC2020: 'Intl' is part of the standard library types but has no scriptc lowering yet +builtin-values-without-lowering.ts:8:31 - error SC2020: 'Atomics' is part of the standard library types but has no scriptc lowering yet 7 | const inferred = { json: JSON }; - 8 | const loose: any = { intl: Intl, proxy: Proxy }; - | ^~~~ + 8 | const loose: any = { atomics: Atomics }; + | ^~~~~~~ 9 | const timers: Record = { setTimeout }; - hint: native Intl supports Intl.Segmenter with grapheme or word granularity and the composed new Intl.NumberFormat("en-US").format(x), x.toLocaleString("en-US") and x.toLocaleString(); formatter objects, locale negotiation and the other Intl constructors remain unsupported + hint: the type checker sees the full standard library, but only the supported surface compiles (https://scriptc.dev/limitations) builtin-values-without-lowering.ts:9:43 - error SC2020: 'setTimeout' is part of the standard library types but has no scriptc lowering yet - 8 | const loose: any = { intl: Intl, proxy: Proxy }; + 8 | const loose: any = { atomics: Atomics }; 9 | const timers: Record = { setTimeout }; | ^~~~~~~~~~ 10 | const list: any[] = [Array, parseInt]; diff --git a/tests/harness/corpus-costs/builtin-values-proxy-intl-views.cjs.json b/tests/harness/corpus-costs/builtin-values-proxy-intl-views.cjs.json new file mode 100644 index 000000000..5c169787d --- /dev/null +++ b/tests/harness/corpus-costs/builtin-values-proxy-intl-views.cjs.json @@ -0,0 +1 @@ +{ "plain": 4800, "san": 7200 }