Merge main

This commit is contained in:
Chris Tate
2026-10-19 17:06:58 -05:00
90 changed files with 2681 additions and 198 deletions
@@ -0,0 +1,7 @@
{
"schemaVersion": 3,
"nodeVersion": "24.22.0",
"nodeCommit": "f14cd3aa7bf9ca8df2cc5b8bb14beb0f335b1d93",
"artifactVersion": "ee72820d45a79aa154f0",
"rowCount": 3880
}
+18 -18
View File
@@ -208,10 +208,10 @@
"entries": 152,
"apiEntries": 152,
"static": {
"supported": 5,
"supported": 6,
"partial": 39,
"refused": 21,
"not-implemented": 87,
"not-implemented": 86,
"by-design": 0,
"unreviewed": 0,
"not-applicable": 0
@@ -314,9 +314,9 @@
"apiEntries": 280,
"static": {
"supported": 7,
"partial": 198,
"partial": 200,
"refused": 0,
"not-implemented": 75,
"not-implemented": 73,
"by-design": 0,
"unreviewed": 0,
"not-applicable": 0
@@ -853,10 +853,10 @@
"entries": 156,
"apiEntries": 156,
"static": {
"supported": 1,
"supported": 2,
"partial": 21,
"refused": 0,
"not-implemented": 134,
"not-implemented": 133,
"by-design": 0,
"unreviewed": 0,
"not-applicable": 0
@@ -12325,9 +12325,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 by a dedicated static compiler/runtime path.",
"verification": "test-backed"
}
},
{
@@ -30288,9 +30288,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 call shapes accepted by the compiler lowering.",
"verification": "test-backed"
}
},
{
@@ -35256,9 +35256,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 call shapes accepted by the compiler lowering.",
"verification": "test-backed"
}
},
{
@@ -80183,9 +80183,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 by a dedicated static compiler/runtime path.",
"verification": "test-backed"
}
},
{
@@ -3,19 +3,19 @@
"nodeVersion": "24.22.0",
"nodeCommit": "f14cd3aa7bf9ca8df2cc5b8bb14beb0f335b1d93",
"summary": {
"taskCount": 3455,
"tierItemCount": 3455,
"taskCount": 3453,
"tierItemCount": 3453,
"tiers": {
"static": {
"audit-partial": 1177,
"verify-gap": 2150,
"audit-partial": 1179,
"verify-gap": 2146,
"replace-refusal": 41,
"classify": 2,
"implement": 85
}
},
"priorities": {
"high": 2289,
"high": 2287,
"normal": 694,
"low": 472
}
@@ -7359,31 +7359,6 @@
}
}
},
{
"id": "crypto:2ab8394e29a7",
"chapter": "crypto",
"label": "crypto.getRandomValues(typedArray)",
"signature": "`crypto.getRandomValues(typedArray)`",
"apiSymbol": "crypto.getRandomValues",
"docsUrl": "https://nodejs.org/docs/v24.22.0/api/crypto.html#cryptogetrandomvaluestypedarray",
"nodeStability": {
"index": "2",
"level": 2,
"text": "Stable",
"inherited": true
},
"priority": "high",
"tiers": {
"static": {
"status": "not-implemented",
"action": "verify-gap",
"verification": "registry-gap",
"confidence": "low",
"source": "compiler-unmatched:crypto",
"tests": []
}
}
},
{
"id": "crypto:21f22c71fb61",
"chapter": "crypto",
@@ -27067,12 +27042,15 @@
"priority": "high",
"tiers": {
"static": {
"status": "not-implemented",
"action": "verify-gap",
"verification": "registry-gap",
"confidence": "low",
"source": "compiler-unmatched:fs",
"tests": []
"status": "partial",
"action": "audit-partial",
"verification": "test-backed",
"confidence": "high",
"source": "surface-manifest:node-builtin.fs.constants",
"tests": [
"tests/corpus/6207-fs-constants-object.ts",
"tests/corpus/6208-fs-constants-require.cjs"
]
}
}
},
@@ -32764,12 +32742,15 @@
"priority": "high",
"tiers": {
"static": {
"status": "not-implemented",
"action": "verify-gap",
"verification": "registry-gap",
"confidence": "low",
"source": "compiler-unmatched:fs",
"tests": []
"status": "partial",
"action": "audit-partial",
"verification": "test-backed",
"confidence": "high",
"source": "surface-manifest:node-builtin.fs.constants",
"tests": [
"tests/corpus/6207-fs-constants-object.ts",
"tests/corpus/6208-fs-constants-require.cjs"
]
}
}
},
@@ -81018,31 +80999,6 @@
}
}
},
{
"id": "webcrypto:dcc24f369f43",
"chapter": "webcrypto",
"label": "crypto.getRandomValues(typedArray)",
"signature": "`crypto.getRandomValues(typedArray)`",
"apiSymbol": "crypto.getRandomValues",
"docsUrl": "https://nodejs.org/docs/v24.22.0/api/webcrypto.html#cryptogetrandomvaluestypedarray",
"nodeStability": {
"index": "2",
"level": 2,
"text": "Stable",
"inherited": true
},
"priority": "high",
"tiers": {
"static": {
"status": "not-implemented",
"action": "verify-gap",
"verification": "registry-gap",
"confidence": "low",
"source": "compiler-unmatched:webcrypto",
"tests": []
}
}
},
{
"id": "webcrypto:8c570ec75666",
"chapter": "webcrypto",
@@ -190,10 +190,10 @@
"entries": 179,
"apiEntries": 152,
"static": {
"supported": 5,
"supported": 6,
"partial": 39,
"refused": 21,
"not-implemented": 87,
"not-implemented": 86,
"by-design": 0,
"unreviewed": 0,
"not-applicable": 0
@@ -326,9 +326,9 @@
"apiEntries": 280,
"static": {
"supported": 7,
"partial": 198,
"partial": 200,
"refused": 0,
"not-implemented": 75,
"not-implemented": 73,
"by-design": 0,
"unreviewed": 0,
"not-applicable": 0
@@ -910,10 +910,10 @@
"entries": 176,
"apiEntries": 156,
"static": {
"supported": 1,
"supported": 2,
"partial": 21,
"refused": 0,
"not-implemented": 134,
"not-implemented": 133,
"by-design": 0,
"unreviewed": 0,
"not-applicable": 0
@@ -19872,6 +19872,8 @@
"tests/corpus/5762-crypto-hash-stream.cjs",
"tests/corpus/5768-crypto-hash-unknown.ts",
"tests/corpus/5769-crypto-hash-unknown.cjs",
"tests/corpus/6204-crypto-get-random-values.ts",
"tests/corpus/6205-crypto-get-random-values.js",
"tests/corpus/node24-crypto-kdf-errors.ts",
"tests/corpus/node24-crypto-kdf-options.ts",
"tests/corpus/node24-crypto-kdf.ts",
@@ -21028,8 +21030,12 @@
"inherited": true
},
"static": {
"status": "not-implemented",
"evidence": "compiler-unmatched:crypto"
"status": "supported",
"evidence": "compiler-dedicated:crypto.getRandomValues",
"tests": [
"tests/corpus/6204-crypto-get-random-values.ts",
"tests/corpus/6205-crypto-get-random-values.js"
]
},
"anchorSource": "exact"
},
@@ -44138,6 +44144,8 @@
"tests/corpus/5568-fs-cp-errors.js",
"tests/corpus/5569-fs-cp-symlinks.ts",
"tests/corpus/5570-fs-cp-async.ts",
"tests/corpus/6207-fs-constants-object.ts",
"tests/corpus/6208-fs-constants-require.cjs",
"tests/corpus/fs-native-realpath.ts",
"tests/corpus/node24-fs-links-callbacks.cjs",
"tests/corpus/node24-fs-links-errors.cjs",
@@ -45925,8 +45933,12 @@
"inherited": true
},
"static": {
"status": "not-implemented",
"evidence": "compiler-unmatched:fs"
"status": "partial",
"evidence": "surface-manifest:node-builtin.fs.constants",
"tests": [
"tests/corpus/6207-fs-constants-object.ts",
"tests/corpus/6208-fs-constants-require.cjs"
]
},
"anchorSource": "ancestor"
},
@@ -51797,8 +51809,12 @@
"inherited": true
},
"static": {
"status": "not-implemented",
"evidence": "compiler-unmatched:fs"
"status": "partial",
"evidence": "surface-manifest:node-builtin.fs.constants",
"tests": [
"tests/corpus/6207-fs-constants-object.ts",
"tests/corpus/6208-fs-constants-require.cjs"
]
},
"anchorSource": "ancestor"
},
@@ -106885,6 +106901,8 @@
"tests/corpus/2890-crypto-static-utility.ts",
"tests/corpus/5442-crypto-random-values.ts",
"tests/corpus/5443-crypto-random-arguments.js",
"tests/corpus/6204-crypto-get-random-values.ts",
"tests/corpus/6205-crypto-get-random-values.js",
"tests/corpus/6291-webcrypto-hmac-sign-verify.ts",
"tests/corpus/6292-webcrypto-hmac-errors.ts",
"tests/corpus/6293-webcrypto-digest.ts",
@@ -110375,8 +110393,12 @@
"inherited": true
},
"static": {
"status": "not-implemented",
"evidence": "compiler-unmatched:webcrypto"
"status": "supported",
"evidence": "compiler-dedicated:crypto.getRandomValues",
"tests": [
"tests/corpus/6204-crypto-get-random-values.ts",
"tests/corpus/6205-crypto-get-random-values.js"
]
},
"anchorSource": "exact"
},
@@ -111,6 +111,8 @@
"node-builtin.os.uptime": ["tests/corpus/5140-os-host-facts.ts"],
"node-builtin.os.version": ["tests/corpus/5140-os-host-facts.ts"],
"node-builtin.os.constants": ["tests/corpus/5141-os-constants.ts"],
"node-builtin.fs.constants": ["tests/corpus/6207-fs-constants-object.ts", "tests/corpus/6208-fs-constants-require.cjs"],
"node-builtin.crypto.getRandomValues": ["tests/corpus/6204-crypto-get-random-values.ts", "tests/corpus/6205-crypto-get-random-values.js"],
"node-builtin.url.parse": ["tests/corpus/5220-url-parse-warnings.ts", "tests/corpus/5221-url-parse-shapes.ts", "tests/corpus/5222-url-parse-hosts.ts", "tests/corpus/5223-url-parse-query.ts", "tests/corpus/5227-url-legacy-dependency/main.ts", "tests/corpus/5229-url-legacy-errors.cjs", "packages/runtime/test/url-legacy.test.ts", "tests/fixtures/node-types/url-legacy.ts"],
"node-builtin.url.format": ["tests/corpus/5224-url-format-objects.ts", "tests/corpus/5225-url-format-whatwg.ts", "tests/corpus/5227-url-legacy-dependency/main.ts", "tests/corpus/5229-url-legacy-errors.cjs", "packages/runtime/test/url-legacy.test.ts", "tests/fixtures/node-types/url-legacy.ts"],
"node-builtin.url.resolve": ["tests/corpus/5226-url-resolve.ts", "tests/corpus/5227-url-legacy-dependency/main.ts", "tests/corpus/5229-url-legacy-errors.cjs", "packages/runtime/test/url-legacy.test.ts", "tests/fixtures/node-types/url-legacy.ts"],
@@ -399,6 +401,7 @@
{ "symbols": ["crypto.createHash", "Hash", "hash.update", "hash.digest", "hash.copy", "crypto.createHmac", "Hmac", "hmac.update", "hmac.digest", "crypto.hash", "crypto.pbkdf2", "crypto.pbkdf2Sync"], "status": "partial", "evidence": ["tests/corpus/5760-crypto-hash-stream.ts", "tests/corpus/5768-crypto-hash-unknown.ts", "tests/corpus/5769-crypto-hash-unknown.cjs", "tests/corpus/5761-crypto-hash-stream-states.ts", "tests/corpus/5762-crypto-hash-stream.cjs", "tests/corpus/2890-crypto-static-utility.ts", "tests/corpus/node24-crypto-sha2.ts", "tests/corpus/5435-crypto-hash-algorithms.ts", "tests/corpus/5436-crypto-hash-names.js", "tests/corpus/5437-crypto-hmac-algorithms.ts", "tests/corpus/5438-crypto-kdf-digests.ts", "tests/corpus/5440-crypto-hash-encodings.js", "tests/corpus/5441-crypto-hash-output-length.ts", "tests/harness/npm-static.test.ts"] },
{ "symbols": ["crypto.randomFill", "crypto.randomFillSync", "crypto.randomUUID", "crypto.randomUUIDv7"], "status": "supported", "evidence": ["tests/corpus/5442-crypto-random-values.ts", "tests/corpus/5443-crypto-random-arguments.js", "tests/corpus/1358-crypto-random.ts"] },
{ "symbols": ["crypto.createSecretKey", "crypto.generateKey", "crypto.generateKeySync", "KeyObject", "keyObject.export", "keyObject.equals", "keyObject.symmetricKeySize", "keyObject.type"], "status": "partial", "evidence": ["tests/corpus/5445-crypto-secret-key-objects.ts", "tests/corpus/5446-crypto-secret-key-errors.js"] },
{ "symbols": ["crypto.getRandomValues"], "status": "supported", "evidence": ["tests/corpus/6204-crypto-get-random-values.ts", "tests/corpus/6205-crypto-get-random-values.js"] },
{ "symbols": ["crypto.timingSafeEqual"], "status": "supported", "evidence": ["tests/corpus/5444-crypto-timing-safe-equal.js", "tests/corpus/2890-crypto-static-utility.ts"] },
{ "symbols": ["crypto.randomInt"], "status": "partial", "evidence": ["tests/corpus/5442-crypto-random-values.ts", "tests/corpus/2890-crypto-static-utility.ts"] },
{ "symbols": ["crypto.hkdf", "crypto.hkdfSync", "crypto.scrypt", "crypto.scryptSync"], "status": "partial", "evidence": ["tests/corpus/node24-crypto-kdf.ts", "tests/corpus/node24-crypto-kdf-errors.ts", "tests/corpus/node24-crypto-kdf-options.ts", "tests/corpus/5438-crypto-kdf-digests.ts"] },
+62
View File
@@ -1598,6 +1598,51 @@ declare module "node:fs" {
readonly COPYFILE_EXCL: number;
readonly COPYFILE_FICLONE: number;
readonly COPYFILE_FICLONE_FORCE: number;
readonly UV_FS_SYMLINK_DIR: number;
readonly UV_FS_SYMLINK_JUNCTION: number;
readonly UV_DIRENT_UNKNOWN: number;
readonly UV_DIRENT_FILE: number;
readonly UV_DIRENT_DIR: number;
readonly UV_DIRENT_LINK: number;
readonly UV_DIRENT_FIFO: number;
readonly UV_DIRENT_SOCKET: number;
readonly UV_DIRENT_CHAR: number;
readonly UV_DIRENT_BLOCK: number;
readonly UV_FS_O_FILEMAP: number;
readonly UV_FS_O_TEMPORARY: number;
readonly UV_FS_O_SHORT_LIVED: number;
readonly UV_FS_O_SEQUENTIAL: number;
readonly UV_FS_O_RANDOM: number;
readonly UV_FS_COPYFILE_EXCL: number;
readonly UV_FS_COPYFILE_FICLONE: number;
readonly UV_FS_COPYFILE_FICLONE_FORCE: number;
readonly S_IFMT: number;
readonly S_IFREG: number;
readonly S_IFDIR: number;
readonly S_IFCHR: number;
readonly S_IFBLK: number;
readonly S_IFIFO: number;
readonly S_IFLNK: number;
readonly S_IFSOCK: number;
readonly O_NOCTTY: number;
readonly O_DIRECTORY: number;
readonly O_NOATIME: number;
readonly O_SYNC: number;
readonly O_DSYNC: number;
readonly O_SYMLINK: number;
readonly O_DIRECT: number;
readonly S_IRWXU: number;
readonly S_IRUSR: number;
readonly S_IWUSR: number;
readonly S_IXUSR: number;
readonly S_IRWXG: number;
readonly S_IRGRP: number;
readonly S_IWGRP: number;
readonly S_IXGRP: number;
readonly S_IRWXO: number;
readonly S_IROTH: number;
readonly S_IWOTH: number;
readonly S_IXOTH: number;
};
/* A stat(2) snapshot (statSync follows symlinks, lstatSync does not —
* Node's split) — immutable; the supported surface is exactly these
@@ -2662,6 +2707,23 @@ declare module "crypto" {
a: ArrayBufferView | ArrayBufferLike,
b: ArrayBufferView | ArrayBufferLike,
): boolean;
/* WebCrypto's getRandomValues (also the webcrypto object's): integer
* typed arrays, filled in place and returned. The crypto GLOBAL stays
* unknown in fallback builds. */
type RandomValuesArray =
| Int8Array
| Uint8Array
| Uint8ClampedArray
| Int16Array
| Uint16Array
| Int32Array
| Uint32Array
| BigInt64Array
| BigUint64Array;
export function getRandomValues<T extends RandomValuesArray>(typedArray: T): T;
export const webcrypto: {
getRandomValues<T extends RandomValuesArray>(typedArray: T): T;
};
export function randomFillSync<T extends ArrayBufferView | ArrayBufferLike>(
buffer: T,
offset?: number,
@@ -94,6 +94,8 @@ export const LIB_FN_SYMS: Record<string, string> = {
"num.parseInt": "scr_parse_int",
"num.parseFloat": "scr_parse_float",
"str.localeCompare": "scr_str_locale_compare",
"str.substr": "scr_str_substr",
"str.localeCase": "scr_str_locale_case",
"str.replaceLiteral": "scr_str_replace_literal",
"str.replaceChars": "scr_str_replace_chars",
"num.fromString": "scr_string_to_number",
@@ -143,6 +145,8 @@ export const LIB_FN_SYMS: Record<string, string> = {
"num.toExponential": "scr_num_to_exponential",
"num.toFixed0": "scr_num_to_fixed0",
"num.toFixed": "scr_num_to_fixed",
"num.toPrecision": "scr_num_to_precision",
"num.toExponentialDigits": "scr_num_to_exponential_digits",
"num.toStringRadix": "scr_num_to_string_radix",
"num.toStringRadixF64": "scr_num_to_string_radix_f64",
"num.sameValue": "scr_num_same_value",
@@ -195,6 +199,7 @@ export const LIB_FN_SYMS: Record<string, string> = {
"os.cpusModel": "scr_os_cpus_model",
"os.cpusField": "scr_os_cpus_field",
"os.constants": "scr_os_constants",
"fs.constants": "scr_fs_constants_object",
"constants.object": "scr_node_constants",
"os.release": "scr_os_release",
"os.userName": "scr_os_user_name",
@@ -408,6 +413,7 @@ export const LIB_FN_SYMS: Record<string, string> = {
"buffer.fromStr": "scr_bytes_from_str",
"buffer.fromStrChecked": "scr_bytes_from_str_checked",
"buffer.brand": "scr_bytes_as_buffer",
"buffer.isBranded": "scr_bytes_is_buffer",
"buffer.fromDyn": "scr_buffer_from_dyn",
"buffer.concat": "scr_buffer_concat",
"buffer.concatLen": "scr_buffer_concat_len",
@@ -574,6 +580,8 @@ export const LIB_FN_SYMS: Record<string, string> = {
"crypto.randomIntMax": "scr_crypto_random_int_max",
"crypto.randomFillView": "scr_crypto_random_fill",
"crypto.randomFillViewRest": "scr_crypto_random_fill_rest",
"crypto.getRandomValues": "scr_crypto_get_random_values",
"crypto.getRandomValuesDyn": "scr_crypto_get_random_values_dyn",
"crypto.randomFillSyncDyn": "scr_crypto_random_fill_sync_dyn",
"crypto.randomUUIDOpts": "scr_crypto_random_uuid_opts",
"crypto.createSecretKey": "scr_crypto_create_secret_key",
@@ -1,11 +1,11 @@
import * as ts from "../../ts7/adapter.js";
import { type Lowerer, own } from "../lowerer.js";
import { canonicalBuiltinModule, locOf, requireSpecOf } from "../../program.js";
import { canonicalBuiltinModule, isJsSourceFile, locOf, requireSpecOf } from "../../program.js";
import { builtinConstLit } from "../surfaces.js";
import { HTTP2_CONSTANTS } from "../http2-constants.js";
import { CRYPTO_CONSTANTS } from "../crypto-tables.js";
import { ZLIB_CONSTANTS } from "../zlib-constants.js";
import { F64, type IrExpr } from "../../../ir/ir.js";
import { DYN, F64, type IrExpr, STRING, type SrcLoc } from "../../../ir/ir.js";
import { builtinImportOf } from "./module-bindings.js";
/** The builtin modules whose `constants` object bakes as literals at
@@ -147,12 +147,30 @@ export function lowerFsConstantsProperty(
COPYFILE_FICLONE_FORCE: 4,
};
const value = own(MODES, expr.name.text);
if (value === undefined) {
lowerer.noLowering(
`fs.constants.${expr.name.text}`,
expr,
"F_OK, R_OK, W_OK, X_OK, and COPYFILE_* are the lowered constants",
);
}
if (value === undefined) return fsConstantMemberRead(expr);
return { kind: "numLit", value, type: F64, loc: locOf(expr) };
}
/** fs.constants as a value: the target's null-prototype constants object,
* built by the runtime from its own libc's macros. */
export function fsConstantsObject(loc: SrcLoc): IrExpr {
return { kind: "libCall", fn: "fs.constants", args: [], type: DYN, loc };
}
/** `fs.constants.O_RDONLY` (any member the access-mode literals do not
* cover): a read of the runtime object, so a constant the target lacks
* reads as undefined in JavaScript and fails the number check in
* TypeScript, whose declarations promise every name. */
export function fsConstantMemberRead(expr: ts.PropertyAccessExpression): IrExpr {
const loc = locOf(expr);
const read: IrExpr = {
kind: "dynKeyGet",
value: fsConstantsObject(loc),
key: { kind: "strLit", value: expr.name.text, type: STRING, loc },
type: DYN,
loc,
};
return isJsSourceFile(expr.getSourceFile())
? read
: { kind: "dynCheck", value: read, type: F64, loc };
}
@@ -263,6 +263,7 @@ export function lowerCryptoModuleCall(
if (args.some(ts.isSpreadElement)) {
lowerer.noLowering(`crypto.${bi.member} with spread arguments`, expr);
}
if (bi.member === "getRandomValues") return lowerCryptoGetRandomValues(lowerer, expr);
if (bi.member === "randomBytes") {
if (args.length === 1) return null;
if (args.length !== 2) {
@@ -1284,3 +1285,49 @@ export function lowerPromisifiedCryptoCall(
"KeyObject",
);
}
/** `crypto.getRandomValues(view)` — the WebCrypto global, node:crypto's
* export and `webcrypto.getRandomValues`: a typed view fills in place and
* returns itself at its own type (float views, DataViews and views over
* 65536 bytes throw Node's DOMExceptions at run time); any other value
* takes the checked form. Null for other arities (the member fence keeps
* them). */
export function lowerCryptoGetRandomValues(
lowerer: Lowerer,
call: ts.CallExpression,
): IrExpr | null {
if (call.arguments.nodes.length !== 1 || ts.isSpreadElement(call.arguments.nodes[0]!))
return null;
const loc = locOf(call);
const value = lowerer.lowerExpr(call.arguments.nodes[0]!);
if (value.type.kind === "bytes")
return { kind: "libCall", fn: "crypto.getRandomValues", args: [value], type: value.type, loc };
return {
kind: "libCall",
fn: "crypto.getRandomValuesDyn",
args: [lowerer.coerceInto(call.arguments.nodes[0]!, value, DYN)],
type: DYN,
loc,
};
}
/** Whether `receiver` names a WebCrypto Crypto object: the `crypto`
* global (also through globalThis) or node:crypto's `webcrypto`. */
export function isWebCryptoReceiver(lowerer: Lowerer, receiver: ts.Expression): boolean {
if (ts.isIdentifier(receiver)) {
if (receiver.text === "crypto" && lowerer.isStdlibGlobal(receiver, "crypto")) return true;
const imported = lowerer.builtinImportOf(receiver);
return imported?.module === "crypto" && imported.member === "webcrypto";
}
if (ts.isPropertyAccessExpression(receiver) && !receiver.questionDotToken) {
if (
receiver.name.text === "crypto" &&
ts.isIdentifier(receiver.expression) &&
lowerer.isStdlibGlobal(receiver.expression, "globalThis")
)
return true;
const member = lowerer.builtinMemberOf(receiver);
return member?.module === "crypto" && member.member === "webcrypto";
}
return false;
}
@@ -20,6 +20,7 @@ import {
arrayOf,
bytesOf,
funcOf,
isUnitType,
typeEquals,
} from "../../../ir/ir.js";
import { isJsSourceFile, locOf } from "../../program.js";
@@ -1429,7 +1430,14 @@ function lowerBufferStaticValue(
}
return { kind: "libCall", fn, args: [s, enc], type: BYTES_U8, loc };
}
if (args.length === 1 && srcIr?.kind === "bytes" && srcIr.elem === "u8") {
// A checker type with no mapping (pako's conditional `O extends
// { toText: true } ? string : Uint8Array` result) whose lowered
// value is a u8 view copies like the typed form.
if (
args.length === 1 &&
((srcIr?.kind === "bytes" && srcIr.elem === "u8") ||
(srcIr === null && value.type.kind === "bytes" && value.type.elem === "u8"))
) {
return {
kind: "libCall",
fn: "buffer.poolFromBytes",
@@ -1447,6 +1455,34 @@ function lowerBufferStaticValue(
loc,
};
}
// A union of strings, u8 views and undefined/null (a JS-inferred
// npm result such as pako's deflate): the checked form dispatches
// on the runtime kind exactly as for an untyped JS value, Node's
// TypeError for the unit arms included.
if (
args.length === 1 &&
value.type.kind === "union" &&
lowerer.dynConvertible(value.type) &&
lowerer.unions
.get(value.type.unionId)
?.arms.every(
(arm) =>
arm.kind === "string" ||
(arm.kind === "bytes" && arm.elem === "u8") ||
isUnitType(arm),
) === true
) {
return {
kind: "libCall",
fn: "buffer.poolFromDyn",
args: [
lowerer.coerceToExpected(value, DYN),
{ kind: "strLit", value: "utf8", type: STRING, loc },
],
type: BYTES_U8,
loc,
};
}
}
}
lowerer.noLowering(
@@ -1659,6 +1695,12 @@ function lowerBufferStaticValue(
};
}
}
// A typed view of unknown provenance (a Uint8Array parameter,
// safe-buffer's Buffer.from result): Buffer is a brand of the view,
// which the runtime keeps, so the test reads it.
if (v.type.kind === "bytes") {
return { kind: "libCall", fn: "buffer.isBranded", args: [v], type: BOOL, loc };
}
lowerer.noLowering(
`Buffer.isBuffer of '${lowerer.fmt(v.type)}' values`,
args[0]!,
@@ -153,7 +153,7 @@ export function lowerForOfSearchParams(
}
: singleRead();
if (ts.isIdentifier(decl.name)) {
const varTarget = forOfVarTarget(lowerer, decl);
const varTarget = forOfVarTarget(lowerer, decl, elemT!);
if (varTarget) {
// `for (var x of ...)`: one function-scoped binding, assigned
// per pass — closures made in the loop share it, and the value
@@ -357,7 +357,7 @@ export function lowerForOfArrayIter(
}
: keyRead();
if (ts.isIdentifier(decl.name)) {
const varTarget = forOfVarTarget(lowerer, decl);
const varTarget = forOfVarTarget(lowerer, decl, declT!);
if (varTarget) {
// `for (var x of ...)`: one function-scoped binding, assigned
// per pass — closures made in the loop share it, and the value
@@ -650,7 +650,7 @@ function lowerForOfMapOrSet(
}
: singleRead();
if (ts.isIdentifier(decl!.name)) {
const varTarget = forOfVarTarget(lowerer, decl!);
const varTarget = forOfVarTarget(lowerer, decl!, elemT!);
if (varTarget) {
// `for (var x of ...)`: one function-scoped binding, assigned
// per pass — closures made in the loop share it, and the value
@@ -1,5 +1,12 @@
import { matchAllRowsReadByElements } from "./match-row-uses.js";
import { dynUndefinedExpr, nodeThrowExpr, numLit, strLit, varRef } from "../../../ir/build.js";
import {
boolLit,
dynUndefinedExpr,
nodeThrowExpr,
numLit,
strLit,
varRef,
} from "../../../ir/build.js";
import * as ts from "../../ts7/adapter.js";
import {
BOOL,
@@ -316,6 +323,21 @@ export function lowerStringIndexCall(
) {
index = lowerer.coerceInto(indexNode, index, DYN);
}
return stringIndexHelperCall(lowerer, call, method, receiver, receiverNode, index, indexNode);
}
/** The relative-index / code-point helper call over an already lowered
* index (any type positionNumber converts — dyn indices included). */
function stringIndexHelperCall(
lowerer: Lowerer,
call: ts.CallExpression,
method: "at" | "codePointAt",
receiver: IrExpr,
receiverNode: ts.Node,
index: IrExpr,
indexNode: ts.Expression | undefined,
): IrExpr {
const loc = locOf(call);
const valueType = method === "at" ? STRING : F64;
const resultType = lowerer.withUndefinedArmOf(valueType);
if (!resultType || resultType.kind !== "union")
@@ -1061,7 +1083,8 @@ export function lowerStringMethodCall(
const replaceMethod = STRING_REPLACE_METHODS.has(access.name.text)
? (access.name.text as "replace" | "replaceAll")
: null;
if (!entry && !indexMethod && !replaceMethod) return null;
const extraMethod = EXTRA_STRING_METHODS.has(access.name.text);
if (!entry && !indexMethod && !replaceMethod && !extraMethod) return null;
// A validated dyn receiver (`pkg.name.replace(...)` on a JSON.parse
// value) arrives pre-extracted through `dynReceiver`; its checker type
// is `any`, so the type/symbol gates don't apply — the dyn value's
@@ -1089,6 +1112,12 @@ export function lowerStringMethodCall(
return null;
if (!lowerer.isStdlibMember(access)) return null;
}
if (extraMethod && argumentNodes === call.arguments.nodes) {
const receiver = dynReceiver
? dynReceiver()
: lowerMethodReceiver(lowerer, access.expression, STRING, access.name.text);
return lowerExtraStringMethodCall(lowerer, call, access.name.text, receiver, access.expression);
}
// The lib declares optional parameters beyond some lowered forms; fence
// those arities instead of passing arguments the runtime doesn't take.
if (
@@ -1386,3 +1415,393 @@ function lowerLocaleCompareCall(
lowerer.badType(call.arguments.nodes[0]!, lowerer.typeOf(call.arguments.nodes[0]!));
return { kind: "libCall", fn: "str.localeCompare", args: [receiver, arg], type: F64, loc };
}
/* ── String.prototype / Number.prototype members beyond the core tables ──
* substr, the locale case pair, the Annex B HTML methods, toPrecision and
* toExponential(fractionDigits) on typed receivers, plus the kind dispatch
* for every String/Number method a checked-dynamic receiver can name
* whose runtime dispatch had no answer (`character.codePointAt(0)` in a
* JS package, `n.toPrecision(3)` on a JS-inferred number). Each path
* replaces a compile-time refusal, a runtime 'not supported yet' fence or
* Node's is-not-a-function answer for a real method, so programs that
* never spell these names compile exactly as before. */
/** String.prototype members (Annex B included) whose typed-receiver
* lowering lives here. */
export const EXTRA_STRING_METHODS: ReadonlySet<string> = new Set([
"substr",
"toLocaleLowerCase",
"toLocaleUpperCase",
"anchor",
"big",
"blink",
"bold",
"fixed",
"fontcolor",
"fontsize",
"italics",
"link",
"small",
"strike",
"sub",
"sup",
]);
/** String methods a dyn receiver dispatches by runtime kind here: a string
* runs the typed lowering, every other kind takes the runtime dispatch
* (an object's own member, Node's is-not-a-function otherwise). Only names
* no other dyn kind's prototype declares: the shared ones (at, concat,
* slice, ...) already dispatch at run time for arrays, and a kind test in
* front of them would change array code that works today. The Annex B
* HTML names (bold, italics, link, ...) stay out too: chalk-style builders
* call them on functions and objects, whose calls must stay as they are. */
const DYN_STRING_KIND_METHODS: ReadonlySet<string> = new Set([
"substr",
"toLocaleLowerCase",
"toLocaleUpperCase",
"codePointAt",
"isWellFormed",
"toWellFormed",
"trimLeft",
"trimRight",
]);
/** Number methods a dyn receiver dispatches by runtime kind here. */
const DYN_NUMBER_KIND_METHODS: ReadonlySet<string> = new Set(["toPrecision", "toExponential"]);
/** Annex B CreateHTML: the tag and the attribute (null for none). */
const HTML_METHODS: Readonly<Record<string, readonly [string, string | null]>> = {
anchor: ["a", "name"],
big: ["big", null],
blink: ["blink", null],
bold: ["b", null],
fixed: ["tt", null],
fontcolor: ["font", "color"],
fontsize: ["font", "size"],
italics: ["i", null],
link: ["a", "href"],
small: ["small", null],
strike: ["strike", null],
sub: ["sub", null],
sup: ["sup", null],
};
export function isDynPrimitiveKindMethod(name: string): boolean {
return DYN_STRING_KIND_METHODS.has(name) || DYN_NUMBER_KIND_METHODS.has(name);
}
/** Evaluate `value` once into a hidden local (effect-free reads stay). */
function hoist(lowerer: Lowerer, value: IrExpr, stmts: IrStmt[], name: string): IrExpr {
if (
value.kind === "varRef" ||
value.kind === "numLit" ||
value.kind === "strLit" ||
value.kind === "boolLit" ||
value.kind === "unitLit"
)
return value;
const local = lowerer.declareHiddenLocal(name, value.type);
stmts.push({ kind: "varDecl", localId: local.id, init: value, loc: value.loc });
return varRef(local.id, value.type, value.loc);
}
function seq(stmts: IrStmt[], result: IrExpr): IrExpr {
return stmts.length === 0
? result
: { kind: "seqExpr", stmts, result, type: result.type, loc: result.loc };
}
/** ToString of a method argument: the dyn coercion (symbols throw), the
* typed conversions elsewhere. */
function stringArg(lowerer: Lowerer, value: IrExpr, node: ts.Node): IrExpr {
if (value.type.kind === "dyn")
return {
kind: "libCall",
fn: "dyn.toStringCoerce",
args: [value],
type: STRING,
loc: value.loc,
};
return lowerer.ensureString(value, node);
}
/** An optional numeric argument: its ToNumber value and whether it was
* supplied (undefined counts as absent, as the spec's checks do). */
function optionalNumber(
lowerer: Lowerer,
value: IrExpr | undefined,
node: ts.Expression,
stmts: IrStmt[],
subject: string,
loc: SrcLoc,
): { value: IrExpr; has: IrExpr } {
if (!value || value.type.kind === "undefinedT" || value.type.kind === "void")
return { value: numLit(0, loc), has: boolLit(false, loc) };
if (
value.type.kind === "f64" ||
value.type.kind === "string" ||
value.type.kind === "bool" ||
value.type.kind === "nullT"
)
return {
value: positionNumber(lowerer, value, numLit(0, loc), node, subject),
has: boolLit(true, loc),
};
const boxed =
value.type.kind === "dyn"
? value
: hoist(lowerer, lowerer.coerceToExpected(value, DYN), stmts, "%numArg");
const absent: IrExpr = { kind: "dynTest", test: "undefined", value: boxed, type: BOOL, loc };
return {
value: positionNumber(lowerer, boxed, numLit(0, loc), node, subject),
has: { kind: "unary", op: "!", operand: absent, type: BOOL, loc },
};
}
/** The typed result of a String.prototype member over a STRING receiver
* and already evaluated arguments, or null when the name is not one of
* this module's. */
function buildStringMethod(
lowerer: Lowerer,
call: ts.CallExpression,
name: string,
s: IrExpr,
args: IrExpr[],
receiverNode: ts.Node,
stmts: IrStmt[],
): IrExpr | null {
const loc = locOf(call);
const node = (i: number): ts.Expression => call.arguments.nodes[i] ?? call;
if (name === "substr") {
const start = args[0]
? positionNumber(lowerer, args[0], numLit(0, loc), node(0), "substr start")
: numLit(0, loc);
const length = optionalNumber(lowerer, args[1], node(1), stmts, "substr length", loc);
return {
kind: "libCall",
fn: "str.substr",
args: [s, start, length.value, length.has],
type: STRING,
loc,
};
}
if (name === "toLocaleLowerCase" || name === "toLocaleUpperCase") {
const locales = args[0] ? lowerer.coerceToExpected(args[0], DYN) : dynUndefinedExpr(loc);
return {
kind: "libCall",
fn: "str.localeCase",
args: [s, locales, boolLit(name === "toLocaleUpperCase", loc)],
type: STRING,
loc,
};
}
const html = Object.hasOwn(HTML_METHODS, name) ? HTML_METHODS[name] : undefined;
if (html) {
const [tag, attribute] = html;
const subject = hoist(lowerer, s, stmts, "%htmlSubject");
const pieces: IrExpr[] = [strLit(`<${tag}`, loc)];
if (attribute !== null) {
const text = args[0] ? stringArg(lowerer, args[0], node(0)) : strLit("undefined", loc);
pieces.push(strLit(` ${attribute}="`, loc));
pieces.push({
kind: "libCall",
fn: "str.replaceLiteral",
args: [text, strLit('"', loc), strLit("&quot;", loc), boolLit(true, loc)],
type: STRING,
loc,
});
pieces.push(strLit('"', loc));
}
pieces.push(strLit(">", loc), subject, strLit(`</${tag}>`, loc));
return pieces.reduce((left, right) => ({ kind: "strConcat", left, right, type: STRING, loc }));
}
if (name === "codePointAt")
return stringIndexHelperCall(
lowerer,
call,
name,
s,
receiverNode,
args[0] ?? numLit(0, loc),
call.arguments.nodes[0],
);
if (name === "isWellFormed" || name === "toWellFormed")
return {
kind: "strIntrinsic",
method: name,
receiver: s,
args: [],
type: name === "isWellFormed" ? BOOL : STRING,
loc,
};
if (name === "trimLeft" || name === "trimRight")
return {
kind: "strIntrinsic",
method: name === "trimLeft" ? "trimStart" : "trimEnd",
receiver: s,
args: [],
type: STRING,
loc,
};
return null;
}
/** The typed result of toPrecision/toExponential over an F64 receiver. */
function buildNumberMethod(
lowerer: Lowerer,
call: ts.CallExpression,
name: string,
x: IrExpr,
args: IrExpr[],
stmts: IrStmt[],
): IrExpr | null {
const loc = locOf(call);
if (name !== "toPrecision" && name !== "toExponential") return null;
const digits = optionalNumber(
lowerer,
args[0],
call.arguments.nodes[0] ?? call,
stmts,
`${name} digits`,
loc,
);
if (name === "toPrecision")
return {
kind: "libCall",
fn: "num.toPrecision",
args: [x, digits.value, digits.has],
type: STRING,
loc,
};
const shortest: IrExpr = {
kind: "libCall",
fn: "num.toExponential",
args: [x],
type: STRING,
loc,
};
if (digits.has.kind === "boolLit" && !digits.has.value) return shortest;
const fixed: IrExpr = {
kind: "libCall",
fn: "num.toExponentialDigits",
args: [x, digits.value],
type: STRING,
loc,
};
if (digits.has.kind === "boolLit") return fixed;
return { kind: "ternary", cond: digits.has, then: fixed, else_: shortest, type: STRING, loc };
}
/** Evaluate the call's arguments, in order, into hidden locals. */
function evaluatedArgs(
lowerer: Lowerer,
call: ts.CallExpression,
stmts: IrStmt[],
expected: IrType | undefined,
): IrExpr[] {
return call.arguments.nodes.map((arg) =>
hoist(
lowerer,
expected ? lowerer.lowerExprExpecting(arg, expected) : lowerer.lowerExpr(arg),
stmts,
"%methodArg",
),
);
}
/** A typed STRING receiver calling one of EXTRA_STRING_METHODS: receiver,
* then arguments, then the method's own conversions — the spec order. */
export function lowerExtraStringMethodCall(
lowerer: Lowerer,
call: ts.CallExpression,
name: string,
receiver: IrExpr,
receiverNode: ts.Node,
): IrExpr | null {
if (call.arguments.nodes.some(ts.isSpreadElement)) return null;
const stmts: IrStmt[] = [];
const s = hoist(lowerer, receiver, stmts, "%stringReceiver");
const args = evaluatedArgs(lowerer, call, stmts, undefined);
const result = buildStringMethod(lowerer, call, name, s, args, receiverNode, stmts);
return result ? seq(stmts, result) : null;
}
/** A typed F64 receiver calling toPrecision(p) or toExponential(f). */
export function lowerExtraNumberMethodCall(
lowerer: Lowerer,
call: ts.CallExpression,
name: string,
receiver: IrExpr,
): IrExpr | null {
if (call.arguments.nodes.some(ts.isSpreadElement) || call.arguments.nodes.length > 1) return null;
if (name !== "toPrecision" && !(name === "toExponential" && call.arguments.nodes.length === 1))
return null;
const stmts: IrStmt[] = [];
const x = hoist(lowerer, receiver, stmts, "%numberReceiver");
const args = evaluatedArgs(lowerer, call, stmts, undefined);
const result = buildNumberMethod(lowerer, call, name, x, args, stmts);
return result ? seq(stmts, result) : null;
}
/** `recv.m(args)` on a checked-dynamic receiver for a String/Number method
* the runtime dispatch cannot answer: the receiver and arguments evaluate
* once, a string (or number) receiver runs the typed lowering, and every
* other kind keeps the runtime dispatch's answer. Null for other names. */
export function lowerDynPrimitiveMethodCall(
lowerer: Lowerer,
call: ts.CallExpression,
name: string,
calleeName: string,
recv: IrExpr,
): IrExpr | null {
const stringMethod = DYN_STRING_KIND_METHODS.has(name);
const numberMethod = DYN_NUMBER_KIND_METHODS.has(name);
if (!stringMethod && !numberMethod) return null;
if (call.arguments.nodes.some(ts.isSpreadElement)) return null;
const loc = locOf(call);
const stmts: IrStmt[] = [];
const local = lowerer.declareHiddenLocal("%primitiveReceiver", DYN);
stmts.push({ kind: "varDecl", localId: local.id, init: recv, loc });
const r = varRef(local.id, DYN, loc);
const args = evaluatedArgs(lowerer, call, stmts, DYN);
let result: IrExpr = {
kind: "dynInvoke",
recv: r,
method: name,
calleeName,
args,
type: DYN,
loc,
};
const branch = (
test: "string" | "number",
build: (value: IrExpr, branchStmts: IrStmt[]) => IrExpr | null,
): void => {
const branchStmts: IrStmt[] = [];
const value: IrExpr = {
kind: "dynCheck",
value: r,
type: test === "string" ? STRING : F64,
loc,
};
const built = build(value, branchStmts);
if (!built) return;
result = {
kind: "ternary",
cond: { kind: "dynTest", test, value: r, type: BOOL, loc },
then: { kind: "dynFrom", value: seq(branchStmts, built), type: DYN, loc },
else_: result,
type: DYN,
loc,
};
};
if (numberMethod)
branch("number", (x, branchStmts) =>
buildNumberMethod(lowerer, call, name, x, args, branchStmts),
);
if (stringMethod)
branch("string", (s, branchStmts) =>
buildStringMethod(lowerer, call, name, s, args, call.expression, branchStmts),
);
return seq(stmts, result);
}
@@ -114,6 +114,9 @@ import {
lowerStringMethodCall,
lowerStringPaddingCall,
lowerStringSplitCall,
isDynPrimitiveKindMethod,
lowerDynPrimitiveMethodCall,
lowerExtraNumberMethodCall,
} from "./containers/string-and-regexp.js";
import { createRequireSpecOf } from "./builtins/module-bindings.js";
import {
@@ -126,7 +129,9 @@ import {
lowerRequireResolveCall,
} from "./builtins/module-resolution.js";
import {
isWebCryptoReceiver,
lowerCipherMethodCall,
lowerCryptoGetRandomValues,
lowerCryptoHashMethodCall,
lowerPromisifiedCryptoCall,
} from "./builtins/crypto.js";
@@ -1500,6 +1505,9 @@ export function lowerCall(lowerer: Lowerer, expr: ts.CallExpression): IrExpr {
lowerer.fenceStaticReadableStreamMember(expr.expression, "call") ??
lowerDisposableStackUseCall(lowerer, expr, expr.expression) ??
lowerer.lowerStdlibMethodCall(expr, expr.expression) ??
// crypto.getRandomValues on the WebCrypto global (untyped in
// fallback builds, so before the dyn receiver path) or webcrypto.
lowerWebCryptoGetRandomValues(lowerer, expr, expr.expression) ??
// Dyn receivers (JSON.parse-derived `unknown`/`any` values) —
lowerDynReceiverMethodCall(lowerer, expr, expr.expression) ??
@@ -3427,6 +3435,23 @@ function lowerDynReceiverMethodCall(
lowerStringMethodCall(lowerer, call, access, () => recv)
);
}
// A checker-untyped JS binding holding `string | undefined` (string-
// width's `const visibleSegment = baseVisible(segment)` after an
// `=== undefined` guard the checker cannot see through): no typed
// lowering claims the union, so a String method boxes it and takes the
// dyn receiver path, whose undefined/null arms throw Node's read errors.
if (
checkerUntyped &&
recv.type.kind === "union" &&
isJsSourceFile(call.getSourceFile()) &&
isStringMethodName(access.name.text) &&
lowerer.unions
.get(recv.type.unionId)
?.arms.every((arm) => arm.kind === "string" || isUnitType(arm)) === true &&
lowerer.dynConvertible(recv.type)
) {
recv = lowerer.coerceToExpected(recv, DYN);
}
// A native handle read through `as any` (`(hash as any).update(x)`)
// dispatches through the handle, as the same value held in an
// 'unknown' slot does.
@@ -3551,6 +3576,26 @@ function lowerDynReceiverMethodCall(
lowerStringMethodCall(lowerer, call, access, checked)
);
}
// String/Number methods the runtime dispatch has no answer for
// (codePointAt, substr, toLocaleLowerCase, toPrecision, ...): a string
// or number receiver runs the typed lowering, every other kind keeps the
// runtime dispatch.
if (
!ownCallable &&
!call.questionDotToken &&
!access.questionDotToken &&
isDynPrimitiveKindMethod(access.name.text) &&
!lowerer.chainBlocked(access, call)
) {
const primitive = lowerDynPrimitiveMethodCall(
lowerer,
call,
access.name.text,
access.getText(),
recv,
);
if (primitive) return primitive;
}
// The argument is a radix for bigint and an encoding for Buffer.
// Preserve its runtime value so receiver dispatch can choose correctly.
if (access.name.text === "toString" && call.arguments.nodes.length <= 1) {
@@ -4128,6 +4173,19 @@ export function lowerDynDispatchMethodCall(
};
}
/** Every String.prototype method name a typed or dyn string lowering
* answers. */
function isStringMethodName(name: string): boolean {
return (
Object.hasOwn(STR_METHODS, name) ||
STRING_INDEX_METHODS.has(name) ||
DYN_STRING_ONLY_METHODS.has(name) ||
isDynPrimitiveKindMethod(name) ||
name === "normalize" ||
name === "lastIndexOf"
);
}
/** STR_METHODS ∪ the regex-form names, MINUS everything Array (or any
* other dyn kind's prototype) also declares. */
const DYN_STRING_ONLY_METHODS = new Set([
@@ -4599,6 +4657,21 @@ function lowerPrimitiveProtoCall(
isJsSourceFile(call.getSourceFile())
) {
const operand = lowerer.lowerExpr(recv);
if (
name === "toExponential" &&
call.arguments.nodes.length === 1 &&
(operand.type.kind === "dyn" ||
(operand.type.kind === "union" && lowerer.dynConvertible(operand.type)))
) {
const dispatched = lowerDynPrimitiveMethodCall(
lowerer,
call,
name,
`${recv.getText()}.${name}`,
lowerer.coerceToExpected(operand, DYN),
);
if (dispatched) return dispatched;
}
if (
operand.type.kind === "dyn" ||
(operand.type.kind === "union" && lowerer.dynConvertible(operand.type))
@@ -4613,6 +4686,32 @@ function lowerPrimitiveProtoCall(
loc: locOf(call),
};
}
if (
name === "toPrecision" &&
call.arguments.nodes.length <= 1 &&
!call.arguments.nodes.some(ts.isSpreadElement) &&
isJsSourceFile(call.getSourceFile()) &&
recvKind === "union"
) {
const operand = lowerer.lowerExpr(recv);
if (operand.type.kind === "union" && lowerer.dynConvertible(operand.type)) {
const dispatched = lowerDynPrimitiveMethodCall(
lowerer,
call,
name,
`${recv.getText()}.${name}`,
lowerer.coerceToExpected(operand, DYN),
);
if (dispatched) return dispatched;
}
}
if (
recvKind === "f64" &&
(name === "toPrecision" || (name === "toExponential" && call.arguments.nodes.length === 1))
) {
const typed = lowerExtraNumberMethodCall(lowerer, call, name, lowerer.lowerExpr(recv));
if (typed) return typed;
}
if (recvKind !== "f64" && recvKind !== "bool" && recvKind !== "string") return null;
const loc = locOf(call);
if (name === "toString" && call.arguments.nodes.length === 0) {
@@ -9896,6 +9995,22 @@ export function lowerStaticNumberGlobal(
return null;
}
/** `crypto.getRandomValues(view)` on the WebCrypto global or webcrypto. */
function lowerWebCryptoGetRandomValues(
lowerer: Lowerer,
call: ts.CallExpression,
access: ts.PropertyAccessExpression,
): IrExpr | null {
if (
access.name.text !== "getRandomValues" ||
call.questionDotToken ||
access.questionDotToken ||
!isWebCryptoReceiver(lowerer, access.expression)
)
return null;
return lowerCryptoGetRandomValues(lowerer, call);
}
/** A then handler's promise-armed result (`.then(() => cached ?? load())`
* typed `T | Promise<T>`, or a `Promise<void> | void` callback's result):
* the lifted async body adopts the promise arm and fulfills with the
@@ -746,6 +746,31 @@ function lowerExprInner(lowerer: Lowerer, expr: ts.Expression): IrExpr {
}
}
}
// `typeof globalThis.AbortSignal` / `typeof Headers`: Node 24's Web
// platform constructor globals are functions. JavaScript sources
// represent such a value as an identity token, and the fallback
// declarations type some (AbortSignal) without a construct signature,
// so neither the operand nor the type can answer; the read has no
// effects.
if (constructorGlobalTypeof(lowerer, tested) !== null)
return { kind: "strLit", value: "function", type: STRING, loc };
// `typeof globalThis.AbortSignal.any` (ky's feature probe): a static
// member of a standard-library global, declared callable, whose value
// has no representation. The read has no effects and Node's answer
// for every such function is "function"; members that lower as values
// keep their own answer.
if (ts.isPropertyAccessExpression(tested) && !tested.questionDotToken) {
const owner = stdlibGlobalNameOf(lowerer, tested.expression);
if (owner !== null && lowerer.isStdlibMember(tested)) {
const t = lowerer.typeOf(tested);
if (
(lowerer.checker.getCallSignatures(t).length > 0 ||
lowerer.checker.getConstructSignatures(t).length > 0) &&
tryLowerExpression(lowerer, tested) === null
)
return { kind: "strLit", value: "function", type: STRING, loc };
}
}
if (ts.isIdentifier(tested)) {
const sym = lowerer.checker.getSymbolAtLocation(tested);
if (!sym) {
@@ -1339,6 +1364,8 @@ function lowerExprInner(lowerer: Lowerer, expr: ts.Expression): IrExpr {
if (bi.module === "os" && bi.member === "constants") {
return { kind: "libCall", fn: "os.constants", args: [], type: DYN, loc };
}
if (bi.module === "fs" && bi.member === "constants")
return { kind: "libCall", fn: "fs.constants", args: [], type: DYN, loc };
// `import { O_RDONLY } from "constants"`: the member of the frozen
// module object.
if (bi.module === "constants") {
@@ -9084,6 +9111,76 @@ export function lowerEnvironmentKey(lowerer: Lowerer, node: ts.Expression): IrEx
return lowerRecordPropertyKey(lowerer, key, node);
}
/** `typeof` of a Node 24 Web-platform constructor global or one of its
* static methods: "function", decided by name. The read has no effects.
* Static methods answer by name because after a `typeof
* globalThis.AbortSignal === "function" &&` guard the checker narrows a
* fallback-typed receiver to never, hiding the member's declaration. */
function constructorGlobalTypeof(lowerer: Lowerer, tested: ts.Expression): string | null {
while (ts.isParenthesizedExpression(tested)) tested = tested.expression;
const globalName = stdlibGlobalNameOf(lowerer, tested);
if (globalName !== null && NODE_CONSTRUCTOR_GLOBALS.has(globalName)) return "function";
if (!ts.isPropertyAccessExpression(tested) || tested.questionDotToken) return null;
const owner = stdlibGlobalNameOf(lowerer, tested.expression);
return owner !== null &&
Object.hasOwn(NODE_CONSTRUCTOR_STATICS, owner) &&
NODE_CONSTRUCTOR_STATICS[owner]!.includes(tested.name.text)
? "function"
: null;
}
/** `typeof globalThis.AbortSignal.any === "function"`: the comparison
* folds with constructorGlobalTypeof's answer. */
function lowerConstructorGlobalTypeofTest(
lowerer: Lowerer,
expr: ts.BinaryExpression,
loc: SrcLoc,
): IrExpr | null {
const negated = expr.operatorToken.kind === ts.SyntaxKind.ExclamationEqualsEqualsToken;
for (const [a, b] of operandPairs(expr)) {
if (!ts.isTypeOfExpression(a) || !ts.isStringLiteral(b)) continue;
const answer = constructorGlobalTypeof(lowerer, a.expression);
if (answer !== null)
return { kind: "boolLit", value: (answer === b.text) !== negated, type: BOOL, loc };
}
return null;
}
/** Static methods of Node 24's Web-platform constructor globals. */
const NODE_CONSTRUCTOR_STATICS: Readonly<Record<string, readonly string[]>> = {
AbortSignal: ["abort", "any", "timeout"],
Response: ["error", "json", "redirect"],
URL: ["canParse", "createObjectURL", "parse", "revokeObjectURL"],
};
/** Node 24's Web-platform constructor globals: `typeof` answers
* "function" for each. */
const NODE_CONSTRUCTOR_GLOBALS: ReadonlySet<string> = new Set([
"AbortController",
"AbortSignal",
"Blob",
"BroadcastChannel",
"CustomEvent",
"DOMException",
"Event",
"EventTarget",
"File",
"FormData",
"Headers",
"MessageChannel",
"MessageEvent",
"MessagePort",
"ReadableStream",
"Request",
"Response",
"TextDecoder",
"TextEncoder",
"TransformStream",
"URL",
"URLSearchParams",
"WritableStream",
]);
/** An operand of `+` with a string: ToString of ToPrimitive with the
* default hint, which tries a class instance's valueOf before toString. */
function concatOperand(lowerer: Lowerer, e: IrExpr, node: ts.Node): IrExpr {
@@ -10652,6 +10749,7 @@ export function lowerBinary(lowerer: Lowerer, expr: ts.BinaryExpression): IrExpr
op === ts.SyntaxKind.ExclamationEqualsEqualsToken
) {
const test =
lowerConstructorGlobalTypeofTest(lowerer, expr, loc) ??
lowerWebAssemblyTypeofTest(lowerer, expr, loc) ??
lowerErrorCodeTypeofTest(lowerer, expr, loc) ??
lowerer.lowerCaughtTypeofTest(expr, loc) ??
@@ -114,6 +114,7 @@ import {
isVarDeclared,
numericIteratorSourceOf,
provenanceElidedConstDecl,
stdlibBufferVarAlias,
} from "./lower-stmts.js";
import { objectFactoryGlobalType } from "./object-factory-new.js";
import { streamClassAliasDecl } from "./lower-stream.js";
@@ -1896,6 +1897,7 @@ export function collectGlobals(
// same test).
const stableStdlibAlias =
isConst ||
stdlibBufferVarAlias(lowerer, decl) ||
((list.flags & ts.NodeFlags.Let) !== 0 &&
ts.isIdentifier(decl.name) &&
stdlibGlobalAliasNameOf(lowerer, decl.initializer) !== null &&
@@ -2879,11 +2881,16 @@ export function lowerFileInit(
// a function called above the declaration reads `undefined`, exactly
// Node); checked-dynamic var globals hold the dyn undefined the same
// way. After the guard: a cache-hit revisit must not reset them.
// A union slot the body retypes checked-dynamic (lowerVarDecl: a
// JavaScript var initialized with an existing checked-dynamic object)
// gets the dyn undefined once the body has lowered.
const unionEntryInits: { at: number; g: IrGlobal }[] = [];
for (const g of lowerer.varGlobalEntryInits.get(sf) ?? []) {
if (g.type.kind !== "union" && g.type.kind !== "dyn") continue;
const wrapped =
g.type.kind === "dyn" ? dynUndefinedExpr(loc0) : lowerer.unassignedSlotInit(g.type, loc0);
if (wrapped) {
if (g.type.kind === "union") unionEntryInits.push({ at: header.length, g });
// A let global whose early reads are checked (initFlag) only
// gets its storage filled here: this is not the source write, so
// it must neither run the TDZ check nor set the flag.
@@ -2978,6 +2985,16 @@ export function lowerFileInit(
);
at++;
}
for (const { at, g } of unionEntryInits) {
if (g.type.kind !== "dyn") continue;
body[at] = {
kind: "assign",
localId: g.id,
value: dynUndefinedExpr(loc0),
...(g.initFlag !== undefined ? { initializes: true as const } : {}),
loc: loc0,
};
}
const moduleRef = cjsModuleRef(lowerer, sf);
if (moduleRef !== null) {
const guardedBody = body.splice(guardId !== undefined ? 2 : 0);
@@ -87,6 +87,7 @@ import {
isJsSourceFile,
isNodeEsmFile,
locOf,
requireEarlyReference,
requireSpecOf,
} from "../program.js";
import {
@@ -98,6 +99,7 @@ import {
sideEffectFreeOptionValue,
stdlibGlobalAliasDecl,
stdlibGlobalAliasNameOf,
bufferNamespaceMember,
stdlibGlobalNameOf,
} from "./surfaces.js";
import { isNativeBuiltinValueInitializer } from "./lower-builtin-values.js";
@@ -191,6 +193,7 @@ import {
import { globalSymbolKey } from "./expressions/global-symbols.js";
import { lowerShortCircuitAssignment } from "./expressions/nullish-assignment.js";
import {
jsHoldsDynObject,
lowerEnvironmentKey,
lowerNativeFunctionAssignment,
lowerStrictEquality,
@@ -1067,13 +1070,124 @@ function hostVariableDeclarationOf(name: ts.Identifier): ts.VariableDeclaration
export function forOfVarTarget(
lowerer: Lowerer,
decl: ts.VariableDeclaration,
valueType?: IrType,
): { id: string; type: IrType } | null {
if (!ts.isIdentifier(decl.name) || !isVarDeclared(decl)) return null;
const symbol = lowerer.checker.getSymbolAtLocation(decl.name);
if (!symbol) return null;
const g = lowerer.globalsBySymbol.get(symbol);
if (g) return g;
return hoistVarBinding(lowerer, symbol, decl.name);
if (g) {
if (valueType?.kind === "dyn" && jsBindingKeepsCheckedObject(lowerer, decl.name, g.type))
g.type = DYN;
return g;
}
return hoistVarBinding(lowerer, symbol, decl.name, valueType);
}
/** JavaScript: whether an unannotated variable binding whose static type
* is `type` keeps a checked-dynamic value unconverted. The checker types
* such a binding by the object it receives (an element of an evolving
* `[]`, a `find` result, a member of a checked-dynamic object), but the
* value is an existing checked-dynamic object or array: a record- or
* array-typed slot would hold a converted copy, so writes through the
* binding would miss the object other references see, and `===` against
* them would fail. Block-scoped bindings with an identifier name keep such
* values by the same rule in lowerVarDecl (preservesObjectIdentity). */
function jsBindingKeepsCheckedObject(
lowerer: Lowerer,
nameNode: ts.Identifier,
type: IrType,
): boolean {
if (!isJsSourceFile(nameNode.getSourceFile())) return false;
const host = hostVariableDeclarationOf(nameNode);
if (!host || host.type || hasJsTypeAnnotation(host)) return false;
const arms = type.kind === "union" ? (lowerer.unions.get(type.unionId)?.arms ?? []) : [type];
return (
arms.some((arm) => arm.kind === "record" || arm.kind === "array") &&
arms.every((arm) => arm.kind === "record" || arm.kind === "array" || isUnitType(arm))
);
}
/** Whether `node` mentions the binding `symbol` anywhere, nested functions
* included. */
function mentionsSymbol(lowerer: Lowerer, node: ts.Node, symbol: ts.Symbol): boolean {
if (ts.isIdentifier(node)) return lowerer.checker.getSymbolAtLocation(node) === symbol;
return (
node.forEachChild((child) => (mentionsSymbol(lowerer, child, symbol) ? true : undefined)) ===
true
);
}
/** Whether `node` evaluates to an existing value rather than constructing
* one whose layout a destination directs: not a literal, `new`, function
* or class expression, nor a conditional with such an arm (an arm that
* is `null` or `undefined` constructs nothing). */
function existingValueForm(node: ts.Expression, armOfConditional = false): boolean {
while (ts.isParenthesizedExpression(node)) node = node.expression;
if (ts.isConditionalExpression(node))
return existingValueForm(node.whenTrue, true) && existingValueForm(node.whenFalse, true);
if (
armOfConditional &&
(node.kind === ts.SyntaxKind.NullKeyword ||
(ts.isIdentifier(node) && node.text === "undefined"))
)
return true;
return !(
ts.isObjectLiteralExpression(node) ||
ts.isArrayLiteralExpression(node) ||
ts.isNewExpression(node) ||
ts.isFunctionExpression(node) ||
ts.isArrowFunction(node) ||
ts.isClassExpression(node) ||
ts.isLiteralExpression(node) ||
node.kind === ts.SyntaxKind.NullKeyword
);
}
/** JavaScript `var x = e` whose binding could hold an existing
* checked-dynamic object (jsBindingKeepsCheckedObject): lowers `e` BEFORE
* minting the hoisted slot, as a block-scoped declaration does, so a
* checked-dynamic value gets a checked-dynamic slot instead of a converted
* copy. Other values coerce into the planned slot exactly as
* lowerExprExpecting would. Null when the declaration does not qualify:
* the slot already exists, the initializer is fresh construction whose
* layout the slot type directs (existingValueForm; a literal holding an
* existing checked-dynamic object qualifies), or it refers to the binding
* itself (hoist-before-init resolves that reference). */
function lowerJsVarFromExisting(
lowerer: Lowerer,
decl: ts.VariableDeclaration,
symbol: ts.Symbol,
): IrStmt | null {
if (!ts.isIdentifier(decl.name) || !decl.initializer) return null;
if (!isJsSourceFile(decl.getSourceFile()) || lowerer.ctx.hoistedVars.has(symbol)) return null;
if (lowerer.bindingIn(lowerer.ctx, symbol)) return null;
let node: ts.Expression = decl.initializer;
while (ts.isParenthesizedExpression(node) || ts.isSatisfiesExpression(node))
node = node.expression;
// A literal holding an existing checked-dynamic object builds as a
// checked-dynamic literal without a destination (lowerArrayLiteral,
// lowerObjectLiteral): a record- or array-typed slot would copy it.
const holdsChecked =
(ts.isArrayLiteralExpression(node) || ts.isObjectLiteralExpression(node)) &&
jsHoldsDynObject(lowerer, node);
if (!holdsChecked && !existingValueForm(node)) return null;
const planned = varBindingType(lowerer, decl.name);
if (!planned || !jsBindingKeepsCheckedObject(lowerer, decl.name, planned)) return null;
if (mentionsSymbol(lowerer, node, symbol)) return null;
const value = lowerer.lowerExpr(node);
const local = hoistVarBinding(
lowerer,
symbol,
decl.name,
value.type.kind === "dyn" ? DYN : undefined,
);
return {
kind: "assign",
localId: local.id,
value: lowerer.coerceExistingInto(node, value, local.type),
loc: locOf(decl),
};
}
/** The `var` symbol's function-scoped binding TYPE — the declared type at
@@ -1129,7 +1243,12 @@ function varBindingType(lowerer: Lowerer, nameNode: ts.Identifier): IrType | nul
* a loop re-ASSIGNS the one binding per pass and never resets it — the
* classic capture semantics fall out: closures made in a loop share the
* single boxed binding, where `let` gets a fresh box per iteration. */
function hoistVarBinding(lowerer: Lowerer, symbol: ts.Symbol, nameNode: ts.Identifier): IrLocal {
function hoistVarBinding(
lowerer: Lowerer,
symbol: ts.Symbol,
nameNode: ts.Identifier,
valueType?: IrType,
): IrLocal {
const existing = lowerer.ctx.hoistedVars.get(symbol);
if (existing) return existing;
// The parameter merge: the symbol already binds a function-root local.
@@ -1138,7 +1257,15 @@ function hoistVarBinding(lowerer: Lowerer, symbol: ts.Symbol, nameNode: ts.Ident
lowerer.ctx.hoistedVars.set(symbol, bound);
return bound;
}
const type = varBindingType(lowerer, nameNode);
let type = varBindingType(lowerer, nameNode);
// The first value stored is an existing checked-dynamic object: keep it
// (jsBindingKeepsCheckedObject). A checked-dynamic slot holds undefined
// natively, so no runtime-optional widening applies.
const keepsChecked =
valueType?.kind === "dyn" &&
type !== null &&
jsBindingKeepsCheckedObject(lowerer, nameNode, type);
if (keepsChecked) type = DYN;
if (!type) lowerer.badType(nameNode, lowerer.typeOf(nameNode));
const root = lowerer.activeStmtLists.find((e) => e.ctx === lowerer.ctx);
if (!root)
@@ -1155,7 +1282,7 @@ function hoistVarBinding(lowerer: Lowerer, symbol: ts.Symbol, nameNode: ts.Ident
};
lowerer.ctx.locals.push(local);
lowerer.ctx.scopes[0]!.set(symbol, local);
noteEarlyReadVar(lowerer, symbol, local);
if (!keepsChecked) noteEarlyReadVar(lowerer, symbol, local);
// A checked-dynamic slot holds the dyn undefined (a NULL dyn is a
// trap); everything else rides unassignedSlotInit (interned union arm,
@@ -4309,6 +4436,8 @@ export function bindPatternTarget(
const symbol = lowerer.checker.getSymbolAtLocation(name);
const g = symbol ? lowerer.globalsBySymbol.get(symbol) : undefined;
if (g) {
if (value.type.kind === "dyn" && jsBindingKeepsCheckedObject(lowerer, name, g.type))
g.type = DYN;
out.push({
kind: "assign",
localId: g.id,
@@ -4325,7 +4454,7 @@ export function bindPatternTarget(
// here by declareParams) carry no block-scope flag either, but their
// bindings are the parameters' own.
if (symbol && hostVariableDeclarationOf(name) !== null && isVarDeclared(name)) {
const hoisted = hoistVarBinding(lowerer, symbol, name);
const hoisted = hoistVarBinding(lowerer, symbol, name, value.type);
out.push({
kind: "assign",
localId: hoisted.id,
@@ -4340,7 +4469,7 @@ export function bindPatternTarget(
value.type.kind === "dyn" &&
(() => {
const mapped = lowerer.mapTypeOf(lowerer.typeOf(name));
return mapped === null;
return mapped === null || jsBindingKeepsCheckedObject(lowerer, name, mapped);
})();
let type = dynStays
? DYN
@@ -4676,6 +4805,7 @@ export function lowerVarDecl(
// alias plumbing (see stdlibGlobalAliasDecl), no storage, no code.
const stableStdlibAlias =
!isLet ||
stdlibBufferVarAlias(lowerer, decl) ||
(ts.isVariableDeclarationList(decl.parent) &&
(decl.parent.flags & ts.NodeFlags.Let) !== 0 &&
ts.isIdentifier(decl.name) &&
@@ -4823,7 +4953,11 @@ export function lowerVarDecl(
!decl.type &&
!hasJsTypeAnnotation(decl) &&
raw.type.kind === "dyn" &&
(g.type.kind === "record" || g.type.kind === "object")
(g.type.kind === "record" ||
g.type.kind === "object" ||
(ts.isIdentifier(decl.name) &&
jsBindingKeepsCheckedObject(lowerer, decl.name, g.type) &&
(existingValueForm(decl.initializer) || jsHoldsDynObject(lowerer, decl.initializer))))
)
g.type = DYN;
g.type = inferredOptionalRecordType(lowerer, decl, g.type, raw.type);
@@ -4870,6 +5004,8 @@ export function lowerVarDecl(
// types already hold the interned undefined arm), and a declaration
// re-run in a loop must not reset the persisting value.
if (isVarDeclared(decl) && declSymbol) {
const fromExisting = lowerJsVarFromExisting(lowerer, decl, declSymbol);
if (fromExisting) return fromExisting;
const local = hoistVarBinding(lowerer, declSymbol, decl.name);
if (!decl.initializer) return null;
const init = lowerer.lowerExprExpecting(decl.initializer, local.type);
@@ -9394,7 +9530,7 @@ export function lowerForOf(lowerer: Lowerer, stmt: ts.ForOfStatement): IrStmt {
// local and the body opens by ASSIGNING it into the one hoisted
// binding — closures capture the shared slot, and the value persists
// after the loop (both Node-exact for var).
const varTarget = forOfVarTarget(lowerer, decl);
const varTarget = forOfVarTarget(lowerer, decl, elemValueT);
if (varTarget) {
const elemType = elemValueT;
const loc = locOf(decl.name);
@@ -10130,7 +10266,7 @@ function lowerForOfDyn(
binds,
);
} else if (ts.isIdentifier(decl.name)) {
const varTarget = forOfVarTarget(lowerer, decl);
const varTarget = forOfVarTarget(lowerer, decl, DYN);
if (varTarget) {
// `for (var x of d)`: the element mechanics stay on a hidden
// per-iteration local; the body opens by assigning the ONE
@@ -10974,3 +11110,25 @@ export function lowerForStatement(lowerer: Lowerer, stmt: ts.ForStatement): IrSt
lowerer.scopes.pop();
}
}
/** safe-buffer's `var Buffer = buffer.Buffer` at file scope: a `var` alias
* of the global Buffer is the same alias plumbing as the const spelling
* when nothing reassigns it and no code that runs before the declaration
* can read it (the require-binding early-reference analysis), so the
* hoisted undefined is unobservable. */
export function stdlibBufferVarAlias(lowerer: Lowerer, decl: ts.VariableDeclaration): boolean {
if (
!ts.isIdentifier(decl.name) ||
!ts.isVariableDeclarationList(decl.parent) ||
(decl.parent.flags & (ts.NodeFlags.Let | ts.NodeFlags.Const)) !== 0 ||
decl.initializer === undefined ||
!bufferNamespaceMember(lowerer, decl.initializer)
)
return false;
const symbol = lowerer.checker.getSymbolAtLocation(decl.name);
return (
symbol !== undefined &&
bindingNeverReassigned(lowerer, symbol, decl) &&
requireEarlyReference(lowerer.program, decl) === null
);
}
@@ -475,6 +475,8 @@ import { lowerConsoleNew } from "./lower-console.js";
import { lowerNumberStaticCall, lowerNumberStaticProperty } from "./builtins/numbers.js";
import { dateJsonType, isDateJsonSource, lowerDateCall } from "./builtins/dates.js";
import {
fsConstantMemberRead,
fsConstantsObject,
lowerFsConstantsProperty,
lowerBuiltinConstantsProperty,
builtinConstantBindingOf,
@@ -9679,16 +9681,23 @@ export class Lowerer {
}
}
}
let e = this.lowerExpr(node);
const e = this.lowerExpr(node);
return expected ? this.coerceExistingInto(node, e, expected) : e;
}
/** The conversion lowerExprExpecting applies to `e`, the lowered value
* of `node` when it is not fresh construction the destination directs
* (a literal, conditional, or empty collection). */
coerceExistingInto(node: ts.Expression, e: IrExpr, expected: IrType): IrExpr {
if (
(expected?.kind === "union" && this.armTag(expected.unionId, UNDEFINED_T) >= 0) ||
(expected?.kind === "dyn" && isJsSourceFile(node.getSourceFile()))
(expected.kind === "union" && this.armTag(expected.unionId, UNDEFINED_T) >= 0) ||
(expected.kind === "dyn" && isJsSourceFile(node.getSourceFile()))
) {
// A destination that accepts undefined must retain an unchecked
// read's storage value even when the checker still calls it present.
e = this.runtimeOptionalSourceValue(node, e) ?? e;
}
return expected ? this.coerceInto(node, e, expected) : e;
return this.coerceInto(node, e, expected);
}
/** An unannotated empty collection has no existing elements or aliases
@@ -12290,13 +12299,7 @@ export class Lowerer {
COPYFILE_FICLONE_FORCE: 4,
};
const value = own(MODES, expr.name.text);
if (value === undefined) {
this.noLowering(
`fs.constants.${expr.name.text}`,
expr,
"F_OK, R_OK, W_OK, X_OK, and COPYFILE_* are the lowered constants",
);
}
if (value === undefined) return fsConstantMemberRead(expr);
return { kind: "numLit", value, type: F64, loc };
}
}
@@ -12365,14 +12368,8 @@ export class Lowerer {
const callable = this.lowerBuiltinCallableValue(bi, loc);
if (callable) return callable;
}
if (bi.member === "constants" && bi.module === "fs") {
// A bare `fs.constants` read (not one of the baked bits above).
this.noLowering(
`fs.constants`,
expr,
"F_OK, R_OK, W_OK, X_OK, and COPYFILE_* are the lowered constants",
);
}
// A bare `fs.constants` read (not one of the baked bits above).
if (bi.member === "constants" && bi.module === "fs") return fsConstantsObject(loc);
if (builtinModuleFnOf(this, bi.module, bi.member)) {
this.unsupported(
"SC1090",
@@ -22,7 +22,7 @@ import {
VOID,
arrayOf,
} from "../../ir/ir.js";
import { isNodeTypesPath, requireSpecOf } from "../program.js";
import { isJsSourceFile, isNodeTypesPath, requireSpecOf } from "../program.js";
import { ZLIB_CONSTANTS } from "./zlib-constants.js";
/** zlib stream instances: runtime Transform streams (scr_zlib_stream.c). */
@@ -773,6 +773,7 @@ export const STATIC_NUMBER_METHODS: Record<
{ fns: readonly IrLibFn[]; minArgs: number; maxArgs: number } | undefined
> = {
toFixed: { fns: ["num.toFixed0", "num.toFixed"], minArgs: 0, maxArgs: 1 },
toPrecision: { fns: ["num.toPrecision"], minArgs: 0, maxArgs: 1 },
};
/** One lowerable builtin-module FUNCTION: `fn`'s libCall with `params`
@@ -1521,6 +1522,7 @@ export const BUILTIN_MODULE_FNS: Record<
hash: { fn: "crypto.hashDigestStr", params: [STRING, STRING, STRING], result: STRING },
timingSafeEqual: { fn: "crypto.timingSafeEqual", params: [BYTES_U8, BYTES_U8], result: BOOL },
randomFillSync: { fn: "crypto.randomFill", params: [BYTES_U8, F64, F64], result: BYTES_U8 },
getRandomValues: { fn: "crypto.getRandomValues", params: [BYTES_U8], result: BYTES_U8 },
randomInt: { fn: "crypto.randomInt", params: [F64, F64], result: F64 },
randomFill: { fn: "crypto.randomFillCb", params: [], result: VOID },
randomUUIDv7: { fn: "crypto.randomUUIDv7", params: [DYN], result: STRING },
@@ -2086,6 +2088,7 @@ export const BUILTIN_MODULE_FN_ALIASES: Record<
"crypto.randomFillPromiseDyn",
],
timingSafeEqual: ["crypto.timingSafeEqualDyn"],
getRandomValues: ["crypto.getRandomValuesDyn"],
randomInt: ["crypto.randomIntMax", "crypto.randomIntCb", "crypto.randomIntPromise"],
randomUUID: ["crypto.randomUUIDOpts"],
},
@@ -2677,6 +2680,15 @@ export const AMBIENT_SURFACE_FNS: readonly AmbientSurfaceRow[] = [
fns: ["os.constants"],
note: "the platform's dlopen, errno, signals and priority groups; members a platform lacks read as undefined",
},
// fs.constants as a value, and its members beyond the baked access-mode
// bits: the target libc's null-prototype constants object.
{
id: "node-builtin.fs.constants",
kind: "node-builtin",
name: "fs.constants",
fns: ["fs.constants"],
note: "node_constants.cc's fs group under the target libc's macros; members a platform lacks read as undefined",
},
];
/** The win32-target overrides of the bare modules' constants: path's
@@ -3302,7 +3314,21 @@ export function stdlibGlobalNameOf(lowerer: Lowerer, expr: ts.Expression): strin
// `Buffer` resolves to a user-file declaration and still misses.
const resolved =
symbol.flags & ts.SymbolFlags.Alias ? lowerer.checker.getAliasedSymbol(symbol) : symbol;
if (!lowerer.isStdlibSymbol(resolved)) return null;
if (!lowerer.isStdlibSymbol(resolved)) {
// A CommonJS binding of the buffer module's Buffer (`var Buffer =
// require('buffer').Buffer`, `const { Buffer } = require('buffer')`
// — safe-buffer and compression): the module member IS the global
// constructor.
if (
decl !== undefined &&
(ts.isVariableDeclaration(decl) || ts.isBindingElement(decl)) &&
isJsSourceFile(decl.getSourceFile())
) {
const imported = lowerer.builtinImportOf(expr);
if (imported?.module === "buffer" && imported.member === "Buffer") return "Buffer";
}
return null;
}
return resolved.name === "global" ? "globalThis" : resolved.name;
}
if (ts.isPropertyAccessExpression(expr) && !expr.questionDotToken) {
@@ -3377,7 +3403,9 @@ export function stdlibGlobalAliasNameOf(
const name =
requireSpec === "process" || requireSpec === "node:process"
? "process"
: stdlibGlobalNameOf(lowerer, aliasExpr);
: bufferNamespaceMember(lowerer, aliasExpr)
? "Buffer"
: stdlibGlobalNameOf(lowerer, aliasExpr);
if (name === null) return null;
// These globals have provenance-aware call or member lowering, so a
// stable snapshot keeps the same dispatch as the original global.
@@ -3398,6 +3426,14 @@ export function stdlibGlobalAliasNameOf(
: null;
}
/** `buffer.Buffer` through a buffer-module namespace binding (`var buffer
* = require('buffer')`): the module's Buffer IS the global constructor. */
export function bufferNamespaceMember(lowerer: Lowerer, expr: ts.Expression): boolean {
if (!ts.isPropertyAccessExpression(expr) || expr.name.text !== "Buffer") return false;
const member = lowerer.builtinMemberOf(expr);
return member?.module === "buffer" && member.member === "Buffer";
}
export function stdlibGlobalAliasDecl(
lowerer: Lowerer,
nameNode: ts.Node,
@@ -137,6 +137,9 @@ export const MAY_THROW_LIB_FNS: ReadonlySet<IrLibFn> = new Set([
"fetch.streamNew",
"fetch.streamFrom",
"num.toFixed",
"num.toPrecision",
"num.toExponentialDigits",
"str.localeCase",
"num.toStringRadix",
"num.toStringRadixF64",
"insp.jsonDyn",
@@ -678,6 +681,8 @@ export const MAY_THROW_LIB_FNS: ReadonlySet<IrLibFn> = new Set([
"crypto.randomIntMax",
"crypto.randomFillView",
"crypto.randomFillViewRest",
"crypto.getRandomValues",
"crypto.getRandomValuesDyn",
"crypto.randomFillSyncDyn",
"crypto.randomFillCb",
"crypto.randomFillCbDyn",
@@ -568,6 +568,8 @@ export const LIB_FN_SIGS = defineLibFnSignatures({
/** os.constants: the per-thread null-prototype constants object (dlopen,
* errno, signals, priority, UV_UDP_REUSEADDR). Never throws. */
"os.constants": { argTypes: [], result: DYN },
/** fs.constants: the target's cached null-prototype constants object. */
"fs.constants": { argTypes: [], result: DYN },
/** The deprecated node:constants object (scr_os.c): the os groups and
* fs.constants merged with the borrowed baked crypto.constants table,
* frozen, one per thread. */
@@ -695,6 +697,13 @@ export const LIB_FN_SIGS = defineLibFnSignatures({
/** `a.localeCompare(b)`: -1/0/1 in Node's root-locale collation for
* Latin-script text (scr_str_locale_compare). Never throws. */
"str.localeCompare": { argTypes: [STRING, STRING], result: F64 },
/** Annex B substr(start, length) with start ToNumber'd and length's
* presence; never throws. */
"str.substr": { argTypes: [STRING, F64, F64, BOOL], result: STRING },
/** toLocaleLowerCase/toLocaleUpperCase (scr_regex.c): the locales value
* as written, upper=true for toLocaleUpperCase. Throws for null and for
* locale forms with no native rules. */
"str.localeCase": { argTypes: [STRING, DYN, BOOL], result: STRING },
"str.replaceLiteral": { argTypes: [STRING, STRING, STRING, BOOL], result: STRING },
// (subject, characters, NUL-separated replacement texts): the fused
// chain of global one-character replacements.
@@ -759,6 +768,11 @@ export const LIB_FN_SIGS = defineLibFnSignatures({
"num.toExponential": { argTypes: [F64], result: STRING },
"num.toFixed0": { argTypes: [F64], result: STRING },
"num.toFixed": { argTypes: [F64, F64], result: STRING },
/** toPrecision(precision) with the argument ToNumber'd and its presence
* (undefined answers ToString); toExponential with a DEFINED
* fractionDigits. Both throw V8's RangeError for out-of-range digits. */
"num.toPrecision": { argTypes: [F64, F64, BOOL], result: STRING },
"num.toExponentialDigits": { argTypes: [F64, F64], result: STRING },
"num.toStringRadix": { argTypes: [F64, DYN], result: STRING },
/** toString(radix) with a number radix (scr_lib.c): truncates it as
* ToIntegerOrInfinity does, THROWS RangeError outside 2..36. */
@@ -2605,6 +2619,11 @@ export const LIB_FN_SIGS = defineLibFnSignatures({
* whether min was given. */
"crypto.randomFillView": { argTypes: [null, F64, F64], result: BYTES_U8 },
"crypto.randomFillViewRest": { argTypes: [null, F64], result: BYTES_U8 },
/** crypto.getRandomValues: a view returns itself at its own type (the
* float/size DOMExceptions are thrown at run time); any other value
* takes the checked form. */
"crypto.getRandomValues": { argTypes: [null], result: BYTES_U8 },
"crypto.getRandomValuesDyn": { argTypes: [DYN], result: DYN },
"crypto.randomFillSyncDyn": { argTypes: [DYN, DYN, DYN], result: DYN },
"crypto.randomFillCb": { argTypes: [null, F64, F64, F64, null], result: VOID },
"crypto.randomFillCbDyn": { argTypes: [DYN, DYN, DYN, F64, null], result: VOID },
@@ -2663,6 +2682,9 @@ export const LIB_FN_SIGS = defineLibFnSignatures({
"buffer.fromStr": { argTypes: [STRING, STRING], result: BYTES_U8 },
"buffer.fromStrChecked": { argTypes: [STRING, STRING], result: BYTES_U8 },
"buffer.brand": { argTypes: [BYTES_U8], result: BYTES_U8 },
/** Buffer.isBuffer over a typed view: whether the view carries the
* Buffer brand (any element kind; only Buffers do). Never throws. */
"buffer.isBranded": { argTypes: [null], result: BOOL },
/** Buffer.from on checked-native strings, bytes, arrays and data-only
* array-like/Buffer-JSON objects. The encoding is a normalized literal;
* non-string inputs ignore it. Copies the input and may throw during
@@ -505,6 +505,8 @@ function scanRuntimeFeatures(mod: IrModule, stopAt?: keyof RuntimeFeatures): Run
}
if (fn.startsWith("dgram.") || fn.startsWith("dns.")) features.dgram = true;
if (fn.startsWith("tlsca.")) features.tlsCa = true;
// Locale case mapping runs the default conversion of the regex unit.
if (fn === "str.localeCase") features.regex = true;
// scr_cipher.c over the mbedTLS archive; DEP0182 reports through the
// process-warning unit, and unknown output encodings render through
// the inspect unit.
+5 -1
View File
@@ -5242,7 +5242,11 @@ function validateFunction(
}
function checkLibSpecialCall(e: IrExpr & { kind: "libCall" }): boolean {
if (e.fn === "crypto.randomFillView" || e.fn === "crypto.randomFillViewRest") {
if (
e.fn === "crypto.randomFillView" ||
e.fn === "crypto.randomFillViewRest" ||
e.fn === "crypto.getRandomValues"
) {
// The filled view is the result, at its own element type.
const view = e.args[0]?.type;
if (view?.kind !== "bytes" || !typeEquals(e.type, view)) {
+15 -2
View File
@@ -618,6 +618,12 @@
"code": "SC2020",
"note": "asymmetric-key operations need a public-key stack (bignum, RSA/EC/EdDSA math) and public and private KeyObjects — neither exists in the static runtime, so no faithful lowering can be small; the lowered crypto surface is hashing, randomness, secret KeyObjects, and the introspection statics"
},
{
"id": "node-builtin.crypto.getRandomValues",
"kind": "node-builtin",
"name": "crypto.getRandomValues",
"status": "static"
},
{
"id": "node-builtin.crypto.hash",
"kind": "node-builtin",
@@ -868,6 +874,13 @@
"name": "fs.closeSync",
"status": "static"
},
{
"id": "node-builtin.fs.constants",
"kind": "node-builtin",
"name": "fs.constants",
"status": "static",
"note": "node_constants.cc's fs group under the target libc's macros; members a platform lacks read as undefined"
},
{
"id": "node-builtin.fs.copyFileSync",
"kind": "node-builtin",
@@ -4849,8 +4862,8 @@
"id": "stdlib.number.toPrecision",
"kind": "stdlib",
"name": "number.prototype.toPrecision",
"status": "unsupported",
"code": "SC2012"
"status": "static",
"note": "the lowered call form takes 0 to 1 arguments"
},
{
"id": "stdlib.number.toString",
@@ -0,0 +1,6 @@
{
"order": [
"<repo>/tests/corpus/1112-number-methods.ts"
],
"diags": []
}
@@ -0,0 +1,6 @@
{
"order": [
"<repo>/tests/corpus/6200-string-annexb-locale-methods.ts"
],
"diags": []
}
@@ -0,0 +1,6 @@
{
"order": [
"<repo>/tests/corpus/6201-number-precision-exponential.ts"
],
"diags": []
}
@@ -0,0 +1,6 @@
{
"order": [
"<repo>/tests/corpus/6202-dyn-string-number-methods.js"
],
"diags": []
}
@@ -0,0 +1,6 @@
{
"order": [
"<repo>/tests/corpus/6203-any-string-number-methods.ts"
],
"diags": []
}
@@ -0,0 +1,6 @@
{
"order": [
"<repo>/tests/corpus/6204-crypto-get-random-values.ts"
],
"diags": []
}
@@ -0,0 +1,6 @@
{
"order": [
"<repo>/tests/corpus/6205-crypto-get-random-values.js"
],
"diags": []
}
@@ -0,0 +1,6 @@
{
"order": [
"<repo>/tests/corpus/6206-buffer-is-buffer-views.ts"
],
"diags": []
}
@@ -0,0 +1,6 @@
{
"order": [
"<repo>/tests/corpus/6207-fs-constants-object.ts"
],
"diags": []
}
@@ -0,0 +1,6 @@
{
"order": [
"<repo>/tests/corpus/6208-fs-constants-require.cjs"
],
"diags": []
}
@@ -0,0 +1,6 @@
{
"order": [
"<repo>/tests/corpus/6209-buffer-module-alias.cjs"
],
"diags": []
}
@@ -0,0 +1,6 @@
{
"order": [
"<repo>/tests/corpus/6210-typeof-global-static-members.js"
],
"diags": []
}
@@ -0,0 +1,6 @@
{
"order": [
"<repo>/tests/corpus/6260-js-var-dyn-identity.js"
],
"diags": []
}
@@ -0,0 +1,6 @@
{
"order": [
"<repo>/tests/corpus/6261-js-destructure-for-of-identity.js"
],
"diags": []
}
@@ -0,0 +1,6 @@
{
"order": [
"<repo>/tests/corpus/6262-js-file-scope-var-identity.js"
],
"diags": []
}
@@ -0,0 +1,6 @@
{
"order": [
"<repo>/tests/corpus/6263-js-factory-return-identity.cjs"
],
"diags": []
}
@@ -1,6 +0,0 @@
{
"order": [
"<repo>/tests/diagnostics/retired-engine-1112-number-methods.ts"
],
"diags": []
}
+3
View File
@@ -293,6 +293,9 @@ ScrBytes *scr_bytes_as_buffer(ScrBytes *bytes) {
return scr_bytes_retain(bytes);
}
/* Nothing on an existing path calls this. */
bool scr_bytes_is_buffer(const ScrBytes *bytes) { return bytes->is_buffer; }
void scr_bytes_release(ScrBytes *b) {
if (!b || b->rc == SIZE_MAX) return; /* NULL: an uninitialized `let` local */
if (--b->rc == 0) {
+2
View File
@@ -159,6 +159,8 @@ ScrBytes *scr_bytes_new(ScrBytesElem elem, double n); /* +1 */
/* Shared u8 view over exactly the source view's raw bytes. Borrows; +1. */
ScrBytes *scr_bytes_raw_view(ScrBytes *bytes);
ScrBytes *scr_bytes_as_buffer(ScrBytes *bytes); /* borrows; +1, Buffer factory only */
/* Buffer.isBuffer over a typed view: the view's Buffer brand. Borrows. */
bool scr_bytes_is_buffer(const ScrBytes *bytes);
/* Views retain the root allocation; offsets are relative to its complete
* byte storage, including when src is itself a subarray. */
ScrBytes *scr_bytes_buffer_view(ScrBytes *src, ScrBytesElem elem,
+4
View File
@@ -51,6 +51,10 @@ ScrBytes *scr_crypto_random_fill_rest(ScrBytes *bytes, double offset);
double scr_crypto_random_int(double min, double max);
double scr_crypto_random_int_max(double max);
ScrDyn *scr_crypto_random_fill_sync_dyn(ScrDyn *buf, ScrDyn *offset, ScrDyn *size);
/* crypto.getRandomValues over a view (returned +1) or any value (the
* value itself, +1); NULL with Node's DOMException pending. */
ScrBytes *scr_crypto_get_random_values(ScrBytes *view);
ScrDyn *scr_crypto_get_random_values_dyn(ScrDyn *value);
double scr_crypto_random_fill_args(ScrBytes *view, bool typed_array, double offset, double size,
int form);
void scr_crypto_random_int_refill(void);
+180
View File
@@ -11408,6 +11408,137 @@ ScrStr *scr_num_to_fixed(double x, double fraction_digits) {
return scr_str_new(out, len);
}
/* String.prototype.substr (Annex B.2.2.1) over substring's UTF-16
* clamp: -Infinity and other starts below -size clamp to 0, an Infinity
* length to the end. Comparisons only (no floating-point literal other
* than zero, which would join the constant pool every binary links).
* Nothing on an existing path calls this. */
ScrStr *scr_str_substr(ScrStr *s, double start, double length, bool has_length) {
double size = scr_str_utf16_len(s);
double first = isnan(start) ? 0 : trunc(start);
if (first < 0) {
first = size + first;
if (first < 0) first = 0;
} else if (first > size) {
first = size;
}
double count = has_length ? (isnan(length) ? 0 : trunc(length)) : size;
if (count < 0) count = 0;
double end = first + count;
if (end > size) end = size;
return scr_str_substring(s, first, end);
}
/* The exponent suffix of toPrecision/toExponential: "e+5", "e-7". */
static size_t scr_num_exp_suffix(char *o, int e) {
size_t n = 0;
o[n++] = 'e';
o[n++] = e < 0 ? '-' : '+';
int a = e < 0 ? -e : e;
char rev[8];
int k = 0;
do {
rev[k++] = (char)('0' + a % 10);
a /= 10;
} while (a != 0);
while (k > 0) o[n++] = rev[--k];
return n;
}
/* Number.prototype.toPrecision(precision) (ECMA-262 §21.1.3.5): undefined
* precision is ToString(x); ToIntegerOrInfinity runs (in the caller's
* coercion) before the non-finite arm, which precedes the 1..100 range
* check. Digits are the exact-value, ties-up n of
* scr_f64_precision_digits; exponential form when e < -6 or e >= p.
* Nothing on an existing path calls this. */
ScrStr *scr_num_to_precision(double x, double precision, bool has_precision) {
if (!has_precision || !isfinite(x)) return scr_f64_to_scrstr(x);
double p = isnan(precision) ? 0 : trunc(precision);
if (!(p >= 1 && p <= 100)) {
static const char msg[] = "toPrecision() argument must be between 1 and 100";
scr_throw_error_msg(SCR_ERR_RANGE, msg, sizeof msg - 1);
return NULL;
}
int digits_count = (int)p;
char out[160];
size_t o = 0;
double a = x;
if (x < 0) {
out[o++] = '-';
a = -x;
}
char d[104];
int e = 0;
if (a == 0) {
memset(d, '0', (size_t)digits_count);
d[digits_count] = '\0';
} else {
scr_f64_precision_digits(a, digits_count, d, &e);
}
if (e < -6 || e >= digits_count) {
out[o++] = d[0];
if (digits_count != 1) {
out[o++] = '.';
memcpy(out + o, d + 1, (size_t)(digits_count - 1));
o += (size_t)(digits_count - 1);
}
o += scr_num_exp_suffix(out + o, e);
} else if (e >= 0) {
memcpy(out + o, d, (size_t)(e + 1));
o += (size_t)(e + 1);
if (e + 1 < digits_count) {
out[o++] = '.';
memcpy(out + o, d + e + 1, (size_t)(digits_count - e - 1));
o += (size_t)(digits_count - e - 1);
}
} else {
out[o++] = '0';
out[o++] = '.';
for (int i = 0; i < -(e + 1); i++) out[o++] = '0';
memcpy(out + o, d, (size_t)digits_count);
o += (size_t)digits_count;
}
return scr_str_new(out, o);
}
/* Number.prototype.toExponential(fractionDigits) with a DEFINED argument
* (the undefined form is scr_num_to_exponential's shortest mantissa):
* f + 1 exact-value, ties-up digits, "d.ddde±X". Nothing on an existing
* path calls this. */
ScrStr *scr_num_to_exponential_digits(double x, double fraction_digits) {
if (!isfinite(x)) return scr_f64_to_scrstr(x);
double f = isnan(fraction_digits) ? 0 : trunc(fraction_digits);
if (!(f >= 0 && f <= 100)) {
static const char msg[] = "toExponential() argument must be between 0 and 100";
scr_throw_error_msg(SCR_ERR_RANGE, msg, sizeof msg - 1);
return NULL;
}
int count = (int)f + 1;
char out[160];
size_t o = 0;
double a = x;
if (x < 0) {
out[o++] = '-';
a = -x;
}
char d[104];
int e = 0;
if (a == 0) {
memset(d, '0', (size_t)count);
d[count] = '\0';
} else {
scr_f64_precision_digits(a, count, d, &e);
}
out[o++] = d[0];
if (count != 1) {
out[o++] = '.';
memcpy(out + o, d + 1, (size_t)(count - 1));
o += (size_t)(count - 1);
}
o += scr_num_exp_suffix(out + o, e);
return scr_str_new(out, o);
}
/* Increment a decimal digit string in place. Returns true on overflow —
* the value becomes 1 followed by len zeros (the caller folds the zeros
* into its scale); an EMPTY string increments to "1" the same way (the
@@ -11739,3 +11870,52 @@ ScrDyn *scr_dyn_native_date_from_ms(double ms) {
scr_native_date_release(date);
return result;
}
/* crypto.getRandomValues(typedArray) (WebCrypto, also node:crypto's
* export): integer typed arrays only (Node's TypeMismatchError DOMException
* for float arrays, DataViews and every other value), at most 65536 bytes
* (QuotaExceededError), filled in place and returned. Nothing on an
* existing path calls these. They stay at the end of the unit: clang emits
* a static helper (scr_random_write) where its first emitted caller
* appears, and an earlier caller would move it in every binary that links
* the random fills. */
ScrBytes *scr_crypto_get_random_values(ScrBytes *view) {
static const char mismatch[] = "TypeMismatchError";
static const char mismatch_msg[] = "The data argument must be an integer-type TypedArray";
static const char quota[] = "QuotaExceededError";
static const char quota_msg[] = "The requested length exceeds 65,536 bytes";
if (view->is_data_view || view->elem == SCR_BYTES_F32 || view->elem == SCR_BYTES_F64 ||
view->elem == SCR_BYTES_F16) {
scr_throw_domex(mismatch, mismatch_msg);
return NULL;
}
/* Integer arithmetic: a new floating-point literal would land in the
* merged constant pool every binary links. */
size_t length = view->len * (size_t)scr_bytes_elem_size(view->elem);
if (length > 65536u) {
scr_throw_domex(quota, quota_msg);
return NULL;
}
scr_random_write(view, 0, length);
return scr_bytes_retain(view);
}
ScrDyn *scr_crypto_get_random_values_dyn(ScrDyn *value) {
if (value->kind == SCR_DYN_TYPED_REF) {
ScrDyn *materialized = scr_dyn_typed_ref_materialize(value);
if (!materialized) return NULL;
ScrDyn *result = scr_crypto_get_random_values_dyn(materialized);
scr_dyn_release(materialized);
return result ? scr_dyn_retain(value) : NULL;
}
if (value->kind != SCR_DYN_BYTES) {
static const char mismatch[] = "TypeMismatchError";
static const char mismatch_msg[] = "The data argument must be an integer-type TypedArray";
scr_throw_domex(mismatch, mismatch_msg);
return NULL;
}
ScrBytes *filled = scr_crypto_get_random_values(value->v.bytes);
if (!filled) return NULL;
scr_bytes_release(filled);
return scr_dyn_retain(value);
}
+160
View File
@@ -654,3 +654,163 @@ size_t scr_f64_to_fixed(double x, int f, char *buf) {
*o = '\0';
return (size_t)(o - buf);
}
/* ── toPrecision / toExponential(fractionDigits) digit engine ──────────
* The spec's n and e for p significant digits: the integer n with
* 10^(p-1) <= n < 10^p for which n × 10^(e-p+1) - x is closest to zero,
* ties picking the LARGER n (half-up on the exact binary value). Exact
* big-integer arithmetic over the double's mantissa: the value scaled by
* 10^(p-1-e) is mantissa × 2^(binary_exp + f) × 5^f, and
* round_half_up(v) = floor((floor(2v) + 1) / 2) composes from floor
* divisions alone. Values reach ~1100 bits (5e-324 at 100 digits and
* 1.8e308), so the limbs are wider than toFixed's. Nothing on an existing
* path calls this. */
#define SCR_PREC_LIMBS 40
typedef struct {
uint32_t limb[SCR_PREC_LIMBS]; /* little-endian */
int len;
} ScrPrecInt;
static void scr_prec_normalize(ScrPrecInt *v) {
while (v->len > 1 && v->limb[v->len - 1] == 0) v->len--;
}
static void scr_prec_mul_small(ScrPrecInt *v, uint32_t m) {
uint64_t carry = 0;
for (int i = 0; i < v->len; i++) {
uint64_t p = (uint64_t)v->limb[i] * m + carry;
v->limb[i] = (uint32_t)p;
carry = p >> 32;
}
if (carry != 0) v->limb[v->len++] = (uint32_t)carry;
}
static uint32_t scr_prec_div_small(ScrPrecInt *v, uint32_t d) {
uint64_t rem = 0;
for (int i = v->len - 1; i >= 0; i--) {
uint64_t cur = (rem << 32) | v->limb[i];
v->limb[i] = (uint32_t)(cur / d);
rem = cur % d;
}
scr_prec_normalize(v);
return (uint32_t)rem;
}
static void scr_prec_shl(ScrPrecInt *v, int bits) {
uint32_t out[SCR_PREC_LIMBS] = {0};
int words = bits / 32;
int rem = bits % 32;
for (int i = 0; i < v->len; i++) {
int at = i + words;
out[at] |= v->limb[i] << rem;
if (rem != 0 && at + 1 < SCR_PREC_LIMBS) out[at + 1] |= v->limb[i] >> (32 - rem);
}
int n = v->len + words + 1;
if (n > SCR_PREC_LIMBS) n = SCR_PREC_LIMBS;
memcpy(v->limb, out, sizeof out);
v->len = n;
scr_prec_normalize(v);
}
static void scr_prec_shr(ScrPrecInt *v, int bits) {
int words = bits / 32;
int rem = bits % 32;
if (words >= v->len) {
v->limb[0] = 0;
v->len = 1;
return;
}
int n = v->len - words;
for (int i = 0; i < n; i++) {
uint32_t lo = v->limb[i + words] >> rem;
uint32_t hi = rem != 0 && i + words + 1 < v->len ? v->limb[i + words + 1] << (32 - rem) : 0;
v->limb[i] = lo | hi;
}
v->len = n;
scr_prec_normalize(v);
}
/* round_half_up(a × 10^f) for positive finite a, as a decimal digit
* string written right-aligned ending at `end`; returns the first digit. */
static char *scr_prec_scaled_digits(double a, int f, char *end) {
uint64_t bits;
memcpy(&bits, &a, sizeof bits);
uint64_t mantissa = bits & ((UINT64_C(1) << 52) - 1);
int ieee_exp = (int)((bits >> 52) & 0x7ffu);
int binary_exp;
if (ieee_exp == 0) {
binary_exp = -1074;
} else {
mantissa |= UINT64_C(1) << 52;
binary_exp = ieee_exp - 1023 - 52;
}
ScrPrecInt v = {{(uint32_t)mantissa, (uint32_t)(mantissa >> 32)}, 2};
scr_prec_normalize(&v);
for (int i = 0; i < f; i++) scr_prec_mul_small(&v, 5);
int twos = binary_exp + f + 1; /* the +1 doubles: floor(2v) */
if (twos > 0) scr_prec_shl(&v, twos);
for (int i = 0; i < -f; i++) scr_prec_div_small(&v, 5);
if (twos < 0) scr_prec_shr(&v, -twos);
/* (floor(2v) + 1) / 2 */
uint64_t carry = 1;
for (int i = 0; i < v.len && carry != 0; i++) {
uint64_t s = (uint64_t)v.limb[i] + carry;
v.limb[i] = (uint32_t)s;
carry = s >> 32;
}
if (carry != 0) v.limb[v.len++] = (uint32_t)carry;
scr_prec_shr(&v, 1);
char *p = end;
for (;;) {
uint32_t chunk = scr_prec_div_small(&v, 1000000000u);
bool last = v.len == 1 && v.limb[0] == 0;
for (int i = 0; i < 9 && (chunk != 0 || !last || p == end); i++) {
*--p = (char)('0' + chunk % 10);
chunk /= 10;
}
if (last) break;
}
return p;
}
int scr_f64_precision_digits(double a, int p, char *digits, int *e_out) {
char shortest[18];
int n;
scr_f64_digits(a, shortest, &n);
int e = n - 1;
char buf[400];
char *end = buf + sizeof buf;
for (int attempt = 0; attempt < 3; attempt++) {
char *first = scr_prec_scaled_digits(a, p - 1 - e, end);
int len = (int)(end - first);
if (len == p) {
memcpy(digits, first, (size_t)p);
digits[p] = '\0';
*e_out = e;
return p;
}
if (len > p) {
/* Rounding carried into a new digit (or the estimate was low). */
if (len == p + 1 && first[0] == '1') {
bool zeros = true;
for (int i = 1; i < len; i++) zeros = zeros && first[i] == '0';
if (zeros) {
digits[0] = '1';
memset(digits + 1, '0', (size_t)(p - 1));
digits[p] = '\0';
*e_out = e + 1;
return p;
}
}
e++;
} else {
e--;
}
}
/* Unreachable for finite positive doubles: the estimate is off by at
* most one decade. */
memset(digits, '0', (size_t)p);
digits[p] = '\0';
*e_out = e;
return p;
}
+11
View File
@@ -37,6 +37,12 @@ int scr_decimal_scan(const char *p, size_t n, double *out);
* the length. */
size_t scr_f64_to_fixed(double x, int f, char *buf);
/* toPrecision / toExponential(fractionDigits) digits for POSITIVE finite
* a and 1 <= p <= 101: the spec's p-digit n (ties to the larger n on the
* exact binary value) into digits (NUL-terminated, >= p + 1 bytes) and its
* decimal exponent e (value ~ d.ddd x 10^e) into *e_out. Returns p. */
int scr_f64_precision_digits(double a, int p, char *digits, int *e_out);
/* ToString for template literals / string coercion. Returns +1. */
ScrStr *scr_f64_to_scrstr(double x);
ScrStr *scr_bool_to_scrstr(bool b); /* interned "true"/"false" */
@@ -73,6 +79,11 @@ bool scr_num_is_safe_integer(double x);
ScrStr *scr_num_to_exponential(double x);
ScrStr *scr_num_to_fixed0(double x);
ScrStr *scr_num_to_fixed(double x, double fraction_digits);
/* Number.prototype.toPrecision(precision) and toExponential(fractionDigits)
* with the argument already coerced (has_precision false = undefined).
* Invalid digit counts throw V8's RangeError (NULL). Result +1. */
ScrStr *scr_num_to_precision(double x, double precision, bool has_precision);
ScrStr *scr_num_to_exponential_digits(double x, double fraction_digits);
ScrStr *scr_num_to_string_radix_f64(double value, double radix);
/* Object.is over two numbers — the spec's SameValue on doubles: NaN
+175
View File
@@ -1526,3 +1526,178 @@ ScrDyn *scr_node_constants(const ScrDyn *crypto_table) {
scr_atexit(scr_node_constants_clear);
return scr_dyn_retain(o);
}
/* fs.constants as a value: node_constants.cc's fs group (the set
* node:constants merges above), in its order and under its #ifdef guards,
* on its own cached null-prototype object with read-only members. It keeps
* its own table instead of calling the scr_fs_constants fill: a second
* caller would stop that static from inlining into scr_node_constants and
* change every binary that links node:constants. The names live inside
* the table entries, so the table is one data object section GC drops.
* Nothing on an existing path calls this. */
typedef struct {
char name[32];
double value;
} ScrFsConstant;
#define SCR_FS_C(n) {#n, (double)(n)}
static const ScrFsConstant scr_fs_constant_table[] = {
{"UV_FS_SYMLINK_DIR", 1},
{"UV_FS_SYMLINK_JUNCTION", 2},
#ifdef O_RDONLY
SCR_FS_C(O_RDONLY),
#endif
#ifdef O_WRONLY
SCR_FS_C(O_WRONLY),
#endif
#ifdef O_RDWR
SCR_FS_C(O_RDWR),
#endif
{"UV_DIRENT_UNKNOWN", 0},
{"UV_DIRENT_FILE", 1},
{"UV_DIRENT_DIR", 2},
{"UV_DIRENT_LINK", 3},
{"UV_DIRENT_FIFO", 4},
{"UV_DIRENT_SOCKET", 5},
{"UV_DIRENT_CHAR", 6},
{"UV_DIRENT_BLOCK", 7},
#ifdef S_IFMT
SCR_FS_C(S_IFMT),
#endif
#ifdef S_IFREG
SCR_FS_C(S_IFREG),
#endif
#ifdef S_IFDIR
SCR_FS_C(S_IFDIR),
#endif
#ifdef S_IFCHR
SCR_FS_C(S_IFCHR),
#endif
#ifdef S_IFBLK
SCR_FS_C(S_IFBLK),
#endif
#ifdef S_IFIFO
SCR_FS_C(S_IFIFO),
#endif
#ifdef S_IFLNK
SCR_FS_C(S_IFLNK),
#endif
#ifdef S_IFSOCK
SCR_FS_C(S_IFSOCK),
#endif
#ifdef O_CREAT
SCR_FS_C(O_CREAT),
#endif
#ifdef O_EXCL
SCR_FS_C(O_EXCL),
#endif
{"UV_FS_O_FILEMAP", 0},
{"UV_FS_O_TEMPORARY", 0},
{"UV_FS_O_SHORT_LIVED", 0},
{"UV_FS_O_SEQUENTIAL", 0},
{"UV_FS_O_RANDOM", 0},
#ifdef O_NOCTTY
SCR_FS_C(O_NOCTTY),
#endif
#ifdef O_TRUNC
SCR_FS_C(O_TRUNC),
#endif
#ifdef O_APPEND
SCR_FS_C(O_APPEND),
#endif
#ifdef O_DIRECTORY
SCR_FS_C(O_DIRECTORY),
#endif
#ifdef O_NOATIME
SCR_FS_C(O_NOATIME),
#endif
#ifdef O_NOFOLLOW
SCR_FS_C(O_NOFOLLOW),
#endif
#ifdef O_SYNC
SCR_FS_C(O_SYNC),
#endif
#ifdef O_DSYNC
SCR_FS_C(O_DSYNC),
#endif
#ifdef O_SYMLINK
SCR_FS_C(O_SYMLINK),
#endif
#ifdef O_DIRECT
SCR_FS_C(O_DIRECT),
#endif
#ifdef O_NONBLOCK
SCR_FS_C(O_NONBLOCK),
#endif
#ifdef S_IRWXU
SCR_FS_C(S_IRWXU),
#endif
#ifdef S_IRUSR
SCR_FS_C(S_IRUSR),
#endif
#ifdef S_IWUSR
SCR_FS_C(S_IWUSR),
#endif
#ifdef S_IXUSR
SCR_FS_C(S_IXUSR),
#endif
#ifdef S_IRWXG
SCR_FS_C(S_IRWXG),
#endif
#ifdef S_IRGRP
SCR_FS_C(S_IRGRP),
#endif
#ifdef S_IWGRP
SCR_FS_C(S_IWGRP),
#endif
#ifdef S_IXGRP
SCR_FS_C(S_IXGRP),
#endif
#ifdef S_IRWXO
SCR_FS_C(S_IRWXO),
#endif
#ifdef S_IROTH
SCR_FS_C(S_IROTH),
#endif
#ifdef S_IWOTH
SCR_FS_C(S_IWOTH),
#endif
#ifdef S_IXOTH
SCR_FS_C(S_IXOTH),
#endif
#ifdef F_OK
SCR_FS_C(F_OK),
#endif
#ifdef R_OK
SCR_FS_C(R_OK),
#endif
#ifdef W_OK
SCR_FS_C(W_OK),
#endif
#ifdef X_OK
SCR_FS_C(X_OK),
#endif
{"UV_FS_COPYFILE_EXCL", 1},
{"COPYFILE_EXCL", 1},
{"UV_FS_COPYFILE_FICLONE", 2},
{"COPYFILE_FICLONE", 2},
{"UV_FS_COPYFILE_FICLONE_FORCE", 4},
{"COPYFILE_FICLONE_FORCE", 4},
};
#undef SCR_FS_C
static SCR_TL ScrDyn *scr_fs_constants_value;
static void scr_fs_constants_clear(void) {
ScrDyn *value = scr_fs_constants_value;
scr_fs_constants_value = NULL;
scr_dyn_release(value);
}
ScrDyn *scr_fs_constants_object(void) {
if (scr_fs_constants_value) return scr_dyn_retain(scr_fs_constants_value);
ScrDyn *o = scr_dyn_new_obj_null_proto();
for (size_t i = 0; i < sizeof scr_fs_constant_table / sizeof scr_fs_constant_table[0]; i++)
scr_os_constant(o, scr_fs_constant_table[i].name, scr_fs_constant_table[i].value);
scr_fs_constants_value = o;
scr_atexit(scr_fs_constants_clear);
return scr_dyn_retain(o);
}
+2
View File
@@ -38,6 +38,8 @@ ScrStr *scr_os_cpus_model(double i);
double scr_os_cpus_field(double i, double field);
/* os.constants: the per-thread null-prototype constants object (+1). */
ScrDyn *scr_os_constants(void);
/* fs.constants: the target's null-prototype constants object (cached, +1). */
ScrDyn *scr_fs_constants_object(void);
/* The deprecated node:constants module object (scr_os.c): the os groups,
* fs.constants and the crypto table the borrowed function answers, merged
* and frozen (+1). */
+216
View File
@@ -2020,6 +2020,222 @@ static ScrStr *scr_str_case_conv(const ScrStr *s, int to_lower) {
ScrStr *scr_str_to_lower(const ScrStr *s) { return scr_str_case_conv(s, 1); }
ScrStr *scr_str_to_upper(const ScrStr *s) { return scr_str_case_conv(s, 0); }
/* ── String.prototype.toLocaleLowerCase / toLocaleUpperCase ────────────
* V8 decides the case mapping from the FIRST requested locale only
* (probed: ["en", 5] maps like "en", [5] throws, a non-object primitive
* requests nothing). ICU tailors case mapping for Turkic (tr, az: dotted
* and dotless I), Lithuanian (dot above retention) and Greek uppercasing
* (accent removal); every other language uses the default conversion.
* The Turkic rules are SpecialCasing's: lowercase I → ı unless a
* U+0307 follows (then I + U+0307 → i), İ → i; uppercase i → İ. Tags are
* checked against the unicode_locale_id structure; anything this unit
* cannot decide exactly (malformed tags, whose V8 RangeError texts vary,
* objects, lt, el uppercasing, I before other combining marks) throws a
* 'not supported yet' Error rather than guess. Nothing on an existing
* path calls this. */
static bool scr_tag_alpha(const char *p, size_t n) {
for (size_t i = 0; i < n; i++)
if (!((p[i] >= 'a' && p[i] <= 'z') || (p[i] >= 'A' && p[i] <= 'Z'))) return false;
return true;
}
static bool scr_tag_digit(const char *p, size_t n) {
for (size_t i = 0; i < n; i++)
if (!(p[i] >= '0' && p[i] <= '9')) return false;
return true;
}
static bool scr_tag_alnum(const char *p, size_t n) {
for (size_t i = 0; i < n; i++)
if (!scr_tag_alpha(p + i, 1) && !scr_tag_digit(p + i, 1)) return false;
return true;
}
static bool scr_tag_same(const char *a, size_t an, const char *b, size_t bn) {
if (an != bn) return false;
for (size_t i = 0; i < an; i++) {
char x = a[i] >= 'A' && a[i] <= 'Z' ? (char)(a[i] + 32) : a[i];
char y = b[i] >= 'A' && b[i] <= 'Z' ? (char)(b[i] + 32) : b[i];
if (x != y) return false;
}
return true;
}
/* 0 default, 1 Turkic, 2 Lithuanian, 3 Greek; -1 when the tag is outside
* the structural grammar handled here. */
static int scr_locale_case_rules(const char *tag, size_t len) {
const char *subtags[64];
size_t lens[64];
size_t count = 0;
size_t start = 0;
for (size_t i = 0; i <= len; i++) {
if (i < len && tag[i] != '-') continue;
if (i == start || count == 64) return -1;
subtags[count] = tag + start;
lens[count++] = i - start;
start = i + 1;
}
size_t k = 0;
size_t lang = lens[0];
if (!scr_tag_alpha(subtags[0], lang) || !((lang >= 2 && lang <= 3) || (lang >= 5 && lang <= 8)))
return -1;
k = 1;
if (k < count && lens[k] == 4 && scr_tag_alpha(subtags[k], 4)) k++;
if (k < count && ((lens[k] == 2 && scr_tag_alpha(subtags[k], 2)) ||
(lens[k] == 3 && scr_tag_digit(subtags[k], 3))))
k++;
size_t variants = k;
while (k < count && scr_tag_alnum(subtags[k], lens[k]) &&
((lens[k] >= 5 && lens[k] <= 8) || (lens[k] == 4 && scr_tag_digit(subtags[k], 1)))) {
for (size_t j = variants; j < k; j++)
if (scr_tag_same(subtags[j], lens[j], subtags[k], lens[k])) return -1;
k++;
}
size_t singletons = k;
while (k < count) {
if (lens[k] != 1 || !scr_tag_alnum(subtags[k], 1)) return -1;
for (size_t j = singletons; j < k; j++)
if (lens[j] == 1 && scr_tag_same(subtags[j], 1, subtags[k], 1)) return -1;
bool private_use = subtags[k][0] == 'x' || subtags[k][0] == 'X';
size_t min = private_use ? 1 : 2;
size_t first = ++k;
while (k < count && lens[k] >= min && lens[k] <= 8 && scr_tag_alnum(subtags[k], lens[k]) &&
(private_use || lens[k] > 1))
k++;
if (k == first) return -1;
if (private_use && k != count) return -1;
}
if (lang > 3) return 0;
char l[3] = {0, 0, 0};
for (size_t i = 0; i < lang; i++)
l[i] = subtags[0][i] >= 'A' && subtags[0][i] <= 'Z' ? (char)(subtags[0][i] + 32) : subtags[0][i];
bool two = lang == 2;
if ((two && l[0] == 't' && l[1] == 'r') || (two && l[0] == 'a' && l[1] == 'z') ||
(!two && l[0] == 't' && l[1] == 'u' && l[2] == 'r') ||
(!two && l[0] == 'a' && l[1] == 'z' && l[2] == 'e'))
return 1;
if ((two && l[0] == 'l' && l[1] == 't') || (!two && l[0] == 'l' && l[1] == 'i' && l[2] == 't'))
return 2;
if ((two && l[0] == 'e' && l[1] == 'l') || (!two && l[0] == 'e' && l[1] == 'l' && l[2] == 'l') ||
(!two && l[0] == 'g' && l[1] == 'r' && l[2] == 'e'))
return 3;
return 0;
}
static ScrStr *scr_locale_case_unsupported(bool upper) {
static const char lower_msg[] =
"String.prototype.toLocaleLowerCase with this locale argument is not supported yet";
static const char upper_msg[] =
"String.prototype.toLocaleUpperCase with this locale argument is not supported yet";
/* The unit's existing "SC2020" literal: a separate constant array would
* absorb it (LLVM merges identical constants) and move it out of the
* string section every regex-linking binary keeps. */
if (upper) scr_throw_error_msg_code(SCR_ERR_ERROR, upper_msg, sizeof upper_msg - 1, "SC2020");
else scr_throw_error_msg_code(SCR_ERR_ERROR, lower_msg, sizeof lower_msg - 1, "SC2020");
return NULL;
}
static ScrStr *scr_turkic_case(const ScrStr *s, bool upper) {
ScrJsonBuf b;
scr_jb_init(&b);
for (size_t i = 0; i < s->len; i++) {
unsigned char c = (unsigned char)s->data[i];
if (upper && c == 'i') {
scr_jb_putc(&b, (char)0xC4); /* İ */
scr_jb_putc(&b, (char)0xB0);
continue;
}
if (!upper && c == 'I') {
unsigned char n0 = i + 1 < s->len ? (unsigned char)s->data[i + 1] : 0;
unsigned char n1 = i + 2 < s->len ? (unsigned char)s->data[i + 2] : 0;
if (n0 == 0xCC && n1 == 0x87) { /* I + U+0307 → i */
scr_jb_putc(&b, 'i');
i += 2;
continue;
}
/* Another combining mark (U+0300..U+036F) may precede a U+0307. */
if ((n0 == 0xCC && n1 >= 0x80) || (n0 == 0xCD && n1 >= 0x80 && n1 <= 0xAF)) {
scr_str_release(scr_jb_finish(&b));
return scr_locale_case_unsupported(upper);
}
scr_jb_putc(&b, (char)0xC4); /* ı */
scr_jb_putc(&b, (char)0xB1);
continue;
}
if (!upper && c == 0xC4 && i + 1 < s->len && (unsigned char)s->data[i + 1] == 0xB0) {
scr_jb_putc(&b, 'i'); /* İ → i */
i++;
continue;
}
scr_jb_putc(&b, (char)c);
}
ScrStr *mapped = scr_jb_finish(&b);
ScrStr *result = scr_str_case_conv(mapped, upper ? 0 : 1);
scr_str_release(mapped);
return result;
}
ScrStr *scr_str_locale_case(const ScrStr *s, ScrDyn *locales, bool upper) {
ScrDyn *owned = NULL;
if (locales->kind == SCR_DYN_TYPED_REF) {
owned = scr_dyn_typed_ref_materialize(locales);
if (!owned) return NULL;
locales = owned;
}
const ScrDyn *tag = NULL;
int rules = 0;
switch (locales->kind) {
case SCR_DYN_NULL: {
static const char msg[] = "Cannot convert undefined or null to object";
scr_throw_error_msg(SCR_ERR_TYPE, msg, sizeof msg - 1);
return NULL;
}
case SCR_DYN_UNDEF:
case SCR_DYN_NUM:
case SCR_DYN_BOOL:
case SCR_DYN_SYMBOL:
case SCR_DYN_BIGINT:
break;
case SCR_DYN_STR:
tag = locales;
break;
case SCR_DYN_ARR:
if (locales->v.arr.len == 0) break;
if (locales->v.arr.presence && !locales->v.arr.presence[0]) {
static const char msg[] = "Language ID should be string or object.";
scr_throw_error_msg(SCR_ERR_TYPE, msg, sizeof msg - 1);
scr_dyn_release(owned);
return NULL;
}
tag = locales->v.arr.items[0];
if (tag->kind != SCR_DYN_STR) {
bool primitive = tag->kind == SCR_DYN_UNDEF || tag->kind == SCR_DYN_NULL ||
tag->kind == SCR_DYN_NUM || tag->kind == SCR_DYN_BOOL ||
tag->kind == SCR_DYN_BIGINT || tag->kind == SCR_DYN_SYMBOL;
scr_dyn_release(owned);
if (!primitive) return scr_locale_case_unsupported(upper);
static const char msg[] = "Language ID should be string or object.";
scr_throw_error_msg(SCR_ERR_TYPE, msg, sizeof msg - 1);
return NULL;
}
break;
default:
scr_dyn_release(owned);
return scr_locale_case_unsupported(upper);
}
if (tag) {
rules = scr_locale_case_rules(tag->v.str->data, tag->v.str->len);
if (rules < 0 || rules == 2 || (rules == 3 && upper)) {
scr_dyn_release(owned);
return scr_locale_case_unsupported(upper);
}
}
scr_dyn_release(owned);
if (rules == 1) return scr_turkic_case(s, upper);
return scr_str_case_conv(s, upper ? 0 : 1);
}
static void *scr_normalize_realloc(void *opaque, void *ptr, size_t size) {
(void)opaque;
if (!size) { free(ptr); return NULL; }
+11
View File
@@ -73,6 +73,10 @@ bool scr_str_ends_with_from(ScrStr *s, ScrStr *needle, double end_position);
ScrStr *scr_str_slice(ScrStr *s, double start, double end);
/* substring: clamp-and-swap (negatives clamp to 0, start > end swaps). */
ScrStr *scr_str_substring(ScrStr *s, double start, double end);
/* Annex B substr(start, length): start already ToNumber'd (NaN → 0), a
* negative start counts from the end, length absent (has_length false)
* runs to the end. Returns +1. */
ScrStr *scr_str_substr(ScrStr *s, double start, double length, bool has_length);
/* Scalar-only local slices retain their original owner without copying.
* Bounds and split-surrogate replacements are snapshotted at construction. */
@@ -111,6 +115,13 @@ ScrStr *scr_str_trim(ScrStr *s);
ScrStr *scr_str_to_lower(const ScrStr *s);
ScrStr *scr_str_to_upper(const ScrStr *s);
ScrStr *scr_str_normalize(const ScrStr *s, const ScrStr *form);
/* toLocaleLowerCase/toLocaleUpperCase(locales) (scr_regex.c): undefined,
* empty lists, and primitives other than strings use the default case
* conversion; the first requested tag selects the Turkic (tr, az) rules or
* the default ones. Lithuanian, Greek uppercasing and tags outside the
* structural grammar throw a 'not supported yet' Error; null throws Node's
* TypeError. Borrows both; +1 or NULL with an exception pending. */
ScrStr *scr_str_locale_case(const ScrStr *s, struct ScrDyn *locales, bool upper);
/* NFC over a code point buffer (scr_regex.c, beside normalize): a fresh
* malloc'd buffer the caller frees, its length in *out_len. Borrows input. */
uint32_t *scr_nfc_code_points(const uint32_t *input, size_t length, size_t *out_len);
@@ -1,7 +1,5 @@
// Explicit toFixed is static; toPrecision/toString(radix) execute in the
// island. The receiver marshals by value there and the engine auto-boxes
// it, so `this` binds the number exactly as in JS. All stay byte-exact
// differential.
// Number.prototype.toFixed, toPrecision and toString(radix) on typed
// numbers all compile statically and stay byte-exact differential.
const n = 1234.5678;
console.log(n.toFixed(0), n.toFixed(2), n.toFixed(6));
console.log((0.1 + 0.2).toFixed(17), (-3.7).toFixed(1), (0).toFixed(2));
@@ -0,0 +1,33 @@
// String.prototype.substr, the locale case pair and the Annex B HTML
// methods on typed strings. substr counts a negative start from the end
// and clamps its length; toLocaleLowerCase/toLocaleUpperCase use the
// default case conversion except for the Turkic languages, decided by the
// first requested locale only; the HTML methods escape '"' in attribute
// values.
const s = "Hello, World";
console.log(s.substr(0), s.substr(7), s.substr(-5), s.substr(-50, 3), s.substr(3, 4));
console.log(JSON.stringify([s.substr(20), s.substr(2, -1), s.substr(NaN, 2), s.substr(1, Infinity)]));
console.log(JSON.stringify([s.substr(-Infinity, 2), s.substr(1.9, 2.9), "😀ab".substr(1, 2)]));
function tail(from: number, length?: number): string {
return s.substr(from, length);
}
console.log(tail(2), tail(2, 3), tail(-3, undefined));
function lower(text: string, locale?: string | string[]): string {
return text.toLocaleLowerCase(locale);
}
function upper(text: string, locale?: string | string[]): string {
return text.toLocaleUpperCase(locale);
}
const sample = "IİiıÍ ΣΑΣ Straße";
for (const locale of [undefined, "en", "en-US", "de-DE", "TR", "tr-TR", "az", "tur", "zh-Hant-TW", "und", "en-u-co-phonebk", "en-x-tr"]) {
console.log(String(locale), lower(sample, locale), upper(sample, locale));
}
console.log(lower(sample, ["tr", "en"]), lower(sample, ["en", "tr"]), lower(sample, []));
console.log(upper("istanbul", ["az"]), lower("DİYARBAKIR"), lower("İx", "tr").length);
console.log("Dž".toLocaleUpperCase(), "ß".toLocaleUpperCase("de"), "ffi".toLocaleUpperCase("en"));
console.log(s.anchor('a"b'), s.big(), s.blink(), s.bold(), s.fixed());
console.log(s.fontcolor("red"), s.fontsize(7), s.italics(), s.link("https://x/?q=\"1\""));
console.log(s.small(), s.strike(), s.sub(), s.sup(), "".anchor(""));
@@ -0,0 +1,34 @@
// Number.prototype.toPrecision and toExponential(fractionDigits): the
// exact binary value rounds half up at the requested digit, exponential
// form takes over when the exponent is below -6 or reaches the precision,
// and the non-finite answers come before the digit-range RangeError.
const values = [0, -0, 1, -1, 2.5, -2.5, 0.5, 1.005, 1.45, 123.456, 0.000001234, 0.0000001234, 1e21, 1e-7, 5e-324, 1.7976931348623157e308, 123456789012345680000, 0.1, 99.99, 9.995, 1e23, -1234.5678];
for (const v of values) {
const p = [1, 2, 3, 5, 10, 21].map((d) => v.toPrecision(d));
const e = [0, 1, 2, 6, 20].map((d) => v.toExponential(d));
console.log(String(v), p.join(" "), e.join(" "));
}
console.log((123.456).toPrecision(), (123.456).toPrecision(undefined), (1 / 3).toPrecision(100));
console.log((1 / 3).toExponential(100), (2 ** 70).toPrecision(25), (2 ** -30).toExponential(30));
console.log(NaN.toPrecision(200), Infinity.toPrecision(0), (-Infinity).toExponential(-1), NaN.toExponential(101));
console.log((15).toPrecision(1.9), (15).toExponential(1.9), (0.00001).toPrecision(1), (1e-6).toPrecision(2));
for (const bad of [0, 101, -1, Infinity]) {
try {
console.log((1).toPrecision(bad));
} catch (e) {
console.log((e as Error).name, (e as Error).message);
}
}
for (const bad of [-1, 101, Infinity]) {
try {
console.log((1).toExponential(bad));
} catch (e) {
console.log((e as Error).name, (e as Error).message);
}
}
let n = 0;
for (let i = 1; i < 2000; i++) {
const x = i / 7 + i * 1e-9;
if (x.toPrecision(7) !== Number(x.toPrecision(7)).toPrecision(7)) n++;
}
console.log("stable", n);
@@ -0,0 +1,92 @@
// String and Number methods on JS-inferred receivers: string receivers run
// the real method (string-width's codePointAt, camelcase's
// toLocaleLowerCase, toml's substr, pretty-bytes' toPrecision); other
// receivers keep JavaScript's answer for the name, an object's own method
// or Node's TypeError; arrays keep the runtime dispatch of shared names.
function width(text) {
let total = 0;
for (const character of text) {
const code = character.codePointAt(0);
total += code >= 0x1100 ? 2 : 1;
}
return total;
}
console.log(width("abc"), width("古"), width("👍x"));
function describe(input) {
return [
input.codePointAt(0),
input.codePointAt(-1),
input.codePointAt("1"),
input.substr(1),
input.substr(-2, 1),
input.toLocaleLowerCase(),
input.toLocaleUpperCase("en-US"),
input.toLocaleLowerCase("tr"),
input.trimLeft(),
input.trimRight(),
input.isWellFormed(),
input.toWellFormed().length,
];
}
console.log(JSON.stringify(describe(" Iİ😀 ")));
console.log(JSON.stringify(describe("\ud800Ab")));
const parsed = JSON.parse('[" I\\u0130\\ud83d\\ude00 ", 1337, 0.000123456, [1, 2, 3]]');
console.log(JSON.stringify(describe(parsed[0])));
function number(value, digits) {
return [value.toPrecision(digits), value.toExponential(digits), value.toPrecision()];
}
console.log(JSON.stringify(number(1337, 3)), JSON.stringify(number(0.000123456, 2)));
console.log(JSON.stringify(number(-1.5, "4")), JSON.stringify(number(2.5, undefined)));
console.log(JSON.stringify(number(parsed[1], 3)), JSON.stringify(number(parsed[2], parsed[1] - 1335)));
function prettyBytes(n) {
const units = ["B", "kB", "MB", "GB"];
const exponent = Math.min(Math.floor(Math.log10(n) / 3), units.length - 1);
return Number((n / 1000 ** exponent).toPrecision(3)) + " " + units[exponent];
}
console.log(prettyBytes(1337), prettyBytes(100), prettyBytes(12345678));
const own = { codePointAt(i) { return "own " + i; }, substr() { return "own substr"; }, toPrecision(d) { return "own " + d; } };
console.log(describeOwn(own));
function describeOwn(value) {
return [value.codePointAt(3), value.substr(), value.toPrecision(2)].join("|");
}
function arrays(list) {
return JSON.stringify([list.at(-1), list.concat([4], 5)]);
}
console.log(arrays([1, 2, 3]), arrays(parsed[3]));
function attempt(f) {
try {
return String(f());
} catch (e) {
return e.name + ": " + e.message;
}
}
function fail(value) {
return [
attempt(() => value.codePointAt(0)),
attempt(() => value.substr(0)),
attempt(() => value.toPrecision(2)),
attempt(() => value.toLocaleLowerCase()),
].join(" / ");
}
for (const value of [42, "str", undefined, null, {}, parsed[3]]) console.log(fail(value));
for (const value of JSON.parse('[42, "str", null, {}, true]')) console.log(fail(value));
console.log(attempt(() => number(1, 200)));
console.log(attempt(() => "x".toLocaleUpperCase(null)));
// A helper whose parameter the checker leaves untyped answers `string |
// undefined` natively; the guarded receiver still runs String methods.
function afterX(segment) {
const index = segment.indexOf("x");
return index === -1 ? undefined : segment.slice(index + 1);
}
function tailCode(text) {
const rest = afterX(text);
if (rest === undefined) return -1;
return [rest.codePointAt(0), rest.substr(1), rest.toUpperCase(), rest.slice(1)].join(",");
}
console.log(tailCode("axé😀"), tailCode("abc"));
@@ -0,0 +1,15 @@
// String and Number methods through 'any'-typed values: string and number
// receivers run the real methods, other values keep JavaScript's answer.
const values: any[] = JSON.parse('["Straße İx", 1234.5678, {"substr": 1}, [1, 2]]');
const [text, num, obj, list] = values;
console.log(text.codePointAt(7), text.substr(-2), text.toLocaleUpperCase(), text.toLocaleLowerCase("az"));
console.log(text.trimRight(), text.isWellFormed(), text.toLocaleLowerCase(["TR"]));
console.log(num.toPrecision(6), num.toExponential(3), num.toPrecision(), num.toExponential());
console.log(list.at(-1), JSON.stringify(list.concat([3])));
for (const call of [() => obj.substr(0), () => num.codePointAt(0), () => text.toPrecision(2)]) {
try {
console.log(call());
} catch (e) {
console.log((e as Error).name + ": " + (e as Error).message);
}
}
@@ -0,0 +1,36 @@
// crypto.getRandomValues through node:crypto's export and webcrypto:
// integer typed arrays fill in place and come back as the same object;
// views over 65536 bytes throw QuotaExceededError. (The global spelling and
// the float/DataView TypeMismatchError are 6205's.)
import { getRandomValues, webcrypto } from "node:crypto";
import * as nodeCrypto from "node:crypto";
const bytes = new Uint8Array(4096);
const same = webcrypto.getRandomValues(bytes);
function nonzero(view: Uint8Array): boolean {
for (let i = 0; i < view.length; i++) if (view[i] !== 0) return true;
return false;
}
function nonzeroWords(view: Uint32Array): boolean {
for (let i = 0; i < view.length; i++) if (view[i] !== 0) return true;
return false;
}
console.log(same === bytes, bytes.length, nonzero(bytes));
const words = new Uint32Array(256);
console.log(getRandomValues(words) === words, nonzeroWords(words));
const shorts = new Int16Array(8);
console.log(webcrypto.getRandomValues(shorts) === shorts, nodeCrypto.getRandomValues(new Uint8ClampedArray(3)).length);
console.log(nodeCrypto.webcrypto.getRandomValues(new Int8Array(65536)).length, getRandomValues(new Uint8Array(0)).length);
const buf = Buffer.alloc(16);
console.log(Buffer.isBuffer(getRandomValues(buf)), buf.subarray(4, 8).length);
function attempt(label: string, f: () => unknown): void {
try {
f();
console.log(label, "ok");
} catch (e) {
console.log(label, (e as DOMException).name, (e as DOMException).code, (e as Error).message, e instanceof DOMException);
}
}
attempt("big", () => getRandomValues(new Uint8Array(65537)));
attempt("big32", () => webcrypto.getRandomValues(new Uint32Array(16385)));
@@ -0,0 +1,38 @@
// nanoid's fillRandom: crypto.getRandomValues over Buffer windows of an
// untyped helper, plus non-view values that throw TypeMismatchError.
import { getRandomValues } from "node:crypto";
const GET_RANDOM_LIMIT = 65536;
function fillRandom(buffer) {
let from = 0;
while (from < buffer.length) {
const to = Math.min(from + GET_RANDOM_LIMIT, buffer.length);
crypto.getRandomValues(buffer.subarray(from, to));
from = to;
}
}
const alphabet = "useandom-26T198340PX75pxJACKVERYMINDBUSHWOLF_GQZbfghjklqvwyzrict";
function nanoid(size = 21) {
const bytes = Buffer.allocUnsafe(size);
fillRandom(bytes);
let id = "";
for (let i = 0; i < size; i++) id += alphabet[bytes[i] & 63];
return id;
}
const id = nanoid();
console.log(id.length, /^[A-Za-z0-9_-]+$/.test(id), nanoid(10).length, nanoid(70000).length);
const filled = globalThis.crypto.getRandomValues(new Uint16Array(3));
console.log(filled.length);
for (const value of [new Float64Array(1), new Float32Array(2), new DataView(new ArrayBuffer(2)), new Uint8Array(65537), [1, 2], "x", 5, {}]) {
try {
crypto.getRandomValues(value);
console.log("ok");
} catch (e) {
console.log(e.name, e.message);
}
}
const untyped = JSON.parse("[1]");
try {
getRandomValues(untyped);
} catch (e) {
console.log(e.name, e.code, e.message);
}
@@ -0,0 +1,16 @@
// Buffer.isBuffer over Uint8Array-typed values reads the view's Buffer
// brand: Buffer factories, Buffer subarrays and slices are Buffers, plain
// typed arrays and copies into them are not.
function check(view: Uint8Array): boolean {
return Buffer.isBuffer(view);
}
const plain = new Uint8Array([1, 2, 3]);
const buffer = Buffer.from(plain);
console.log(check(plain), check(buffer), check(Buffer.alloc(2)), check(Buffer.from("hey").subarray(1)));
console.log(check(plain.subarray(1)), check(new Uint8Array(buffer)), check(buffer.slice(1)), check(Buffer.concat([plain])));
const views: Uint8Array[] = [plain, buffer, new TextEncoder().encode("x"), Buffer.allocUnsafe(1)];
console.log(views.map((v) => Buffer.isBuffer(v)).join(","));
function words(view: Uint32Array): boolean {
return Buffer.isBuffer(view);
}
console.log(words(new Uint32Array(2)), Buffer.from(plain).toString("hex"), Buffer.from(views[2]!).toString());
+19
View File
@@ -0,0 +1,19 @@
// fs.constants as a value and every member: the target platform's flags,
// file-type bits and permission bits, in Node's order, on a null-prototype
// object whose members are read-only. The access-mode bits keep their
// baked literals.
import * as fs from "node:fs";
import { constants } from "node:fs";
// libuv's Windows-only open flags (0 elsewhere) joined the object in later
// Node 24 releases; the rest is the same in every one.
const windowsOnly = ["UV_FS_O_TEMPORARY", "UV_FS_O_SHORT_LIVED", "UV_FS_O_SEQUENTIAL", "UV_FS_O_RANDOM"];
console.log(JSON.stringify(Object.entries(fs.constants).filter(([k]) => !windowsOnly.includes(k))));
console.log(fs.constants.O_RDONLY, fs.constants.O_WRONLY | fs.constants.O_CREAT | fs.constants.O_TRUNC, constants.S_IFMT, constants.S_IFDIR);
console.log(constants.W_OK, fs.constants.F_OK, constants.COPYFILE_EXCL, fs.constants.UV_FS_O_FILEMAP);
const c: unknown = fs.constants;
console.log(typeof c, c === (fs.constants as unknown), Object.getPrototypeOf(c as object) === null, Object.keys(fs.constants).length > 40);
const mode = fs.statSync(".").mode;
console.log((mode & constants.S_IFMT) === constants.S_IFDIR, (mode & fs.constants.S_IRUSR) !== 0);
const descriptor = Object.getOwnPropertyDescriptor(c as object, "O_RDONLY");
console.log(JSON.stringify(descriptor));
@@ -0,0 +1,8 @@
// graceful-fs's reads of require('fs').constants: own-property probes for
// platform-specific flags and member reads through a stored binding.
var fs = require("fs");
var constants = fs.constants;
console.log(typeof constants.O_RDONLY, constants.O_RDWR, Object.prototype.hasOwnProperty.call(constants, "O_SYMLINK") === ("O_SYMLINK" in constants), typeof constants.hasOwnProperty);
console.log(Object.prototype.hasOwnProperty.call(constants, "S_IFREG"), constants.O_EXCL > 0, typeof require("fs").constants.S_IFLNK);
var names = Object.keys(constants).filter(function (k) { return k.indexOf("UV_DIRENT_") === 0; });
console.log(names.join(","), constants.NOT_A_CONSTANT);
+25
View File
@@ -0,0 +1,25 @@
// The buffer module's Buffer through CommonJS bindings — `var Buffer =
// require('buffer').Buffer` and a member of a stored require — is the
// global Buffer constructor, with its statics.
var Buffer = require("buffer").Buffer;
var buffer = require("buffer");
var B2 = buffer.Buffer;
const B3 = require("node:buffer").Buffer;
console.log(Buffer.from("hi").toString("hex"), Buffer.isBuffer(Buffer.alloc(1)), B2.byteLength("héllo"), B2.isBuffer("x"));
console.log(B3.concat([B2.from("a"), Buffer.from([98])]).toString(), B3.alloc(2, 1).toString("base64"));
function toBuffer(chunk) {
return Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
}
console.log(toBuffer("abc").length, toBuffer(Buffer.from("xyz")).toString());
// Buffer.from over a JS-inferred `string | Uint8Array | undefined` result
// dispatches on the runtime kind, Node's TypeError for undefined included.
function make(flag) {
return flag ? "ab" : flag === false ? new Uint8Array([99, 100]) : undefined;
}
console.log(Buffer.from(make(true)).toString(), Buffer.from(make(false)).toString("hex"));
try {
Buffer.from(make(null));
} catch (e) {
console.log(e.name, e.code);
}
@@ -0,0 +1,11 @@
// ky's feature probes: typeof Node's Web-platform constructor globals and
// their static methods answers "function" without materializing the value.
export const supportsAbortController = typeof globalThis.AbortController === "function";
export const supportsAbortSignal =
typeof globalThis.AbortSignal === "function" && typeof globalThis.AbortSignal.any === "function";
console.log(supportsAbortController, supportsAbortSignal);
console.log(typeof AbortSignal.timeout, typeof AbortSignal.abort, typeof globalThis.AbortSignal.any);
console.log(typeof Response.json, typeof Response.error, typeof URL.canParse, typeof Promise.withResolvers);
console.log(typeof AbortSignal, typeof globalThis.URL, typeof Headers, typeof globalThis.TextEncoder, typeof URLSearchParams);
const signal = AbortSignal.abort("stop");
console.log(signal.aborted, signal.reason);
+103
View File
@@ -0,0 +1,103 @@
// JavaScript: a function-scoped `var` initialized with an object that
// already exists (an element of an array built with push, a find result,
// a member of another object) holds that object, not a copy: writes
// through the binding are visible through the array, and `===` holds.
function make(i) {
return { id: i, x: i % 17 };
}
function updateInPlace() {
var all = [];
for (var i = 0; i < 3; i++) all.push(make(i));
var same = 0;
for (var j = 0; j < all.length; j++) {
var o = all[j];
o.x += 1;
if (o === all[j]) same++;
}
console.log(JSON.stringify(all), same);
}
updateInPlace();
function literalElements() {
var all = [];
for (var i = 0; i < 3; i++) all.push({ id: i, x: i });
for (var j = 0; j < all.length; j++) {
var o = all[j];
o.x *= 10;
}
console.log(JSON.stringify(all));
}
literalElements();
function initializerForms(flag) {
var all = [];
all.push({ n: 0, inner: { v: 1 } });
all.push({ n: 1, inner: { v: 2 } });
var found = all.find(function (e) {
return e.n === 1;
});
found.n = 11;
var picked = flag ? all[0] : null;
picked.n = 10;
var either = all[5] || all[0];
either.inner.v += 100;
var inner = all[1].inner;
inner.v = 20;
var last = all.pop();
console.log(JSON.stringify(all), last === found, picked === either);
console.log(JSON.stringify(last));
}
initializerForms(true);
function arraysAndClosures() {
var grid = [];
grid.push([1, 2]);
var row = grid[0];
row[0] = 9;
row.push(3);
var list = [];
list.push({ count: 0 });
var target = list[0];
var bump = function () {
target.count++;
};
bump();
bump();
console.log(JSON.stringify(grid), JSON.stringify(list), target === list[0]);
}
arraysAndClosures();
function redeclaredAndReassigned() {
var all = [];
all.push({ x: 1 });
all.push({ x: 2 });
var o = all[0];
o.x = 10;
var o = all[1];
o.x = 20;
o = { x: 30 };
all.push(o);
o.x = 40;
console.log(JSON.stringify(all), o === all[2]);
}
redeclaredAndReassigned();
const arrow = () => {
var all = [];
all.push({ x: 1 });
var o = all[0];
o.x = 5;
console.log(JSON.stringify(all));
};
arrow();
// An annotated binding keeps its declared type.
function annotated() {
var all = [];
all.push({ x: 1 });
/** @type {{ x: number }} */
var o = all[0];
console.log(o.x);
}
annotated();
@@ -0,0 +1,46 @@
// JavaScript: array destructuring (var, let and const) and `for (var ...
// of ...)` bind the existing element objects of an array built with push,
// so writes through the bindings reach the array's objects.
function destructure() {
var all = [];
all.push({ x: 1 });
all.push({ x: 2 });
var [first] = all;
first.x = 10;
let [, second] = all;
second.x = 20;
const [again] = all;
again.x += 1;
console.log(JSON.stringify(all), first === all[0], again === first, second === all[1]);
}
destructure();
function forVarOf() {
var all = [];
all.push({ x: 1 });
all.push({ x: 2 });
var seen = [];
for (var o of all) {
o.x *= 10;
seen.push(o === all[seen.length]);
}
console.log(JSON.stringify(all), seen.join());
for (let p of all) p.x += 1;
console.log(JSON.stringify(all));
}
forVarOf();
function* tagged(list) {
for (var o of list) {
o.seen = true;
yield o;
}
}
function generators() {
var items = [];
items.push({ seen: false });
items.push({ seen: false });
for (var it of tagged(items)) it.extra = items.indexOf(it);
console.log(JSON.stringify(items));
}
generators();
@@ -0,0 +1,40 @@
// JavaScript: file-scope `var` and `let` bindings initialized with an
// existing object (an element of an array built with push) hold that
// object, including bindings declared in a loop body, by destructuring or
// by for-of, and functions declared above them read the same object.
function show() {
return typeof o === "undefined" ? "unset" : o.x;
}
console.log(show());
var all = [];
all.push({ x: 1 });
all.push({ x: 2 });
var o = all[0];
o.x = 10;
console.log(show(), JSON.stringify(all), o === all[0]);
var same = 0;
for (var j = 0; j < all.length; j++) {
var item = all[j];
item.x += 5;
if (item === all[j]) same++;
}
console.log(JSON.stringify(all), same);
var [head] = all;
head.x = 100;
for (var each of all) {
each.x += 1;
if (each === all[1]) console.log("last", each.x);
}
console.log(JSON.stringify(all), head === o);
let current = all[1];
current.x = 7;
function touch() {
current.x++;
}
touch();
console.log(JSON.stringify(all), current === all[1]);
@@ -0,0 +1,56 @@
// JavaScript: an object a function returns is the object the function
// holds elsewhere (a cache entry, a registry element). Bindings that
// receive it and array or object literals built from those bindings keep
// that object, so writes through them reach the cache and registry.
'use strict';
const cache = { n: 0 };
function get() {
return cache;
}
const registry = [];
function make(name) {
const o = { name: name, hits: 0 };
registry.push(o);
return o;
}
function identity(v) {
return v;
}
function wrapped(i) {
return identity({ id: i, x: i % 17 });
}
function main() {
var a = get();
a.n = 5;
console.log(cache.n, a === cache);
var list = [a];
list[0].n = 6;
var holder = { item: a };
holder.item.n += 1;
console.log(cache.n, list[0] === cache, holder.item === cache);
var x = make("x");
let y = make("y");
const z = make("z");
x.hits++;
y.hits += 2;
z.hits += 3;
var group = [x, y, z];
for (var j = 0; j < group.length; j++) group[j].hits *= 10;
console.log(JSON.stringify(registry), group[0] === registry[0]);
var all = [];
for (var i = 0; i < 4; i++) all.push(wrapped(i));
var sum = 0;
for (var r = 0; r < 3; r++)
for (var k = 0; k < all.length; k++) {
var e = all[k];
e.x += 1;
sum += e.x;
}
console.log(all.length, sum, JSON.stringify(all.slice(0, 2)));
}
main();
+2 -2
View File
@@ -1,9 +1,9 @@
// Native checked values and loose equality share this program with
// island-backed APIs (SC2012) with native compilation refusals.
// native compilation refusals (a default-locale toLocaleString, SC2020).
const v: any = 21;
const doubled = v * 2;
const root = Math.cbrt(81);
const up = (19.99).toPrecision(3);
const up = (19.99).toLocaleString();
const parsed = Number.parseFloat("1.5"); // a string argument compiles statically, like the global
const unknownScore: unknown = 81;
const flags = unknownScore == 81;
+2 -2
View File
@@ -17,9 +17,9 @@ function readName(text: string): string {
return String(data.name);
}
// An operation without a static lowering, on a parameter.
// An operation without a static lowering (the host's default locale), on a parameter.
function precise(n: number): string {
return n.toPrecision(3);
return n.toLocaleString();
}
console.log(readName('{"name":"a"}'), precise(1.5), new Point(1, 2).x);
+5 -4
View File
@@ -4,11 +4,12 @@
// flag — never an ICE, never a link error. Static Math methods include
// the scalar functions and variadic Math.hypot. The .split(string),
// trim/pad variants, parseInt, isNaN, the global parseFloat/isFinite and
// Number.parseFloat/Number.parseInt over exactly-typed arguments, and
// string-pattern replacement compile statically now and no longer appear
// here.)
// Number.parseFloat/Number.parseInt over exactly-typed arguments,
// string-pattern replacement and toPrecision compile statically now and no
// longer appear here; toLocaleString without a locale still has no
// lowering.)
const up = Math.cbrt(2);
const tau = Math.PI * 2;
const price = (19.99).toPrecision(4);
const price = (19.99).toLocaleString();
const n = Number.parseFloat("3.14");
void n;
@@ -5,6 +5,6 @@ scriptc coverage tests/coverage-fixtures/dynamic-mix.ts
in dynamic tier 2 (operate on values typed only at runtime; --explain-dynamic lists them)
blockers:
×1 SC2012 '.toPrecision()' on numbers cannot compile statically
×1 SC2020 'Number.prototype.toLocaleString without a locale' is part of the standard library types but has no scriptc lowering yet
at tests/coverage-fixtures/dynamic-mix.ts:6:12
hint: use a supported API or rewrite this operation for native compilation
hint: the default locale is the host environment's, which a compiled binary cannot carry — pass it explicitly: x.toLocaleString("en-US")
@@ -12,5 +12,5 @@
function <top level> tests/coverage-fixtures/tier-demotions.ts:1:1 (1 site inside)
function precise tests/coverage-fixtures/tier-demotions.ts:21:1 (1 site inside)
binding n in precise tests/coverage-fixtures/tier-demotions.ts:21:18 (1 request)
<- SC2012 operation at tests/coverage-fixtures/tier-demotions.ts:22:10: '.toPrecision()' on numbers cannot compile statically
<- SC2020 operation at tests/coverage-fixtures/tier-demotions.ts:22:10: 'Number.prototype.toLocaleString without a locale' is part of the standard library types but has no scriptc lowering yet
-> function precise turns dynamic
@@ -8,6 +8,6 @@ scriptc coverage tests/coverage-fixtures/tier-demotions.ts
×1 SC1090 assignment to non-variables
at tests/coverage-fixtures/tier-demotions.ts:10:1
hint: this form is valid TypeScript but does not compile yet — rewrite it with a supported equivalent of the construct the message names (https://scriptc.dev/limitations lists the boundary)
×1 SC2012 '.toPrecision()' on numbers cannot compile statically
×1 SC2020 'Number.prototype.toLocaleString without a locale' is part of the standard library types but has no scriptc lowering yet
at tests/coverage-fixtures/tier-demotions.ts:22:10
hint: use a supported API or rewrite this operation for native compilation
hint: the default locale is the host environment's, which a compiled binary cannot carry — pass it explicitly: x.toLocaleString("en-US")
@@ -1,8 +1,8 @@
dynamic-surface.ts:12:15 - error SC2012: '.toPrecision()' on numbers cannot compile statically
dynamic-surface.ts:13:15 - error SC2020: 'Number.prototype.toLocaleString without a locale' is part of the standard library types but has no scriptc lowering yet
11 | const tau = Math.PI * 2;
12 | const price = (19.99).toPrecision(4);
| ^~~~~~~~~~~~~~~~~~~~~~
13 | const n = Number.parseFloat("3.14");
12 | const tau = Math.PI * 2;
13 | const price = (19.99).toLocaleString();
| ^~~~~~~~~~~~~~~~~~~~~~~~
14 | const n = Number.parseFloat("3.14");
hint: use a supported API or rewrite this operation for native compilation
hint: the default locale is the host environment's, which a compiled binary cannot carry — pass it explicitly: x.toLocaleString("en-US")
@@ -1,8 +0,0 @@
retired-engine-1112-number-methods.ts:9:13 - error SC2012: '.toPrecision()' on numbers cannot compile statically
8 | console.log((1e21).toFixed(2), (0 / 0).toFixed(2));
9 | console.log(n.toPrecision(2), n.toPrecision(8), (0.000123).toPrecision(2));
| ^~~~~~~~~~~~~~~~
10 | console.log((255).toString(16), (255).toString(2), (511).toString(8), (12345).toString(36));
hint: use a supported API or rewrite this operation for native compilation
@@ -0,0 +1 @@
{ "plain": 3859, "san": 43669 }
@@ -0,0 +1 @@
{ "plain": 2795, "san": 4147 }
@@ -0,0 +1 @@
{ "plain": 1595, "san": 2653 }
@@ -0,0 +1 @@
{ "plain": 4232, "san": 4434 }
@@ -0,0 +1 @@
{ "plain": 2843, "san": 5904 }
@@ -0,0 +1 @@
{ "plain": 3066, "san": 3476 }
@@ -0,0 +1 @@
{ "plain": 2527, "san": 3675 }
@@ -0,0 +1 @@
{ "plain": 3062, "san": 3821 }
@@ -0,0 +1 @@
{ "plain": 2367, "san": 1937 }
@@ -0,0 +1 @@
{ "plain": 3082, "san": 3887 }
@@ -0,0 +1 @@
{ "plain": 2310, "san": 4242 }
@@ -0,0 +1 @@
{ "plain": 3244, "san": 5476 }
+1 -1
View File
@@ -295,7 +295,7 @@ test("--explain-dynamic attributes each refusal to the unit the dynamic tier wou
requests.map((r) => [r.code, r.seed, r.scope, r.unit.kind, r.unit.name, r.enclosing.name]),
).toEqual([
["SC1090", "reflection", "reached", "class", "Point", "<top level>"],
["SC2012", "operation", "reached", "binding", "n", "precise"],
["SC2020", "operation", "reached", "binding", "n", "precise"],
]);
expect(tiers.units.map((u) => [u.kind, u.name, u.requests, u.sites])).toEqual([
["class", "Point", 1, 0],
+12 -9
View File
@@ -1053,12 +1053,13 @@ describe.each(EMISSIONS)("K14: determinism fences, %s emission", (emission) => {
test("a manifest-id-keyed teachings entry attaches to that surface's own refusal", async () => {
const diags = await refusal(
`export function f(): number { return (1.234).toPrecision(2).length; }\n`,
`import { brotliCompressSync } from "node:zlib";\nexport function f(): number { return brotliCompressSync("data").length; }\n`,
{
exports: [{ export: "f", symbol: "kx_f", params: [], returns: "f64" }],
determinism: {
teachings: {
"stdlib.number.toPrecision": "formatting runs in the host; request it as an effect",
"node-builtin.zlib.brotliCompressSync":
"compression runs in the host; request it as an effect",
},
},
},
@@ -1066,27 +1067,29 @@ describe.each(EMISSIONS)("K14: determinism fences, %s emission", (emission) => {
);
// The surface's own code, not a fence code: the id key attaches text
// to the refusal that already fires.
expect(diags[0]!.code).toBe("SC2012");
expect(diags[0]!.message).toContain(".toPrecision()");
expect(diags[0]!.code).toBe("SC2020");
expect(diags[0]!.message).toContain("zlib.brotliCompressSync");
expect(diags[0]!.note).toBe(
"from the 'refusal-fixture' profile: formatting runs in the host; request it as an effect",
"from the 'refusal-fixture' profile: compression runs in the host; request it as an effect",
);
});
test("fencing a surface the static tier refuses anyway changes only the message", async () => {
const diags = await refusal(
`export function f(): number { return (1.234).toPrecision(2).length; }\n`,
`import { brotliCompressSync } from "node:zlib";\nexport function f(): number { return brotliCompressSync("data").length; }\n`,
{
exports: [{ export: "f", symbol: "kx_f", params: [], returns: "f64" }],
determinism: {
fences: [{ id: "stdlib.number.toPrecision", teaching: "formatting is host work" }],
fences: [
{ id: "node-builtin.zlib.brotliCompressSync", teaching: "compression is host work" },
],
},
},
emission,
);
// The existing refusal's code survives — the fence never re-codes a
// surface that already refuses; its teaching rides as the note.
expect(diags[0]!.code).toBe("SC2012");
expect(diags[0]!.note).toBe("from the 'refusal-fixture' profile: formatting is host work");
expect(diags[0]!.code).toBe("SC2020");
expect(diags[0]!.note).toBe("from the 'refusal-fixture' profile: compression is host work");
});
});
+1 -1
View File
@@ -481,7 +481,7 @@ describe("library profile fences", () => {
determinism: {
remediations: { SC2012: "the explicit map key wins" },
fences: [
{ id: "stdlib.number.toPrecision", remediation: "request it as an effect" },
{ id: "diagnostic.sc2012", remediation: "request it as an effect" },
{ id: "node-builtin.crypto.createSign", remediation: "signatures come from the host" },
],
},
+1 -1
View File
@@ -52,7 +52,7 @@ const KNOWN: Record<string, Known> = {
"esm-missing-default": { build: "SC1090" },
"esm-missing-export": { build: "SC1090" },
"fetch-destructure": { run: "SC1031" },
"fs-shims": { run: "SC2020" },
"fs-shims": { run: "SC1101" },
"hoisted-workspace": { build: "SC2013" },
"import-refusal": { build: "SC2020" },
"island-registry": { build: "SC2011" },