diff --git a/packages/compiler/src/frontend/lowering/assertion-identity.ts b/packages/compiler/src/frontend/lowering/assertion-identity.ts new file mode 100644 index 000000000..6a00b7afe --- /dev/null +++ b/packages/compiler/src/frontend/lowering/assertion-identity.ts @@ -0,0 +1,78 @@ +/* Type assertions of the global object. + * + * `globalThis as { [KEY]?: State }` is the process-global state idiom: a + * module asserts the global object to a record type and reads and writes + * its members, and every other module that asserts it sees the same + * members. Node evaluates the assertion to the global object itself. + * + * The global object is a checked-dynamic value, and a checked-dynamic value + * asserted to a record or array type takes the checked extraction + * (dynCheck), which builds a new native value. For the global object that + * copy is always wrong: a write through it never reached the global object, + * and a view bound before another module's write never saw it. So an + * assertion of the global object keeps the checked-dynamic value itself: a + * view whose member reads and writes go to the global object, and bindings + * of it hold that object. + * + * Other checked-dynamic operands keep the checked extraction: a native value + * held as `unknown` unwraps by identity there, and a checked-dynamic tree is + * copied at the documented checked-value boundary. + */ +import * as ts from "../ts7/adapter.js"; +import type { IrType } from "../../ir/ir.js"; +import type { Lowerer } from "./lowerer.js"; +import { stdlibGlobalNameOf } from "./surfaces.js"; + +/** A target whose checked extraction builds a new mutable value. */ +export function extractionCopiesReference(lowerer: Lowerer, type: IrType): boolean { + const seen = new Set(); + const copies = (t: IrType): boolean => { + if (t.kind === "record" || t.kind === "array") return true; + if (t.kind !== "union" || seen.has(t.unionId)) return false; + seen.add(t.unionId); + return lowerer.unions.get(t.unionId)?.arms.some(copies) ?? false; + }; + return copies(type); +} + +/** The asserted expression without parentheses, `!` and nested + * assertions (`globalThis as unknown as T`). */ +function assertedSource(expr: ts.Expression): ts.Expression { + let e = expr; + while ( + ts.isParenthesizedExpression(e) || + ts.isNonNullExpression(e) || + ts.isAsExpression(e) || + ts.isTypeAssertion(e) || + ts.isSatisfiesExpression(e) + ) + e = e.expression; + return e; +} + +/** `expr` asserts the global object (`globalThis`, `global`). */ +export function assertsGlobalObject( + lowerer: Lowerer, + expr: ts.AsExpression | ts.TypeAssertion, +): boolean { + const name = stdlibGlobalNameOf(lowerer, assertedSource(expr.expression)); + return name === "globalThis" || name === "global"; +} + +/** The assertion a binding initializer is, when it keeps the global object: + * bindings of it hold that object rather than a native value of the + * asserted type. A target with no native layout keeps the object as well + * (the assertion lowers to the checked-dynamic value either way). */ +export function keptIdentityAssertion( + lowerer: Lowerer, + initializer: ts.Expression, +): ts.AsExpression | ts.TypeAssertion | undefined { + let e = initializer; + while (ts.isParenthesizedExpression(e)) e = e.expression; + if (!ts.isAsExpression(e) && !ts.isTypeAssertion(e)) return undefined; + if (!assertsGlobalObject(lowerer, e)) return undefined; + const target = lowerer.mapTypeOf(lowerer.checker.getTypeFromTypeNode(e.type)); + return !target || target.kind === "dyn" || extractionCopiesReference(lowerer, target) + ? e + : undefined; +} diff --git a/packages/compiler/src/frontend/lowering/divergences.ts b/packages/compiler/src/frontend/lowering/divergences.ts index 40e1d9073..37ede2f7e 100644 --- a/packages/compiler/src/frontend/lowering/divergences.ts +++ b/packages/compiler/src/frontend/lowering/divergences.ts @@ -23,6 +23,7 @@ import { } from "../../diagnostics/diagnostic.js"; import { isCollationCovered } from "./collation-repertoire.js"; import type { Lowerer } from "./lowerer.js"; +import { keptIdentityAssertion } from "./assertion-identity.js"; /** How an identity-sensitive binding is used after a copy could replace it. */ interface IdentityUse { @@ -217,6 +218,8 @@ export function scanDivergences( } for (const { decl, source, type } of castDecls) { + // An assertion of the global object keeps that object: no copy. + if (keptIdentityAssertion(lowerer, decl.initializer!) !== undefined) continue; const name = (decl.name as ts.Identifier).text; const scope = enclosingScope(decl); if (scope === undefined) continue; diff --git a/packages/compiler/src/frontend/lowering/lower-exprs.ts b/packages/compiler/src/frontend/lowering/lower-exprs.ts index 18f17f390..055bda67c 100644 --- a/packages/compiler/src/frontend/lowering/lower-exprs.ts +++ b/packages/compiler/src/frontend/lowering/lower-exprs.ts @@ -148,6 +148,7 @@ import { unsupportedDiag, } from "../../diagnostics/diagnostic.js"; import { PoisonError, dynFallbackType, jsFuncNameOf, neverTaintedJsType, own } from "./lowerer.js"; +import { assertsGlobalObject, extractionCopiesReference } from "./assertion-identity.js"; import { lowerCollectionSpread } from "./containers/collection-methods.js"; import { lowerHelperReturnCall } from "./containers/iterator-helpers.js"; import { @@ -9624,6 +9625,11 @@ export function lowerAsExpression( return inner; } } + // The global object: the checked extraction would build a separate + // value, so the assertion keeps the object and its member operations + // reach it (assertion-identity.ts). + if (extractionCopiesReference(lowerer, target) && assertsGlobalObject(lowerer, expr)) + return inner; if ( target.kind === "void" || !canDynCheckTo( diff --git a/packages/compiler/src/frontend/lowering/lower-modules.ts b/packages/compiler/src/frontend/lowering/lower-modules.ts index 0580b06ff..4c6f155de 100644 --- a/packages/compiler/src/frontend/lowering/lower-modules.ts +++ b/packages/compiler/src/frontend/lowering/lower-modules.ts @@ -145,6 +145,7 @@ import { workerTargetPaths } from "../worker-target.js"; import { isNativeProxyInitializer } from "./expressions/native-proxy.js"; import { handleEventReaderPrelude } from "./lower-event-target.js"; import { jsHoldsDynObject } from "./lower-exprs.js"; +import { keptIdentityAssertion } from "./assertion-identity.js"; /** One file's declarations, split for collection and init-body lowering. */ export interface FileParts { @@ -1972,6 +1973,30 @@ export function collectGlobals( } continue; } + // A file-scope binding of an assertion of the global object + // (`const state = globalThis as { [KEY]?: State }`): the binding + // holds that object, so writes through it reach the object other + // modules read (assertion-identity.ts). + const asserted = decl.initializer + ? keptIdentityAssertion(lowerer, decl.initializer) + : undefined; + if (nameNode === decl.name && !decl.type && asserted) { + const symbol = lowerer.checker.getSymbolAtLocation(nameNode); + if (symbol && !lowerer.globalsBySymbol.has(symbol)) { + const g: IrGlobal = { + id: `%g.${tag}${nsPrefix}${nameNode.text}`, + name: nameNode.text, + type: DYN, + mutable: isLet, + source: bindingSource(nameNode), + ...(!isVarDeclared(decl) ? { tdz: true as const } : {}), + }; + lowerer.globalsBySymbol.set(symbol, g); + lowerer.globalsList.push(g); + if (isVarDeclared(decl)) noteVarGlobalEntryInit(lowerer, sf, g); + } + continue; + } // A JS file-scope evolving ARRAY (`const mustCallChecks = [];` // — test/common's exit-accounting ledger): the strict type // (any[]) has no mapping, but the VALUE is the dyn array the diff --git a/packages/compiler/src/frontend/lowering/lower-stmts.ts b/packages/compiler/src/frontend/lowering/lower-stmts.ts index 5b6eb0d5b..5ee545717 100644 --- a/packages/compiler/src/frontend/lowering/lower-stmts.ts +++ b/packages/compiler/src/frontend/lowering/lower-stmts.ts @@ -198,6 +198,7 @@ import { lowerNativeFunctionAssignment, lowerStrictEquality, } from "./lower-exprs.js"; +import { keptIdentityAssertion } from "./assertion-identity.js"; import { isNativeProxyInitializer } from "./expressions/native-proxy.js"; import { tryLowerExpression } from "./expressions/try-lower-expression.js"; import { lowerUnionFieldWrite } from "./expressions/union-fields.js"; @@ -5054,9 +5055,16 @@ export function lowerVarDecl( lowerer.unions.get(inferredObjectType.unionId)?.arms.every(isUnitType))))) && !decl.type && !hasJsTypeAnnotation(decl); + // The binding of an assertion of the global object holds that object, + // whatever its asserted type (assertion-identity.ts). + const keepsAssertedObject = + init.type.kind === "dyn" && + !decl.type && + keptIdentityAssertion(lowerer, decl.initializer) !== undefined; if ( expandsObject || preservesObjectIdentity || + keepsAssertedObject || (isJsSourceFile(decl.getSourceFile()) && !decl.type && isNativeBuiltinValueInitializer(lowerer, decl.initializer)) @@ -5269,7 +5277,7 @@ export function lowerVarDecl( ? npmStaticOptionalSelfWriteType(lowerer, decl, settledType) : null; settledType = - expandsObject || preservesObjectIdentity + expandsObject || preservesObjectIdentity || keepsAssertedObject ? DYN : lowerer.runtimeOptionalBindingType(decl.name, settledType); if (optionalSelfWriteType !== null) settledType = optionalSelfWriteType; diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6350-globalthis-assertion-state.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6350-globalthis-assertion-state.ts.json new file mode 100644 index 000000000..e4f556b43 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6350-globalthis-assertion-state.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/6350-globalthis-assertion-state.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6352-globalthis-state-modules/main.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6352-globalthis-state-modules/main.ts.json new file mode 100644 index 000000000..2d11062c1 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6352-globalthis-state-modules/main.ts.json @@ -0,0 +1,8 @@ +{ + "order": [ + "/tests/corpus/6352-globalthis-state-modules/boot.ts", + "/tests/corpus/6352-globalthis-state-modules/state.ts", + "/tests/corpus/6352-globalthis-state-modules/main.ts" + ], + "diags": [] +} diff --git a/packages/runtime/src/scr_dyn.h b/packages/runtime/src/scr_dyn.h index 17b68712e..1be81a54e 100644 --- a/packages/runtime/src/scr_dyn.h +++ b/packages/runtime/src/scr_dyn.h @@ -926,6 +926,10 @@ void scr_dyn_key_set(ScrDyn *recv, ScrStr *key, ScrDyn *value); void scr_dyn_arr_named_put(ScrDyn *arr, const char *key, size_t key_len, ScrDyn *value); void scr_dyn_key_set_computed(ScrDyn *recv, ScrDyn *key, ScrDyn *value); void scr_dyn_key_delete(ScrDyn *recv, const ScrStr *key, bool strict); +/* This thread's global object box (borrowed). Every spelling of the global + * object answers it; its symbol-keyed properties are the global object's + * (scr_node_builtin.c). */ +ScrDyn *scr_global_object(void); ScrDyn *scr_dyn_global_symbol_get(ScrSym *key); /* borrowed key; +1 value */ void scr_dyn_global_symbol_set(ScrSym *key, ScrDyn *value); /* both borrowed */ bool scr_dyn_global_symbol_has(ScrSym *key); diff --git a/packages/runtime/src/scr_node_builtin.c b/packages/runtime/src/scr_node_builtin.c index 34c700350..233617092 100644 --- a/packages/runtime/src/scr_node_builtin.c +++ b/packages/runtime/src/scr_node_builtin.c @@ -86,6 +86,10 @@ ScrDyn *scr_util_system_error_entries(void) { static SCR_TL ScrDyn *scr_global_known; static SCR_TL ScrDyn *scr_global_own; +/* The one box denoting this thread's global object. Symbol-keyed properties + * live on the box (string-keyed ones in scr_global_own), so every spelling + * of the global object answers this box. */ +static SCR_TL ScrDyn *scr_global_box; static SCR_TL bool scr_global_cleanup_registered; static SCR_TL ScrDyn *(*scr_global_console_get)(void); static SCR_TL ScrDyn *(*scr_global_fetch_get)(void); @@ -180,6 +184,8 @@ ScrDyn *scr_process_hrtime_value(void) { } static void scr_global_cleanup(void) { + scr_dyn_release(scr_global_box); + scr_global_box = NULL; scr_dyn_release(scr_global_known); scr_dyn_release(scr_global_own); scr_global_known = NULL; @@ -195,7 +201,7 @@ static ScrDyn *scr_global_get(void *handle, const char *key, size_t length) { if (own) return scr_dyn_retain(own); if ((length == 10 && memcmp(key, "globalThis", length) == 0) || (length == 6 && memcmp(key, "global", length) == 0)) - return scr_dyn_new_handle(&scr_global_known, SCR_DYNH_GLOBAL); + return scr_dyn_retain(scr_global_box); if (length == 7 && memcmp(key, "console", length) == 0 && scr_global_console_get) return scr_global_console_get(); if (length == 5 && memcmp(key, "fetch", length) == 0 && scr_global_fetch_get) return scr_global_fetch_get(); @@ -223,26 +229,32 @@ static bool scr_global_set(void *handle, const char *key, size_t length, const S return true; } -ScrDyn *scr_global_native_init(ScrArr *known, ScrDyn *(*console_get)(void)) { +ScrDyn *scr_global_object(void) { static const ScrDynHandleOps ops = { .cls = "Object", .retain = scr_global_retain, .release = scr_global_release, .get = scr_global_get, .set = scr_global_set, }; scr_dyn_handle_install(SCR_DYNH_GLOBAL, &ops); - scr_global_console_get = console_get; if (!scr_global_known) scr_global_known = scr_dyn_new_obj(); if (!scr_global_own) scr_global_own = scr_dyn_new_obj(); + if (!scr_global_box) scr_global_box = scr_dyn_new_handle(&scr_global_known, SCR_DYNH_GLOBAL); if (!scr_global_cleanup_registered) { scr_atexit(scr_global_cleanup); scr_global_cleanup_registered = true; } + return scr_global_box; +} + +ScrDyn *scr_global_native_init(ScrArr *known, ScrDyn *(*console_get)(void)) { + ScrDyn *global = scr_global_object(); + scr_global_console_get = console_get; for (size_t i = 0; i < known->len; i++) { ScrStr *key = (ScrStr *)scr_arr_get_ref(known, (double)i); if (!scr_dyn_obj_get(scr_global_known, key->data, key->len)) scr_dyn_obj_set(scr_global_known, key->data, key->len, scr_dyn_new_bool(true)); scr_str_release(key); } - return scr_dyn_new_handle(&scr_global_known, SCR_DYNH_GLOBAL); + return scr_dyn_retain(global); } typedef struct ScrBuiltinModule { diff --git a/packages/runtime/src/scr_symbol.c b/packages/runtime/src/scr_symbol.c index 383b95986..304f2544f 100644 --- a/packages/runtime/src/scr_symbol.c +++ b/packages/runtime/src/scr_symbol.c @@ -181,72 +181,33 @@ void scr_assert_eq_sym(ScrSym *a, ScrSym *b, bool negated, bool deep, scr_str_release(ib); } -/* Symbol-keyed globals share the symbol unit's link gate. The always-linked - * JSON unit must not depend on optional symbol retain/release functions. */ -typedef struct ScrGlobalSymbolProperty { - ScrSym *key; - ScrDyn *value; - struct ScrGlobalSymbolProperty *next; -} ScrGlobalSymbolProperty; - -static SCR_TL ScrGlobalSymbolProperty *scr_global_symbols = NULL; -static SCR_TL bool scr_global_symbols_cleanup_registered = false; - -static void scr_global_symbols_cleanup(void) { - while (scr_global_symbols) { - ScrGlobalSymbolProperty *entry = scr_global_symbols; - scr_global_symbols = entry->next; - scr_sym_release(entry->key); - scr_dyn_release(entry->value); - free(entry); - } -} - +/* Direct `globalThis[symbol]` accesses use the symbol-keyed properties of + * the global object's box (scr_global_object), the storage every other + * spelling of the global object reaches, so all of them see one property. */ ScrDyn *scr_dyn_global_symbol_get(ScrSym *key) { - for (ScrGlobalSymbolProperty *entry = scr_global_symbols; entry; entry = entry->next) { - if (entry->key == key) return scr_dyn_retain(entry->value); - } - return scr_dyn_retain(scr_dyn_undefined()); + ScrDyn *k = scr_dyn_new_symbol(key); + ScrDyn *value = scr_dyn_symbol_key_get(scr_global_object(), k, false); + scr_dyn_release(k); + return value; } void scr_dyn_global_symbol_set(ScrSym *key, ScrDyn *value) { - for (ScrGlobalSymbolProperty *entry = scr_global_symbols; entry; entry = entry->next) { - if (entry->key != key) continue; - ScrDyn *previous = entry->value; - entry->value = scr_dyn_retain(value); - scr_dyn_release(previous); - return; - } - if (!scr_global_symbols_cleanup_registered) { - scr_atexit(scr_global_symbols_cleanup); - scr_global_symbols_cleanup_registered = true; - } - ScrGlobalSymbolProperty *entry = malloc(sizeof *entry); - if (!entry) scr_trap("scriptc: out of memory\n"); - entry->key = scr_sym_retain(key); - entry->value = scr_dyn_retain(value); - entry->next = scr_global_symbols; - scr_global_symbols = entry; + ScrDyn *k = scr_dyn_new_symbol(key); + scr_dyn_symbol_key_set(scr_global_object(), k, value); + scr_dyn_release(k); } bool scr_dyn_global_symbol_has(ScrSym *key) { - for (ScrGlobalSymbolProperty *entry = scr_global_symbols; entry; entry = entry->next) { - if (entry->key == key) return true; - } - return false; + ScrDyn *k = scr_dyn_new_symbol(key); + bool present = scr_dyn_has_own_computed(scr_global_object(), k); + scr_dyn_release(k); + return present; } void scr_dyn_global_symbol_delete(ScrSym *key) { - ScrGlobalSymbolProperty **slot = &scr_global_symbols; - while (*slot) { - ScrGlobalSymbolProperty *entry = *slot; - if (entry->key != key) { slot = &entry->next; continue; } - *slot = entry->next; - scr_sym_release(entry->key); - scr_dyn_release(entry->value); - free(entry); - return; - } + ScrDyn *k = scr_dyn_new_symbol(key); + scr_dyn_key_delete_computed(scr_global_object(), k, false); + scr_dyn_release(k); } /* ── fs.Dirent as a dynamic value ──────────────────────────────────── diff --git a/tests/corpus/6350-globalthis-assertion-state.ts b/tests/corpus/6350-globalthis-assertion-state.ts new file mode 100644 index 000000000..4a7f5a990 --- /dev/null +++ b/tests/corpus/6350-globalthis-assertion-state.ts @@ -0,0 +1,50 @@ +// Process-global state on the global object through a type assertion: +// every assertion of globalThis denotes the global object itself, so a +// write through one view is visible through every other view and through +// a view bound before the write. +const KEY = Symbol.for("example.state"); +interface State { + n: number; + log: string[]; +} +type Holder = { [KEY]?: State; label?: string }; + +const early = globalThis as Holder; + +function get(): State | undefined { + return early[KEY]; +} + +early[KEY] = { n: 1, log: [] }; +console.log(get()?.n, (globalThis as Holder)[KEY]?.n); + +function touch(entry: string): void { + const view = globalThis as Holder; + const state = view[KEY] ?? (view[KEY] = { n: 0, log: [] }); + state.n++; + state.log.push(entry); +} +touch("a"); +touch("b"); +console.log(early[KEY]?.n, early[KEY]?.log.join(","), get() === early[KEY]); + +// string-keyed members of the same object +(globalThis as Holder).label = "ready"; +console.log(early.label, (globalThis as unknown as { label?: string }).label); + +// ??= initializes once; delete removes the member for every view +const COUNTER = Symbol.for("example.counter"); +type Counters = { [COUNTER]?: { hits: number } }; +(globalThis as Counters)[COUNTER] ??= { hits: 0 }; +(globalThis as Counters)[COUNTER] ??= { hits: 100 }; +const counters = globalThis as Counters; +counters[COUNTER]!.hits += 2; +console.log((globalThis as Counters)[COUNTER]?.hits, COUNTER in globalThis); +delete counters[COUNTER]; +console.log((globalThis as Counters)[COUNTER], COUNTER in globalThis); + +// an index-signature view +const dict = globalThis as unknown as Record; +dict.exampleTotal = 1; +dict["exampleTotal"] = (dict["exampleTotal"] ?? 0) + 1; +console.log((globalThis as unknown as { exampleTotal?: number }).exampleTotal); diff --git a/tests/corpus/6352-globalthis-state-modules/boot.ts b/tests/corpus/6352-globalthis-state-modules/boot.ts new file mode 100644 index 000000000..856ff1baa --- /dev/null +++ b/tests/corpus/6352-globalthis-state-modules/boot.ts @@ -0,0 +1,14 @@ +// The boot module writes the state through its own assertion of globalThis. +const STATE = Symbol.for("example.boot-state"); +const PENDING = Symbol.for("example.boot-pending"); + +interface Early { + startedAt: number; + steps: string[]; +} + +export function boot(): void { + const view = globalThis as { [STATE]?: Early; [PENDING]?: Promise }; + view[STATE] = { startedAt: 42, steps: ["boot"] }; + view[PENDING] = Promise.resolve("container-1"); +} diff --git a/tests/corpus/6352-globalthis-state-modules/main.ts b/tests/corpus/6352-globalthis-state-modules/main.ts new file mode 100644 index 000000000..5b5aee7eb --- /dev/null +++ b/tests/corpus/6352-globalthis-state-modules/main.ts @@ -0,0 +1,18 @@ +// Boot writes the state; main reads it through a binding exported by +// another module, then takes the pending value out of the global object. +import { boot } from "./boot.ts"; +import { PENDING_KEY, STATE_KEY, globalState, readState } from "./state.ts"; + +console.log(readState() === undefined); +boot(); +const state = readState(); +console.log(state?.startedAt, state?.steps.join(",")); +state?.steps.push("main"); +console.log(globalState[STATE_KEY]?.steps.join(",")); + +async function main(): Promise { + const pending = globalState[PENDING_KEY]; + globalState[PENDING_KEY] = undefined; + console.log(await pending, globalState[PENDING_KEY]); +} +void main(); diff --git a/tests/corpus/6352-globalthis-state-modules/state.ts b/tests/corpus/6352-globalthis-state-modules/state.ts new file mode 100644 index 000000000..cef92c25f --- /dev/null +++ b/tests/corpus/6352-globalthis-state-modules/state.ts @@ -0,0 +1,18 @@ +// Shared by the boot and main modules: the state lives on the global +// object, keyed by registered symbols. +export const STATE_KEY = Symbol.for("example.boot-state"); +export const PENDING_KEY = Symbol.for("example.boot-pending"); + +export interface BootState { + startedAt: number; + steps: string[]; +} + +export const globalState = globalThis as { + [STATE_KEY]?: BootState; + [PENDING_KEY]?: Promise; +}; + +export function readState(): BootState | undefined { + return globalState[STATE_KEY]; +} diff --git a/tests/coverage-fixtures/no-divergences.ts b/tests/coverage-fixtures/no-divergences.ts index 9571add6e..3739336e3 100644 --- a/tests/coverage-fixtures/no-divergences.ts +++ b/tests/coverage-fixtures/no-divergences.ts @@ -1,6 +1,6 @@ // Ordinary code: identity-preserving calls, declaration-ordered literals, -// Latin-script comparisons, related-class casts and ISO dates report no -// divergence. +// Latin-script comparisons, related-class casts, ISO dates and views of the +// global object report no divergence. interface Item { name: string; qty: number; @@ -32,3 +32,9 @@ function wingsOf(a: Animal): number { console.log(wingsOf(new Bird()), wingsOf(new Animal())); console.log(["b", "A", "a"].sort((p, q) => p.localeCompare(q)).join(",")); console.log(Date.parse("2024-01-02"), new Date("2024-01-02T03:04:05+01:00").getTime()); + +// A type assertion of the global object denotes the global object itself. +const COUNT = Symbol.for("no-divergences.count"); +const globalState = globalThis as { [COUNT]?: { n: number } }; +globalState[COUNT] = { n: 1 }; +console.log(globalState[COUNT]?.n);