mirror of
https://github.com/vercel-labs/scriptc.git
synced 2026-11-01 07:33:12 +08:00
Read builtin globals through the global object, and support Node's navigator (#2242)
This commit is contained in:
@@ -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.
|
||||
|
||||
@@ -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"
|
||||
},
|
||||
|
||||
@@ -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
|
||||
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/8161-global-computed-reads.cjs"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/8162-navigator-global.cjs"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/8163-data-function-values.cjs"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/8164-atob-validation.cjs"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/8168-navigator-language-values.cjs"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
@@ -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 */
|
||||
}
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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);
|
||||
@@ -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")));
|
||||
@@ -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);
|
||||
Vendored
+4
-4
@@ -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
@@ -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 }
|
||||
@@ -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" },
|
||||
|
||||
Reference in New Issue
Block a user