diff --git a/docs/src/generated/node-v24-compatibility-meta.json b/docs/src/generated/node-v24-compatibility-meta.json new file mode 100644 index 000000000..167256bca --- /dev/null +++ b/docs/src/generated/node-v24-compatibility-meta.json @@ -0,0 +1,7 @@ +{ + "schemaVersion": 3, + "nodeVersion": "24.22.0", + "nodeCommit": "f14cd3aa7bf9ca8df2cc5b8bb14beb0f335b1d93", + "artifactVersion": "ee72820d45a79aa154f0", + "rowCount": 3880 +} diff --git a/docs/src/generated/node-v24-compatibility.json b/docs/src/generated/node-v24-compatibility.json index e2cd6fdfe..be1a931cc 100644 --- a/docs/src/generated/node-v24-compatibility.json +++ b/docs/src/generated/node-v24-compatibility.json @@ -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" } }, { diff --git a/internal/compatibility/generated/node-v24-backlog.json b/internal/compatibility/generated/node-v24-backlog.json index 83cb57ca8..a02446908 100644 --- a/internal/compatibility/generated/node-v24-backlog.json +++ b/internal/compatibility/generated/node-v24-backlog.json @@ -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", diff --git a/internal/compatibility/generated/node-v24-internal.json b/internal/compatibility/generated/node-v24-internal.json index 254e754c2..3690ce077 100644 --- a/internal/compatibility/generated/node-v24-internal.json +++ b/internal/compatibility/generated/node-v24-internal.json @@ -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" }, diff --git a/internal/compatibility/static-support.json b/internal/compatibility/static-support.json index 3b0bfe31e..855d065f7 100644 --- a/internal/compatibility/static-support.json +++ b/internal/compatibility/static-support.json @@ -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"] }, diff --git a/packages/compiler/ambient/scriptc-node-fallback.d.ts b/packages/compiler/ambient/scriptc-node-fallback.d.ts index 9e85c66dc..a8f1f8a8b 100644 --- a/packages/compiler/ambient/scriptc-node-fallback.d.ts +++ b/packages/compiler/ambient/scriptc-node-fallback.d.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(typedArray: T): T; + export const webcrypto: { + getRandomValues(typedArray: T): T; + }; export function randomFillSync( buffer: T, offset?: number, diff --git a/packages/compiler/src/backend/llvm/lib-shared.ts b/packages/compiler/src/backend/llvm/lib-shared.ts index 33cf91abd..7bbb97814 100644 --- a/packages/compiler/src/backend/llvm/lib-shared.ts +++ b/packages/compiler/src/backend/llvm/lib-shared.ts @@ -94,6 +94,8 @@ export const LIB_FN_SYMS: Record = { "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 = { "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 = { "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 = { "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 = { "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", diff --git a/packages/compiler/src/frontend/lowering/builtins/constants.ts b/packages/compiler/src/frontend/lowering/builtins/constants.ts index dd9fcf987..abfc20cdc 100644 --- a/packages/compiler/src/frontend/lowering/builtins/constants.ts +++ b/packages/compiler/src/frontend/lowering/builtins/constants.ts @@ -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 }; +} diff --git a/packages/compiler/src/frontend/lowering/builtins/crypto.ts b/packages/compiler/src/frontend/lowering/builtins/crypto.ts index 575748ef6..425b15f22 100644 --- a/packages/compiler/src/frontend/lowering/builtins/crypto.ts +++ b/packages/compiler/src/frontend/lowering/builtins/crypto.ts @@ -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; +} diff --git a/packages/compiler/src/frontend/lowering/containers/bytes.ts b/packages/compiler/src/frontend/lowering/containers/bytes.ts index 7f1f758ca..0e14a2e42 100644 --- a/packages/compiler/src/frontend/lowering/containers/bytes.ts +++ b/packages/compiler/src/frontend/lowering/containers/bytes.ts @@ -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]!, diff --git a/packages/compiler/src/frontend/lowering/containers/for-of.ts b/packages/compiler/src/frontend/lowering/containers/for-of.ts index 79f970eb1..2331ee25a 100644 --- a/packages/compiler/src/frontend/lowering/containers/for-of.ts +++ b/packages/compiler/src/frontend/lowering/containers/for-of.ts @@ -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 diff --git a/packages/compiler/src/frontend/lowering/containers/string-and-regexp.ts b/packages/compiler/src/frontend/lowering/containers/string-and-regexp.ts index 832dc7def..39154a820 100644 --- a/packages/compiler/src/frontend/lowering/containers/string-and-regexp.ts +++ b/packages/compiler/src/frontend/lowering/containers/string-and-regexp.ts @@ -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 = 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 = 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 = new Set(["toPrecision", "toExponential"]); + +/** Annex B CreateHTML: the tag and the attribute (null for none). */ +const HTML_METHODS: Readonly> = { + 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(``, 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); +} diff --git a/packages/compiler/src/frontend/lowering/lower-calls.ts b/packages/compiler/src/frontend/lowering/lower-calls.ts index 4845fc1a1..2650852cf 100644 --- a/packages/compiler/src/frontend/lowering/lower-calls.ts +++ b/packages/compiler/src/frontend/lowering/lower-calls.ts @@ -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`, or a `Promise | void` callback's result): * the lifted async body adopts the promise arm and fulfills with the diff --git a/packages/compiler/src/frontend/lowering/lower-exprs.ts b/packages/compiler/src/frontend/lowering/lower-exprs.ts index 45379806f..f06a0475d 100644 --- a/packages/compiler/src/frontend/lowering/lower-exprs.ts +++ b/packages/compiler/src/frontend/lowering/lower-exprs.ts @@ -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> = { + 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 = 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) ?? diff --git a/packages/compiler/src/frontend/lowering/lower-modules.ts b/packages/compiler/src/frontend/lowering/lower-modules.ts index b97274bd4..be5949e37 100644 --- a/packages/compiler/src/frontend/lowering/lower-modules.ts +++ b/packages/compiler/src/frontend/lowering/lower-modules.ts @@ -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); diff --git a/packages/compiler/src/frontend/lowering/lower-stmts.ts b/packages/compiler/src/frontend/lowering/lower-stmts.ts index 51284e1c8..a02e106aa 100644 --- a/packages/compiler/src/frontend/lowering/lower-stmts.ts +++ b/packages/compiler/src/frontend/lowering/lower-stmts.ts @@ -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 + ); +} diff --git a/packages/compiler/src/frontend/lowering/lowerer.ts b/packages/compiler/src/frontend/lowering/lowerer.ts index 4a807a82c..6d2bab0ab 100644 --- a/packages/compiler/src/frontend/lowering/lowerer.ts +++ b/packages/compiler/src/frontend/lowering/lowerer.ts @@ -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", diff --git a/packages/compiler/src/frontend/lowering/surfaces.ts b/packages/compiler/src/frontend/lowering/surfaces.ts index b737afcff..1132e3c92 100644 --- a/packages/compiler/src/frontend/lowering/surfaces.ts +++ b/packages/compiler/src/frontend/lowering/surfaces.ts @@ -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, diff --git a/packages/compiler/src/ir/builtin-effects.ts b/packages/compiler/src/ir/builtin-effects.ts index f20d0945e..20dc82555 100644 --- a/packages/compiler/src/ir/builtin-effects.ts +++ b/packages/compiler/src/ir/builtin-effects.ts @@ -137,6 +137,9 @@ export const MAY_THROW_LIB_FNS: ReadonlySet = 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 = new Set([ "crypto.randomIntMax", "crypto.randomFillView", "crypto.randomFillViewRest", + "crypto.getRandomValues", + "crypto.getRandomValuesDyn", "crypto.randomFillSyncDyn", "crypto.randomFillCb", "crypto.randomFillCbDyn", diff --git a/packages/compiler/src/ir/builtin-signatures.ts b/packages/compiler/src/ir/builtin-signatures.ts index f494f5ca0..102521a09 100644 --- a/packages/compiler/src/ir/builtin-signatures.ts +++ b/packages/compiler/src/ir/builtin-signatures.ts @@ -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 diff --git a/packages/compiler/src/ir/runtime-features.ts b/packages/compiler/src/ir/runtime-features.ts index 826189cbb..8d2d6bc63 100644 --- a/packages/compiler/src/ir/runtime-features.ts +++ b/packages/compiler/src/ir/runtime-features.ts @@ -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. diff --git a/packages/compiler/src/ir/validate.ts b/packages/compiler/src/ir/validate.ts index 5cf262e1f..fa3b693a3 100644 --- a/packages/compiler/src/ir/validate.ts +++ b/packages/compiler/src/ir/validate.ts @@ -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)) { diff --git a/packages/compiler/surface-manifest.json b/packages/compiler/surface-manifest.json index 26634df2f..01d12dedc 100644 --- a/packages/compiler/surface-manifest.json +++ b/packages/compiler/surface-manifest.json @@ -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", diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/1112-number-methods.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/1112-number-methods.ts.json new file mode 100644 index 000000000..c95861fe3 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/1112-number-methods.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/1112-number-methods.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6200-string-annexb-locale-methods.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6200-string-annexb-locale-methods.ts.json new file mode 100644 index 000000000..17a5c1e30 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6200-string-annexb-locale-methods.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/6200-string-annexb-locale-methods.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6201-number-precision-exponential.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6201-number-precision-exponential.ts.json new file mode 100644 index 000000000..b80384ada --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6201-number-precision-exponential.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/6201-number-precision-exponential.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6202-dyn-string-number-methods.js.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6202-dyn-string-number-methods.js.json new file mode 100644 index 000000000..dbf5146ed --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6202-dyn-string-number-methods.js.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/6202-dyn-string-number-methods.js" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6203-any-string-number-methods.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6203-any-string-number-methods.ts.json new file mode 100644 index 000000000..42b17b7bb --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6203-any-string-number-methods.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/6203-any-string-number-methods.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6204-crypto-get-random-values.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6204-crypto-get-random-values.ts.json new file mode 100644 index 000000000..86fcbcf58 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6204-crypto-get-random-values.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/6204-crypto-get-random-values.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6205-crypto-get-random-values.js.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6205-crypto-get-random-values.js.json new file mode 100644 index 000000000..42d90fd62 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6205-crypto-get-random-values.js.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/6205-crypto-get-random-values.js" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6206-buffer-is-buffer-views.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6206-buffer-is-buffer-views.ts.json new file mode 100644 index 000000000..7a9c25279 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6206-buffer-is-buffer-views.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/6206-buffer-is-buffer-views.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6207-fs-constants-object.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6207-fs-constants-object.ts.json new file mode 100644 index 000000000..6f24442b0 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6207-fs-constants-object.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/6207-fs-constants-object.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6208-fs-constants-require.cjs.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6208-fs-constants-require.cjs.json new file mode 100644 index 000000000..eac8fa544 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6208-fs-constants-require.cjs.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/6208-fs-constants-require.cjs" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6209-buffer-module-alias.cjs.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6209-buffer-module-alias.cjs.json new file mode 100644 index 000000000..3d3c194b2 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6209-buffer-module-alias.cjs.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/6209-buffer-module-alias.cjs" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6210-typeof-global-static-members.js.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6210-typeof-global-static-members.js.json new file mode 100644 index 000000000..33ccb2715 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6210-typeof-global-static-members.js.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/6210-typeof-global-static-members.js" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6260-js-var-dyn-identity.js.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6260-js-var-dyn-identity.js.json new file mode 100644 index 000000000..bcd9bcf0f --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6260-js-var-dyn-identity.js.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/6260-js-var-dyn-identity.js" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6261-js-destructure-for-of-identity.js.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6261-js-destructure-for-of-identity.js.json new file mode 100644 index 000000000..3733ef399 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6261-js-destructure-for-of-identity.js.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/6261-js-destructure-for-of-identity.js" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6262-js-file-scope-var-identity.js.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6262-js-file-scope-var-identity.js.json new file mode 100644 index 000000000..2db0c3083 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6262-js-file-scope-var-identity.js.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/6262-js-file-scope-var-identity.js" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6263-js-factory-return-identity.cjs.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6263-js-factory-return-identity.cjs.json new file mode 100644 index 000000000..d0e4d3fcb --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6263-js-factory-return-identity.cjs.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/6263-js-factory-return-identity.cjs" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/diagnostics/retired-engine-1112-number-methods.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/diagnostics/retired-engine-1112-number-methods.ts.json deleted file mode 100644 index 27b24a99a..000000000 --- a/packages/compiler/test/ts7/baselines/order-parity/tests/diagnostics/retired-engine-1112-number-methods.ts.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "order": [ - "/tests/diagnostics/retired-engine-1112-number-methods.ts" - ], - "diags": [] -} diff --git a/packages/runtime/src/scr_bytes.c b/packages/runtime/src/scr_bytes.c index d33f32a97..64fc36484 100644 --- a/packages/runtime/src/scr_bytes.c +++ b/packages/runtime/src/scr_bytes.c @@ -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) { diff --git a/packages/runtime/src/scr_bytes.h b/packages/runtime/src/scr_bytes.h index 993921043..b7b913d7f 100644 --- a/packages/runtime/src/scr_bytes.h +++ b/packages/runtime/src/scr_bytes.h @@ -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, diff --git a/packages/runtime/src/scr_crypto.h b/packages/runtime/src/scr_crypto.h index edefb9660..4b6b6a6a6 100644 --- a/packages/runtime/src/scr_crypto.h +++ b/packages/runtime/src/scr_crypto.h @@ -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); diff --git a/packages/runtime/src/scr_lib.c b/packages/runtime/src/scr_lib.c index ae9c5c2b1..930d44818 100644 --- a/packages/runtime/src/scr_lib.c +++ b/packages/runtime/src/scr_lib.c @@ -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); +} diff --git a/packages/runtime/src/scr_number.c b/packages/runtime/src/scr_number.c index 1ccc414b7..dc8743e14 100644 --- a/packages/runtime/src/scr_number.c +++ b/packages/runtime/src/scr_number.c @@ -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; +} diff --git a/packages/runtime/src/scr_number.h b/packages/runtime/src/scr_number.h index 2d95d95f5..f43195116 100644 --- a/packages/runtime/src/scr_number.h +++ b/packages/runtime/src/scr_number.h @@ -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 diff --git a/packages/runtime/src/scr_os.c b/packages/runtime/src/scr_os.c index 2e70d0d85..500283995 100644 --- a/packages/runtime/src/scr_os.c +++ b/packages/runtime/src/scr_os.c @@ -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); +} diff --git a/packages/runtime/src/scr_os.h b/packages/runtime/src/scr_os.h index 219726660..57f219ad1 100644 --- a/packages/runtime/src/scr_os.h +++ b/packages/runtime/src/scr_os.h @@ -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). */ diff --git a/packages/runtime/src/scr_regex.c b/packages/runtime/src/scr_regex.c index 44a91410b..fa104058f 100644 --- a/packages/runtime/src/scr_regex.c +++ b/packages/runtime/src/scr_regex.c @@ -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; } diff --git a/packages/runtime/src/scr_string_methods.h b/packages/runtime/src/scr_string_methods.h index 62354142e..6ed2e09df 100644 --- a/packages/runtime/src/scr_string_methods.h +++ b/packages/runtime/src/scr_string_methods.h @@ -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); diff --git a/tests/diagnostics/retired-engine-1112-number-methods.ts b/tests/corpus/1112-number-methods.ts similarity index 73% rename from tests/diagnostics/retired-engine-1112-number-methods.ts rename to tests/corpus/1112-number-methods.ts index c96f9df00..510850dca 100644 --- a/tests/diagnostics/retired-engine-1112-number-methods.ts +++ b/tests/corpus/1112-number-methods.ts @@ -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)); diff --git a/tests/corpus/6200-string-annexb-locale-methods.ts b/tests/corpus/6200-string-annexb-locale-methods.ts new file mode 100644 index 000000000..8c0c2f66b --- /dev/null +++ b/tests/corpus/6200-string-annexb-locale-methods.ts @@ -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("")); diff --git a/tests/corpus/6201-number-precision-exponential.ts b/tests/corpus/6201-number-precision-exponential.ts new file mode 100644 index 000000000..7c3d4eff8 --- /dev/null +++ b/tests/corpus/6201-number-precision-exponential.ts @@ -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); diff --git a/tests/corpus/6202-dyn-string-number-methods.js b/tests/corpus/6202-dyn-string-number-methods.js new file mode 100644 index 000000000..3f50a4636 --- /dev/null +++ b/tests/corpus/6202-dyn-string-number-methods.js @@ -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")); diff --git a/tests/corpus/6203-any-string-number-methods.ts b/tests/corpus/6203-any-string-number-methods.ts new file mode 100644 index 000000000..202365e5e --- /dev/null +++ b/tests/corpus/6203-any-string-number-methods.ts @@ -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); + } +} diff --git a/tests/corpus/6204-crypto-get-random-values.ts b/tests/corpus/6204-crypto-get-random-values.ts new file mode 100644 index 000000000..fae3c4049 --- /dev/null +++ b/tests/corpus/6204-crypto-get-random-values.ts @@ -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))); diff --git a/tests/corpus/6205-crypto-get-random-values.js b/tests/corpus/6205-crypto-get-random-values.js new file mode 100644 index 000000000..97d3e67d5 --- /dev/null +++ b/tests/corpus/6205-crypto-get-random-values.js @@ -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); +} diff --git a/tests/corpus/6206-buffer-is-buffer-views.ts b/tests/corpus/6206-buffer-is-buffer-views.ts new file mode 100644 index 000000000..55e9fd49c --- /dev/null +++ b/tests/corpus/6206-buffer-is-buffer-views.ts @@ -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()); diff --git a/tests/corpus/6207-fs-constants-object.ts b/tests/corpus/6207-fs-constants-object.ts new file mode 100644 index 000000000..860cc4708 --- /dev/null +++ b/tests/corpus/6207-fs-constants-object.ts @@ -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)); diff --git a/tests/corpus/6208-fs-constants-require.cjs b/tests/corpus/6208-fs-constants-require.cjs new file mode 100644 index 000000000..c8488a8bf --- /dev/null +++ b/tests/corpus/6208-fs-constants-require.cjs @@ -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); diff --git a/tests/corpus/6209-buffer-module-alias.cjs b/tests/corpus/6209-buffer-module-alias.cjs new file mode 100644 index 000000000..cec2d06d8 --- /dev/null +++ b/tests/corpus/6209-buffer-module-alias.cjs @@ -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); +} diff --git a/tests/corpus/6210-typeof-global-static-members.js b/tests/corpus/6210-typeof-global-static-members.js new file mode 100644 index 000000000..88c3c40a1 --- /dev/null +++ b/tests/corpus/6210-typeof-global-static-members.js @@ -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); diff --git a/tests/corpus/6260-js-var-dyn-identity.js b/tests/corpus/6260-js-var-dyn-identity.js new file mode 100644 index 000000000..f22ce2888 --- /dev/null +++ b/tests/corpus/6260-js-var-dyn-identity.js @@ -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(); diff --git a/tests/corpus/6261-js-destructure-for-of-identity.js b/tests/corpus/6261-js-destructure-for-of-identity.js new file mode 100644 index 000000000..a8821b3b4 --- /dev/null +++ b/tests/corpus/6261-js-destructure-for-of-identity.js @@ -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(); diff --git a/tests/corpus/6262-js-file-scope-var-identity.js b/tests/corpus/6262-js-file-scope-var-identity.js new file mode 100644 index 000000000..cb8678b5f --- /dev/null +++ b/tests/corpus/6262-js-file-scope-var-identity.js @@ -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]); diff --git a/tests/corpus/6263-js-factory-return-identity.cjs b/tests/corpus/6263-js-factory-return-identity.cjs new file mode 100644 index 000000000..eb3f77557 --- /dev/null +++ b/tests/corpus/6263-js-factory-return-identity.cjs @@ -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(); diff --git a/tests/coverage-fixtures/dynamic-mix.ts b/tests/coverage-fixtures/dynamic-mix.ts index 0c9779a64..d70141b91 100644 --- a/tests/coverage-fixtures/dynamic-mix.ts +++ b/tests/coverage-fixtures/dynamic-mix.ts @@ -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; diff --git a/tests/coverage-fixtures/tier-demotions.ts b/tests/coverage-fixtures/tier-demotions.ts index 52f2058f1..3dbd0c3fe 100644 --- a/tests/coverage-fixtures/tier-demotions.ts +++ b/tests/coverage-fixtures/tier-demotions.ts @@ -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); diff --git a/tests/diagnostics/dynamic-surface.ts b/tests/diagnostics/dynamic-surface.ts index 537a992eb..3af6671f4 100644 --- a/tests/diagnostics/dynamic-surface.ts +++ b/tests/diagnostics/dynamic-surface.ts @@ -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; diff --git a/tests/harness/__snapshots__/coverage-dynamic-mix.txt b/tests/harness/__snapshots__/coverage-dynamic-mix.txt index 2ad84f0af..0bff0f03c 100644 --- a/tests/harness/__snapshots__/coverage-dynamic-mix.txt +++ b/tests/harness/__snapshots__/coverage-dynamic-mix.txt @@ -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 \ No newline at end of file + hint: the default locale is the host environment's, which a compiled binary cannot carry — pass it explicitly: x.toLocaleString("en-US") \ No newline at end of file diff --git a/tests/harness/__snapshots__/coverage-explain-dynamic.txt b/tests/harness/__snapshots__/coverage-explain-dynamic.txt index 1c86a808c..f168988e6 100644 --- a/tests/harness/__snapshots__/coverage-explain-dynamic.txt +++ b/tests/harness/__snapshots__/coverage-explain-dynamic.txt @@ -12,5 +12,5 @@ function 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 \ No newline at end of file diff --git a/tests/harness/__snapshots__/coverage-tier-demotions.txt b/tests/harness/__snapshots__/coverage-tier-demotions.txt index 7b52f9d63..aaf1f4e3b 100644 --- a/tests/harness/__snapshots__/coverage-tier-demotions.txt +++ b/tests/harness/__snapshots__/coverage-tier-demotions.txt @@ -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 \ No newline at end of file + hint: the default locale is the host environment's, which a compiled binary cannot carry — pass it explicitly: x.toLocaleString("en-US") \ No newline at end of file diff --git a/tests/harness/__snapshots__/dynamic-surface.ts.txt b/tests/harness/__snapshots__/dynamic-surface.ts.txt index 2744d29e5..d4fe2ac7e 100644 --- a/tests/harness/__snapshots__/dynamic-surface.ts.txt +++ b/tests/harness/__snapshots__/dynamic-surface.ts.txt @@ -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 \ No newline at end of file + hint: the default locale is the host environment's, which a compiled binary cannot carry — pass it explicitly: x.toLocaleString("en-US") \ No newline at end of file diff --git a/tests/harness/__snapshots__/retired-engine-1112-number-methods.ts.txt b/tests/harness/__snapshots__/retired-engine-1112-number-methods.ts.txt deleted file mode 100644 index dfd5f0eaa..000000000 --- a/tests/harness/__snapshots__/retired-engine-1112-number-methods.ts.txt +++ /dev/null @@ -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 \ No newline at end of file diff --git a/tests/harness/corpus-costs/1112-number-methods.ts.json b/tests/harness/corpus-costs/1112-number-methods.ts.json new file mode 100644 index 000000000..b99785c0f --- /dev/null +++ b/tests/harness/corpus-costs/1112-number-methods.ts.json @@ -0,0 +1 @@ +{ "plain": 3859, "san": 43669 } diff --git a/tests/harness/corpus-costs/6200-string-annexb-locale-methods.ts.json b/tests/harness/corpus-costs/6200-string-annexb-locale-methods.ts.json new file mode 100644 index 000000000..645cfe9a7 --- /dev/null +++ b/tests/harness/corpus-costs/6200-string-annexb-locale-methods.ts.json @@ -0,0 +1 @@ +{ "plain": 2795, "san": 4147 } diff --git a/tests/harness/corpus-costs/6201-number-precision-exponential.ts.json b/tests/harness/corpus-costs/6201-number-precision-exponential.ts.json new file mode 100644 index 000000000..98160b1ea --- /dev/null +++ b/tests/harness/corpus-costs/6201-number-precision-exponential.ts.json @@ -0,0 +1 @@ +{ "plain": 1595, "san": 2653 } diff --git a/tests/harness/corpus-costs/6202-dyn-string-number-methods.js.json b/tests/harness/corpus-costs/6202-dyn-string-number-methods.js.json new file mode 100644 index 000000000..d4cc558f1 --- /dev/null +++ b/tests/harness/corpus-costs/6202-dyn-string-number-methods.js.json @@ -0,0 +1 @@ +{ "plain": 4232, "san": 4434 } diff --git a/tests/harness/corpus-costs/6203-any-string-number-methods.ts.json b/tests/harness/corpus-costs/6203-any-string-number-methods.ts.json new file mode 100644 index 000000000..27c124839 --- /dev/null +++ b/tests/harness/corpus-costs/6203-any-string-number-methods.ts.json @@ -0,0 +1 @@ +{ "plain": 2843, "san": 5904 } diff --git a/tests/harness/corpus-costs/6204-crypto-get-random-values.ts.json b/tests/harness/corpus-costs/6204-crypto-get-random-values.ts.json new file mode 100644 index 000000000..ecb1d73b4 --- /dev/null +++ b/tests/harness/corpus-costs/6204-crypto-get-random-values.ts.json @@ -0,0 +1 @@ +{ "plain": 3066, "san": 3476 } diff --git a/tests/harness/corpus-costs/6205-crypto-get-random-values.js.json b/tests/harness/corpus-costs/6205-crypto-get-random-values.js.json new file mode 100644 index 000000000..dff4ea74a --- /dev/null +++ b/tests/harness/corpus-costs/6205-crypto-get-random-values.js.json @@ -0,0 +1 @@ +{ "plain": 2527, "san": 3675 } diff --git a/tests/harness/corpus-costs/6206-buffer-is-buffer-views.ts.json b/tests/harness/corpus-costs/6206-buffer-is-buffer-views.ts.json new file mode 100644 index 000000000..80af3c965 --- /dev/null +++ b/tests/harness/corpus-costs/6206-buffer-is-buffer-views.ts.json @@ -0,0 +1 @@ +{ "plain": 3062, "san": 3821 } diff --git a/tests/harness/corpus-costs/6207-fs-constants-object.ts.json b/tests/harness/corpus-costs/6207-fs-constants-object.ts.json new file mode 100644 index 000000000..9a68cf16c --- /dev/null +++ b/tests/harness/corpus-costs/6207-fs-constants-object.ts.json @@ -0,0 +1 @@ +{ "plain": 2367, "san": 1937 } diff --git a/tests/harness/corpus-costs/6208-fs-constants-require.cjs.json b/tests/harness/corpus-costs/6208-fs-constants-require.cjs.json new file mode 100644 index 000000000..377f8328c --- /dev/null +++ b/tests/harness/corpus-costs/6208-fs-constants-require.cjs.json @@ -0,0 +1 @@ +{ "plain": 3082, "san": 3887 } diff --git a/tests/harness/corpus-costs/6209-buffer-module-alias.cjs.json b/tests/harness/corpus-costs/6209-buffer-module-alias.cjs.json new file mode 100644 index 000000000..db5f0ebc8 --- /dev/null +++ b/tests/harness/corpus-costs/6209-buffer-module-alias.cjs.json @@ -0,0 +1 @@ +{ "plain": 2310, "san": 4242 } diff --git a/tests/harness/corpus-costs/6210-typeof-global-static-members.js.json b/tests/harness/corpus-costs/6210-typeof-global-static-members.js.json new file mode 100644 index 000000000..ccf3a0f37 --- /dev/null +++ b/tests/harness/corpus-costs/6210-typeof-global-static-members.js.json @@ -0,0 +1 @@ +{ "plain": 3244, "san": 5476 } diff --git a/tests/harness/coverage.test.ts b/tests/harness/coverage.test.ts index f1347bf23..8cf5d6719 100644 --- a/tests/harness/coverage.test.ts +++ b/tests/harness/coverage.test.ts @@ -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", ""], - ["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], diff --git a/tests/harness/library-mode.test.ts b/tests/harness/library-mode.test.ts index 0eb0cf2c3..ffd42bd8e 100644 --- a/tests/harness/library-mode.test.ts +++ b/tests/harness/library-mode.test.ts @@ -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"); }); }); diff --git a/tests/harness/library-profile.test.ts b/tests/harness/library-profile.test.ts index c7cb13751..eb1bcc4cd 100644 --- a/tests/harness/library-profile.test.ts +++ b/tests/harness/library-profile.test.ts @@ -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" }, ], }, diff --git a/tests/harness/npm.test.ts b/tests/harness/npm.test.ts index b6af0dbb6..54207f246 100644 --- a/tests/harness/npm.test.ts +++ b/tests/harness/npm.test.ts @@ -52,7 +52,7 @@ const KNOWN: Record = { "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" },