mirror of
https://github.com/vercel-labs/scriptc.git
synced 2026-11-05 08:13:06 +08:00
Merge main
This commit is contained in:
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"schemaVersion": 3,
|
||||
"nodeVersion": "24.22.0",
|
||||
"nodeCommit": "f14cd3aa7bf9ca8df2cc5b8bb14beb0f335b1d93",
|
||||
"artifactVersion": "ee72820d45a79aa154f0",
|
||||
"rowCount": 3880
|
||||
}
|
||||
@@ -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"] },
|
||||
|
||||
@@ -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(""", 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.
|
||||
|
||||
@@ -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)) {
|
||||
|
||||
@@ -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",
|
||||
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/1112-number-methods.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/6200-string-annexb-locale-methods.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/6201-number-precision-exponential.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/6202-dyn-string-number-methods.js"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/6203-any-string-number-methods.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/6204-crypto-get-random-values.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/6205-crypto-get-random-values.js"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/6206-buffer-is-buffer-views.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/6207-fs-constants-object.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/6208-fs-constants-require.cjs"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/6209-buffer-module-alias.cjs"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/6210-typeof-global-static-members.js"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/6260-js-var-dyn-identity.js"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/6261-js-destructure-for-of-identity.js"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/6262-js-file-scope-var-identity.js"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/6263-js-factory-return-identity.cjs"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
-6
@@ -1,6 +0,0 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/diagnostics/retired-engine-1112-number-methods.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
@@ -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) {
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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). */
|
||||
|
||||
@@ -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; }
|
||||
|
||||
@@ -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);
|
||||
|
||||
+2
-4
@@ -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());
|
||||
@@ -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);
|
||||
@@ -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);
|
||||
@@ -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();
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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 }
|
||||
@@ -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],
|
||||
|
||||
@@ -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");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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" },
|
||||
],
|
||||
},
|
||||
|
||||
@@ -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" },
|
||||
|
||||
Reference in New Issue
Block a user