Read builtin globals through the global object, and support Node's navigator (#2242)

This commit is contained in:
Chris Tate
2026-10-26 06:16:08 -05:00
committed by GitHub
38 changed files with 1196 additions and 189 deletions
@@ -1,4 +1,8 @@
- **Globals:** Stored `globalThis` references support identity and probes for absent capabilities.
Most builtin members require direct global access; access through a stored global object can raise `SC2020`.
In JavaScript, reading a builtin global through the global object by name (`globalThis[name]`, or a stored global object) answers what the global's own spelling reads, and builtin module members that are global objects in Node, such as `require("buffer").atob` and `require("timers").setTimeout`, are the same values.
`WebAssembly` read this way, and builtin members read through a global object that untyped code received, can raise `SC2020`.
`for (key in globalThis)` visits Node's enumerable globals in Node's order, then the program's own global properties.
`navigator` answers Node's `hardwareConcurrency`, `language`, `languages`, `platform`, and `userAgent`, with `language` derived from `LC_ALL`, `LC_MESSAGES`, or `LANG` as Node does.
`navigator.locks` raises `SC2020`, as does `navigator.language` for a locale outside the `language_TERRITORY.codeset@modifier` form, or one whose language or territory needs ICU's tables (a three-letter territory, for example).
`Temporal` is not defined, as in Node.js 24: `typeof Temporal` is `"undefined"`, `globalThis.Temporal` is `undefined`, and any other use throws `ReferenceError: Temporal is not defined`.
Programs that name it type-check against TypeScript's `Temporal` declarations.
@@ -7,6 +7,7 @@ Some builtins can be stored and passed as functions:
In JavaScript, `setTimeout`, `setInterval`, `setImmediate`, `clearTimeout`, `clearInterval`, `clearImmediate`, `queueMicrotask`, and `process.nextTick` are function values wherever they are stored, including conditional and logical expressions and assignments: `typeof` answers `"function"`, identity holds across spellings, and calls schedule as the direct calls do, returning the same numeric timer handles.
Calls through these values bypass `node:test` mock timers.
In JavaScript, `atob`, `btoa`, and `structuredClone` are function values the same way, and calls through them run what the direct calls run.
Other builtin globals stored in JavaScript remain opaque identity values.
In JavaScript, `require("events")` and the `EventEmitter` class assigned to an untyped binding or stored in an object (`ReleaseEmitter = EventEmitter`) are the untyped `EventEmitter` constructor that constructor functions inherit from (see below): `new` through it builds such an emitter object, and identity and `instanceof` agree with the class.
A class can extend `require("events")` directly.
+12 -12
View File
@@ -36684,9 +36684,9 @@
"inherited": true
},
"static": {
"status": "not-implemented",
"detail": "No static compiler lowering is registered for this API yet.",
"verification": "registry-gap"
"status": "supported",
"detail": "Implemented for the documented scriptc module-loader subset.",
"verification": "test-backed"
}
},
{
@@ -36707,9 +36707,9 @@
"inherited": true
},
"static": {
"status": "not-implemented",
"detail": "No static compiler lowering is registered for this API yet.",
"verification": "registry-gap"
"status": "partial",
"detail": "Implemented for the documented scriptc module-loader subset.",
"verification": "test-backed"
}
},
{
@@ -36730,9 +36730,9 @@
"inherited": true
},
"static": {
"status": "not-implemented",
"detail": "No static compiler lowering is registered for this API yet.",
"verification": "registry-gap"
"status": "partial",
"detail": "Implemented for the documented scriptc module-loader subset.",
"verification": "test-backed"
}
},
{
@@ -36776,9 +36776,9 @@
"inherited": true
},
"static": {
"status": "not-implemented",
"detail": "No static compiler lowering is registered for this API yet.",
"verification": "registry-gap"
"status": "supported",
"detail": "Implemented for the documented scriptc module-loader subset.",
"verification": "test-backed"
}
},
{
@@ -52431,6 +52431,7 @@
"tests/corpus/6331-jose-root-entry/main.ts",
"tests/corpus/7743-transform-stream-identity.ts",
"tests/corpus/7747-text-codec-untyped-slot.js",
"tests/corpus/8162-navigator-global.cjs",
"tests/corpus/text-codec-values/main.ts",
"tests/corpus/webassembly-api.mjs",
"tests/corpus/webassembly-dylink/main.mjs",
@@ -54483,6 +54484,7 @@
"status": "partial",
"evidence": "compiler-feature:globals.navigator",
"tests": [
"tests/corpus/8162-navigator-global.cjs",
"tests/fixtures/node-types/navigator.ts",
"tests/harness/project-config.test.ts",
"tests/corpus/6331-jose-root-entry/main.ts"
@@ -54508,8 +54510,12 @@
"inherited": true
},
"static": {
"status": "not-implemented",
"evidence": "compiler-unmatched:globals"
"status": "supported",
"evidence": "compiler-feature:globals.hardwareConcurrency",
"tests": [
"tests/corpus/8162-navigator-global.cjs",
"tests/fixtures/node-types/navigator.ts"
]
},
"anchorSource": "ancestor"
},
@@ -54531,8 +54537,12 @@
"inherited": true
},
"static": {
"status": "not-implemented",
"evidence": "compiler-unmatched:globals"
"status": "partial",
"evidence": "compiler-feature:globals.language",
"tests": [
"tests/corpus/8162-navigator-global.cjs",
"tests/fixtures/node-types/navigator.ts"
]
},
"anchorSource": "ancestor"
},
@@ -54554,8 +54564,12 @@
"inherited": true
},
"static": {
"status": "not-implemented",
"evidence": "compiler-unmatched:globals"
"status": "partial",
"evidence": "compiler-feature:globals.languages",
"tests": [
"tests/corpus/8162-navigator-global.cjs",
"tests/fixtures/node-types/navigator.ts"
]
},
"anchorSource": "ancestor"
},
@@ -54600,8 +54614,12 @@
"inherited": true
},
"static": {
"status": "not-implemented",
"evidence": "compiler-unmatched:globals"
"status": "supported",
"evidence": "compiler-feature:globals.platform",
"tests": [
"tests/corpus/8162-navigator-global.cjs",
"tests/fixtures/node-types/navigator.ts"
]
},
"anchorSource": "ancestor"
},
+3 -1
View File
@@ -222,8 +222,10 @@
{ "chapter": "webcrypto", "signatures": ["Class: `CryptoKey`", "`cryptoKey.algorithm`", "`extractable` Type: {boolean}", "`type` Type: {string} One of `'secret'`, `'private'`, or `'public'`.", "`usages` Type: {string\\[]}"], "status": "partial", "evidence": ["tests/corpus/7124-webcrypto-secret-keys.ts", "tests/corpus/6291-webcrypto-hmac-sign-verify.ts", "tests/corpus/6294-webcrypto-js-values.mjs", "tests/corpus/6295-jose-hs256/main.mjs"] },
{ "chapter": "webcrypto", "signatures": ["Class: `Crypto`"], "status": "partial", "evidence": ["tests/corpus/7120-webcrypto-random-uuid.ts", "tests/corpus/7121-webcrypto-crypto-object.mjs", "tests/corpus/6293-webcrypto-digest.ts"] },
{ "chapter": "webcrypto", "symbols": ["crypto.randomUUID", "crypto.getRandomValues"], "status": "supported", "evidence": ["tests/corpus/7120-webcrypto-random-uuid.ts", "tests/corpus/7121-webcrypto-crypto-object.mjs", "tests/corpus/6204-crypto-get-random-values.ts", "tests/corpus/6205-crypto-get-random-values.js"] },
{ "chapter": "globals", "signatures": ["`navigator`"], "status": "partial", "evidence": ["tests/fixtures/node-types/navigator.ts", "tests/harness/project-config.test.ts", "tests/corpus/6331-jose-root-entry/main.ts"] },
{ "chapter": "globals", "signatures": ["`navigator`"], "status": "partial", "evidence": ["tests/corpus/8162-navigator-global.cjs", "tests/fixtures/node-types/navigator.ts", "tests/harness/project-config.test.ts", "tests/corpus/6331-jose-root-entry/main.ts"] },
{ "chapter": "globals", "signatures": ["`userAgent` Type: {string}"], "status": "supported", "evidence": ["tests/fixtures/node-types/navigator.ts", "tests/harness/project-config.test.ts"] },
{ "chapter": "globals", "signatures": ["`hardwareConcurrency` Type: {number}", "`platform` Type: {string}"], "status": "supported", "evidence": ["tests/corpus/8162-navigator-global.cjs", "tests/fixtures/node-types/navigator.ts"] },
{ "chapter": "globals", "signatures": ["`language` Type: {string}", "`languages` Type: {string\\[]}"], "status": "partial", "evidence": ["tests/corpus/8162-navigator-global.cjs", "tests/fixtures/node-types/navigator.ts"] },
{ "chapter": "globals", "signatures": ["Class: `CryptoKey`", "Class: `SubtleCrypto`"], "status": "partial", "evidence": ["tests/corpus/6291-webcrypto-hmac-sign-verify.ts", "tests/corpus/6292-webcrypto-hmac-errors.ts", "tests/corpus/6293-webcrypto-digest.ts", "tests/corpus/6294-webcrypto-js-values.mjs", "tests/corpus/6295-jose-hs256/main.mjs"] },
{ "chapter": "errors", "parent": "Class: `SystemError`", "symbols": ["code", "errno", "syscall", "path", "dest"], "status": "partial", "evidence": ["tests/corpus/caught-system-error-fields.ts", "tests/fixtures/node-types/terminal-error-fields.ts", "tests/harness/project-config.test.ts"] },
{"chapter": "events", "symbols": ["EventTarget", "eventTarget.addEventListener", "eventTarget.removeEventListener", "eventTarget.dispatchEvent"], "status": "partial", "evidence": ["tests/corpus/5130-event-target-dispatch.js", "tests/corpus/5131-event-target-listener-errors.js", "tests/corpus/5132-event-target-typed.ts", "tests/corpus/5133-abort-signal-event-target.ts", "tests/corpus/5135-event-target-warnings.js", "tests/corpus/5240-event-target-subclass.ts", "tests/corpus/5242-event-target-subclass-hierarchy.ts", "tests/corpus/5243-event-target-subclass-dynamic.ts", "tests/corpus/5244-event-target-subclass.js", "tests/corpus/5245-event-target-subclass-signals.ts", "tests/corpus/5248-event-target-subclass-warnings.ts"]},
@@ -1232,6 +1232,7 @@ export function emitLibCall(host: LlvmEmitterContext, e: LibCallExpr): LlValue {
case "punycode":
case "constants":
case "promise":
case "navigator":
case "mime":
return host.emitGenericLibCall(e);
case "test":
@@ -294,6 +294,12 @@ export const LIB_FN_SYMS: Record<string, string> = {
"console.table": "scr_console_table",
"console.dirDyn": "scr_console_dir_dyn",
"global.native": "scr_global_native",
"global.installValues": "scr_global_values_install",
"global.forInObject": "scr_global_for_in_object",
"navigator.object": "scr_navigator_object",
"navigator.language": "scr_navigator_language",
"navigator.languages": "scr_navigator_languages",
"navigator.platform": "scr_navigator_platform",
"process.builtinModule": "scr_process_builtin_module",
"process.builtinUnsupported": "scr_process_builtin_unsupported",
"module.namespaceValue": "scr_module_namespace_value",
@@ -1,13 +1,13 @@
/* Node 24's `navigator` global (lib/internal/navigator.js), the part
* libraries read to tell runtimes apart: `typeof navigator` is "object"
* and `navigator.userAgent` is `Node.js/<major>`. jose reads exactly
* that at module load (jwks/remote.js). Both fold to string literals, so
* nothing links. The other members (hardwareConcurrency, language,
* languages, platform, locks) and the object as a value keep a named
/* Node 24's `navigator` global (lib/internal/navigator.js): `typeof
* navigator` is "object", `navigator.userAgent` is `Node.js/<major>` (both
* fold to string literals; jose reads them at module load), and
* hardwareConcurrency, language, languages and platform read the runtime's
* answers (scr_node_builtin.c). The object as a value is the runtime's
* Navigator handle, whose getters answer the same. `locks` keeps a named
* refusal. */
import * as ts from "../../ts7/adapter.js";
import { strLit } from "../../../ir/build.js";
import type { IrExpr } from "../../../ir/ir.js";
import { DYN, F64, STRING, type IrExpr, type SrcLoc } from "../../../ir/ir.js";
import { isJsSourceFile, locOf } from "../../program.js";
import type { Lowerer } from "../lowerer.js";
@@ -34,9 +34,18 @@ export function lowerNavigatorMember(
nodeVersion: string,
): IrExpr | null {
if (!isNavigatorGlobal(lowerer, access.expression)) return null;
const loc = locOf(access);
if (access.name.text === "userAgent") {
return strLit(`Node.js/${nodeVersion.split(".")[0]}`, locOf(access));
return strLit(`Node.js/${nodeVersion.split(".")[0]}`, loc);
}
if (access.name.text === "hardwareConcurrency")
return { kind: "libCall", fn: "os.availableParallelism", args: [], type: F64, loc };
if (access.name.text === "language" || access.name.text === "platform") {
const fn = access.name.text === "language" ? "navigator.language" : "navigator.platform";
return { kind: "libCall", fn, args: [], type: STRING, loc };
}
if (access.name.text === "languages")
return { kind: "libCall", fn: "navigator.languages", args: [], type: DYN, loc };
return refuseNavigator(
lowerer,
access,
@@ -55,7 +64,12 @@ export function refuseNavigator(
return lowerer.noLowering(
what,
node,
"of Node's navigator global only typeof navigator and navigator.userAgent compile",
"navigator.locks (the Web Locks API) has no lowering",
symbol ?? null,
);
}
/** The navigator object as a value: the runtime's Navigator handle. */
export function navigatorValue(loc: SrcLoc): IrExpr {
return { kind: "libCall", fn: "navigator.object", args: [], type: DYN, loc };
}
@@ -1,4 +1,5 @@
/* Identifier references in expression position: lowerExprInner's identifier branch and its helpers. */
import { globalMemberValue } from "../global-values.js";
import { predeclareForwardClass } from "../lower-stmts.js";
import { cjsMemberAliasValueOf } from "../cjs-exports-object.js";
import { builtinClassValue, builtinMemberClassKind } from "../builtins/builtin-class-dyn.js";
@@ -89,7 +90,7 @@ import { unsupportedModuleFeatureOf } from "../../builtin-modules.js";
import { lowerGlobalValue } from "../lower-global-value.js";
import { fenceEnumObjectValue } from "../lower-enums.js";
import { mixinFnOfCallee } from "../lower-mixins.js";
import { isNavigatorGlobal, refuseNavigator } from "../builtins/navigator.js";
import { isNavigatorGlobal, navigatorValue } from "../builtins/navigator.js";
import { moduleFileName } from "../import-meta.js";
export function lowerIdentifierExpr(lowerer: Lowerer, expr: ts.Identifier, loc: SrcLoc): IrExpr {
@@ -599,6 +600,9 @@ export function lowerIdentifierExpr(lowerer: Lowerer, expr: ts.Identifier, loc:
// the same identity-token story as stdlib globals above (the
// harness adds worker_threads.Worker to its identity Set).
if (isJsSourceFile(ts.getSourceFileOfNode(expr))) {
// A member that is a global object reads as the global does.
const global = globalMemberValue(lowerer, bi, loc);
if (global) return global;
// EventEmitter and the stream classes are the runtime's untyped
// constructors, so constructor functions can inherit from them.
const classKind = builtinMemberClassKind(bi.module, bi.member);
@@ -608,12 +612,7 @@ export function lowerIdentifierExpr(lowerer: Lowerer, expr: ts.Identifier, loc:
return builtinFunctionFenceValue(lowerer, bi, expr, loc);
return {
kind: "strLit",
// node:stream/web exports the web stream globals themselves:
// the same identity token as the global spelling.
value:
bi.module === "stream/web"
? `[builtin ${bi.member}]`
: `[builtin ${bi.module}.${bi.member}]`,
value: `[builtin ${bi.module}.${bi.member}]`,
type: STRING,
loc,
};
@@ -731,43 +730,11 @@ export function lowerIdentifierExpr(lowerer: Lowerer, expr: ts.Identifier, loc:
expr.text === "decodeURI") &&
lowerer.isStdlibGlobal(expr, expr.text)
) {
const name = `%builtin.${expr.text}`;
const type = funcOf([DYN], STRING);
if (!lowerer.liftedFns.some((fn) => fn.name === name)) {
const value: IrExpr = {
kind: "libCall",
fn: "dyn.toStringCoerce",
args: [varRef("value", DYN, loc)],
type: STRING,
loc,
};
const fn =
expr.text === "encodeURIComponent"
? "str.encodeUriComponent"
: expr.text === "encodeURI"
? "str.encodeUri"
: expr.text === "decodeURI"
? "str.decodeUri"
: "str.decodeUriComponent";
lowerer.liftedFns.push({
name,
params: [{ localId: "value", name: "value", type: DYN }],
locals: [{ id: "value", name: "value", type: DYN, mutable: false }],
returnType: STRING,
body: [
{
kind: "return",
value: { kind: "libCall", fn, args: [value], type: STRING, loc },
loc,
},
],
loc,
});
}
return { kind: "closure", fnName: name, captures: [], type, loc };
return uriFunctionValue(lowerer, expr.text, loc);
}
if (lowerer.isStdlibGlobal(expr, "crypto"))
return { kind: "libCall", fn: "webcrypto.object", args: [], type: DYN, loc };
if (isNavigatorGlobal(lowerer, expr)) return navigatorValue(loc);
const nativeAlias = stdlibGlobalNameOf(lowerer, expr);
if (nativeAlias === "Buffer" && expr.text !== "Buffer") {
// An alias through a cast of globalThis carries the cast's static type,
@@ -1026,14 +993,6 @@ export function lowerIdentifierExpr(lowerer: Lowerer, expr: ts.Identifier, loc:
const ns = lowerNsIdentifierValue(lowerer, expr);
if (ns) return ns;
}
if (isNavigatorGlobal(lowerer, expr)) {
refuseNavigator(
lowerer,
expr,
"the navigator object as a value",
lowerer.checker.getSymbolAtLocation(expr),
);
}
// An import of a CommonJS member export whose value is a binding of the
// exporting module (`module.exports = Client; module.exports.StatsD =
// Client`): the member holds that binding's value.
@@ -1163,3 +1122,42 @@ function cjsExportAccessorRead(lowerer: Lowerer, ident: ts.Identifier): IrExpr |
}
return cjsAccessorCall(lowerer, getter, locOf(ident));
}
/** encodeURIComponent, encodeURI, decodeURI or decodeURIComponent as a
* function value: one interned closure per function. */
export function uriFunctionValue(lowerer: Lowerer, which: string, loc: SrcLoc): IrExpr {
const name = `%builtin.${which}`;
const type = funcOf([DYN], STRING);
if (!lowerer.liftedFns.some((fn) => fn.name === name)) {
const value: IrExpr = {
kind: "libCall",
fn: "dyn.toStringCoerce",
args: [varRef("value", DYN, loc)],
type: STRING,
loc,
};
const fn =
which === "encodeURIComponent"
? "str.encodeUriComponent"
: which === "encodeURI"
? "str.encodeUri"
: which === "decodeURI"
? "str.decodeUri"
: "str.decodeUriComponent";
lowerer.liftedFns.push({
name,
params: [{ localId: "value", name: "value", type: DYN }],
locals: [{ id: "value", name: "value", type: DYN, mutable: false }],
returnType: STRING,
body: [
{
kind: "return",
value: { kind: "libCall", fn, args: [value], type: STRING, loc },
loc,
},
],
loc,
});
}
return { kind: "closure", fnName: name, captures: [], type, loc };
}
@@ -1,6 +1,7 @@
import { widensReducedRecords } from "../function-returns.js";
import { widthNarrowHelper } from "../coercions/unions.js";
/* Property reads (`a.b`): lowerExprInner's property-access branch, intrinsic and union properties, field reads and targets, and match-group reads. */
import { jsSpecialGlobalValue } from "../global-values.js";
import {
arrayClassMemberOwner,
arrayClassReceiver,
@@ -43,7 +44,6 @@ import {
} from "../../type-mapper.js";
import {
BOOL,
BYTES_ELEMENT_NAME,
BYTES_U8,
DYN,
F64,
@@ -68,7 +68,7 @@ import {
unionContainerArmsOk,
BYTES_ELEMENT_SIZE,
} from "../../../ir/ir.js";
import { lowerNavigatorMember } from "../builtins/navigator.js";
import { lowerNavigatorMember, navigatorValue } from "../builtins/navigator.js";
import { lowerX509PublicKey } from "../builtins/x509.js";
import { fenceMimeStaticRead } from "../builtins/mime.js";
import { NODE_COMPAT_VERSION } from "../builtins/process.js";
@@ -619,7 +619,10 @@ export function lowerPropertyAccessExpr(
// (`(_a = globalThis).__config ?? (_a.__config = {})`) reads back from
// the global object's own properties.
const globalType = lowerer.typeOf(staticallySelected(lowerer, expr.expression));
if (globalThisWrittenNames(lowerer, globalType).has(expr.name.text)) {
if (
globalThisWrittenNames(lowerer, globalType).has(expr.name.text) ||
lowerer.globalThisComputedWrite
) {
return {
kind: "dynKeyGet",
value: lowerer.coerceToExpected(lowerer.lowerExpr(expr.expression), DYN),
@@ -628,6 +631,10 @@ export function lowerPropertyAccessExpr(
loc,
};
}
// Node's navigator, which JavaScript reads as a free name when no
// declaration types it (isNavigatorGlobal).
if (expr.name.text === "navigator" && isJsSourceFile(ts.getSourceFileOfNode(expr)))
return navigatorValue(loc);
return dynUndefinedExpr(loc);
}
// `Uint8Array.prototype.toBase64` and its siblings, the base64/hex
@@ -1002,18 +1009,8 @@ export function lowerPropertyAccessExpr(
if (canonical === "globalThis") return lowerGlobalValue(lowerer, expr);
// Typed for the programs that name it, absent in Node 24.
if (canonical === TEMPORAL_GLOBAL) return dynUndefinedExpr(loc);
if (canonical === "ArrayBuffer")
return { kind: "libCall", fn: "arrayBuffer.constructor", args: [], type: DYN, loc };
if (canonical === "fetch")
return { kind: "libCall", fn: "fetch.function", args: [], type: DYN, loc };
if (Object.values(BYTES_ELEMENT_NAME).includes(canonical))
return {
kind: "libCall",
fn: "bytes.constructor",
args: [{ kind: "strLit", value: canonical, type: STRING, loc }],
type: DYN,
loc,
};
const value = jsSpecialGlobalValue(lowerer, canonical, loc);
if (value) return value;
return (
builtinGlobalValueAt(lowerer, expr, canonical, loc, true) ?? {
kind: "strLit",
@@ -4349,9 +4346,13 @@ const GLOBAL_CONSTRUCTOR_VALUES: ReadonlySet<string> = new Set([
/** The builtin global functions JavaScript sources read as interned
* function values (the `cond ? setImmediate : process.nextTick` feature
* probe of bcryptjs and async libraries): typeof answers "function",
* identity holds across spellings, and calls run the timer APIs. */
* probe of bcryptjs and async libraries, and the known globals Node's test
* harness collects): typeof answers "function", identity holds across
* spellings, and calls run what the direct calls run. */
const GLOBAL_FUNCTION_VALUES: ReadonlySet<string> = new Set([
"atob",
"btoa",
"structuredClone",
"setTimeout",
"setInterval",
"setImmediate",
@@ -4729,11 +4730,24 @@ function globalThisWrittenNames(lowerer: Lowerer, globalType: ts.Type): Set<stri
(node.operatorToken.kind === ts.SyntaxKind.EqualsToken ||
node.operatorToken.kind === ts.SyntaxKind.QuestionQuestionEqualsToken ||
node.operatorToken.kind === ts.SyntaxKind.BarBarEqualsToken ||
node.operatorToken.kind === ts.SyntaxKind.AmpersandAmpersandEqualsToken) &&
ts.isPropertyAccessExpression(node.left) &&
lowerer.typeOf(node.left.expression) === globalType
)
names.add(node.left.name.text);
node.operatorToken.kind === ts.SyntaxKind.AmpersandAmpersandEqualsToken)
) {
if (
ts.isPropertyAccessExpression(node.left) &&
lowerer.typeOf(node.left.expression) === globalType
)
names.add(node.left.name.text);
// A write by computed name may store any name: every read of a
// name the program does not declare consults the global object.
if (
ts.isElementAccessExpression(node.left) &&
lowerer.typeOf(node.left.expression) === globalType
) {
if (ts.isStringLiteralLike(node.left.argumentExpression))
names.add(node.left.argumentExpression.text);
else lowerer.globalThisComputedWrite = true;
}
}
return undefined;
});
}
@@ -0,0 +1,295 @@
/* Builtin globals read through the global object by a name the compiler
* does not see (`globalThis[name]`, `for (name in globalThis)` followed by
* `globalThis[name]`). Such a read answers what the name's static spelling
* reads in a JavaScript source: the interned function and constructor
* values, the WebCrypto object and Buffer, and the opaque
* identity token for the other declared globals.
*
* The values come from one lookup function per program
* (`%global.builtinValue`), which a JavaScript module with such a read
* installs on the global object when its body starts
* (scr_global_values_install). The runtime answers the global object,
* console, fetch and the constants itself and calls the lookup for the
* other declared names: a value, `undefined` for the token, or `null` for
* a name without one, whose read keeps its SC2020 refusal. */
import * as ts from "../ts7/adapter.js";
import {
BYTES_ELEMENT_NAME,
DYN,
STRING,
UNDEFINED_T,
type IrExpr,
type IrStmt,
type IrType,
type SrcLoc,
} from "../../ir/ir.js";
import { isJsSourceFile } from "../program.js";
import type { Lowerer } from "./lowerer.js";
import { TEMPORAL_GLOBAL } from "../builtin-modules.js";
import { stdlibGlobalNameOf } from "./surfaces.js";
import { builtinGlobalValue } from "./expressions/property-access.js";
import { builtinNamespaceValue } from "./builtin-namespaces.js";
import { uriFunctionValue } from "./expressions/identifiers.js";
import { navigatorValue } from "./builtins/navigator.js";
import { primitiveCtorClosure } from "./lower-exprs.js";
const LOOKUP_FN = "%global.builtinValue";
/** Names the runtime answers before the lookup runs. */
const RUNTIME_ANSWERED = new Set([
"globalThis",
"global",
"console",
"fetch",
"undefined",
"NaN",
"Infinity",
]);
/** Declared globals whose reads through the global object stay refused:
* WebAssembly's namespace links the translated modules a program that
* names it statically carries. */
const REFUSED = new Set(["WebAssembly"]);
/** The receiver of the first element read of the global object
* (`globalThis[name]`, `globalThis["name"]`), or of the first for-in over
* it, in the JavaScript source `sf`; null when it has none. */
export function computedGlobalReadOf(lowerer: Lowerer, sf: ts.SourceFile): ts.Expression | null {
if (!isJsSourceFile(sf)) return null;
const cached = lowerer.computedGlobalReads.get(sf);
if (cached !== undefined) return cached;
let found: ts.Expression | null = null;
// An iterative walk: sources can nest expressions thousands deep.
ts.walkPreorder(sf, (node) => {
if (
(ts.isElementAccessExpression(node) || ts.isForInStatement(node)) &&
isGlobalObjectRead(lowerer, node.expression) &&
(ts.isForInStatement(node) || readsBuiltinByName(lowerer, node))
) {
found = node.expression;
return "stop";
}
return undefined;
});
lowerer.computedGlobalReads.set(sf, found);
return found;
}
/** Whether `expr` reads the global object: a spelling of it, or a variable
* whose only declaration initializes it with one (`const g = globalThis`).
* The scan runs before function bodies are lowered, so the alias of a
* variable declared in a function is not registered yet. */
function isGlobalObjectRead(lowerer: Lowerer, expr: ts.Expression): boolean {
if (stdlibGlobalNameOf(lowerer, expr) === "globalThis") return true;
let inner = expr;
while (ts.isParenthesizedExpression(inner)) inner = inner.expression;
if (!ts.isIdentifier(inner)) return false;
const declarations = lowerer.checker.getSymbolAtLocation(inner)?.declarations ?? [];
const declaration = declarations[0];
return (
declarations.length === 1 &&
declaration !== undefined &&
ts.isVariableDeclaration(declaration) &&
ts.isIdentifier(declaration.name) &&
declaration.initializer !== undefined &&
stdlibGlobalNameOf(lowerer, declaration.initializer) === "globalThis"
);
}
/** Whether the element access `node` of the global object can read a
* builtin global by name: its key is not a symbol (`globalThis[key]` with
* `key = Symbol.for(...)` reads the global object's symbol properties),
* and it is neither the target of `=` nor the operand of `delete`. */
function readsBuiltinByName(lowerer: Lowerer, node: ts.ElementAccessExpression): boolean {
if (lowerer.typeOf(node.argumentExpression).flags & ts.TypeFlags.ESSymbolLike) return false;
let outer: ts.Node = node;
while (ts.isParenthesizedExpression(outer.parent)) outer = outer.parent;
const parent = outer.parent;
if (ts.isDeleteExpression(parent)) return false;
return !(
ts.isBinaryExpression(parent) &&
parent.left === outer &&
parent.operatorToken.kind === ts.SyntaxKind.EqualsToken
);
}
/** The value a JavaScript source reads for the declared global `name`
* taken as a value, whatever its spelling (`name`, `globalThis.name`,
* `const { name } = globalThis`, `globalThis[name]`), or null when it
* reads the opaque identity token. */
export function jsGlobalValueByName(lowerer: Lowerer, name: string, loc: SrcLoc): IrExpr | null {
return (
jsSpecialGlobalValue(lowerer, name, loc) ??
builtinNamespaceValue(lowerer, name, loc) ??
builtinGlobalValue(name, loc, true)
);
}
/** jsGlobalValueByName for the globals other than the interned builtin
* functions and constructors (builtinGlobalValue). */
export function jsSpecialGlobalValue(lowerer: Lowerer, name: string, loc: SrcLoc): IrExpr | null {
if (name === "String" || name === "Number" || name === "Boolean" || name === "BigInt")
return primitiveCtorClosure(lowerer, name, loc, true);
if (name === "ArrayBuffer")
return { kind: "libCall", fn: "arrayBuffer.constructor", args: [], type: DYN, loc };
if (name === "Array")
return { kind: "libCall", fn: "dyn.arrayConstructor", args: [], type: DYN, loc };
if ((Object.values(BYTES_ELEMENT_NAME) as string[]).includes(name))
return {
kind: "libCall",
fn: "bytes.constructor",
args: [{ kind: "strLit", value: name, type: STRING, loc }],
type: DYN,
loc,
};
if (URI_FUNCTIONS.has(name)) return uriFunctionValue(lowerer, name, loc);
if (name === "crypto")
return { kind: "libCall", fn: "webcrypto.object", args: [], type: DYN, loc };
if (name === "navigator") return navigatorValue(loc);
if (name === "Buffer")
return { kind: "libCall", fn: "buffer.constructor", args: [], type: DYN, loc };
if (name === "fetch") return { kind: "libCall", fn: "fetch.function", args: [], type: DYN, loc };
if (name === "console")
return { kind: "libCall", fn: "console.native", args: [], type: DYN, loc };
return null;
}
const URI_FUNCTIONS = new Set([
"encodeURIComponent",
"encodeURI",
"decodeURI",
"decodeURIComponent",
]);
/** The statement that installs the program's lookup, built on first use
* from the names the checker declares on `receiver` (a global object). */
export function globalValuesInstall(
lowerer: Lowerer,
receiver: ts.Expression,
loc: SrcLoc,
): IrStmt {
const fnType: IrType & { kind: "func" } = { kind: "func", params: [STRING], ret: DYN };
if (!lowerer.liftedFnNamed(LOOKUP_FN)) {
const names = lowerer.checker
.getPropertiesOfType(lowerer.typeOf(receiver))
.map((property) => ts.symbolName(property))
.filter((name) => name !== TEMPORAL_GLOBAL && !RUNTIME_ANSWERED.has(name));
const name: IrExpr = { kind: "varRef", localId: "name.0", type: STRING, loc };
const body: IrStmt[] = [];
for (const global of [...new Set(names)].sort()) {
const value: IrExpr | null = REFUSED.has(global)
? { kind: "unitLit", unit: "null", type: { kind: "nullT" }, loc }
: jsGlobalValueByName(lowerer, global, loc);
if (value === null) continue;
body.push({
kind: "if",
cond: {
kind: "strEq",
negated: false,
left: name,
right: { kind: "strLit", value: global, type: STRING, loc },
type: { kind: "bool" },
loc,
},
then: [{ kind: "return", value: lowerer.coerceToExpected(value, DYN), loc }],
else_: null,
loc,
});
}
body.push({
kind: "return",
value: lowerer.coerceToExpected(
{ kind: "unitLit", unit: "undefined", type: UNDEFINED_T, loc },
DYN,
),
loc,
});
lowerer.liftedFns.push({
name: LOOKUP_FN,
params: [{ localId: "name.0", name: "name", type: STRING }],
returnType: DYN,
locals: [{ id: "name.0", name: "name", type: STRING, mutable: false }],
body,
loc,
});
}
const lookup = lowerer.coerceToExpected(
{ kind: "closure", fnName: LOOKUP_FN, captures: [], type: fnType, loc },
DYN,
);
return {
kind: "exprStmt",
expr: {
kind: "libCall",
fn: "global.installValues",
args: [lookup],
type: { kind: "void" },
loc,
},
loc,
};
}
/** Builtin module members that are the global objects of the same name in
* Node 24 (`require("buffer").atob === globalThis.atob`). */
const GLOBAL_MEMBERS: Readonly<Record<string, ReadonlySet<string>>> = {
buffer: new Set(["atob", "btoa", "Blob", "File", "Buffer"]),
timers: new Set([
"setTimeout",
"setInterval",
"setImmediate",
"clearTimeout",
"clearInterval",
"clearImmediate",
]),
perf_hooks: new Set([
"performance",
"PerformanceEntry",
"PerformanceMark",
"PerformanceMeasure",
"PerformanceObserver",
"PerformanceObserverEntryList",
"PerformanceResourceTiming",
]),
util: new Set(["TextEncoder", "TextDecoder"]),
url: new Set(["URL", "URLSearchParams"]),
worker_threads: new Set(["BroadcastChannel", "MessageChannel", "MessagePort"]),
"stream/web": new Set([
"ReadableStream",
"ReadableStreamDefaultReader",
"ReadableStreamBYOBReader",
"ReadableStreamBYOBRequest",
"ReadableByteStreamController",
"ReadableStreamDefaultController",
"TransformStream",
"TransformStreamDefaultController",
"WritableStream",
"WritableStreamDefaultWriter",
"WritableStreamDefaultController",
"ByteLengthQueuingStrategy",
"CountQueuingStrategy",
"TextEncoderStream",
"TextDecoderStream",
"CompressionStream",
"DecompressionStream",
]),
};
/** In a JavaScript source, the value of a builtin module member that is a
* global object (GLOBAL_MEMBERS): what the global's own spelling reads,
* so identity holds across the spellings. Null for other members. */
export function globalMemberValue(
lowerer: Lowerer,
bi: { module: string; member: string },
loc: SrcLoc,
): IrExpr | null {
if (!GLOBAL_MEMBERS[bi.module]?.has(bi.member)) return null;
return (
jsGlobalValueByName(lowerer, bi.member, loc) ?? {
kind: "strLit",
value: `[builtin ${bi.member}]`,
type: STRING,
loc,
}
);
}
@@ -1,3 +1,4 @@
import { computedGlobalReadOf, globalValuesInstall } from "./global-values.js";
import type { TypeFailureSlot } from "../../diagnostics/diagnostic.js";
import { THREADS_MODULE } from "../threads-import.js";
import { positionTag } from "./structural-positions.js";
@@ -3070,6 +3071,10 @@ export function lowerFileInit(
});
}
if (sf === lowerer.entry) header.push(...lowerer.punycodeLinkWarning());
// A module that reads builtin globals through the global object by
// computed names installs the program's lookup before its body runs.
const computedGlobalRead = computedGlobalReadOf(lowerer, sf);
if (computedGlobalRead) header.push(globalValuesInstall(lowerer, computedGlobalRead, loc0));
// Top-level `this` of a CommonJS module: its object exists before the
// module's first statement (lowerCjsTopLevelThis).
const exportsThis = lowerer.cjsExportsThis.get(sf);
@@ -1,3 +1,4 @@
import { jsGlobalValueByName } from "./global-values.js";
import { lowerTupleAliasDecl, stripTypeWrappers } from "./typed-arguments.js";
import {
allBytesArms,
@@ -33,7 +34,6 @@ import { InternalCompilerError } from "../../errors.js";
* explicit resource management, and blocked-binding poisoning. */
import * as ts from "../ts7/adapter.js";
import { lowerOptionalIndex } from "./undefined-index.js";
import { builtinNamespaceValue } from "./builtin-namespaces.js";
import { processDataMemberValue, programReadsExitCode } from "./builtins/process.js";
import { requireInitFlagStmt } from "./require-initialization.js";
import { isJsonRequireDecl } from "./json-require.js";
@@ -240,7 +240,6 @@ import { globalSymbolKey } from "./expressions/global-symbols.js";
import { identifiersNamed } from "./identifier-uses.js";
import { lowerShortCircuitAssignment } from "./expressions/nullish-assignment.js";
import {
builtinGlobalValue,
jsHoldsDynObject,
lowerEnvironmentKey,
lowerNativeFunctionAssignment,
@@ -4419,14 +4418,9 @@ function stdlibGlobalTokenDestructure(
const canonical = ts.symbolName(member) === "global" ? "globalThis" : ts.symbolName(member);
token = `[builtin ${canonical}]`;
alias = canonical;
// Builtin constructors read as their interned function values, and
// crypto as the WebCrypto object: the same values the bare and
// `globalThis.X` spellings read.
value =
canonical === "crypto"
? { kind: "libCall", fn: "webcrypto.object", args: [], type: DYN, loc: locOf(el) }
: (builtinNamespaceValue(lowerer, canonical, locOf(el)) ??
builtinGlobalValue(canonical, locOf(el), true));
// The values the bare and `globalThis.X` spellings read
// (jsGlobalValueByName).
value = jsGlobalValueByName(lowerer, canonical, locOf(el));
} else if (globalName === "console" && !CONSOLE_CALL_MEMBERS.has(propName)) {
token = `[builtin console.${propName}]`;
} else if (TOKEN_OPAQUE_GLOBALS.has(globalName)) {
@@ -12355,11 +12349,8 @@ function lowerForOfString(
* own-keys snapshot, verified against Node) while each visit re-checks
* presence (i < live length), so pops during the body skip exactly
* like Node. Sparse arrays cannot exist here (hole creation traps).
* - globalThis: the harness's leaked-globals sweep — a compiled binary's
* global bindings are compile-time names, never enumerable runtime
* properties, so the enumeration is honestly EMPTY (zero iterations;
* the body typechecked but never evaluates — SEMANTICS.md documents
* the divergence).
* - globalThis: Node 24's enumerable globals in Node's order, then the
* program's own global properties, as keys of a fresh object.
* - JavaScript program classes use their live property-creation table.
* TypeScript and runtime-owned layouts keep the class-instance fence.
* Bindings mirror for-of's: const/let declare the per-iteration string
@@ -12369,10 +12360,12 @@ function lowerForOfString(
function lowerForIn(lowerer: Lowerer, stmt: ts.ForInStatement): IrStmt {
const labels = lowerer.takeLabels();
const loc = locOf(stmt);
if (stdlibGlobalNameOf(lowerer, stmt.expression) === "globalThis") {
return { kind: "block", body: [], loc };
}
let receiver = lowerer.lowerExpr(stmt.expression);
// The global object visits Node's enumerable globals, then the
// program's own global properties (scr_global_for_in_object).
let receiver: IrExpr =
stdlibGlobalNameOf(lowerer, stmt.expression) === "globalThis"
? { kind: "libCall", fn: "global.forInObject", args: [], type: DYN, loc }
: lowerer.lowerExpr(stmt.expression);
if (receiver.type.kind === "object") {
const info = lowerer.classes.get(receiver.type.className);
let root = info;
@@ -1,3 +1,4 @@
import { globalMemberValue } from "./global-values.js";
import { unionNarrowFailureMessage } from "./union-narrow.js";
import {
arrayClassFieldsFn,
@@ -3282,6 +3283,9 @@ export class Lowerer {
/** The property names the program assigns on the global object
* (property-access.ts), collected on first use. */
globalThisWrittenNames: Set<string> | null = null;
/** Set with globalThisWrittenNames: the program writes the global object
* by a computed name somewhere. */
globalThisComputedWrite = false;
/** Well-known symbol methods (`sym:iterator`) the program may replace
* through a symbol-keyed write: an instance or prototype property, or a
* reflection call. */
@@ -3515,6 +3519,9 @@ export class Lowerer {
* values other than fresh literals (noteUntypedIntegrity). */
untypedIntegrity = false;
readonly liftedFns: IrFunction[] = [];
/** global-values.ts's answer per JavaScript source: the receiver of its
* first read of the global object by a computed name, or null. */
readonly computedGlobalReads = new Map<ts.SourceFile, ts.Expression | null>();
/** The first lifted function of each name (liftedFnNamed), for the
* liftedFns indexed so far. liftedFns only grows. */
private readonly liftedFnsByName = new Map<string, IrFunction>();
@@ -15451,11 +15458,12 @@ export class Lowerer {
if (bi.module === "crypto" && bi.member === "webcrypto") {
return { kind: "libCall", fn: "webcrypto.object", args: [], type: DYN, loc };
}
// node:stream/web's exports are the web stream globals themselves: in
// JavaScript sources the same identity token as the global spelling
// (`require("stream/web").ReadableStream === globalThis.ReadableStream`).
if (bi.module === "stream/web" && isJsSourceFile(ts.getSourceFileOfNode(expr))) {
return { kind: "strLit", value: `[builtin ${bi.member}]`, type: STRING, loc };
// Members that are global objects (`require("stream/web").ReadableStream
// === globalThis.ReadableStream`) read in JavaScript sources as their
// global spelling does.
if (isJsSourceFile(ts.getSourceFileOfNode(expr))) {
const global = globalMemberValue(this, bi, loc);
if (global) return global;
}
// A bare `fs.constants` read (not one of the baked bits above).
if (bi.member === "constants" && bi.module === "fs") return fsConstantsObject(loc);
@@ -3736,6 +3736,15 @@ function buildLibFnSignatures() {
"console.dirDyn": { argTypes: [DYN, F64], result: STRING },
/** A native reference to the global object; known names retain value fences. */
"global.native": { argTypes: [arrayOf(STRING)], result: DYN },
/** Installs the program's builtin-global lookup (global-values.ts). */
"global.installValues": { argTypes: [DYN], result: VOID },
/** The keys `for (key in globalThis)` visits, as a fresh object. */
"global.forInObject": { argTypes: [], result: DYN },
/** Node's navigator global (scr_node_builtin.c). */
"navigator.object": { argTypes: [], result: DYN },
"navigator.language": { argTypes: [], result: STRING },
"navigator.languages": { argTypes: [], result: DYN },
"navigator.platform": { argTypes: [], result: STRING },
// `X.name` through a class value: the arg is a program-dependent
// classval (a null slot; the libCall case checks the kind).
/** `X.name` through a class VALUE (scr_object.c): args[0] is a borrowed
@@ -0,0 +1,6 @@
{
"order": [
"<repo>/tests/corpus/8161-global-computed-reads.cjs"
],
"diags": []
}
@@ -0,0 +1,6 @@
{
"order": [
"<repo>/tests/corpus/8162-navigator-global.cjs"
],
"diags": []
}
@@ -0,0 +1,6 @@
{
"order": [
"<repo>/tests/corpus/8163-data-function-values.cjs"
],
"diags": []
}
@@ -0,0 +1,6 @@
{
"order": [
"<repo>/tests/corpus/8164-atob-validation.cjs"
],
"diags": []
}
@@ -0,0 +1,6 @@
{
"order": [
"<repo>/tests/corpus/8168-navigator-language-values.cjs"
],
"diags": []
}
+13
View File
@@ -189,6 +189,7 @@ typedef enum {
X(MIME_PARAMS, 81) /* util.MIMEParams (scr_mime.c) */ \
X(MIME_TYPE, 82) /* util.MIMEType (scr_mime.c) */ \
X(MODULE_NAMESPACE, 79) /* module namespace object read through its exports (scr_module_namespace.c) */ \
X(NAVIGATOR, 83) /* Node's navigator global (scr_node_builtin.c) */ \
X(NET_BLOCK_LIST, 48) /* net.BlockList (scr_net.c) */ \
X(NET_SERVER, 3) /* ScrNetServer — net.Server (http.Server rides the same handle) */ \
X(NET_SOCKET, 2) /* ScrNetSocket — net.Socket */ \
@@ -1300,6 +1301,18 @@ void scr_dyn_obj_delete_symbol_field(ScrDyn *obj, ScrDyn *symbol);
* object answers it; its symbol-keyed properties are the global object's
* (scr_node_builtin.c). */
ScrDyn *scr_global_object(void);
/* The program's builtin-global lookup, a dyn function (borrowed); the first
* installation stands. */
void scr_global_values_install(ScrDyn *lookup);
/* An object whose keys are what `for (key in globalThis)` visits (+1). */
ScrDyn *scr_global_for_in_object(void);
/* Node's navigator global (+1, one box per thread) and its members:
* navigator.language (+1), navigator.languages (+1, one frozen array per
* thread) and navigator.platform (+1). */
ScrDyn *scr_navigator_object(void);
ScrStr *scr_navigator_language(void);
ScrDyn *scr_navigator_languages(void);
ScrStr *scr_navigator_platform(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);
+37 -1
View File
@@ -1975,6 +1975,12 @@ static const char dyn_s32[] SCR_DYN_COMPAT_STR = "";
static const char dyn_s33[] SCR_DYN_COMPAT_STR = "constructing %s%s through a stored value is not supported yet";
static const char dyn_s34[] SCR_DYN_COMPAT_STR = " from a primitive";
static const char dyn_s35[] SCR_DYN_COMPAT_STR = "FinalizationRegistry";
static const char dyn_s36[] SCR_DYN_COMPAT_STR = "atob";
static const char dyn_s37[] SCR_DYN_COMPAT_STR = "btoa";
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 *const dyn_global_ctor_names[] = {
dyn_s26, dyn_s29, dyn_s25, dyn_s30, dyn_s31, dyn_s21, dyn_s22, dyn_s24, dyn_s35,
@@ -2240,7 +2246,36 @@ static ScrClosure *dyn_builtin_fn_deferred(ScrDyn *const *args, size_t argc, siz
#endif
/* atob, btoa and structuredClone as values: the calls the direct
* spellings make, with Node's error for a missing argument. NULL with the
* exception pending, or the result (+1); `*handled` reports whether `name`
* is one of them. */
static ScrDyn *dyn_builtin_data_fn_call(const ScrStr *name, ScrDyn *const *args, size_t argc, bool *handled) {
bool atob = strcmp(name->data, dyn_s36) == 0, btoa = strcmp(name->data, dyn_s37) == 0;
bool clone = strcmp(name->data, dyn_s38) == 0;
*handled = atob || btoa || clone;
if (!*handled) return NULL;
if (argc == 0) {
const char *message = clone ? dyn_s40 : dyn_s39;
scr_throw_error_msg_code(SCR_ERR_TYPE, message, strlen(message), dyn_s41);
return NULL;
}
if (clone) return scr_structured_clone(args[0], argc > 1 ? args[1] : scr_dyn_undefined());
ScrStr *text = atob ? scr_atob(args[0]) : scr_btoa(args[0]);
if (!text) return NULL;
ScrDyn *result = scr_dyn_new_str(text); /* retains */
scr_str_release(text);
return result;
}
static ScrDyn *dyn_builtin_fn_call(ScrClosure *clo, ScrDyn *const *args, size_t argc) {
{
ScrStr *name = scr_box_get_ref(clo->caps[0]);
bool handled;
ScrDyn *result = dyn_builtin_data_fn_call(name, args, argc, &handled);
scr_str_release(name);
if (handled) return result;
}
#ifdef SCR_LIB
(void)clo;
(void)args;
@@ -2299,7 +2334,8 @@ ScrDyn *scr_dyn_builtin_function(const ScrStr *name) {
closure->caps[0] = scr_box_new(SCR_BOX_STR);
scr_box_set_ref(closure->caps[0], scr_str_retain((ScrStr *)name));
const char *dot = memchr(name->data, '.', name->len);
bool two = strcmp(name->data, dyn_s16) == 0 || strcmp(name->data, dyn_s15) == 0;
bool two = strcmp(name->data, dyn_s16) == 0 || strcmp(name->data, dyn_s15) == 0 ||
strcmp(name->data, dyn_s38) == 0;
value = scr_dyn_new_func(closure, dyn_builtin_fn_call, two ? 2 : 1, dyn_s10, dot ? dot + 1 : name->data);
scr_dyn_obj_set(dyn_builtin_fns, name->data, name->len, value); /* the table owns it */
}
+58 -48
View File
@@ -16782,9 +16782,10 @@ ScrError *scr_domex_clone(ScrError *e, const ScrDyn *options) {
* They live HERE (not scr_string.c) because the argument is a dyn value:
* WebIDL ToString runs over the dyn kind (Node's atob(null) decodes the
* string "null"), and the string unit must stay linkable without the
* dyn. atob is forgiving-base64 exactly — ASCII whitespace stripped, a
* %4==0 input sheds up to two trailing '=', %4==1 refuses, leftover
* bits discard — decoding to the latin1 code points as a UTF-8 string;
* dyn. atob checks its input as Node's lib/buffer.js does (an invalid
* character or a misplaced '=' is "Invalid character", a length the
* forgiving-base64 rules refuse "not correctly encoded"), then decodes
* with leftover bits discarded to the latin1 code points as a UTF-8 string;
* btoa refuses any code point over U+00FF. Malformed input throws the
* catchable DOMException InvalidCharacterError with Node's exact
* message. */
@@ -16800,61 +16801,70 @@ static int scr_b64_val(unsigned char c) {
ScrStr *scr_atob(const ScrDyn *data) {
ScrStr *s = scr_dyn_string_coerce(data);
/* Strip ASCII whitespace (the forgiving step). */
char *buf = malloc(s->len ? s->len : 1);
if (!buf) scr_json_oom();
size_t n = 0;
/* Node's checks (lib/buffer.js): only ASCII whitespace and the base64
* alphabet with '=' are allowed, '=' only at the end and at most twice;
* the count of the other characters decides the rest. One pass checks
* the input and keeps the 6-bit value of each alphabet character. */
unsigned char *values = malloc(s->len ? s->len : 1);
if (!values) scr_json_oom();
size_t n = 0, equals = 0;
const char *message = NULL;
for (size_t i = 0; i < s->len; i++) {
unsigned char c = (unsigned char)s->data[i];
if (c == 0x09 || c == 0x0a || c == 0x0c || c == 0x0d || c == 0x20) continue;
buf[n++] = (char)c;
if (c == '=') {
if (++equals > 2) {
message = "Invalid character";
break;
}
continue;
}
int v = scr_b64_val(c);
if (v < 0 || equals) {
message = "Invalid character";
break;
}
values[n++] = (unsigned char)v;
}
if (!message) {
size_t remainder = (n + equals) % 4;
if (!remainder) remainder = n % 4;
else if (equals) message = "Invalid character";
if (!message && remainder == 1) message = "The string to be decoded is not correctly encoded.";
}
scr_str_release(s);
/* A %4==0 input sheds one or two trailing '='. */
if (n % 4 == 0 && n > 0) {
if (buf[n - 1] == '=') n--;
if (n > 0 && buf[n - 1] == '=') n--;
if (message) {
free(values);
scr_throw_domex("InvalidCharacterError", message);
return NULL;
}
if (n % 4 == 1) goto invalid;
/* Decode 6-bit groups; latin1 code points expand to UTF-8 (bytes over
* 0x7F become two-byte sequences). */
{
size_t outBytes = (n / 4) * 3 + (n % 4 == 2 ? 1 : n % 4 == 3 ? 2 : 0);
char *out = malloc(outBytes * 2 ? outBytes * 2 : 1);
if (!out) scr_json_oom();
size_t w = 0;
unsigned acc = 0;
int bits = 0;
for (size_t i = 0; i < n; i++) {
int v = scr_b64_val((unsigned char)buf[i]);
if (v < 0) {
free(out);
goto invalid;
}
acc = (acc << 6) | (unsigned)v;
bits += 6;
if (bits >= 8) {
bits -= 8;
unsigned char b = (unsigned char)((acc >> bits) & 0xff);
if (b < 0x80) {
out[w++] = (char)b;
} else {
out[w++] = (char)(0xc0 | (b >> 6));
out[w++] = (char)(0x80 | (b & 0x3f));
}
* 0x7F become two-byte sequences). Leftover bits discard
* (forgiving-base64's final step). */
size_t outBytes = (n / 4) * 3 + (n % 4 == 2 ? 1 : n % 4 == 3 ? 2 : 0);
char *out = malloc(outBytes * 2 ? outBytes * 2 : 1);
if (!out) scr_json_oom();
size_t w = 0;
unsigned acc = 0;
int bits = 0;
for (size_t i = 0; i < n; i++) {
acc = (acc << 6) | values[i];
bits += 6;
if (bits >= 8) {
bits -= 8;
unsigned char b = (unsigned char)((acc >> bits) & 0xff);
if (b < 0x80) {
out[w++] = (char)b;
} else {
out[w++] = (char)(0xc0 | (b >> 6));
out[w++] = (char)(0x80 | (b & 0x3f));
}
}
/* Leftover bits discard (forgiving-base64's final step). */
free(buf);
ScrStr *result = scr_str_new(out, w);
free(out);
return result;
}
invalid:
free(buf);
scr_throw_domex("InvalidCharacterError",
"The string to be decoded is not correctly encoded.");
return NULL;
free(values);
ScrStr *result = scr_str_new(out, w);
free(out);
return result;
}
ScrStr *scr_btoa(const ScrDyn *data) {
+304
View File
@@ -97,6 +97,10 @@ static SCR_TL const ScrArr *scr_global_registered;
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);
/* The program's builtin-global lookup (scr_global_values_install), or
* NULL: a dyn function from a name to the value its static spelling reads,
* undefined for the opaque identity token, or null for a name without one. */
static SCR_TL ScrDyn *scr_global_values;
void scr_global_fetch_install(ScrDyn *(*get)(void)) { scr_global_fetch_get = get; }
@@ -200,6 +204,13 @@ static void scr_global_cleanup(void) {
static void *scr_global_retain(void *handle) { return handle; }
static void scr_global_release(void *handle) { (void)handle; }
/* A dyn string holding `s` (+1); takes over the caller's reference. */
static ScrDyn *scr_global_take_str(ScrStr *s) {
ScrDyn *value = scr_dyn_new_str(s);
scr_str_release(s);
return value;
}
static ScrDyn *scr_global_get(void *handle, const char *key, size_t length) {
(void)handle;
ScrDyn *own = scr_global_own ? scr_dyn_obj_get(scr_global_own, key, length) : NULL;
@@ -210,10 +221,29 @@ static ScrDyn *scr_global_get(void *handle, const char *key, size_t length) {
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();
if (length == 9 && memcmp(key, "navigator", length) == 0) return scr_navigator_object();
if (length == 9 && memcmp(key, "undefined", length) == 0) return scr_dyn_undefined();
if (length == 3 && memcmp(key, "NaN", length) == 0) return scr_dyn_new_num(NAN);
if (length == 8 && memcmp(key, "Infinity", length) == 0) return scr_dyn_new_num(INFINITY);
if (!scr_dyn_obj_get(scr_global_known, key, length)) return scr_dyn_undefined();
if (scr_global_values) {
ScrDyn *name = scr_global_take_str(scr_str_new(key, length));
ScrDyn *value = scr_global_values->v.fn.thunk(scr_global_values->v.fn.clo, &name, 1);
scr_dyn_release(name);
if (!value) return NULL;
if (value->kind == SCR_DYN_UNDEF) {
/* The opaque identity token a JavaScript source reads for the name. */
scr_dyn_release(value);
ScrJsonBuf token;
scr_jb_init(&token);
scr_jb_puts(&token, "[builtin ");
for (size_t i = 0; i < length; i++) scr_jb_putc(&token, key[i]);
scr_jb_putc(&token, ']');
return scr_global_take_str(scr_jb_finish(&token));
}
if (value->kind != SCR_DYN_NULL) return value;
scr_dyn_release(value);
}
ScrJsonBuf text;
scr_jb_init(&text);
scr_jb_puts(&text, "reading globalThis.");
@@ -250,6 +280,47 @@ ScrDyn *scr_global_object(void) {
return scr_global_box;
}
/* Installs the program's builtin-global lookup (borrowed): reads of
* builtin names through the global object answer what their static
* spellings read. The first installation stands; modules that read the
* global object by computed names each install the same function. */
static void scr_global_values_cleanup(void) {
if (scr_global_values) scr_dyn_release(scr_global_values);
scr_global_values = NULL;
}
void scr_global_values_install(ScrDyn *lookup) {
if (scr_global_values || lookup->kind != SCR_DYN_FUNC) return;
scr_global_values = scr_dyn_retain(lookup);
scr_atexit(scr_global_values_cleanup);
}
/* What `for (key in globalThis)` visits (+1): Node 24's enumerable global
* properties in its order, then the program's own global properties in
* creation order, as keys of a fresh object. */
ScrDyn *scr_global_for_in_object(void) {
static const char *const node_names[] = {
"global", "clearImmediate", "setImmediate", "clearInterval", "clearTimeout",
"setInterval", "setTimeout", "queueMicrotask", "structuredClone", "atob",
"btoa", "performance", "fetch", "crypto", "navigator",
};
ScrDyn *object = scr_dyn_new_obj();
for (size_t i = 0; i < sizeof node_names / sizeof node_names[0]; i++)
scr_dyn_obj_set(object, node_names[i], strlen(node_names[i]), scr_dyn_undefined());
if (scr_global_own) {
ScrDyn *keys = scr_dyn_obj_keys(scr_global_own);
double count = scr_dyn_arr_len(keys);
for (double i = 0; i < count; i++) {
ScrDyn *key = scr_dyn_arr_at(keys, i);
if (key->kind == SCR_DYN_STR && !scr_dyn_obj_get(object, key->v.str->data, key->v.str->len))
scr_dyn_obj_set(object, key->v.str->data, key->v.str->len, scr_dyn_undefined());
scr_dyn_release(key);
}
scr_dyn_release(keys);
}
return object;
}
ScrDyn *scr_global_native_init(ScrArr *known, ScrDyn *(*console_get)(void)) {
/* The compiler passes one immortal name table per program: once it is
* registered, later reads of the global object only take a reference. */
@@ -388,3 +459,236 @@ ScrDyn *scr_process_builtin_unsupported(ScrStr *id, ScrStr *member) {
scr_str_release(message);
return NULL;
}
/* ── Node's navigator global (lib/internal/navigator.js) ─────────────────
* hardwareConcurrency is os.availableParallelism(), language the default
* locale as Node's ICU answers it from the environment, languages a frozen
* one-element array of it, platform Node's name for the process platform
* and architecture, and userAgent `Node.js/<major>`. locks has no lowering.
* The members are getters on Navigator.prototype, so the object owns no
* properties: Object.keys answers [] and JSON.stringify `{}`. */
static SCR_TL ScrDyn *scr_navigator_box;
static SCR_TL ScrDyn *scr_navigator_langs;
static SCR_TL ScrStr *scr_navigator_lang;
static SCR_TL bool scr_navigator_cleanup_registered;
static void scr_navigator_cleanup(void);
/* Registers the release of the cached navigator values at exit, once per
* thread: a program can read navigator.language or navigator.languages
* without ever taking the navigator object. */
static void scr_navigator_keep(void) {
if (scr_navigator_cleanup_registered) return;
scr_navigator_cleanup_registered = true;
scr_atexit(scr_navigator_cleanup);
}
static void scr_navigator_refuse(const char *what) {
ScrJsonBuf text;
scr_jb_init(&text);
scr_jb_puts(&text, "'");
scr_jb_puts(&text, what);
scr_jb_puts(&text, "' is part of the standard library types but has no scriptc lowering yet [SC2020]");
ScrStr *message = scr_jb_finish(&text);
scr_throw_error_msg_code(SCR_ERR_ERROR, message->data, message->len, "SC2020");
scr_str_release(message);
}
static bool scr_navigator_alpha(char c) { return (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z'); }
static bool scr_navigator_digit(char c) { return c >= '0' && c <= '9'; }
static char scr_navigator_lower(char c) { return c >= 'A' && c <= 'Z' ? (char)(c + 32) : c; }
static char scr_navigator_upper(char c) { return c >= 'a' && c <= 'z' ? (char)(c - 32) : c; }
/* The BCP 47 tag of the POSIX locale ID `id`, as ICU derives the default
* locale: `language[_TERRITORY][.codeset][@modifier]`, the codeset
* dropped, the C and POSIX locales and en_US_POSIX as en-US, the ISO 639
* codes ICU replaces, and the modifier as a variant (5 to 8 characters) or
* an lvariant private-use subtag (1 to 4). NULL for an ID outside that
* grammar, whose tag needs ICU's tables. */
static ScrStr *scr_navigator_tag(const char *id) {
if (!id) return scr_str_new("en-US", 5);
const char *end = id + strlen(id);
const char *at = strchr(id, '@');
const char *dot = strchr(id, '.');
const char *base_end = dot && (!at || dot < at) ? dot : at ? at : end;
const char *modifier = at ? at + 1 : NULL;
size_t base_len = (size_t)(base_end - id);
if (base_len == 0 && !modifier) return scr_str_new("und", 3);
if ((base_len == 1 && id[0] == 'C') || (base_len == 5 && memcmp(id, "POSIX", 5) == 0)) {
if (modifier) return NULL;
return scr_str_new("en-US", 5);
}
const char *underscore = memchr(id, '_', base_len);
size_t lang_len = underscore ? (size_t)(underscore - id) : base_len;
const char *region = underscore ? underscore + 1 : NULL;
size_t region_len = region ? (size_t)(base_end - region) : 0;
if (region && memchr(region, '_', region_len)) {
/* en_US_POSIX is ICU's name for the C locale. */
if (lang_len == 2 && scr_navigator_lower(id[0]) == 'e' && scr_navigator_lower(id[1]) == 'n' &&
region_len == 8 && scr_navigator_upper(region[0]) == 'U' && scr_navigator_upper(region[1]) == 'S' &&
region[2] == '_') {
bool posix = true;
for (size_t i = 0; i < 5; i++) posix = posix && scr_navigator_upper(region[3 + i]) == "POSIX"[i];
if (posix && !modifier) return scr_str_new("en-US", 5);
}
return NULL;
}
for (size_t i = 0; i < lang_len; i++)
if (!scr_navigator_alpha(id[i])) return NULL;
if (lang_len == 1 || lang_len == 4 || lang_len > 8) return NULL;
ScrJsonBuf tag;
scr_jb_init(&tag);
char lang[9] = {0};
for (size_t i = 0; i < lang_len; i++) lang[i] = scr_navigator_lower(id[i]);
static const char *const replaced[][2] = {
{"in", "id"}, {"iw", "he"}, {"ji", "yi"}, {"jw", "jv"}, {"mo", "ro"},
};
const char *language = lang_len == 0 ? "und" : lang;
for (size_t i = 0; i < sizeof replaced / sizeof replaced[0]; i++)
if (!strcmp(lang, replaced[i][0])) language = replaced[i][1];
scr_jb_puts(&tag, language);
if (region_len > 0) {
bool alpha2 = region_len == 2 && scr_navigator_alpha(region[0]) && scr_navigator_alpha(region[1]);
bool digits3 = region_len == 3 && scr_navigator_digit(region[0]) && scr_navigator_digit(region[1]) &&
scr_navigator_digit(region[2]);
if (!alpha2 && !digits3) {
scr_str_release(scr_jb_finish(&tag));
return NULL;
}
scr_jb_putc(&tag, '-');
for (size_t i = 0; i < region_len; i++) scr_jb_putc(&tag, scr_navigator_upper(region[i]));
}
if (modifier && *modifier) {
size_t length = (size_t)(end - modifier);
bool alnum = length <= 8;
for (size_t i = 0; i < length && alnum; i++)
alnum = scr_navigator_alpha(modifier[i]) || scr_navigator_digit(modifier[i]);
if (!alnum || (length == 7 && !memcmp(modifier, "nynorsk", 7))) {
scr_str_release(scr_jb_finish(&tag));
return NULL;
}
scr_jb_puts(&tag, length >= 5 ? "-" : "-x-lvariant-");
for (size_t i = 0; i < length; i++) scr_jb_putc(&tag, scr_navigator_lower(modifier[i]));
}
return scr_jb_finish(&tag);
}
ScrStr *scr_navigator_language(void) {
if (!scr_navigator_lang) {
/* ICU reads LC_ALL, then LC_MESSAGES, then LANG; a variable that is
* set, even to the empty string, decides. */
const char *id = getenv("LC_ALL");
if (!id) id = getenv("LC_MESSAGES");
if (!id) id = getenv("LANG");
ScrStr *tag = scr_navigator_tag(id);
if (!tag) {
scr_navigator_refuse("navigator.language for this locale");
return NULL;
}
scr_navigator_lang = tag;
scr_navigator_keep();
}
return scr_str_retain(scr_navigator_lang);
}
ScrDyn *scr_navigator_languages(void) {
if (!scr_navigator_langs) {
ScrStr *language = scr_navigator_language();
if (!language) return NULL;
ScrDyn *languages = scr_dyn_new_arr();
scr_dyn_arr_push(languages, scr_global_take_str(language));
scr_dyn_release(scr_dyn_freeze_data(languages));
scr_navigator_langs = languages;
scr_navigator_keep();
}
return scr_dyn_retain(scr_navigator_langs);
}
ScrStr *scr_navigator_platform(void) {
ScrStr *platform = scr_process_platform();
ScrStr *arch = scr_process_arch();
const char *p = platform->data, *a = arch->data;
ScrJsonBuf text;
scr_jb_init(&text);
bool ia32 = !strcmp(a, "ia32"), x64 = !strcmp(a, "x64");
if (!strcmp(p, "darwin")) scr_jb_puts(&text, "MacIntel");
else if (!strcmp(p, "win32")) scr_jb_puts(&text, "Win32");
else if (!strcmp(p, "linux")) {
scr_jb_puts(&text, ia32 ? "Linux i686" : x64 ? "Linux x86_64" : "Linux ");
if (!ia32 && !x64) scr_jb_puts(&text, a);
} else if (!strcmp(p, "freebsd") || !strcmp(p, "openbsd")) {
scr_jb_puts(&text, p[0] == 'f' ? "FreeBSD " : "OpenBSD ");
scr_jb_puts(&text, ia32 ? "i386" : x64 ? "amd64" : a);
} else if (!strcmp(p, "sunos")) {
scr_jb_puts(&text, "SunOS ");
scr_jb_puts(&text, ia32 ? "i86pc" : a);
} else if (!strcmp(p, "aix")) scr_jb_puts(&text, "AIX");
else {
scr_jb_putc(&text, scr_navigator_upper(p[0]));
if (p[0]) scr_jb_puts(&text, p + 1);
scr_jb_putc(&text, ' ');
scr_jb_puts(&text, a);
}
scr_str_release(platform);
scr_str_release(arch);
return scr_jb_finish(&text);
}
static ScrDyn *scr_navigator_get(void *handle, const char *key, size_t length) {
(void)handle;
if (length == 19 && !memcmp(key, "hardwareConcurrency", length))
return scr_dyn_new_num(scr_os_available_parallelism());
if (length == 8 && !memcmp(key, "language", length)) {
ScrStr *language = scr_navigator_language();
return language ? scr_global_take_str(language) : NULL;
}
if (length == 9 && !memcmp(key, "languages", length)) return scr_navigator_languages();
if (length == 8 && !memcmp(key, "platform", length)) return scr_global_take_str(scr_navigator_platform());
if (length == 9 && !memcmp(key, "userAgent", length)) {
ScrStr *version = scr_process_versions_node();
const char *dot = memchr(version->data, '.', version->len);
size_t major = dot ? (size_t)(dot - version->data) : version->len;
ScrJsonBuf text;
scr_jb_init(&text);
scr_jb_puts(&text, "Node.js/");
for (size_t i = 0; i < major; i++) scr_jb_putc(&text, version->data[i]);
scr_str_release(version);
return scr_global_take_str(scr_jb_finish(&text));
}
if (length == 5 && !memcmp(key, "locks", length)) {
scr_navigator_refuse("navigator.locks");
return NULL;
}
return NULL;
}
static ScrDyn *scr_navigator_inspect_view(void *handle) {
(void)handle;
return scr_dyn_new_obj();
}
static void scr_navigator_cleanup(void) {
if (scr_navigator_box) scr_dyn_release(scr_navigator_box);
if (scr_navigator_langs) scr_dyn_release(scr_navigator_langs);
if (scr_navigator_lang) scr_str_release(scr_navigator_lang);
scr_navigator_box = NULL;
scr_navigator_langs = NULL;
scr_navigator_lang = NULL;
scr_navigator_cleanup_registered = false;
}
ScrDyn *scr_navigator_object(void) {
static const ScrDynHandleOps ops = {
.cls = "Navigator", .retain = scr_global_retain, .release = scr_global_release,
.get = scr_navigator_get, .inspect_view = scr_navigator_inspect_view,
.json_empty_object = true, .inspect_view_empty = true,
};
if (!scr_navigator_box) {
scr_dyn_handle_install(SCR_DYNH_NAVIGATOR, &ops);
static SCR_TL char token;
scr_navigator_box = scr_dyn_new_handle(&token, SCR_DYNH_NAVIGATOR);
scr_navigator_keep();
}
return scr_dyn_retain(scr_navigator_box);
}
@@ -0,0 +1,98 @@
// @no-node-shims
// Builtin globals read through the global object by computed names, and
// builtin module members that are global objects: each answers what the
// global's own spelling reads, so identity holds across the spellings.
// for-in over the global object visits Node's enumerable globals first and
// the program's own global properties after them.
"use strict";
const { atob: bufferAtob, btoa: bufferBtoa } = require("buffer");
const timers = require("timers");
const { performance: perf } = require("perf_hooks");
const util = require("util");
const url = require("url");
const { MessageChannel } = require("worker_threads");
const web = require("stream/web");
const names = [
"setTimeout",
"setInterval",
"setImmediate",
"clearTimeout",
"clearInterval",
"clearImmediate",
"queueMicrotask",
"global",
"globalThis",
"fetch",
"Map",
"Object",
"Promise",
"Buffer",
"crypto",
];
for (const name of names) {
const value = globalThis[name];
console.log(name, typeof value, value === globalThis[name]);
}
console.log(
globalThis["setTimeout"] === setTimeout,
globalThis["Map"] === Map,
globalThis["crypto"] === crypto,
globalThis["Buffer"] === Buffer,
);
console.log(timers.setTimeout === setTimeout, timers.clearInterval === globalThis["clearInterval"]);
console.log(
bufferAtob === atob,
bufferBtoa === globalThis["btoa"],
perf === performance,
util.TextEncoder === TextEncoder,
url.URL === URL,
MessageChannel === globalThis["MessageChannel"],
web.ReadableStream === ReadableStream,
);
console.log(globalThis["noSuchGlobal"], typeof globalThis["Temporal"]);
// A snapshot of the global object in a function reads the same values.
function readThroughSnapshot(name) {
const g = globalThis;
return g[name];
}
console.log(
readThroughSnapshot("setTimeout") === setTimeout,
readThroughSnapshot("Map") === Map,
typeof readThroughSnapshot("structuredClone"),
readThroughSnapshot("noSuchGlobal"),
);
const pick = (name) => globalThis[name];
console.log(pick("Math") === Math, pick("JSON") === JSON, pick("Reflect") === Reflect, pick("Math").max(1, 3));
const before = [];
for (const key in globalThis) before.push(key);
globalThis.ownA = 1;
globalThis["own" + "B"] = { b: 2 };
const after = [];
for (const key in globalThis) after.push(key);
console.log(after.length - before.length, after.slice(-2).join(","));
console.log(before.slice(0, 9).join(","));
console.log(globalThis.ownA, globalThis.ownB.b, globalThis["ownB"] === globalThis.ownB);
// The harness's leak check: every enumerated global is a known value.
const known = new Set([
global,
clearImmediate,
setImmediate,
clearInterval,
clearTimeout,
setInterval,
setTimeout,
queueMicrotask,
structuredClone,
bufferAtob,
bufferBtoa,
performance,
fetch,
crypto,
globalThis.ownA,
globalThis.ownB,
]);
const leaked = after.filter((key) => key !== "navigator" && !known.has(globalThis[key]));
console.log("leaked", leaked.length);
+47
View File
@@ -0,0 +1,47 @@
// @no-node-shims
// Node 24's navigator global: its members are getters on
// Navigator.prototype answering the process's values, and the object
// itself owns no properties. It is one of the global object's enumerable
// properties.
"use strict";
const os = require("os");
const util = require("util");
console.log(typeof navigator, navigator.userAgent);
console.log(navigator.hardwareConcurrency === os.availableParallelism());
console.log(typeof navigator.language, navigator.language.length > 0);
console.log(navigator.languages.length, navigator.languages[0] === navigator.language);
console.log(navigator.languages === navigator.languages, Object.isFrozen(navigator.languages));
const platforms = {
linux: { x64: "Linux x86_64", arm64: "Linux arm64" },
darwin: { x64: "MacIntel", arm64: "MacIntel" },
};
console.log(navigator.platform === platforms[process.platform]?.[process.arch]);
const value = navigator;
console.log(value === navigator, value === globalThis.navigator, value === globalThis["navigator"]);
console.log(value.userAgent, value.platform === navigator.platform, value.missing);
console.log(Object.keys(value), JSON.stringify(value), util.inspect(value));
console.log(String(value), Object.prototype.toString.call(value));
const keys = [];
for (const key in globalThis) keys.push(key);
console.log(keys.join(","));
const known = new Set([
global,
clearImmediate,
setImmediate,
clearInterval,
clearTimeout,
setInterval,
setTimeout,
queueMicrotask,
structuredClone,
atob,
btoa,
performance,
fetch,
crypto,
navigator,
]);
console.log("leaked", keys.filter((key) => !known.has(globalThis[key])).length);
@@ -0,0 +1,32 @@
// atob, btoa and structuredClone as JavaScript values: function values
// whose calls run what the direct calls run, the same value through every
// spelling.
"use strict";
const buffer = require("buffer");
const fns = [atob, btoa, structuredClone];
for (const fn of fns) console.log(typeof fn, fn.name, fn.length);
console.log(buffer.atob === atob, buffer.btoa === globalThis.btoa, globalThis["structuredClone"] === structuredClone);
const pick = (name) => ({ atob, btoa, structuredClone })[name];
const encode = pick("btoa");
const decode = pick("atob");
console.log(encode("scriptc"), decode(encode("scriptc")), decode("aGk="));
const clone = pick("structuredClone");
const original = { list: [1, { deep: true }], text: "t" };
const copy = clone(original);
console.log(copy !== original, copy.list[1].deep, copy.list !== original.list, copy.text);
const report = (label, run) => {
try {
console.log(label, run());
} catch (error) {
console.log(label, error.name, error.code, error.message);
}
};
report("atob()", () => decode());
report("btoa()", () => encode());
report("structuredClone()", () => clone());
report("atob invalid", () => decode("a"));
report("btoa wide", () => encode("Ā"));
report("clone symbol", () => clone(Symbol("s")));
+35
View File
@@ -0,0 +1,35 @@
// atob checks its input as Node does: only ASCII whitespace and the base64
// alphabet are allowed, '=' only at the end and at most twice, and a
// length the forgiving-base64 rules refuse is a different error.
"use strict";
const inputs = [
"@@@",
"a",
"ab",
"ab=",
"ab==",
"ab=c",
"abc==",
"a===",
"ab===",
" Y W J j ",
"YQ",
"Y Q = =",
"YWJjZA==",
"YWJjZA=",
"\tYW\nJj\fZA\r==",
"é",
"YW-J",
"",
];
const decode = (label, input) => {
try {
console.log(label, JSON.stringify(atob(input)));
} catch (error) {
console.log(label, error.name, error.code, error.message);
}
};
for (const input of inputs) decode(JSON.stringify(input), input);
decode("null", null);
decode("undefined", undefined);
decode("12", 12);
@@ -0,0 +1,8 @@
// @no-node-shims
// navigator.language, navigator.languages and navigator.platform read
// without the navigator object ever being taken as a value: the runtime
// caches the answers and releases them at exit.
"use strict";
console.log(typeof navigator.language, navigator.language.length > 0);
console.log(navigator.languages.length, navigator.languages[0] === navigator.language);
console.log(Object.isFrozen(navigator.languages), typeof navigator.platform);
+4 -4
View File
@@ -108,8 +108,8 @@ async function publicKeyWithWebCrypto(key: import("node:crypto").webcrypto.Crypt
await crypto.subtle.getPublicKey(key, ["verify"]);
}
void publicKeyWithWebCrypto;
/* Node's navigator global: typeof navigator and navigator.userAgent
* compile; the other members fence by name. */
const cores = navigator.hardwareConcurrency;
void cores;
/* Node's navigator global: navigator.locks (the Web Locks API) fences
* by name. */
const locks = navigator.locks;
void locks;
// End of the declared-but-not-lowered surface.
+5 -1
View File
@@ -1,6 +1,10 @@
/* Node 24's navigator global under @types/node: the runtime probe
* libraries use (jose's user-agent check) folds to Node's values. */
* libraries use (jose's user-agent check) folds to Node's values, and the
* other members read the process's values. */
const browser = !navigator.userAgent.startsWith("Mozilla/5.0 ");
console.log(typeof navigator, navigator.userAgent, browser);
console.log(typeof navigator !== "undefined" && navigator.userAgent === "Cloudflare-Workers");
console.log(navigator.userAgent.split("/")[0], navigator.userAgent.length);
console.log(navigator.hardwareConcurrency > 0, Number.isInteger(navigator.hardwareConcurrency));
console.log(typeof navigator.language, navigator.languages.length, navigator.languages[0] === navigator.language);
console.log(navigator.platform.length > 0, navigator.languages === navigator.languages);
@@ -232,11 +232,29 @@ fenced.ts:108:9 - error SC2020: 'crypto.subtle.getPublicKey' is part of the stan
hint: the native WebCrypto surface is crypto.subtle's importKey, generateKey and exportKey for HMAC and AES keys (importKey also for PBKDF2 and HKDF keys), encrypt and decrypt with AES-GCM, AES-CBC or AES-CTR, wrapKey and unwrapKey of secret keys with AES-KW or those ciphers, deriveBits and deriveKey with PBKDF2 or HKDF, sign and verify with HMAC, generateKey, importKey, exportKey, sign and verify for ECDSA, Ed25519, RSASSA-PKCS1-v1_5 and RSA-PSS, and digest with SHA-1, SHA-256, SHA-384 or SHA-512, each called directly on crypto.subtle
fenced.ts:113:15 - error SC2020: 'navigator.hardwareConcurrency' is typed by @types/node but has no scriptc lowering yet
fenced.ts:113:7 - error SC2020: 'LockManager' is typed by @types/node but has no scriptc lowering yet
112 | * compile; the other members fence by name. */
113 | const cores = navigator.hardwareConcurrency;
| ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~
114 | void cores;
112 | * by name. */
113 | const locks = navigator.locks;
| ^~~~~
114 | void locks;
hint: of Node's navigator global only typeof navigator and navigator.userAgent compile
hint: the type checker sees everything @types/node declares, but only the supported surface compiles (https://scriptc.dev/limitations)
fenced.ts:113:15 - error SC2020: 'navigator.locks' is typed by @types/node but has no scriptc lowering yet
112 | * by name. */
113 | const locks = navigator.locks;
| ^~~~~~~~~~~~~~~
114 | void locks;
hint: navigator.locks (the Web Locks API) has no lowering
fenced.ts:114:6 - error SC2004: uses of 'locks' inherit the blocker on its declaration
113 | const locks = navigator.locks;
114 | void locks;
| ^~~~~
115 | // End of the declared-but-not-lowered surface.
hint: the declaration of 'locks' did not compile — fix the diagnostic reported there and these sites clear with it
@@ -0,0 +1 @@
{ "plain": 3708, "san": 12618 }
@@ -0,0 +1 @@
{ "plain": 3686, "san": 6899 }
@@ -0,0 +1 @@
{ "plain": 3362, "san": 7680 }
@@ -0,0 +1 @@
{ "plain": 3065, "san": 4575 }
@@ -0,0 +1 @@
{ "plain": 3104, "san": 4714 }
+2 -3
View File
@@ -50,7 +50,6 @@ const KNOWN: Record<string, Known> = {
"esbuild-require": { build: "SC2013" },
"esm-missing-default": { build: "SC1090" },
"esm-missing-export": { build: "SC1090" },
"fetch-destructure": { run: "SC1031" },
"fs-shims": { run: "SC1101" },
"import-refusal": { build: "SC2020" },
"island-registry": { build: "SC2011" },
@@ -58,9 +57,9 @@ const KNOWN: Record<string, Known> = {
divergence:
"Timeout handles are numbers (typeof) and lose their ref flag once fired or cleared",
},
"island-web-plumbing": { run: "SC2020" },
"island-web-plumbing": { run: "SC1090" },
"lazy-traps": { build: "SC2013" },
"misc-shims": { run: "SC2020" },
"misc-shims": { run: "SC1090" },
"namespace": { build: "SC1013" },
"node24-async-events": { run: "SC2020" },
"node24-commonjs-metadata": { run: "SC2020" },