Merge main

This commit is contained in:
Chris Tate
2026-10-19 17:32:14 -05:00
120 changed files with 4373 additions and 233 deletions
+1 -1
View File
@@ -76,7 +76,7 @@ Coverage and `scriptc build` report the same diagnostic codes. Common ones inclu
Some code compiles but can behave differently than under Node.js. Coverage reports the cases it can detect as `SC6xxx` warnings, and `scriptc build` prints the same warnings after a successful build. They never fail a build, but `--fail-on=divergences` can fail a CI job on them.
- `SC6001`: a record passed to, stored as, asserted as, or stored as an array element of a record type with different properties is copied, and the program then writes through the copy or compares it by identity.
- `SC6002`: an object literal's key order differs from its type's declaration order, and the program observes the order through `JSON.stringify`, `Object.keys`, `Object.values`, `Object.entries`, `for...in`, or console output.
- `SC6002`: values of one record type are built in different key orders, so the type keeps its declaration order, an object literal's key order differs from it, and the program observes the order through `JSON.stringify`, `Object.keys`, `Object.values`, `Object.entries`, `for...in`, or console output.
- `SC6003`: `localeCompare` compares a string literal outside the built-in Latin-script collation.
- `SC6004`: a call passes a class instance to a function that casts it to an unrelated class.
- `SC6005`: a `Date` string literal uses a form the native parser does not accept.
@@ -2,7 +2,7 @@ A record converted to a record type with a different set of properties, such as
Typed records with declared fields, indexed records, and arrays retain identity and mutations through native checked storage. Class instances retain identity at their exact type or a base class, and at any interface or object type that a class instance is passed, stored, returned, or asserted as when the program could tell a copy from the instance: values of such a type are checked views of the original object, so writes, identity comparisons, Set and Map keys, `util.inspect`, and `JSON.stringify` through them reach the instance, as in Node.js. These types use checked dynamic storage, which is slower than native record fields, for all of their values, including plain objects. A type whose values are only read field by field, from fields the class does not write after construction and does not implement as accessors, stays a native record and holds a copy. Object spread of a class instance is refused. Dense array literals created directly in checked storage preserve identity, and supported native handles pass by reference.
`Object.keys`, `Object.values`, `Object.entries`, and `JSON.stringify` use a record's declaration order rather than per-object insertion order. Their order matches Node.js when objects are built in declaration order. `SC6002` reports object literals whose order differs where the program observes it.
`Object.keys`, `Object.values`, `Object.entries`, `for...in`, `JSON.stringify`, and `util.inspect` enumerate a record's keys in one order per record type rather than per object. scriptc chooses that order from the order in which the program's object literals create the keys, follows object spread, rest destructuring, and record conversions back to the values they copy, and places an optional key that the program assigns after construction after the keys the object was built with. The order matches Node.js when every value of the type is created in the same order. When values of one record type are created in different orders, including values of two types with the same properties and property types, or when a key is deleted and assigned again, the type keeps the declaration order of its first type instead, and `SC6002` reports object literals whose order differs where the program observes it.
A record converted from a checked value, such as `const rows: { score: number }[] = JSON.parse(text)`, keeps the properties its type does not declare. `JSON.stringify`, `Object.keys`, `Object.values`, `Object.entries`, `util.inspect`, and untyped views of the record see them, together with the declared fields, in the source object's key order. `Object.values` and `Object.entries` check those values against their static result type and throw a `TypeError` if one does not fit. Copies of such a record made by object spread, `Object.assign`, rest destructuring, or conversion to another record type contain only the declared fields. Index-signature records list declared fields before the other keys.
@@ -0,0 +1,7 @@
{
"schemaVersion": 3,
"nodeVersion": "24.22.0",
"nodeCommit": "f14cd3aa7bf9ca8df2cc5b8bb14beb0f335b1d93",
"artifactVersion": "ee72820d45a79aa154f0",
"rowCount": 3880
}
+18 -18
View File
@@ -208,10 +208,10 @@
"entries": 152,
"apiEntries": 152,
"static": {
"supported": 5,
"supported": 6,
"partial": 39,
"refused": 21,
"not-implemented": 87,
"not-implemented": 86,
"by-design": 0,
"unreviewed": 0,
"not-applicable": 0
@@ -314,9 +314,9 @@
"apiEntries": 280,
"static": {
"supported": 7,
"partial": 198,
"partial": 200,
"refused": 0,
"not-implemented": 75,
"not-implemented": 73,
"by-design": 0,
"unreviewed": 0,
"not-applicable": 0
@@ -853,10 +853,10 @@
"entries": 156,
"apiEntries": 156,
"static": {
"supported": 1,
"supported": 2,
"partial": 10,
"refused": 0,
"not-implemented": 145,
"not-implemented": 144,
"by-design": 0,
"unreviewed": 0,
"not-applicable": 0
@@ -12325,9 +12325,9 @@
"inherited": true
},
"static": {
"status": "not-implemented",
"detail": "No static compiler lowering is registered for this API yet.",
"verification": "registry-gap"
"status": "supported",
"detail": "Implemented by a dedicated static compiler/runtime path.",
"verification": "test-backed"
}
},
{
@@ -30288,9 +30288,9 @@
"inherited": true
},
"static": {
"status": "not-implemented",
"detail": "No static compiler lowering is registered for this API yet.",
"verification": "registry-gap"
"status": "partial",
"detail": "Implemented for the call shapes accepted by the compiler lowering.",
"verification": "test-backed"
}
},
{
@@ -35256,9 +35256,9 @@
"inherited": true
},
"static": {
"status": "not-implemented",
"detail": "No static compiler lowering is registered for this API yet.",
"verification": "registry-gap"
"status": "partial",
"detail": "Implemented for the call shapes accepted by the compiler lowering.",
"verification": "test-backed"
}
},
{
@@ -80183,9 +80183,9 @@
"inherited": true
},
"static": {
"status": "not-implemented",
"detail": "No static compiler lowering is registered for this API yet.",
"verification": "registry-gap"
"status": "supported",
"detail": "Implemented by a dedicated static compiler/runtime path.",
"verification": "test-backed"
}
},
{
@@ -3,19 +3,19 @@
"nodeVersion": "24.22.0",
"nodeCommit": "f14cd3aa7bf9ca8df2cc5b8bb14beb0f335b1d93",
"summary": {
"taskCount": 3456,
"tierItemCount": 3456,
"taskCount": 3454,
"tierItemCount": 3454,
"tiers": {
"static": {
"audit-partial": 1164,
"verify-gap": 2165,
"audit-partial": 1166,
"verify-gap": 2161,
"replace-refusal": 40,
"classify": 2,
"implement": 85
}
},
"priorities": {
"high": 2289,
"high": 2287,
"normal": 694,
"low": 473
}
@@ -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"
]
}
}
},
@@ -81036,31 +81017,6 @@
}
}
},
{
"id": "webcrypto:dcc24f369f43",
"chapter": "webcrypto",
"label": "crypto.getRandomValues(typedArray)",
"signature": "`crypto.getRandomValues(typedArray)`",
"apiSymbol": "crypto.getRandomValues",
"docsUrl": "https://nodejs.org/docs/v24.22.0/api/webcrypto.html#cryptogetrandomvaluestypedarray",
"nodeStability": {
"index": "2",
"level": 2,
"text": "Stable",
"inherited": true
},
"priority": "high",
"tiers": {
"static": {
"status": "not-implemented",
"action": "verify-gap",
"verification": "registry-gap",
"confidence": "low",
"source": "compiler-unmatched:webcrypto",
"tests": []
}
}
},
{
"id": "webcrypto:8c570ec75666",
"chapter": "webcrypto",
@@ -190,10 +190,10 @@
"entries": 179,
"apiEntries": 152,
"static": {
"supported": 5,
"supported": 6,
"partial": 39,
"refused": 21,
"not-implemented": 87,
"not-implemented": 86,
"by-design": 0,
"unreviewed": 0,
"not-applicable": 0
@@ -326,9 +326,9 @@
"apiEntries": 280,
"static": {
"supported": 7,
"partial": 198,
"partial": 200,
"refused": 0,
"not-implemented": 75,
"not-implemented": 73,
"by-design": 0,
"unreviewed": 0,
"not-applicable": 0
@@ -910,10 +910,10 @@
"entries": 176,
"apiEntries": 156,
"static": {
"supported": 1,
"supported": 2,
"partial": 10,
"refused": 0,
"not-implemented": 145,
"not-implemented": 144,
"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"
},
@@ -106872,6 +106888,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/node24-crypto-kdf-errors.ts",
"tests/corpus/node24-crypto-kdf-options.ts",
"tests/corpus/node24-crypto-kdf.ts",
@@ -110356,8 +110374,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"
},
@@ -112,6 +112,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"],
@@ -392,6 +394,7 @@
{ "symbols": ["crypto.createHash", "Hash", "hash.update", "hash.digest", "hash.copy", "crypto.createHmac", "Hmac", "hmac.update", "hmac.digest", "crypto.hash", "crypto.pbkdf2", "crypto.pbkdf2Sync"], "status": "partial", "evidence": ["tests/corpus/5760-crypto-hash-stream.ts", "tests/corpus/5768-crypto-hash-unknown.ts", "tests/corpus/5769-crypto-hash-unknown.cjs", "tests/corpus/5761-crypto-hash-stream-states.ts", "tests/corpus/5762-crypto-hash-stream.cjs", "tests/corpus/2890-crypto-static-utility.ts", "tests/corpus/node24-crypto-sha2.ts", "tests/corpus/5435-crypto-hash-algorithms.ts", "tests/corpus/5436-crypto-hash-names.js", "tests/corpus/5437-crypto-hmac-algorithms.ts", "tests/corpus/5438-crypto-kdf-digests.ts", "tests/corpus/5440-crypto-hash-encodings.js", "tests/corpus/5441-crypto-hash-output-length.ts", "tests/harness/npm-static.test.ts"] },
{ "symbols": ["crypto.randomFill", "crypto.randomFillSync", "crypto.randomUUID", "crypto.randomUUIDv7"], "status": "supported", "evidence": ["tests/corpus/5442-crypto-random-values.ts", "tests/corpus/5443-crypto-random-arguments.js", "tests/corpus/1358-crypto-random.ts"] },
{ "symbols": ["crypto.createSecretKey", "crypto.generateKey", "crypto.generateKeySync", "KeyObject", "keyObject.export", "keyObject.equals", "keyObject.symmetricKeySize", "keyObject.type"], "status": "partial", "evidence": ["tests/corpus/5445-crypto-secret-key-objects.ts", "tests/corpus/5446-crypto-secret-key-errors.js"] },
{ "symbols": ["crypto.getRandomValues"], "status": "supported", "evidence": ["tests/corpus/6204-crypto-get-random-values.ts", "tests/corpus/6205-crypto-get-random-values.js"] },
{ "symbols": ["crypto.timingSafeEqual"], "status": "supported", "evidence": ["tests/corpus/5444-crypto-timing-safe-equal.js", "tests/corpus/2890-crypto-static-utility.ts"] },
{ "symbols": ["crypto.randomInt"], "status": "partial", "evidence": ["tests/corpus/5442-crypto-random-values.ts", "tests/corpus/2890-crypto-static-utility.ts"] },
{ "symbols": ["crypto.hkdf", "crypto.hkdfSync", "crypto.scrypt", "crypto.scryptSync"], "status": "partial", "evidence": ["tests/corpus/node24-crypto-kdf.ts", "tests/corpus/node24-crypto-kdf-errors.ts", "tests/corpus/node24-crypto-kdf-options.ts", "tests/corpus/5438-crypto-kdf-digests.ts"] },
+62
View File
@@ -1493,6 +1493,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
@@ -2557,6 +2602,23 @@ declare module "crypto" {
a: ArrayBufferView | ArrayBufferLike,
b: ArrayBufferView | ArrayBufferLike,
): boolean;
/* WebCrypto's getRandomValues (also the webcrypto object's): integer
* typed arrays, filled in place and returned. The crypto GLOBAL stays
* unknown in fallback builds. */
type RandomValuesArray =
| Int8Array
| Uint8Array
| Uint8ClampedArray
| Int16Array
| Uint16Array
| Int32Array
| Uint32Array
| BigInt64Array
| BigUint64Array;
export function getRandomValues<T extends RandomValuesArray>(typedArray: T): T;
export const webcrypto: {
getRandomValues<T extends RandomValuesArray>(typedArray: T): T;
};
export function randomFillSync<T extends ArrayBufferView | ArrayBufferLike>(
buffer: T,
offset?: number,
@@ -94,6 +94,8 @@ export const LIB_FN_SYMS: Record<string, string> = {
"num.parseInt": "scr_parse_int",
"num.parseFloat": "scr_parse_float",
"str.localeCompare": "scr_str_locale_compare",
"str.substr": "scr_str_substr",
"str.localeCase": "scr_str_locale_case",
"str.replaceLiteral": "scr_str_replace_literal",
"str.replaceChars": "scr_str_replace_chars",
"num.fromString": "scr_string_to_number",
@@ -143,6 +145,8 @@ export const LIB_FN_SYMS: Record<string, string> = {
"num.toExponential": "scr_num_to_exponential",
"num.toFixed0": "scr_num_to_fixed0",
"num.toFixed": "scr_num_to_fixed",
"num.toPrecision": "scr_num_to_precision",
"num.toExponentialDigits": "scr_num_to_exponential_digits",
"num.toStringRadix": "scr_num_to_string_radix",
"num.toStringRadixF64": "scr_num_to_string_radix_f64",
"num.sameValue": "scr_num_same_value",
@@ -195,6 +199,7 @@ export const LIB_FN_SYMS: Record<string, string> = {
"os.cpusModel": "scr_os_cpus_model",
"os.cpusField": "scr_os_cpus_field",
"os.constants": "scr_os_constants",
"fs.constants": "scr_fs_constants_object",
"constants.object": "scr_node_constants",
"os.release": "scr_os_release",
"os.userName": "scr_os_user_name",
@@ -408,6 +413,7 @@ export const LIB_FN_SYMS: Record<string, string> = {
"buffer.fromStr": "scr_bytes_from_str",
"buffer.fromStrChecked": "scr_bytes_from_str_checked",
"buffer.brand": "scr_bytes_as_buffer",
"buffer.isBranded": "scr_bytes_is_buffer",
"buffer.fromDyn": "scr_buffer_from_dyn",
"buffer.concat": "scr_buffer_concat",
"buffer.concatLen": "scr_buffer_concat_len",
@@ -574,6 +580,8 @@ export const LIB_FN_SYMS: Record<string, string> = {
"crypto.randomIntMax": "scr_crypto_random_int_max",
"crypto.randomFillView": "scr_crypto_random_fill",
"crypto.randomFillViewRest": "scr_crypto_random_fill_rest",
"crypto.getRandomValues": "scr_crypto_get_random_values",
"crypto.getRandomValuesDyn": "scr_crypto_get_random_values_dyn",
"crypto.randomFillSyncDyn": "scr_crypto_random_fill_sync_dyn",
"crypto.randomUUIDOpts": "scr_crypto_random_uuid_opts",
"crypto.createSecretKey": "scr_crypto_create_secret_key",
@@ -1,11 +1,11 @@
import * as ts from "../../ts7/adapter.js";
import { type Lowerer, own } from "../lowerer.js";
import { canonicalBuiltinModule, locOf, requireSpecOf } from "../../program.js";
import { canonicalBuiltinModule, isJsSourceFile, locOf, requireSpecOf } from "../../program.js";
import { builtinConstLit } from "../surfaces.js";
import { HTTP2_CONSTANTS } from "../http2-constants.js";
import { CRYPTO_CONSTANTS } from "../crypto-tables.js";
import { ZLIB_CONSTANTS } from "../zlib-constants.js";
import { F64, type IrExpr } from "../../../ir/ir.js";
import { DYN, F64, type IrExpr, STRING, type SrcLoc } from "../../../ir/ir.js";
import { builtinImportOf } from "./module-bindings.js";
/** The builtin modules whose `constants` object bakes as literals at
@@ -147,12 +147,30 @@ export function lowerFsConstantsProperty(
COPYFILE_FICLONE_FORCE: 4,
};
const value = own(MODES, expr.name.text);
if (value === undefined) {
lowerer.noLowering(
`fs.constants.${expr.name.text}`,
expr,
"F_OK, R_OK, W_OK, X_OK, and COPYFILE_* are the lowered constants",
);
}
if (value === undefined) return fsConstantMemberRead(expr);
return { kind: "numLit", value, type: F64, loc: locOf(expr) };
}
/** fs.constants as a value: the target's null-prototype constants object,
* built by the runtime from its own libc's macros. */
export function fsConstantsObject(loc: SrcLoc): IrExpr {
return { kind: "libCall", fn: "fs.constants", args: [], type: DYN, loc };
}
/** `fs.constants.O_RDONLY` (any member the access-mode literals do not
* cover): a read of the runtime object, so a constant the target lacks
* reads as undefined in JavaScript and fails the number check in
* TypeScript, whose declarations promise every name. */
export function fsConstantMemberRead(expr: ts.PropertyAccessExpression): IrExpr {
const loc = locOf(expr);
const read: IrExpr = {
kind: "dynKeyGet",
value: fsConstantsObject(loc),
key: { kind: "strLit", value: expr.name.text, type: STRING, loc },
type: DYN,
loc,
};
return isJsSourceFile(expr.getSourceFile())
? read
: { kind: "dynCheck", value: read, type: F64, loc };
}
@@ -263,6 +263,7 @@ export function lowerCryptoModuleCall(
if (args.some(ts.isSpreadElement)) {
lowerer.noLowering(`crypto.${bi.member} with spread arguments`, expr);
}
if (bi.member === "getRandomValues") return lowerCryptoGetRandomValues(lowerer, expr);
if (bi.member === "randomBytes") {
if (args.length === 1) return null;
if (args.length !== 2) {
@@ -1284,3 +1285,49 @@ export function lowerPromisifiedCryptoCall(
"KeyObject",
);
}
/** `crypto.getRandomValues(view)` — the WebCrypto global, node:crypto's
* export and `webcrypto.getRandomValues`: a typed view fills in place and
* returns itself at its own type (float views, DataViews and views over
* 65536 bytes throw Node's DOMExceptions at run time); any other value
* takes the checked form. Null for other arities (the member fence keeps
* them). */
export function lowerCryptoGetRandomValues(
lowerer: Lowerer,
call: ts.CallExpression,
): IrExpr | null {
if (call.arguments.nodes.length !== 1 || ts.isSpreadElement(call.arguments.nodes[0]!))
return null;
const loc = locOf(call);
const value = lowerer.lowerExpr(call.arguments.nodes[0]!);
if (value.type.kind === "bytes")
return { kind: "libCall", fn: "crypto.getRandomValues", args: [value], type: value.type, loc };
return {
kind: "libCall",
fn: "crypto.getRandomValuesDyn",
args: [lowerer.coerceInto(call.arguments.nodes[0]!, value, DYN)],
type: DYN,
loc,
};
}
/** Whether `receiver` names a WebCrypto Crypto object: the `crypto`
* global (also through globalThis) or node:crypto's `webcrypto`. */
export function isWebCryptoReceiver(lowerer: Lowerer, receiver: ts.Expression): boolean {
if (ts.isIdentifier(receiver)) {
if (receiver.text === "crypto" && lowerer.isStdlibGlobal(receiver, "crypto")) return true;
const imported = lowerer.builtinImportOf(receiver);
return imported?.module === "crypto" && imported.member === "webcrypto";
}
if (ts.isPropertyAccessExpression(receiver) && !receiver.questionDotToken) {
if (
receiver.name.text === "crypto" &&
ts.isIdentifier(receiver.expression) &&
lowerer.isStdlibGlobal(receiver.expression, "globalThis")
)
return true;
const member = lowerer.builtinMemberOf(receiver);
return member?.module === "crypto" && member.member === "webcrypto";
}
return false;
}
@@ -1,4 +1,4 @@
import { planRecordUnionWrap } from "../union-retag.js";
import { planRecordUnionLift, planRecordUnionWrap } from "../union-retag.js";
import type { WidthLift } from "../width-lift.js";
import type { IrType } from "../../../ir/ir.js";
import { isUnitType, typeEquals, UNDEFINED_T } from "../../../ir/ir.js";
@@ -78,6 +78,24 @@ export function widthLiftPlan(lowerer: Lowerer, src: IrType, dst: IrType): Width
});
if (absentOnly.length === 1) selected = absentOnly;
}
// A record that fits several arms of a discriminated union selects one
// by its discriminant at run time. Without this, an arm that a nested
// field could reach only ambiguously dropped out of the candidates, and
// an arm reached by dropping fields could win alone.
if (selected.length !== 1 && src.kind === "record") {
const source = lowerer.shapes.get(src.shapeId);
if (
source &&
planRecordUnionLift(
source,
def,
(id) => lowerer.shapes.get(id),
(from, to) => lowerer.widthLiftPlan(from, to),
)
) {
return { how: "discriminantRoute" };
}
}
if (selected.length !== 1) return null;
return { how: "liftWrap", tag: selected[0]!.tag, arm: selected[0]!.arm };
}
@@ -106,6 +106,16 @@ export function applyWidthLift(
);
return { kind: "call", callee: helper, args: [value], type: dst, loc };
}
case "discriminantRoute": {
if (dst.kind !== "union" || value.type.kind !== "record")
throw new InternalCompilerError("lowerer bug: record discriminant route shape");
const helper = lowerer.recordUnionRouteHelper(value.type, dst.unionId, loc);
if (!helper)
throw new InternalCompilerError(
"lowerer bug: planned record discriminant route failed to intern",
);
return { kind: "call", callee: helper, args: [value], type: dst, loc };
}
case "retag": {
if (dst.kind !== "union" || value.type.kind !== "union")
throw new InternalCompilerError("lowerer bug: retag lift shape");
@@ -603,6 +613,10 @@ function buildRecordProjection(
const localId = parameter + ".0";
const receiver = varRef(localId, source, loc);
const result: IrType = { kind: "record", shapeId: target.id };
// A record source's copy keeps the source's key order; a class
// instance's fields keep today's table.
const copyOf = (node: IrExpr): IrExpr =>
source.kind === "record" ? lowerer.keyOrder.copy(node, source.shapeId) : node;
return {
name,
params: [{ localId, name: parameter, type: source }],
@@ -611,7 +625,7 @@ function buildRecordProjection(
body: [
{
kind: "return",
value: {
value: copyOf({
kind: "recordLit",
fields: target.fields.map((field) => {
const conversion = plan.get(field.name)!;
@@ -661,7 +675,7 @@ function buildRecordProjection(
}),
type: result,
loc,
},
}),
loc,
},
],
@@ -4,6 +4,7 @@ import { nodeThrowExpr } from "../../../ir/build.js";
import {
planUnionRetag,
buildUnionRetag,
planRecordUnionLift,
planRecordUnionWrap,
buildRecordUnionWrap,
} from "../union-retag.js";
@@ -284,6 +285,38 @@ export function recordUnionWrapHelper(
return name;
}
/** Interned `%record.route.<n>(r)`: a record that fits several arms of a
* discriminated union, converted into the arm its discriminant names
* (planRecordUnionLift). The width plan's `discriminantRoute` lift. */
export function recordUnionRouteHelper(
lowerer: Lowerer,
source: IrType & { kind: "record" },
toId: string,
loc: SrcLoc,
): string | null {
const shape = lowerer.shapes.get(source.shapeId);
const to = lowerer.unions.get(toId);
if (!shape || !to) return null;
const key = `recordRoute:${source.shapeId}:${toId}`;
const existing = lowerer.coercions.retags.get(key);
if (existing) return existing;
const plan = planRecordUnionLift(
shape,
to,
(id) => lowerer.shapes.get(id),
(from, dst) => lowerer.widthLiftPlan(from, dst),
);
if (!plan) return null;
const name = `%record.route.${lowerer.coercions.retags.size}`;
lowerer.coercions.retags.set(key, name);
lowerer.liftedFns.push(
buildRecordUnionWrap(name, source, to, plan, loc, (lift, value, dst) =>
lowerer.applyWidthLift(lift, value, dst, loc),
),
);
return name;
}
export function unionRetagHelper(
lowerer: Lowerer,
fromId: string,
@@ -20,6 +20,7 @@ import {
arrayOf,
bytesOf,
funcOf,
isUnitType,
typeEquals,
} from "../../../ir/ir.js";
import { isJsSourceFile, locOf } from "../../program.js";
@@ -1429,7 +1430,14 @@ function lowerBufferStaticValue(
}
return { kind: "libCall", fn, args: [s, enc], type: BYTES_U8, loc };
}
if (args.length === 1 && srcIr?.kind === "bytes" && srcIr.elem === "u8") {
// A checker type with no mapping (pako's conditional `O extends
// { toText: true } ? string : Uint8Array` result) whose lowered
// value is a u8 view copies like the typed form.
if (
args.length === 1 &&
((srcIr?.kind === "bytes" && srcIr.elem === "u8") ||
(srcIr === null && value.type.kind === "bytes" && value.type.elem === "u8"))
) {
return {
kind: "libCall",
fn: "buffer.poolFromBytes",
@@ -1447,6 +1455,34 @@ function lowerBufferStaticValue(
loc,
};
}
// A union of strings, u8 views and undefined/null (a JS-inferred
// npm result such as pako's deflate): the checked form dispatches
// on the runtime kind exactly as for an untyped JS value, Node's
// TypeError for the unit arms included.
if (
args.length === 1 &&
value.type.kind === "union" &&
lowerer.dynConvertible(value.type) &&
lowerer.unions
.get(value.type.unionId)
?.arms.every(
(arm) =>
arm.kind === "string" ||
(arm.kind === "bytes" && arm.elem === "u8") ||
isUnitType(arm),
) === true
) {
return {
kind: "libCall",
fn: "buffer.poolFromDyn",
args: [
lowerer.coerceToExpected(value, DYN),
{ kind: "strLit", value: "utf8", type: STRING, loc },
],
type: BYTES_U8,
loc,
};
}
}
}
lowerer.noLowering(
@@ -1659,6 +1695,12 @@ function lowerBufferStaticValue(
};
}
}
// A typed view of unknown provenance (a Uint8Array parameter,
// safe-buffer's Buffer.from result): Buffer is a brand of the view,
// which the runtime keeps, so the test reads it.
if (v.type.kind === "bytes") {
return { kind: "libCall", fn: "buffer.isBranded", args: [v], type: BOOL, loc };
}
lowerer.noLowering(
`Buffer.isBuffer of '${lowerer.fmt(v.type)}' values`,
args[0]!,
@@ -153,7 +153,7 @@ export function lowerForOfSearchParams(
}
: singleRead();
if (ts.isIdentifier(decl.name)) {
const varTarget = forOfVarTarget(lowerer, decl);
const varTarget = forOfVarTarget(lowerer, decl, elemT!);
if (varTarget) {
// `for (var x of ...)`: one function-scoped binding, assigned
// per pass — closures made in the loop share it, and the value
@@ -357,7 +357,7 @@ export function lowerForOfArrayIter(
}
: keyRead();
if (ts.isIdentifier(decl.name)) {
const varTarget = forOfVarTarget(lowerer, decl);
const varTarget = forOfVarTarget(lowerer, decl, declT!);
if (varTarget) {
// `for (var x of ...)`: one function-scoped binding, assigned
// per pass — closures made in the loop share it, and the value
@@ -650,7 +650,7 @@ function lowerForOfMapOrSet(
}
: singleRead();
if (ts.isIdentifier(decl!.name)) {
const varTarget = forOfVarTarget(lowerer, decl!);
const varTarget = forOfVarTarget(lowerer, decl!, elemT!);
if (varTarget) {
// `for (var x of ...)`: one function-scoped binding, assigned
// per pass — closures made in the loop share it, and the value
@@ -810,6 +810,7 @@ export function lowerRecordOvfCaptureHelper(
// Every source declared field must flow into the keyed-write slot —
// in DECLARATION order (insertion order below; declaredOrder omits
// internal '%'-fields, which never enter the copy).
lowerer.keyOrder.observed.add(from.id);
const orderedFields = from.declaredOrder
? from.declaredOrder.flatMap((n) => {
const f = from.fields.find((x) => x.name === n);
@@ -1097,6 +1098,7 @@ export function lowerObjectAssignIndexShape(
const from = lowerer.shapes.get(srcIr.shapeId);
if (!from || from.tuple) return null;
if (from.fields.some((f) => f.name.startsWith("%"))) return null;
lowerer.keyOrder.observed.add(from.id);
const orderedFields = from.declaredOrder
? from.declaredOrder.flatMap((n) => {
const f = from.fields.find((x) => x.name === n);
@@ -1800,9 +1800,15 @@ function foldStages(
...inner(value),
];
} else {
// A take before this stage sets `done` as it delivers its last
// value, which still flattens in full: the inner walk runs with the
// flag clear, and keeps it set afterwards.
const afterTake = slots.slice(0, si).some((slot) => slot.kind === "take");
emit = (value) => {
const innerT = s.fnT!.ret;
const innerId = frame.local("fm", innerT, false);
const heldId = afterTake ? frame.local("held", BOOL, false) : undefined;
const walk = walkInner(lowerer, frame, innerId, innerT, done(), inner, loc);
return [
{
kind: "varDecl",
@@ -1811,7 +1817,26 @@ function foldStages(
loc,
},
...(s.cntId !== undefined ? [frame.bump(s.cntId)] : []),
...walkInner(lowerer, frame, innerId, innerT, done(), inner, loc),
...(heldId === undefined
? walk
: [
{ kind: "varDecl", localId: heldId, init: done(), loc } as IrStmt,
{ kind: "assign", localId: doneId, value: boolLit(false, loc), loc } as IrStmt,
...walk,
{
kind: "assign",
localId: doneId,
value: {
kind: "logical",
op: "||",
left: varRef(heldId, BOOL, loc),
right: done(),
type: BOOL,
loc,
},
loc,
} as IrStmt,
]),
];
};
}
@@ -1,5 +1,12 @@
import { matchAllRowsReadByElements } from "./match-row-uses.js";
import { dynUndefinedExpr, nodeThrowExpr, numLit, strLit, varRef } from "../../../ir/build.js";
import {
boolLit,
dynUndefinedExpr,
nodeThrowExpr,
numLit,
strLit,
varRef,
} from "../../../ir/build.js";
import * as ts from "../../ts7/adapter.js";
import {
BOOL,
@@ -316,6 +323,21 @@ export function lowerStringIndexCall(
) {
index = lowerer.coerceInto(indexNode, index, DYN);
}
return stringIndexHelperCall(lowerer, call, method, receiver, receiverNode, index, indexNode);
}
/** The relative-index / code-point helper call over an already lowered
* index (any type positionNumber converts — dyn indices included). */
function stringIndexHelperCall(
lowerer: Lowerer,
call: ts.CallExpression,
method: "at" | "codePointAt",
receiver: IrExpr,
receiverNode: ts.Node,
index: IrExpr,
indexNode: ts.Expression | undefined,
): IrExpr {
const loc = locOf(call);
const valueType = method === "at" ? STRING : F64;
const resultType = lowerer.withUndefinedArmOf(valueType);
if (!resultType || resultType.kind !== "union")
@@ -1061,7 +1083,8 @@ export function lowerStringMethodCall(
const replaceMethod = STRING_REPLACE_METHODS.has(access.name.text)
? (access.name.text as "replace" | "replaceAll")
: null;
if (!entry && !indexMethod && !replaceMethod) return null;
const extraMethod = EXTRA_STRING_METHODS.has(access.name.text);
if (!entry && !indexMethod && !replaceMethod && !extraMethod) return null;
// A validated dyn receiver (`pkg.name.replace(...)` on a JSON.parse
// value) arrives pre-extracted through `dynReceiver`; its checker type
// is `any`, so the type/symbol gates don't apply — the dyn value's
@@ -1089,6 +1112,12 @@ export function lowerStringMethodCall(
return null;
if (!lowerer.isStdlibMember(access)) return null;
}
if (extraMethod && argumentNodes === call.arguments.nodes) {
const receiver = dynReceiver
? dynReceiver()
: lowerMethodReceiver(lowerer, access.expression, STRING, access.name.text);
return lowerExtraStringMethodCall(lowerer, call, access.name.text, receiver, access.expression);
}
// The lib declares optional parameters beyond some lowered forms; fence
// those arities instead of passing arguments the runtime doesn't take.
if (
@@ -1386,3 +1415,393 @@ function lowerLocaleCompareCall(
lowerer.badType(call.arguments.nodes[0]!, lowerer.typeOf(call.arguments.nodes[0]!));
return { kind: "libCall", fn: "str.localeCompare", args: [receiver, arg], type: F64, loc };
}
/* ── String.prototype / Number.prototype members beyond the core tables ──
* substr, the locale case pair, the Annex B HTML methods, toPrecision and
* toExponential(fractionDigits) on typed receivers, plus the kind dispatch
* for every String/Number method a checked-dynamic receiver can name
* whose runtime dispatch had no answer (`character.codePointAt(0)` in a
* JS package, `n.toPrecision(3)` on a JS-inferred number). Each path
* replaces a compile-time refusal, a runtime 'not supported yet' fence or
* Node's is-not-a-function answer for a real method, so programs that
* never spell these names compile exactly as before. */
/** String.prototype members (Annex B included) whose typed-receiver
* lowering lives here. */
export const EXTRA_STRING_METHODS: ReadonlySet<string> = new Set([
"substr",
"toLocaleLowerCase",
"toLocaleUpperCase",
"anchor",
"big",
"blink",
"bold",
"fixed",
"fontcolor",
"fontsize",
"italics",
"link",
"small",
"strike",
"sub",
"sup",
]);
/** String methods a dyn receiver dispatches by runtime kind here: a string
* runs the typed lowering, every other kind takes the runtime dispatch
* (an object's own member, Node's is-not-a-function otherwise). Only names
* no other dyn kind's prototype declares: the shared ones (at, concat,
* slice, ...) already dispatch at run time for arrays, and a kind test in
* front of them would change array code that works today. The Annex B
* HTML names (bold, italics, link, ...) stay out too: chalk-style builders
* call them on functions and objects, whose calls must stay as they are. */
const DYN_STRING_KIND_METHODS: ReadonlySet<string> = new Set([
"substr",
"toLocaleLowerCase",
"toLocaleUpperCase",
"codePointAt",
"isWellFormed",
"toWellFormed",
"trimLeft",
"trimRight",
]);
/** Number methods a dyn receiver dispatches by runtime kind here. */
const DYN_NUMBER_KIND_METHODS: ReadonlySet<string> = new Set(["toPrecision", "toExponential"]);
/** Annex B CreateHTML: the tag and the attribute (null for none). */
const HTML_METHODS: Readonly<Record<string, readonly [string, string | null]>> = {
anchor: ["a", "name"],
big: ["big", null],
blink: ["blink", null],
bold: ["b", null],
fixed: ["tt", null],
fontcolor: ["font", "color"],
fontsize: ["font", "size"],
italics: ["i", null],
link: ["a", "href"],
small: ["small", null],
strike: ["strike", null],
sub: ["sub", null],
sup: ["sup", null],
};
export function isDynPrimitiveKindMethod(name: string): boolean {
return DYN_STRING_KIND_METHODS.has(name) || DYN_NUMBER_KIND_METHODS.has(name);
}
/** Evaluate `value` once into a hidden local (effect-free reads stay). */
function hoist(lowerer: Lowerer, value: IrExpr, stmts: IrStmt[], name: string): IrExpr {
if (
value.kind === "varRef" ||
value.kind === "numLit" ||
value.kind === "strLit" ||
value.kind === "boolLit" ||
value.kind === "unitLit"
)
return value;
const local = lowerer.declareHiddenLocal(name, value.type);
stmts.push({ kind: "varDecl", localId: local.id, init: value, loc: value.loc });
return varRef(local.id, value.type, value.loc);
}
function seq(stmts: IrStmt[], result: IrExpr): IrExpr {
return stmts.length === 0
? result
: { kind: "seqExpr", stmts, result, type: result.type, loc: result.loc };
}
/** ToString of a method argument: the dyn coercion (symbols throw), the
* typed conversions elsewhere. */
function stringArg(lowerer: Lowerer, value: IrExpr, node: ts.Node): IrExpr {
if (value.type.kind === "dyn")
return {
kind: "libCall",
fn: "dyn.toStringCoerce",
args: [value],
type: STRING,
loc: value.loc,
};
return lowerer.ensureString(value, node);
}
/** An optional numeric argument: its ToNumber value and whether it was
* supplied (undefined counts as absent, as the spec's checks do). */
function optionalNumber(
lowerer: Lowerer,
value: IrExpr | undefined,
node: ts.Expression,
stmts: IrStmt[],
subject: string,
loc: SrcLoc,
): { value: IrExpr; has: IrExpr } {
if (!value || value.type.kind === "undefinedT" || value.type.kind === "void")
return { value: numLit(0, loc), has: boolLit(false, loc) };
if (
value.type.kind === "f64" ||
value.type.kind === "string" ||
value.type.kind === "bool" ||
value.type.kind === "nullT"
)
return {
value: positionNumber(lowerer, value, numLit(0, loc), node, subject),
has: boolLit(true, loc),
};
const boxed =
value.type.kind === "dyn"
? value
: hoist(lowerer, lowerer.coerceToExpected(value, DYN), stmts, "%numArg");
const absent: IrExpr = { kind: "dynTest", test: "undefined", value: boxed, type: BOOL, loc };
return {
value: positionNumber(lowerer, boxed, numLit(0, loc), node, subject),
has: { kind: "unary", op: "!", operand: absent, type: BOOL, loc },
};
}
/** The typed result of a String.prototype member over a STRING receiver
* and already evaluated arguments, or null when the name is not one of
* this module's. */
function buildStringMethod(
lowerer: Lowerer,
call: ts.CallExpression,
name: string,
s: IrExpr,
args: IrExpr[],
receiverNode: ts.Node,
stmts: IrStmt[],
): IrExpr | null {
const loc = locOf(call);
const node = (i: number): ts.Expression => call.arguments.nodes[i] ?? call;
if (name === "substr") {
const start = args[0]
? positionNumber(lowerer, args[0], numLit(0, loc), node(0), "substr start")
: numLit(0, loc);
const length = optionalNumber(lowerer, args[1], node(1), stmts, "substr length", loc);
return {
kind: "libCall",
fn: "str.substr",
args: [s, start, length.value, length.has],
type: STRING,
loc,
};
}
if (name === "toLocaleLowerCase" || name === "toLocaleUpperCase") {
const locales = args[0] ? lowerer.coerceToExpected(args[0], DYN) : dynUndefinedExpr(loc);
return {
kind: "libCall",
fn: "str.localeCase",
args: [s, locales, boolLit(name === "toLocaleUpperCase", loc)],
type: STRING,
loc,
};
}
const html = Object.hasOwn(HTML_METHODS, name) ? HTML_METHODS[name] : undefined;
if (html) {
const [tag, attribute] = html;
const subject = hoist(lowerer, s, stmts, "%htmlSubject");
const pieces: IrExpr[] = [strLit(`<${tag}`, loc)];
if (attribute !== null) {
const text = args[0] ? stringArg(lowerer, args[0], node(0)) : strLit("undefined", loc);
pieces.push(strLit(` ${attribute}="`, loc));
pieces.push({
kind: "libCall",
fn: "str.replaceLiteral",
args: [text, strLit('"', loc), strLit("&quot;", loc), boolLit(true, loc)],
type: STRING,
loc,
});
pieces.push(strLit('"', loc));
}
pieces.push(strLit(">", loc), subject, strLit(`</${tag}>`, loc));
return pieces.reduce((left, right) => ({ kind: "strConcat", left, right, type: STRING, loc }));
}
if (name === "codePointAt")
return stringIndexHelperCall(
lowerer,
call,
name,
s,
receiverNode,
args[0] ?? numLit(0, loc),
call.arguments.nodes[0],
);
if (name === "isWellFormed" || name === "toWellFormed")
return {
kind: "strIntrinsic",
method: name,
receiver: s,
args: [],
type: name === "isWellFormed" ? BOOL : STRING,
loc,
};
if (name === "trimLeft" || name === "trimRight")
return {
kind: "strIntrinsic",
method: name === "trimLeft" ? "trimStart" : "trimEnd",
receiver: s,
args: [],
type: STRING,
loc,
};
return null;
}
/** The typed result of toPrecision/toExponential over an F64 receiver. */
function buildNumberMethod(
lowerer: Lowerer,
call: ts.CallExpression,
name: string,
x: IrExpr,
args: IrExpr[],
stmts: IrStmt[],
): IrExpr | null {
const loc = locOf(call);
if (name !== "toPrecision" && name !== "toExponential") return null;
const digits = optionalNumber(
lowerer,
args[0],
call.arguments.nodes[0] ?? call,
stmts,
`${name} digits`,
loc,
);
if (name === "toPrecision")
return {
kind: "libCall",
fn: "num.toPrecision",
args: [x, digits.value, digits.has],
type: STRING,
loc,
};
const shortest: IrExpr = {
kind: "libCall",
fn: "num.toExponential",
args: [x],
type: STRING,
loc,
};
if (digits.has.kind === "boolLit" && !digits.has.value) return shortest;
const fixed: IrExpr = {
kind: "libCall",
fn: "num.toExponentialDigits",
args: [x, digits.value],
type: STRING,
loc,
};
if (digits.has.kind === "boolLit") return fixed;
return { kind: "ternary", cond: digits.has, then: fixed, else_: shortest, type: STRING, loc };
}
/** Evaluate the call's arguments, in order, into hidden locals. */
function evaluatedArgs(
lowerer: Lowerer,
call: ts.CallExpression,
stmts: IrStmt[],
expected: IrType | undefined,
): IrExpr[] {
return call.arguments.nodes.map((arg) =>
hoist(
lowerer,
expected ? lowerer.lowerExprExpecting(arg, expected) : lowerer.lowerExpr(arg),
stmts,
"%methodArg",
),
);
}
/** A typed STRING receiver calling one of EXTRA_STRING_METHODS: receiver,
* then arguments, then the method's own conversions — the spec order. */
export function lowerExtraStringMethodCall(
lowerer: Lowerer,
call: ts.CallExpression,
name: string,
receiver: IrExpr,
receiverNode: ts.Node,
): IrExpr | null {
if (call.arguments.nodes.some(ts.isSpreadElement)) return null;
const stmts: IrStmt[] = [];
const s = hoist(lowerer, receiver, stmts, "%stringReceiver");
const args = evaluatedArgs(lowerer, call, stmts, undefined);
const result = buildStringMethod(lowerer, call, name, s, args, receiverNode, stmts);
return result ? seq(stmts, result) : null;
}
/** A typed F64 receiver calling toPrecision(p) or toExponential(f). */
export function lowerExtraNumberMethodCall(
lowerer: Lowerer,
call: ts.CallExpression,
name: string,
receiver: IrExpr,
): IrExpr | null {
if (call.arguments.nodes.some(ts.isSpreadElement) || call.arguments.nodes.length > 1) return null;
if (name !== "toPrecision" && !(name === "toExponential" && call.arguments.nodes.length === 1))
return null;
const stmts: IrStmt[] = [];
const x = hoist(lowerer, receiver, stmts, "%numberReceiver");
const args = evaluatedArgs(lowerer, call, stmts, undefined);
const result = buildNumberMethod(lowerer, call, name, x, args, stmts);
return result ? seq(stmts, result) : null;
}
/** `recv.m(args)` on a checked-dynamic receiver for a String/Number method
* the runtime dispatch cannot answer: the receiver and arguments evaluate
* once, a string (or number) receiver runs the typed lowering, and every
* other kind keeps the runtime dispatch's answer. Null for other names. */
export function lowerDynPrimitiveMethodCall(
lowerer: Lowerer,
call: ts.CallExpression,
name: string,
calleeName: string,
recv: IrExpr,
): IrExpr | null {
const stringMethod = DYN_STRING_KIND_METHODS.has(name);
const numberMethod = DYN_NUMBER_KIND_METHODS.has(name);
if (!stringMethod && !numberMethod) return null;
if (call.arguments.nodes.some(ts.isSpreadElement)) return null;
const loc = locOf(call);
const stmts: IrStmt[] = [];
const local = lowerer.declareHiddenLocal("%primitiveReceiver", DYN);
stmts.push({ kind: "varDecl", localId: local.id, init: recv, loc });
const r = varRef(local.id, DYN, loc);
const args = evaluatedArgs(lowerer, call, stmts, DYN);
let result: IrExpr = {
kind: "dynInvoke",
recv: r,
method: name,
calleeName,
args,
type: DYN,
loc,
};
const branch = (
test: "string" | "number",
build: (value: IrExpr, branchStmts: IrStmt[]) => IrExpr | null,
): void => {
const branchStmts: IrStmt[] = [];
const value: IrExpr = {
kind: "dynCheck",
value: r,
type: test === "string" ? STRING : F64,
loc,
};
const built = build(value, branchStmts);
if (!built) return;
result = {
kind: "ternary",
cond: { kind: "dynTest", test, value: r, type: BOOL, loc },
then: { kind: "dynFrom", value: seq(branchStmts, built), type: DYN, loc },
else_: result,
type: DYN,
loc,
};
};
if (numberMethod)
branch("number", (x, branchStmts) =>
buildNumberMethod(lowerer, call, name, x, args, branchStmts),
);
if (stringMethod)
branch("string", (s, branchStmts) =>
buildStringMethod(lowerer, call, name, s, args, call.expression, branchStmts),
);
return seq(stmts, result);
}
@@ -781,7 +781,11 @@ function keyOrderFinding(
written: readonly string[],
assignedNames: ReadonlySet<string>,
): { native: string; node: string; detail: "reordered" | "added-later" } | null {
const present = declared.filter((name) => written.includes(name));
// A shape whose table the witness pass re-chose (key-order-witness.ts)
// enumerates that table, not the checker's declaration order.
const table = witnessTable(lowerer, contextual);
const native = table ?? declared;
const present = native.filter((name) => written.includes(name) && declared.includes(name));
const writtenKnown = written.filter((name) => declared.includes(name));
if (present.join("\0") !== writtenKnown.join("\0") && writtenKnown.length >= 2) {
return { native: present.join(", "), node: writtenKnown.join(", "), detail: "reordered" };
@@ -797,12 +801,21 @@ function keyOrderFinding(
const prop = checker.getPropertyOfType(contextual, name);
if (prop === undefined || (prop.flags & ts.SymbolFlags.Optional) === 0) continue;
const nodeOrder = [...writtenKnown, name];
const nativeOrder = declared.filter((d) => nodeOrder.includes(d));
const nativeOrder = native.filter((d) => nodeOrder.includes(d));
if (table !== null && nativeOrder.join("\0") === nodeOrder.join("\0")) continue;
return { native: nativeOrder.join(", "), node: nodeOrder.join(", "), detail: "added-later" };
}
return null;
}
/** The re-chosen enumeration table of the record shape a contextual type
* lowered to, or null when the witness pass kept today's table. */
function witnessTable(lowerer: Lowerer, contextual: ts.Type): readonly string[] | null {
const mapped = safely(() => lowerer.mapTypeOf(contextual));
if (mapped?.kind !== "record" || !lowerer.keyOrder.changed.has(mapped.shapeId)) return null;
return lowerer.shapes.get(mapped.shapeId)?.declaredOrder ?? null;
}
function classDeclarationOfTypeNode(
lowerer: Lowerer,
node: ts.TypeNode,
@@ -24,6 +24,7 @@ import { PoisonError, neverTaintedJsType } from "../lowerer.js";
import type { Lowerer } from "../lowerer.js";
import { lowerIndexMergeHelper } from "../containers/indexed-objects.js";
import type { IndexMergeContributor } from "../containers/indexed-objects.js";
import type { KeyOrderSegment } from "../key-order-witness.js";
import {
PERF_ENTRY_MARKER,
isGenericCallableMemberType,
@@ -1596,6 +1597,8 @@ export function lowerObjectLiteral(
// Member names DROPPED from the shape (JS deferral — unlowerable pure
// member reads narrow away; see the catch in the property loop).
const droppedNames = new Set<string>();
// The order this literal creates its keys in (key-order-witness.ts).
const orderSegments: KeyOrderSegment[] = [];
for (const prop of expr.properties.nodes) {
// A later contributor can mutate an earlier source or overwrite a
// value with effects. Snapshot pending fields at their source position,
@@ -1651,6 +1654,7 @@ export function lowerObjectLiteral(
v = lowerer.coerceInto(valueNode, v, fieldType);
if (!typeEquals(v.type, fieldType)) lowerer.badType(valueNode, lowerer.typeOf(valueNode));
conditionalNames.add(name);
orderSegments.push({ kind: "key", name, definite: false });
fields.push({
name,
value: {
@@ -1758,6 +1762,7 @@ export function lowerObjectLiteral(
}
}
const srcRef = (): IrExpr => srcLowered ?? lowerer.lowerExpr(srcNode);
orderSegments.push({ kind: "spread", shapeId: recArm.shapeId, maybeAbsent: true });
for (const f of srcShape.fields) {
if (laterNames.has(f.name)) continue;
const targetType = fieldTypes.get(f.name);
@@ -1993,6 +1998,12 @@ export function lowerObjectLiteral(
laterNames.add(propNameText(lowerer, later.name));
}
}
// A checked-dynamic source's keys are a run-time fact.
orderSegments.push(
srcLowered.type.kind === "dyn"
? { kind: "opaque" }
: { kind: "spread", shapeId: srcType.shapeId, maybeAbsent: false },
);
for (const f of srcShape.fields) {
if (laterNames.has(f.name)) continue;
let targetType = fieldTypes.get(f.name);
@@ -2307,6 +2318,7 @@ export function lowerObjectLiteral(
const at = fields.findIndex((x) => x.name === name);
if (at >= 0) fields.splice(at, 1);
fields.push({ name, value });
orderSegments.push({ kind: "key", name, definite: true });
}
// Optional fields may be omitted: the field holds its absent state.
// tsc rejects omission of required fields before lowering.
@@ -2335,7 +2347,10 @@ export function lowerObjectLiteral(
fields.push({ name: f.name, value: absent });
}
}
const result: IrExpr = { kind: "recordLit", fields, type, loc };
const result: IrExpr = lowerer.keyOrder.literal(
{ kind: "recordLit", fields, type, loc },
orderSegments,
);
return spreadStmts.length === 0
? result
: { kind: "seqExpr", stmts: spreadStmts, result, type, loc };
@@ -0,0 +1,329 @@
import { expect, test } from "vitest";
import { F64, type IrExpr, type IrFunction, type IrStmt, type IrType } from "../../ir/ir.js";
import { ShapeRegistry } from "../type-mapper.js";
import {
applyKeyOrderWitnesses,
KeyOrderNotes,
type KeyOrderSegment,
} from "./key-order-witness.js";
import type { Lowerer } from "./lowerer.js";
const loc = { file: "order.ts", start: 0, end: 1 };
/** `number | undefined`: the optional-field slot type. */
const OPT: IrType = { kind: "union", unionId: "u0" };
const num = (value: number): IrExpr => ({ kind: "numLit", value, type: F64, loc });
const absent: IrExpr = { kind: "fieldAbsent", unionId: "u0", type: OPT, loc };
interface Fixture {
lowerer: Lowerer;
shapes: ShapeRegistry;
notes: KeyOrderNotes;
record(fields: [string, IrType][], order: string[]): IrType & { kind: "record" };
literal(type: IrType, keys: string[], segments?: KeyOrderSegment[]): IrExpr;
run(body: IrStmt[]): void;
order(type: IrType & { kind: "record" }): string[] | undefined;
}
function fixture(): Fixture {
const shapes = new ShapeRegistry();
const notes = new KeyOrderNotes();
const lowerer = {
shapes,
keyOrder: notes,
unions: new Map([["u0", { id: "u0", arms: [F64, { kind: "undefinedT" }] }]]),
classes: new Map(),
armTag: (unionId: string, arm: IrType): number =>
unionId === "u0" && arm.kind === "undefinedT" ? 1 : -1,
} as unknown as Lowerer;
const record = (fields: [string, IrType][], order: string[]): IrType & { kind: "record" } => ({
kind: "record",
shapeId: shapes.intern(
[...fields].sort((a, b) => (a[0] < b[0] ? -1 : 1)).map(([name, type]) => ({ name, type })),
false,
undefined,
order,
),
});
const literal = (type: IrType, keys: string[], segments?: KeyOrderSegment[]): IrExpr => {
const shape = shapes.get((type as IrType & { kind: "record" }).shapeId)!;
const fields = keys.map((name) => ({ name, value: num(1) }));
for (const f of shape.fields)
if (!keys.includes(f.name)) fields.push({ name: f.name, value: absent });
return notes.literal(
{ kind: "recordLit", fields, type, loc },
segments ?? keys.map((name) => ({ kind: "key", name, definite: true })),
);
};
const run = (body: IrStmt[]): void => {
const fn: IrFunction = { name: "main", params: [], returnType: F64, locals: [], body, loc };
applyKeyOrderWitnesses(lowerer, [fn]);
};
const order = (type: IrType & { kind: "record" }): string[] | undefined =>
shapes.get(type.shapeId)?.declaredOrder;
return { lowerer, shapes, notes, record, literal, run, order };
}
const observe = (value: IrExpr): IrStmt => ({
kind: "exprStmt",
expr: { kind: "jsonStringify", value, type: { kind: "string" }, loc },
loc,
});
const decl = (localId: string, init: IrExpr): IrStmt => ({ kind: "varDecl", localId, init, loc });
const ref = (localId: string, type: IrType): IrExpr => ({ kind: "varRef", localId, type, loc });
const set = (obj: IrExpr, field: string): IrStmt => ({
kind: "recordSet",
obj,
shapeId: (obj.type as IrType & { kind: "record" }).shapeId,
field,
value: num(2),
loc,
});
test("a shape takes the one order its literals write", () => {
const f = fixture();
const ab = f.record(
[
["a", F64],
["b", F64],
],
["a", "b"],
);
f.run([observe(f.literal(ab, ["b", "a"])), observe(f.literal(ab, ["b", "a"]))]);
expect(f.order(ab)).toEqual(["b", "a"]);
expect(f.notes.changed.has(ab.shapeId)).toBe(true);
});
test("literals that agree with the table leave the same array in place", () => {
const f = fixture();
const ab = f.record(
[
["a", F64],
["b", F64],
],
["a", "b"],
);
const before = f.order(ab);
f.run([observe(f.literal(ab, ["a", "b"]))]);
expect(f.order(ab)).toBe(before);
expect(f.notes.changed.size).toBe(0);
});
test("values built in two orders keep today's table", () => {
const f = fixture();
const ab = f.record(
[
["a", F64],
["b", F64],
],
["a", "b"],
);
f.run([observe(f.literal(ab, ["b", "a"])), observe(f.literal(ab, ["a", "b"]))]);
expect(f.order(ab)).toEqual(["a", "b"]);
});
test("an unobserved shape keeps today's table", () => {
const f = fixture();
const ab = f.record(
[
["a", F64],
["b", F64],
],
["a", "b"],
);
f.run([decl("x", f.literal(ab, ["b", "a"]))]);
expect(f.order(ab)).toEqual(["a", "b"]);
});
test("a synthesized construction pins today's table", () => {
const f = fixture();
const ab = f.record(
[
["a", F64],
["b", F64],
],
["a", "b"],
);
const synthesized: IrExpr = {
kind: "recordLit",
fields: [
{ name: "a", value: num(1) },
{ name: "b", value: num(2) },
],
type: ab,
loc,
};
f.run([observe(f.literal(ab, ["b", "a"])), observe(synthesized)]);
expect(f.order(ab)).toEqual(["a", "b"]);
});
test("a checked conversion pins today's table", () => {
const f = fixture();
const ab = f.record(
[
["a", F64],
["b", F64],
],
["a", "b"],
);
const parsed: IrExpr = { kind: "dynCheck", value: ref("j", { kind: "dyn" }), type: ab, loc };
f.run([observe(f.literal(ab, ["b", "a"])), observe(parsed)]);
expect(f.order(ab)).toEqual(["a", "b"]);
});
test("a spread contributes its source's order, and an override keeps its position", () => {
const f = fixture();
const src = f.record(
[
["a", F64],
["b", F64],
],
["a", "b"],
);
const out = f.record(
[
["a", F64],
["b", F64],
["c", F64],
],
["a", "b", "c"],
);
const spread: KeyOrderSegment = { kind: "spread", shapeId: src.shapeId, maybeAbsent: false };
f.run([
decl("s", f.literal(src, ["b", "a"])),
// { ...s, a: 1, c: 2 } — `a` stays where the spread put it.
observe(
f.literal(
out,
["a", "b", "c"],
[
spread,
{ kind: "key", name: "a", definite: true },
{ kind: "key", name: "c", definite: true },
],
),
),
]);
expect(f.order(out)).toEqual(["b", "a", "c"]);
});
test("a spread of a checked-conversion source cannot be ordered statically", () => {
const f = fixture();
const src = f.record(
[
["a", F64],
["b", F64],
],
["a", "b"],
);
const out = f.record(
[
["a", F64],
["b", F64],
["c", F64],
],
["c", "a", "b"],
);
const parsed: IrExpr = { kind: "dynCheck", value: ref("j", { kind: "dyn" }), type: src, loc };
const spread: KeyOrderSegment = { kind: "spread", shapeId: src.shapeId, maybeAbsent: false };
f.run([
decl("s", parsed),
observe(f.literal(out, ["a", "b", "c"], [spread, { kind: "key", name: "c", definite: true }])),
]);
expect(f.order(out)).toEqual(["c", "a", "b"]);
});
test("a late-only optional key follows the keys values are built with", () => {
const f = fixture();
const o = f.record(
[
["a", OPT],
["b", F64],
],
["a", "b"],
);
f.run([decl("o", f.literal(o, ["b"])), set(ref("o", o), "a"), observe(ref("o", o))]);
expect(f.order(o)).toEqual(["b", "a"]);
});
test("straight-line writes of late-only keys keep program order", () => {
const f = fixture();
const o = f.record(
[
["id", F64],
["x", OPT],
["y", OPT],
],
["id", "x", "y"],
);
f.run([
decl("o", f.literal(o, ["id"])),
set(ref("o", o), "y"),
set(ref("o", o), "x"),
observe(ref("o", o)),
]);
expect(f.order(o)).toEqual(["id", "y", "x"]);
});
test("late-only keys written through different bindings keep today's table", () => {
const f = fixture();
const o = f.record(
[
["id", F64],
["x", OPT],
["y", OPT],
],
["id", "x", "y"],
);
f.run([
decl("o", f.literal(o, ["id"])),
set(ref("o", o), "y"),
set(ref("p", o), "x"),
observe(ref("o", o)),
]);
expect(f.order(o)).toEqual(["id", "x", "y"]);
});
test("a late-only key written in a loop over an outer binding keeps today's table", () => {
const f = fixture();
const o = f.record(
[
["id", F64],
["x", OPT],
["y", OPT],
],
["id", "x", "y"],
);
f.run([
decl("o", f.literal(o, ["id"])),
set(ref("o", o), "y"),
{
kind: "while",
cond: { kind: "boolLit", value: false, type: { kind: "bool" }, loc },
body: [set(ref("o", o), "x")],
loc,
},
observe(ref("o", o)),
]);
expect(f.order(o)).toEqual(["id", "x", "y"]);
});
test("re-adding a key a literal creates earlier keeps today's table", () => {
const f = fixture();
const o = f.record(
[
["a", OPT],
["b", F64],
],
["a", "b"],
);
const del: IrStmt = {
kind: "recordSet",
obj: ref("o", o),
shapeId: o.shapeId,
field: "a",
value: absent,
loc,
};
f.run([decl("o", f.literal(o, ["a", "b"])), del, set(ref("o", o), "a"), observe(ref("o", o))]);
expect(f.order(o)).toEqual(["a", "b"]);
});
@@ -0,0 +1,675 @@
/* Record enumeration order chosen from the orders values are built in.
*
* Node enumerates an object's string keys as canonical array indices
* ascending, then every other key in creation order. A record stores its
* fields name-sorted and enumerates them through one per-shape table,
* `IrRecordShape.declaredOrder`, which the type mapper fills with the
* checker's property order of the first type interned to the shape. That
* table is exact only while every value of the shape is created in that
* order. This pass replaces it, after lowering, with the order the
* program's construction sites actually create, when that order is one
* order. It changes the table's contents only: storage layout, shape
* identity and every helper's structure stay as they are, so the generated
* code differs only in the constant order the enumeration surfaces walk.
*
* Witnesses, per shape:
* - an object literal contributes the order it writes its keys in. Spread
* segments contribute their source shape's order (an inheritance edge,
* never the desugar's field list), and a key keeps the position of its
* first occurrence, so an override inside a spread stays where the
* spread put it. A key whose earlier occurrence may be absent at run
* time contributes every position it can take;
* - a width copy or a rest binding contributes its source shape's order;
* - any other construction (synthesized builtin results, opaque runtime
* producers) pins today's table;
* - checked conversions from dynamic values (typed JSON.parse, values
* from JavaScript) pin today's table: the record-source table keeps
* their own key order relative to it, and another table would make
* values parsed in today's order keep a source view. Their order is
* unknown, so a copy out of such a shape cannot be ordered statically;
* - a write to an optional key that may be absent appends it in Node. A
* key no construction creates ("late-only") goes after every key a
* construction can create; any other moving write goes after every key.
* Two late-only keys are ordered only by a straight-line sequence of
* writes through one binding.
*
* The orders merge into a precedence graph per shape, topologically sorted
* with ties broken by today's table, so a shape whose witnesses agree with
* the table keeps it untouched. A cycle means the order depends on the
* value: that shape, and every shape copied from it, keeps today's table
* (and today's SC6002 warnings). Shapes that no order surface observes
* keep today's table too. */
import type { IrExpr, IrFunction, IrRecordShape, IrStmt, IrType } from "../../ir/ir.js";
import { shapeHasAccessorSlots, typeEquals, UNDEFINED_T } from "../../ir/ir.js";
import { everyExprChild, everyStmtChild, everyTypeChild } from "../../ir/traverse.js";
import type { Lowerer } from "./lowerer.js";
/** One property of an object literal, in source order. */
export type KeyOrderSegment =
/** An explicit key (`definite`), or a conditional spread's key, which
* exists only when its condition holds. */
| { kind: "key"; name: string; definite: boolean }
/** `...source` of a record shape; `maybeAbsent` when the whole source
* can be undefined (`...options` of `T | undefined`). */
| { kind: "spread"; shapeId: string; maybeAbsent: boolean }
/** A spread whose keys are only known at run time. */
| { kind: "opaque" };
/** Witness notes the lowering attaches to the IR nodes it builds. Keyed by
* node identity, so nodes a speculative lowering discards are never walked,
* and a node some later pass rebuilds falls back to pinning. */
export class KeyOrderNotes {
private readonly literals = new Map<IrExpr, KeyOrderSegment[]>();
private readonly copies = new Map<IrExpr, string>();
/** Shapes a frontend order surface (keys, values/entries, inspect,
* index-signature merges) walks. */
readonly observed = new Set<string>();
/** Shapes whose table this pass replaced. */
readonly changed = new Set<string>();
literal(node: IrExpr, segments: KeyOrderSegment[]): IrExpr {
this.literals.set(node, segments);
return node;
}
copy(node: IrExpr, sourceShapeId: string): IrExpr {
this.copies.set(node, sourceShapeId);
return node;
}
observe(type: IrType): void {
if (type.kind === "record") this.observed.add(type.shapeId);
}
literalOf(node: IrExpr): KeyOrderSegment[] | undefined {
return this.literals.get(node);
}
copyOf(node: IrExpr): string | undefined {
return this.copies.get(node);
}
}
const isArrayIndex = (name: string): boolean => {
const n = Number(name);
return Number.isInteger(n) && n >= 0 && n < 4294967295 && String(n) === name;
};
interface WriteSite {
field: string;
kind: "write" | "delete" | "clone";
fn: string;
binding: string | null;
loop: string;
pos: number;
}
interface BindingInit {
count: number;
fn: string;
loop: string;
pos: number;
fresh: boolean;
}
interface ShapeFacts {
literals: KeyOrderSegment[][];
copies: { source: string; keys: Set<string> }[];
/** A construction in today's order (synthesized literal). */
pinned: boolean;
/** An opaque runtime producer: today's order, any key may be absent. */
opaque: boolean;
/** A checked conversion from a dynamic value. */
runtime: boolean;
clones: boolean;
present: Set<string>;
absent: Set<string>;
deleted: Set<string>;
writes: WriteSite[];
}
type Resolved = { order: string[] } | "unknown";
/** `structuredClone(v)` checked back into v's own type: the copy is built
* from v's dyn view, so it creates exactly v's keys in v's order. */
function orderPreservingClone(node: IrExpr & { kind: "dynCheck" }): boolean {
const value = node.value;
if (value.kind !== "libCall" || value.fn !== "dyn.structuredClone") return false;
const source = value.args[0];
return (
source !== undefined && source.kind === "dynFrom" && typeEquals(source.value.type, node.type)
);
}
/** Chooses each in-scope record shape's enumeration table from its
* witnesses. Runs once retained reachability has settled the historical
* tables and derived metadata, before the order-snapshotting helper bodies
* rebuild. */
export function applyKeyOrderWitnesses(lowerer: Lowerer, fns: readonly IrFunction[]): void {
const notes = lowerer.keyOrder;
const shapes = lowerer.shapes;
const facts = new Map<string, ShapeFacts>();
const factsOf = (shapeId: string): ShapeFacts => {
let f = facts.get(shapeId);
if (f === undefined) {
f = {
literals: [],
copies: [],
pinned: false,
opaque: false,
runtime: false,
clones: false,
present: new Set(),
absent: new Set(),
deleted: new Set(),
writes: [],
};
facts.set(shapeId, f);
}
return f;
};
const observedRoots: IrType[] = [];
const inits = new Map<string, BindingInit>();
const undefinedArmed = (t: IrType): boolean =>
t.kind === "union" && lowerer.armTag(t.unionId, UNDEFINED_T) >= 0;
const containsAbsent = (value: IrExpr): boolean =>
value.kind === "fieldAbsent" ||
(value.kind === "ternary" && (containsAbsent(value.then) || containsAbsent(value.else_)));
/** Every record shape a type can hold, through fields, arms, elements,
* callables, promises and class fields. */
/** Each walk shares one `seen` set per result set, so every type graph
* is walked once however many roots reach it. */
const shapesIn = (type: IrType, out: Set<string>, seen: Set<string>): void => {
const visit = (t: IrType): boolean => {
if (t.kind === "record") {
if (seen.has("r:" + t.shapeId)) return true;
seen.add("r:" + t.shapeId);
out.add(t.shapeId);
const shape = shapes.get(t.shapeId);
if (shape) {
for (const f of shape.fields) visit(f.type);
if (shape.indexValue) visit(shape.indexValue);
}
return true;
}
if (t.kind === "union") {
if (seen.has("u:" + t.unionId)) return true;
seen.add("u:" + t.unionId);
for (const arm of lowerer.unions.get(t.unionId)?.arms ?? []) visit(arm);
return true;
}
if (t.kind === "object") {
if (seen.has("c:" + t.className)) return true;
seen.add("c:" + t.className);
for (const f of lowerer.classes.get(t.className)?.def.fields ?? []) visit(f.type);
return true;
}
return everyTypeChild(t, visit);
};
visit(type);
};
const opaqueShapes = new Set<string>();
const opaqueSeen = new Set<string>();
const runtimeShapes = new Set<string>();
const runtimeSeen = new Set<string>();
const producedBy = (type: IrType, opaque: boolean): void => {
if (opaque) shapesIn(type, opaqueShapes, opaqueSeen);
else shapesIn(type, runtimeShapes, runtimeSeen);
};
// ── collection ──────────────────────────────────────────────────────
let pos = 0;
let fnName = "";
const loops: number[] = [];
let loopIds = 0;
const loopKey = (): string => loops.join("/");
const noteInit = (id: string, init: IrExpr): void => {
const previous = inits.get(id);
if (previous !== undefined) {
previous.count++;
return;
}
const fresh =
init.kind === "recordLit" ||
init.kind === "recordClone" ||
(init.kind === "seqExpr" && init.result.kind === "recordLit");
inits.set(id, { count: 1, fn: fnName, loop: loopKey(), pos, fresh });
};
const visitExpr = (e: IrExpr): void => {
switch (e.kind) {
case "recordLit": {
if (e.type.kind !== "record") return;
const f = factsOf(e.type.shapeId);
const presentKeys = new Set<string>();
for (const field of e.fields) {
if (field.overflow) continue;
if (field.value.kind === "fieldAbsent") {
f.absent.add(field.name);
continue;
}
presentKeys.add(field.name);
f.present.add(field.name);
if (containsAbsent(field.value)) f.absent.add(field.name);
}
const segments = notes.literalOf(e);
const copy = notes.copyOf(e);
if (segments !== undefined) f.literals.push(segments);
else if (copy !== undefined) f.copies.push({ source: copy, keys: presentKeys });
else f.pinned = true;
return;
}
case "recordClone": {
if (e.type.kind !== "record") return;
const f = factsOf(e.type.shapeId);
f.clones = true;
for (const o of e.overrides)
f.writes.push({ field: o.name, kind: "clone", fn: fnName, binding: null, loop: "", pos });
return;
}
case "assignExpr":
noteInit(e.localId, e.value);
return;
case "jsonStringify":
case "dynFrom":
observedRoots.push(e.value.type);
return;
case "recordKeyGet":
observedRoots.push(e.obj.type);
return;
case "libCall":
// Runtime surfaces (console, util.format, workers) that take a
// record may walk its keys; runtime builders make new records.
for (const arg of e.args) observedRoots.push(arg.type);
producedBy(e.type, true);
return;
case "intrinsic":
// Intrinsics hand back values built elsewhere, except allSettled's
// result records; console output renders through the inspect
// helpers, which note their shapes.
if (e.name === "promise.allSettled") producedBy(e.type, true);
return;
case "ffiCall":
case "strIntrinsic":
case "regexIntrinsic":
case "bytesIntrinsic":
producedBy(e.type, true);
return;
case "dynCheck":
if (!orderPreservingClone(e)) producedBy(e.type, false);
return;
case "dynDestrCheck":
case "caughtCheck":
case "caughtNarrow":
producedBy(e.type, false);
return;
default:
return;
}
};
const visitStmt = (st: IrStmt): void => {
switch (st.kind) {
case "recordSet":
if (st.value.kind === "fieldAbsent") factsOf(st.shapeId).deleted.add(st.field);
factsOf(st.shapeId).writes.push({
field: st.field,
kind: st.value.kind === "fieldAbsent" ? "delete" : "write",
fn: fnName,
binding: st.obj.kind === "varRef" ? st.obj.localId : null,
loop: loopKey(),
pos,
});
return;
case "varDecl":
if (st.init !== null) noteInit(st.localId, st.init);
return;
case "assign":
noteInit(st.localId, st.value);
return;
default:
return;
}
};
const expr = (e: IrExpr): boolean => {
pos++;
visitExpr(e);
return everyExprChild(e, expr, stmt);
};
const stmt = (st: IrStmt): boolean => {
pos++;
visitStmt(st);
if (st.kind === "while" || st.kind === "doWhile" || st.kind === "for" || st.kind === "forOf") {
loops.push(++loopIds);
everyStmtChild(st, expr, stmt);
loops.pop();
return true;
}
return everyStmtChild(st, expr, stmt);
};
for (const fn of fns) {
fnName = fn.name;
for (const st of fn.body) stmt(st);
}
for (const id of opaqueShapes) {
const f = factsOf(id);
f.pinned = true;
f.opaque = true;
}
for (const id of runtimeShapes) factsOf(id).runtime = true;
// ── scope ───────────────────────────────────────────────────────────
// Only plain declared-field shapes with a table take part.
const inScope = (shape: IrRecordShape | undefined): shape is IrRecordShape =>
shape !== undefined &&
!shape.tuple &&
shape.indexValue === undefined &&
shape.declaredOrder !== undefined &&
!shapeHasAccessorSlots(shape);
const tableOf = (shape: IrRecordShape): string[] =>
shape.declaredOrder!.filter((k) => !k.startsWith("%") && !isArrayIndex(k));
// A checked conversion builds values in their source's own order, which
// the record-source table reproduces relative to the shape's table. A new
// table would make every value parsed in today's order keep a source
// view, so such shapes keep today's table.
for (const f of facts.values()) if (f.runtime) f.pinned = true;
// Nothing to choose: every witness agrees with today's table and no
// optional key is written late.
const interesting = [...facts].some(([id, f]) => {
const shape = shapes.get(id);
if (!inScope(shape)) return false;
if (f.writes.some((w) => w.kind !== "delete") || f.copies.length > 0) return true;
const table = tableOf(shape).join("\0");
return f.literals.some((segments) => {
const keys: string[] = [];
for (const segment of segments) {
if (segment.kind !== "key") return true;
if (!isArrayIndex(segment.name)) keys.push(segment.name);
}
return keys.join("\0") !== table;
});
});
if (!interesting) return;
// ── presence classes and late-write constraints ─────────────────────
const maybeAbsent = (id: string, key: string): boolean => {
const shape = shapes.get(id);
const field = shape?.fields.find((x) => x.name === key);
if (!field || !undefinedArmed(field.type)) return false;
const f = facts.get(id);
if (f === undefined) return true;
return f.absent.has(key) || f.runtime || f.opaque || f.deleted.has(key);
};
/** Edges every value of the shape satisfies whatever its sources'
* orders are, or "unknown" when late writes make the order depend on
* the value. */
const lateEdges = (id: string, keys: readonly string[]): [string, string][] | "unknown" => {
const f = facts.get(id);
if (f === undefined) return [];
const edges: [string, string][] = [];
const keySet = new Set(keys);
const createdKeys = (k: string): boolean => f.present.has(k) || f.runtime || f.opaque;
const required = keys.filter((k) => !maybeAbsent(id, k));
const created = keys.filter((k) => maybeAbsent(id, k) && createdKeys(k));
const lateOnly = new Set(keys.filter((k) => maybeAbsent(id, k) && !createdKeys(k)));
// The constraint depends on the written key only, not on the site.
const moving = new Set<string>();
for (const w of f.writes)
if (w.kind !== "delete" && keySet.has(w.field) && maybeAbsent(id, w.field))
moving.add(w.field);
for (const field of moving) {
const before = lateOnly.has(field) ? [...required, ...created] : keys;
for (const k of before) if (k !== field) edges.push([k, field]);
}
const lateSites = f.writes.filter((w) => lateOnly.has(w.field));
const lateWritten = new Set(lateSites.filter((w) => w.kind !== "delete").map((w) => w.field));
if (lateWritten.size < 2) return edges;
// Two or more late-only keys: each value gains them in the order its
// writes run, so every write must belong to one straight-line
// sequence through one binding initialized once, in the same function
// and loop body as its writes.
const groups = new Map<string, WriteSite[]>();
for (const w of lateSites) {
if (w.kind === "clone" || w.binding === null) return "unknown";
const init = inits.get(w.binding);
if (
init === undefined ||
init.count !== 1 ||
init.fn !== w.fn ||
init.loop !== w.loop ||
init.pos > w.pos
)
return "unknown";
const group = groups.get(w.binding) ?? [];
group.push(w);
groups.set(w.binding, group);
}
if (groups.size > 1 && [...groups.keys()].some((b) => !inits.get(b)!.fresh)) return "unknown";
for (const group of groups.values()) {
const seen = new Set<string>();
for (const w of group) {
if (seen.has(w.field)) return "unknown";
seen.add(w.field);
}
const writes = group.filter((w) => w.kind === "write").sort((a, b) => a.pos - b.pos);
for (let i = 0; i + 1 < writes.length; i++)
edges.push([writes[i]!.field, writes[i + 1]!.field]);
}
return edges;
};
// ── witness sequences ───────────────────────────────────────────────
/** The key sequences one literal can create, or null when a spread
* source's order is unknown. A key sits at its first occurrence that
* certainly defines it; each earlier occurrence that may define it is
* another possible position. */
const literalSequences = (
segments: readonly KeyOrderSegment[],
keySet: ReadonlySet<string>,
resolved: (id: string) => Resolved,
): string[][] | null => {
const occurrences: { key: string; definite: boolean }[] = [];
for (const s of segments) {
if (s.kind === "opaque") return null;
if (s.kind === "key") {
if (keySet.has(s.name)) occurrences.push({ key: s.name, definite: s.definite });
continue;
}
const source = resolved(s.shapeId);
if (source === "unknown") return null;
for (const k of source.order) {
if (keySet.has(k))
occurrences.push({ key: k, definite: !s.maybeAbsent && !maybeAbsent(s.shapeId, k) });
}
}
// Per key, the occurrences that can create it: every one up to and
// including the first definite occurrence. The first such position is
// the base; each other one is an alternative.
const positions = new Map<string, number[]>();
const settled = new Set<string>();
occurrences.forEach((o, i) => {
if (settled.has(o.key)) return;
const list = positions.get(o.key) ?? [];
list.push(i);
positions.set(o.key, list);
if (o.definite) settled.add(o.key);
});
const base = new Map<string, number>();
for (const [key, list] of positions) base.set(key, list[0]!);
const sequenceWith = (at: Map<string, number>): string[] =>
[...at].sort((a, b) => a[1] - b[1]).map(([k]) => k);
const out = [sequenceWith(base)];
for (const [key, list] of positions) {
for (const alternative of list.slice(1)) {
const moved = new Map(base);
moved.set(key, alternative);
out.push(sequenceWith(moved));
}
}
return out;
};
/** Today's table with every witness edge applied, or "unknown". */
const merge = (table: readonly string[], edges: readonly [string, string][]): Resolved => {
const rank = new Map(table.map((k, i) => [k, i]));
const succ = new Map<string, Set<string>>(table.map((k) => [k, new Set<string>()]));
const indeg = new Map<string, number>(table.map((k) => [k, 0]));
for (const [a, b] of edges) {
if (a === b || !rank.has(a) || !rank.has(b)) continue;
const s = succ.get(a)!;
if (s.has(b)) continue;
s.add(b);
indeg.set(b, indeg.get(b)! + 1);
}
const order: string[] = [];
const ready = table.filter((k) => indeg.get(k) === 0);
while (ready.length > 0) {
ready.sort((a, b) => rank.get(a)! - rank.get(b)!);
const k = ready.shift()!;
order.push(k);
for (const next of succ.get(k)!) {
indeg.set(next, indeg.get(next)! - 1);
if (indeg.get(next) === 0) ready.push(next);
}
}
return order.length === table.length ? { order } : "unknown";
};
const chain = (seq: readonly string[], out: [string, string][]): void => {
for (let i = 0; i + 1 < seq.length; i++) out.push([seq[i]!, seq[i + 1]!]);
};
// ── resolution over inheritance edges ───────────────────────────────
const result = new Map<string, Resolved>();
const late = new Map<string, [string, string][] | "unknown">();
const dependencies = (id: string): string[] => {
const f = facts.get(id);
if (f === undefined) return [];
const deps = new Set<string>();
for (const segments of f.literals)
for (const s of segments) if (s.kind === "spread") deps.add(s.shapeId);
for (const c of f.copies) deps.add(c.source);
return [...deps];
};
/** One shape's order given its sources' current orders. */
const resolveOne = (id: string, current: (source: string) => Resolved): Resolved => {
const shape = shapes.get(id);
if (!inScope(shape)) return "unknown";
const table = tableOf(shape);
const f = facts.get(id);
// A shape the program never constructs natively gets its values from
// somewhere this pass cannot see.
if (f === undefined || !(f.literals.length > 0 || f.copies.length > 0 || f.pinned || f.runtime))
return "unknown";
// Clones copy their source's own key order, which a checked
// conversion leaves unknown.
if (f.runtime && f.clones) return "unknown";
const keySet = new Set(table);
const edges: [string, string][] = [];
if (f.pinned) chain(table, edges);
for (const segments of f.literals) {
const sequences = literalSequences(segments, keySet, current);
if (sequences === null) return "unknown";
for (const seq of sequences) chain(seq, edges);
}
for (const c of f.copies) {
const source = current(c.source);
if (source === "unknown") return "unknown";
chain(
source.order.filter((k) => keySet.has(k) && c.keys.has(k)),
edges,
);
}
const cached = late.get(id);
const lateFor = cached !== undefined ? cached : lateEdges(id, table);
if (cached === undefined) late.set(id, lateFor);
if (lateFor === "unknown") return "unknown";
for (const edge of lateFor) edges.push(edge);
return merge(table, edges);
};
/** A source's order as its copies see it: values of a shape built by a
* checked conversion keep their source's own order. */
const asSource = (id: string, resolved: Resolved): Resolved =>
facts.get(id)?.runtime === true ? "unknown" : resolved;
// Tarjan's SCCs over "depends on" edges: sources finish first.
const index = new Map<string, number>();
const low = new Map<string, number>();
const stack: string[] = [];
const onStack = new Set<string>();
let counter = 0;
const sccs: string[][] = [];
const strong = (v: string): void => {
index.set(v, counter);
low.set(v, counter);
counter++;
stack.push(v);
onStack.add(v);
for (const w of dependencies(v)) {
if (!index.has(w)) {
strong(w);
low.set(v, Math.min(low.get(v)!, low.get(w)!));
} else if (onStack.has(w)) low.set(v, Math.min(low.get(v)!, index.get(w)!));
}
if (low.get(v) === index.get(v)) {
const scc: string[] = [];
let w: string;
do {
w = stack.pop()!;
onStack.delete(w);
scc.push(w);
} while (w !== v);
sccs.push(scc);
}
};
for (const id of facts.keys()) if (!index.has(id)) strong(id);
const external = (id: string): Resolved => {
const known = result.get(id);
return known === undefined ? "unknown" : asSource(id, known);
};
for (const scc of sccs) {
const members = new Set(scc);
const guess = new Map<string, Resolved>();
for (const id of scc) {
const shape = shapes.get(id);
guess.set(id, inScope(shape) ? { order: tableOf(shape) } : "unknown");
}
const lookup = (id: string): Resolved =>
members.has(id) ? asSource(id, guess.get(id)!) : external(id);
let stable = false;
for (let round = 0; round < scc.length + 3 && !stable; round++) {
stable = true;
for (const id of scc) {
const next = resolveOne(id, lookup);
const prev = guess.get(id)!;
const same =
next === "unknown"
? prev === "unknown"
: prev !== "unknown" && prev.order.join("\0") === next.order.join("\0");
if (!same) {
stable = false;
guess.set(id, next);
}
}
}
for (const id of scc) result.set(id, stable ? guess.get(id)! : "unknown");
}
// ── observation ─────────────────────────────────────────────────────
const observed = new Set<string>();
const observedSeen = new Set<string>();
for (const id of notes.observed)
shapesIn({ kind: "record", shapeId: id }, observed, observedSeen);
for (const root of observedRoots) shapesIn(root, observed, observedSeen);
// ── apply ───────────────────────────────────────────────────────────
for (const [id, resolved] of result) {
if (resolved === "unknown" || !observed.has(id)) continue;
const shape = shapes.get(id);
if (!inScope(shape)) continue;
const table = tableOf(shape);
if (resolved.order.join("\0") === table.join("\0")) continue;
shape.declaredOrder = [...shape.declaredOrder!.filter(isArrayIndex), ...resolved.order];
notes.changed.add(id);
}
}
@@ -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";
@@ -1489,6 +1494,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) ??
@@ -3416,6 +3424,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.
@@ -3540,6 +3565,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) {
@@ -4117,6 +4162,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([
@@ -4588,6 +4646,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))
@@ -4602,6 +4675,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) {
@@ -6520,6 +6619,7 @@ export function recordKeysArrayCall(
loc: SrcLoc,
): IrExpr {
const resultT = arrayOf(STRING);
lowerer.keyOrder.observe(argIr);
const key = `obj.keys:${argIr.shapeId}:${typeKey(resultT)}`;
let helper = lowerer.arrHofHelpers.get(key);
if (!helper) {
@@ -8000,6 +8100,7 @@ function lowerObjectStaticCall(
valueT = tupleShape.fields.find((f) => f.name === "1")!.type;
}
lowerer.keyOrder.observe(argIr);
const key = `obj.${member}:${argIr.shapeId}:${typeKey(resultT)}`;
let helper = lowerer.arrHofHelpers.get(key);
if (!helper) {
@@ -9885,6 +9986,22 @@ export function lowerStaticNumberGlobal(
return null;
}
/** `crypto.getRandomValues(view)` on the WebCrypto global or webcrypto. */
function lowerWebCryptoGetRandomValues(
lowerer: Lowerer,
call: ts.CallExpression,
access: ts.PropertyAccessExpression,
): IrExpr | null {
if (
access.name.text !== "getRandomValues" ||
call.questionDotToken ||
access.questionDotToken ||
!isWebCryptoReceiver(lowerer, access.expression)
)
return null;
return lowerCryptoGetRandomValues(lowerer, call);
}
/** A then handler's promise-armed result (`.then(() => cached ?? load())`
* typed `T | Promise<T>`, or a `Promise<void> | void` callback's result):
* the lifted async body adopts the promise arm and fulfills with the
@@ -730,6 +730,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) {
@@ -1323,6 +1348,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") {
@@ -9032,6 +9059,76 @@ export function lowerEnvironmentKey(lowerer: Lowerer, node: ts.Expression): IrEx
return lowerRecordPropertyKey(lowerer, key, node);
}
/** `typeof` of a Node 24 Web-platform constructor global or one of its
* static methods: "function", decided by name. The read has no effects.
* Static methods answer by name because after a `typeof
* globalThis.AbortSignal === "function" &&` guard the checker narrows a
* fallback-typed receiver to never, hiding the member's declaration. */
function constructorGlobalTypeof(lowerer: Lowerer, tested: ts.Expression): string | null {
while (ts.isParenthesizedExpression(tested)) tested = tested.expression;
const globalName = stdlibGlobalNameOf(lowerer, tested);
if (globalName !== null && NODE_CONSTRUCTOR_GLOBALS.has(globalName)) return "function";
if (!ts.isPropertyAccessExpression(tested) || tested.questionDotToken) return null;
const owner = stdlibGlobalNameOf(lowerer, tested.expression);
return owner !== null &&
Object.hasOwn(NODE_CONSTRUCTOR_STATICS, owner) &&
NODE_CONSTRUCTOR_STATICS[owner]!.includes(tested.name.text)
? "function"
: null;
}
/** `typeof globalThis.AbortSignal.any === "function"`: the comparison
* folds with constructorGlobalTypeof's answer. */
function lowerConstructorGlobalTypeofTest(
lowerer: Lowerer,
expr: ts.BinaryExpression,
loc: SrcLoc,
): IrExpr | null {
const negated = expr.operatorToken.kind === ts.SyntaxKind.ExclamationEqualsEqualsToken;
for (const [a, b] of operandPairs(expr)) {
if (!ts.isTypeOfExpression(a) || !ts.isStringLiteral(b)) continue;
const answer = constructorGlobalTypeof(lowerer, a.expression);
if (answer !== null)
return { kind: "boolLit", value: (answer === b.text) !== negated, type: BOOL, loc };
}
return null;
}
/** Static methods of Node 24's Web-platform constructor globals. */
const NODE_CONSTRUCTOR_STATICS: Readonly<Record<string, readonly string[]>> = {
AbortSignal: ["abort", "any", "timeout"],
Response: ["error", "json", "redirect"],
URL: ["canParse", "createObjectURL", "parse", "revokeObjectURL"],
};
/** Node 24's Web-platform constructor globals: `typeof` answers
* "function" for each. */
const NODE_CONSTRUCTOR_GLOBALS: ReadonlySet<string> = new Set([
"AbortController",
"AbortSignal",
"Blob",
"BroadcastChannel",
"CustomEvent",
"DOMException",
"Event",
"EventTarget",
"File",
"FormData",
"Headers",
"MessageChannel",
"MessageEvent",
"MessagePort",
"ReadableStream",
"Request",
"Response",
"TextDecoder",
"TextEncoder",
"TransformStream",
"URL",
"URLSearchParams",
"WritableStream",
]);
/** An operand of `+` with a string: ToString of ToPrimitive with the
* default hint, which tries a class instance's valueOf before toString. */
function concatOperand(lowerer: Lowerer, e: IrExpr, node: ts.Node): IrExpr {
@@ -10600,6 +10697,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) ??
@@ -10,6 +10,7 @@ import {
DYN,
F64,
type IrExpr,
type IrFunction,
type IrStmt,
type IrType,
RUNTIME_ERROR_CLASSES,
@@ -992,6 +993,7 @@ function inspectHelper(lowerer: Lowerer, t: IrType, loc: SrcLoc): string {
// Object.keys' declared order (SEMANTICS.md 36's stance). Retained
// reachability can settle this shape after the helper is first
// interned, so rebuild the field walk once metadata is final.
lowerer.keyOrder.observe(t);
rebuildShapeOrderBody = (): void => {
const current = lowerer.shapes.get(t.shapeId) ?? shape;
const order = current.declaredOrder ?? current.fields.map((f) => f.name);
@@ -1342,7 +1344,10 @@ function inspectHelper(lowerer: Lowerer, t: IrType, loc: SrcLoc): string {
prependCycleGuard();
}
const fn = {
// Typed as the IR function itself: the finalizer below replaces the
// body of the very object liftedFns holds (natively, a differently typed
// literal would be copied into the list).
const fn: IrFunction = {
name,
params: [
{ localId: "v.0", name: "v", type: t },
@@ -113,6 +113,7 @@ import {
isVarDeclared,
numericIteratorSourceOf,
provenanceElidedConstDecl,
stdlibBufferVarAlias,
} from "./lower-stmts.js";
import { objectFactoryGlobalType } from "./object-factory-new.js";
import { streamClassAliasDecl } from "./lower-stream.js";
@@ -1895,6 +1896,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 &&
@@ -2877,11 +2879,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.
@@ -2976,6 +2983,16 @@ export function lowerFileInit(
);
at++;
}
for (const { at, g } of unionEntryInits) {
if (g.type.kind !== "dyn") continue;
body[at] = {
kind: "assign",
localId: g.id,
value: dynUndefinedExpr(loc0),
...(g.initFlag !== undefined ? { initializes: true as const } : {}),
loc: loc0,
};
}
const moduleRef = cjsModuleRef(lowerer, sf);
if (moduleRef !== null) {
const guardedBody = body.splice(guardId !== undefined ? 2 : 0);
@@ -87,6 +87,7 @@ import {
isJsSourceFile,
isNodeEsmFile,
locOf,
requireEarlyReference,
requireSpecOf,
} from "../program.js";
import {
@@ -98,6 +99,7 @@ import {
sideEffectFreeOptionValue,
stdlibGlobalAliasDecl,
stdlibGlobalAliasNameOf,
bufferNamespaceMember,
stdlibGlobalNameOf,
} from "./surfaces.js";
import { isNativeBuiltinValueInitializer } from "./lower-builtin-values.js";
@@ -191,6 +193,7 @@ import {
import { globalSymbolKey } from "./expressions/global-symbols.js";
import { lowerShortCircuitAssignment } from "./expressions/nullish-assignment.js";
import {
jsHoldsDynObject,
lowerEnvironmentKey,
lowerNativeFunctionAssignment,
lowerStrictEquality,
@@ -1067,13 +1070,124 @@ function hostVariableDeclarationOf(name: ts.Identifier): ts.VariableDeclaration
export function forOfVarTarget(
lowerer: Lowerer,
decl: ts.VariableDeclaration,
valueType?: IrType,
): { id: string; type: IrType } | null {
if (!ts.isIdentifier(decl.name) || !isVarDeclared(decl)) return null;
const symbol = lowerer.checker.getSymbolAtLocation(decl.name);
if (!symbol) return null;
const g = lowerer.globalsBySymbol.get(symbol);
if (g) return g;
return hoistVarBinding(lowerer, symbol, decl.name);
if (g) {
if (valueType?.kind === "dyn" && jsBindingKeepsCheckedObject(lowerer, decl.name, g.type))
g.type = DYN;
return g;
}
return hoistVarBinding(lowerer, symbol, decl.name, valueType);
}
/** JavaScript: whether an unannotated variable binding whose static type
* is `type` keeps a checked-dynamic value unconverted. The checker types
* such a binding by the object it receives (an element of an evolving
* `[]`, a `find` result, a member of a checked-dynamic object), but the
* value is an existing checked-dynamic object or array: a record- or
* array-typed slot would hold a converted copy, so writes through the
* binding would miss the object other references see, and `===` against
* them would fail. Block-scoped bindings with an identifier name keep such
* values by the same rule in lowerVarDecl (preservesObjectIdentity). */
function jsBindingKeepsCheckedObject(
lowerer: Lowerer,
nameNode: ts.Identifier,
type: IrType,
): boolean {
if (!isJsSourceFile(nameNode.getSourceFile())) return false;
const host = hostVariableDeclarationOf(nameNode);
if (!host || host.type || hasJsTypeAnnotation(host)) return false;
const arms = type.kind === "union" ? (lowerer.unions.get(type.unionId)?.arms ?? []) : [type];
return (
arms.some((arm) => arm.kind === "record" || arm.kind === "array") &&
arms.every((arm) => arm.kind === "record" || arm.kind === "array" || isUnitType(arm))
);
}
/** Whether `node` mentions the binding `symbol` anywhere, nested functions
* included. */
function mentionsSymbol(lowerer: Lowerer, node: ts.Node, symbol: ts.Symbol): boolean {
if (ts.isIdentifier(node)) return lowerer.checker.getSymbolAtLocation(node) === symbol;
return (
node.forEachChild((child) => (mentionsSymbol(lowerer, child, symbol) ? true : undefined)) ===
true
);
}
/** Whether `node` evaluates to an existing value rather than constructing
* one whose layout a destination directs: not a literal, `new`, function
* or class expression, nor a conditional with such an arm (an arm that
* is `null` or `undefined` constructs nothing). */
function existingValueForm(node: ts.Expression, armOfConditional = false): boolean {
while (ts.isParenthesizedExpression(node)) node = node.expression;
if (ts.isConditionalExpression(node))
return existingValueForm(node.whenTrue, true) && existingValueForm(node.whenFalse, true);
if (
armOfConditional &&
(node.kind === ts.SyntaxKind.NullKeyword ||
(ts.isIdentifier(node) && node.text === "undefined"))
)
return true;
return !(
ts.isObjectLiteralExpression(node) ||
ts.isArrayLiteralExpression(node) ||
ts.isNewExpression(node) ||
ts.isFunctionExpression(node) ||
ts.isArrowFunction(node) ||
ts.isClassExpression(node) ||
ts.isLiteralExpression(node) ||
node.kind === ts.SyntaxKind.NullKeyword
);
}
/** JavaScript `var x = e` whose binding could hold an existing
* checked-dynamic object (jsBindingKeepsCheckedObject): lowers `e` BEFORE
* minting the hoisted slot, as a block-scoped declaration does, so a
* checked-dynamic value gets a checked-dynamic slot instead of a converted
* copy. Other values coerce into the planned slot exactly as
* lowerExprExpecting would. Null when the declaration does not qualify:
* the slot already exists, the initializer is fresh construction whose
* layout the slot type directs (existingValueForm; a literal holding an
* existing checked-dynamic object qualifies), or it refers to the binding
* itself (hoist-before-init resolves that reference). */
function lowerJsVarFromExisting(
lowerer: Lowerer,
decl: ts.VariableDeclaration,
symbol: ts.Symbol,
): IrStmt | null {
if (!ts.isIdentifier(decl.name) || !decl.initializer) return null;
if (!isJsSourceFile(decl.getSourceFile()) || lowerer.ctx.hoistedVars.has(symbol)) return null;
if (lowerer.bindingIn(lowerer.ctx, symbol)) return null;
let node: ts.Expression = decl.initializer;
while (ts.isParenthesizedExpression(node) || ts.isSatisfiesExpression(node))
node = node.expression;
// A literal holding an existing checked-dynamic object builds as a
// checked-dynamic literal without a destination (lowerArrayLiteral,
// lowerObjectLiteral): a record- or array-typed slot would copy it.
const holdsChecked =
(ts.isArrayLiteralExpression(node) || ts.isObjectLiteralExpression(node)) &&
jsHoldsDynObject(lowerer, node);
if (!holdsChecked && !existingValueForm(node)) return null;
const planned = varBindingType(lowerer, decl.name);
if (!planned || !jsBindingKeepsCheckedObject(lowerer, decl.name, planned)) return null;
if (mentionsSymbol(lowerer, node, symbol)) return null;
const value = lowerer.lowerExpr(node);
const local = hoistVarBinding(
lowerer,
symbol,
decl.name,
value.type.kind === "dyn" ? DYN : undefined,
);
return {
kind: "assign",
localId: local.id,
value: lowerer.coerceExistingInto(node, value, local.type),
loc: locOf(decl),
};
}
/** The `var` symbol's function-scoped binding TYPE — the declared type at
@@ -1129,7 +1243,12 @@ function varBindingType(lowerer: Lowerer, nameNode: ts.Identifier): IrType | nul
* a loop re-ASSIGNS the one binding per pass and never resets it — the
* classic capture semantics fall out: closures made in a loop share the
* single boxed binding, where `let` gets a fresh box per iteration. */
function hoistVarBinding(lowerer: Lowerer, symbol: ts.Symbol, nameNode: ts.Identifier): IrLocal {
function hoistVarBinding(
lowerer: Lowerer,
symbol: ts.Symbol,
nameNode: ts.Identifier,
valueType?: IrType,
): IrLocal {
const existing = lowerer.ctx.hoistedVars.get(symbol);
if (existing) return existing;
// The parameter merge: the symbol already binds a function-root local.
@@ -1138,7 +1257,15 @@ function hoistVarBinding(lowerer: Lowerer, symbol: ts.Symbol, nameNode: ts.Ident
lowerer.ctx.hoistedVars.set(symbol, bound);
return bound;
}
const type = varBindingType(lowerer, nameNode);
let type = varBindingType(lowerer, nameNode);
// The first value stored is an existing checked-dynamic object: keep it
// (jsBindingKeepsCheckedObject). A checked-dynamic slot holds undefined
// natively, so no runtime-optional widening applies.
const keepsChecked =
valueType?.kind === "dyn" &&
type !== null &&
jsBindingKeepsCheckedObject(lowerer, nameNode, type);
if (keepsChecked) type = DYN;
if (!type) lowerer.badType(nameNode, lowerer.typeOf(nameNode));
const root = lowerer.activeStmtLists.find((e) => e.ctx === lowerer.ctx);
if (!root)
@@ -1155,7 +1282,7 @@ function hoistVarBinding(lowerer: Lowerer, symbol: ts.Symbol, nameNode: ts.Ident
};
lowerer.ctx.locals.push(local);
lowerer.ctx.scopes[0]!.set(symbol, local);
noteEarlyReadVar(lowerer, symbol, local);
if (!keepsChecked) noteEarlyReadVar(lowerer, symbol, local);
// A checked-dynamic slot holds the dyn undefined (a NULL dyn is a
// trap); everything else rides unassignedSlotInit (interned union arm,
@@ -4014,7 +4141,7 @@ function objectRestValue(
),
};
});
return { kind: "recordLit", fields, type: restT, loc };
return lowerer.keyOrder.copy({ kind: "recordLit", fields, type: restT, loc }, srcType.shapeId);
}
/** The fresh record for `{ a, ...rest }` over a CLASS-INSTANCE source:
@@ -4197,6 +4324,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,
@@ -4213,7 +4342,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,
@@ -4228,7 +4357,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
@@ -4564,6 +4693,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) &&
@@ -4711,7 +4841,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);
@@ -4758,6 +4892,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);
@@ -7907,22 +8043,25 @@ function destructuringAssignInto(
),
);
}
const packed: IrExpr = {
kind: "recordLit",
fields: remaining.map((f): Extract<IrExpr, { kind: "recordLit" }>["fields"][number] => ({
name: f.name,
value: {
kind: "recordGet",
obj: { kind: "varRef", localId: tmp.id, type: srcType, loc: locOf(prop) },
shapeId: srcType.shapeId,
field: f.name,
type: f.type,
loc: locOf(prop),
},
})),
type: { kind: "record", shapeId: restShapeId },
loc: locOf(prop),
};
const packed: IrExpr = lowerer.keyOrder.copy(
{
kind: "recordLit",
fields: remaining.map((f): Extract<IrExpr, { kind: "recordLit" }>["fields"][number] => ({
name: f.name,
value: {
kind: "recordGet",
obj: { kind: "varRef", localId: tmp.id, type: srcType, loc: locOf(prop) },
shapeId: srcType.shapeId,
field: f.name,
type: f.type,
loc: locOf(prop),
},
})),
type: { kind: "record", shapeId: restShapeId },
loc: locOf(prop),
},
srcType.shapeId,
);
if (!ts.isIdentifier(restTo)) {
// `({ a, ...box.rest } = src)` — the rest packs identically and
// lands through the property/element write machinery.
@@ -9220,7 +9359,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);
@@ -9956,7 +10095,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
@@ -10800,3 +10939,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
);
}
@@ -20,6 +20,7 @@ import {
} from "./require-initialization.js";
import { identityPreservingWidening } from "./coercions/identity.js";
import { scanDivergences } from "./divergences.js";
import { applyKeyOrderWitnesses, KeyOrderNotes } from "./key-order-witness.js";
import { checkedClassAssertion } from "./class-assertions.js";
import { narrowStoredClassValue, narrowGenericClassValue } from "./class-unions.js";
import { isOptionalProcessStreamProperty } from "./builtins/process.js";
@@ -77,6 +78,7 @@ import {
uninhabitedArmRetagHelper,
strandedUnitTrap,
strandedCoercionTrap,
recordUnionRouteHelper,
recordUnionWrapHelper,
unionRetagHelper,
narrowedArmHelper,
@@ -480,6 +482,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,
@@ -2465,6 +2469,9 @@ export class Lowerer {
* Reachability lowers inits before the declarations they discover, so
* these rebuild after the worklist restores historical shape metadata. */
readonly shapeOrderHelperFinalizers: (() => void)[] = [];
/** Construction witnesses that choose each shape's enumeration order
* once lowering settles (key-order-witness.ts). */
readonly keyOrder = new KeyOrderNotes();
/** Emit-override specializations (`%C.emit:<event>` — lower-event-emitter.ts's
* emit-overrides block): interned names, the drive-loop queue, and the
* currently-lowering specialization's context (the super-forward
@@ -7458,6 +7465,13 @@ export class Lowerer {
this.settleGenericDemandPriorities();
this.shapes.settleDeclaredOrderPriorities();
for (const finalize of this.shapeOrderMetadataFinalizers) finalize();
applyKeyOrderWitnesses(this, [
...orderedUnits,
...initFunctions,
...instanceFunctions,
...this.liftedFns,
...this.implicitFns,
]);
for (const finalize of this.shapeOrderHelperFinalizers) finalize();
const functions = [
...orderedUnits,
@@ -9174,6 +9188,14 @@ export class Lowerer {
return recordUnionWrapHelper(this, source, toId, loc);
}
recordUnionRouteHelper(
source: IrType & { kind: "record" },
toId: string,
loc: SrcLoc,
): string | null {
return recordUnionRouteHelper(this, source, toId, loc);
}
unionRetagHelper(
fromId: string,
toId: string,
@@ -9674,16 +9696,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
@@ -12288,13 +12317,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 };
}
}
@@ -12363,14 +12386,8 @@ export class Lowerer {
const callable = this.lowerBuiltinCallableValue(bi, loc);
if (callable) return callable;
}
if (bi.member === "constants" && bi.module === "fs") {
// A bare `fs.constants` read (not one of the baked bits above).
this.noLowering(
`fs.constants`,
expr,
"F_OK, R_OK, W_OK, X_OK, and COPYFILE_* are the lowered constants",
);
}
// A bare `fs.constants` read (not one of the baked bits above).
if (bi.member === "constants" && bi.module === "fs") return fsConstantsObject(loc);
if (builtinModuleFnOf(this, bi.module, bi.member)) {
this.unsupported(
"SC1090",
@@ -22,7 +22,7 @@ import {
VOID,
arrayOf,
} from "../../ir/ir.js";
import { isNodeTypesPath, requireSpecOf } from "../program.js";
import { isJsSourceFile, isNodeTypesPath, requireSpecOf } from "../program.js";
import { ZLIB_CONSTANTS } from "./zlib-constants.js";
/** zlib stream instances: runtime Transform streams (scr_zlib_stream.c). */
@@ -773,6 +773,7 @@ export const STATIC_NUMBER_METHODS: Record<
{ fns: readonly IrLibFn[]; minArgs: number; maxArgs: number } | undefined
> = {
toFixed: { fns: ["num.toFixed0", "num.toFixed"], minArgs: 0, maxArgs: 1 },
toPrecision: { fns: ["num.toPrecision"], minArgs: 0, maxArgs: 1 },
};
/** One lowerable builtin-module FUNCTION: `fn`'s libCall with `params`
@@ -1521,6 +1522,7 @@ export const BUILTIN_MODULE_FNS: Record<
hash: { fn: "crypto.hashDigestStr", params: [STRING, STRING, STRING], result: STRING },
timingSafeEqual: { fn: "crypto.timingSafeEqual", params: [BYTES_U8, BYTES_U8], result: BOOL },
randomFillSync: { fn: "crypto.randomFill", params: [BYTES_U8, F64, F64], result: BYTES_U8 },
getRandomValues: { fn: "crypto.getRandomValues", params: [BYTES_U8], result: BYTES_U8 },
randomInt: { fn: "crypto.randomInt", params: [F64, F64], result: F64 },
randomFill: { fn: "crypto.randomFillCb", params: [], result: VOID },
randomUUIDv7: { fn: "crypto.randomUUIDv7", params: [DYN], result: STRING },
@@ -2086,6 +2088,7 @@ export const BUILTIN_MODULE_FN_ALIASES: Record<
"crypto.randomFillPromiseDyn",
],
timingSafeEqual: ["crypto.timingSafeEqualDyn"],
getRandomValues: ["crypto.getRandomValuesDyn"],
randomInt: ["crypto.randomIntMax", "crypto.randomIntCb", "crypto.randomIntPromise"],
randomUUID: ["crypto.randomUUIDOpts"],
},
@@ -2663,6 +2666,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
@@ -3288,7 +3300,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) {
@@ -3363,7 +3389,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.
@@ -3384,6 +3412,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,
@@ -1,7 +1,12 @@
import { describe, expect, test } from "vitest";
import { BOOL, F64, NULL_T, STRING, UNDEFINED_T, typeEquals, typeKey } from "../../ir/ir.js";
import type { IrRecordShape, IrType, IrUnionDef, SrcLoc } from "../../ir/ir.js";
import { planUnionRetag, buildUnionRetag, planRecordUnionWrap } from "./union-retag.js";
import {
planUnionRetag,
buildUnionRetag,
planRecordUnionLift,
planRecordUnionWrap,
} from "./union-retag.js";
import type { UnionRetagArm } from "./union-retag.js";
import type { WidthLift } from "./width-lift.js";
@@ -126,6 +131,30 @@ describe("record conversion with shared layouts", () => {
f.shapes[2]!.fields.reverse();
expect(planRecordUnionWrap(f.shapes[1]!, f.to, f.shapeOf)).toBeNull();
});
test("routes a record that fits several arms by its discriminant", () => {
const f = fixture();
expect(planRecordUnionLift(f.shapes[0]!, f.to, f.shapeOf, f.lift)).toEqual({
field: "kind",
fieldType: STRING,
routes: [
{ tag: 0, lift: { how: "width" }, values: ["left"] },
{ tag: 1, lift: { how: "width" }, values: ["right"] },
],
});
// Without the discriminant field, or without an arm it converts into,
// there is no route.
const unnamed: IrRecordShape = { id: "r3", fields: f.shapes[0]!.fields.slice(1) };
expect(planRecordUnionLift(unnamed, f.to, f.shapeOf, f.lift)).toBeNull();
const unrelated: IrRecordShape = {
id: "r4",
fields: [
{ name: "kind", type: STRING },
{ name: "other", type: F64 },
],
};
expect(planRecordUnionLift(unrelated, f.to, f.shapeOf, f.lift)).toBeNull();
});
});
describe("union conversion planning", () => {
@@ -86,6 +86,36 @@ export function planRecordUnionWrap(
return { field: discriminant.field, fieldType, routes };
}
/** A record whose layout converts into more than one arm of a
* discriminated union. The IR keeps the discriminant field as a plain
* string, so its layout alone cannot say which variant it is; the value of
* the discriminant does, as in JavaScript. Every record arm the record
* converts into (by copy or width) becomes a route selected by that arm's
* discriminant values. A value no route names throws the catchable
* TypeError. */
export function planRecordUnionLift(
source: IrRecordShape,
to: IrUnionDef,
shapeOf: (id: string) => IrRecordShape | undefined,
widthLift: (src: IrType, dst: IrType) => WidthLift | null,
): RecordUnionWrapPlan | null {
const discriminant = to.discriminant;
if (!discriminant || source.tuple || source.indexValue || shapeHasAccessorSlots(source))
return null;
const fieldType = discriminantField(source, discriminant.field);
if (!fieldType || discriminantOwners(to, shapeOf) === null) return null;
const sourceType: IrType = { kind: "record", shapeId: source.id };
const routes: UnionRetagRoute[] = [];
for (const entry of discriminant.cases) {
const arm = to.arms[entry.tag];
if (!arm || arm.kind !== "record") continue;
const lift = arm.shapeId === source.id ? { how: "copy" as const } : widthLift(sourceType, arm);
if (lift === null) continue;
routes.push({ tag: entry.tag, lift, values: entry.values });
}
return routes.length > 0 ? { field: discriminant.field, fieldType, routes } : null;
}
/** Pure conversion planning. A source storage tag need not denote just
* one semantic variant: refinements can coalesce recursive record layouts.
* When both unions name the same discriminant, its values select target
@@ -7,6 +7,7 @@ export type WidthLift =
| { how: "copy" }
| { how: "wrap"; tag: number }
| { how: "discriminantWrap" }
| { how: "discriminantRoute" }
| { how: "retag" }
| { how: "liftWrap"; tag: number; arm: IrType }
| { how: "width" }
@@ -143,6 +143,9 @@ export const MAY_THROW_LIB_FNS: ReadonlySet<IrLibFn> = new Set([
"fetch.streamNew",
"fetch.streamFrom",
"num.toFixed",
"num.toPrecision",
"num.toExponentialDigits",
"str.localeCase",
"num.toStringRadix",
"num.toStringRadixF64",
"insp.jsonDyn",
@@ -684,6 +687,8 @@ export const MAY_THROW_LIB_FNS: ReadonlySet<IrLibFn> = new Set([
"crypto.randomIntMax",
"crypto.randomFillView",
"crypto.randomFillViewRest",
"crypto.getRandomValues",
"crypto.getRandomValuesDyn",
"crypto.randomFillSyncDyn",
"crypto.randomFillCb",
"crypto.randomFillCbDyn",
@@ -589,6 +589,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. */
@@ -716,6 +718,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.
@@ -780,6 +789,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. */
@@ -2626,6 +2640,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 },
@@ -2676,6 +2695,9 @@ export const LIB_FN_SIGS = defineLibFnSignatures({
"buffer.fromStr": { argTypes: [STRING, STRING], result: BYTES_U8 },
"buffer.fromStrChecked": { argTypes: [STRING, STRING], result: BYTES_U8 },
"buffer.brand": { argTypes: [BYTES_U8], result: BYTES_U8 },
/** Buffer.isBuffer over a typed view: whether the view carries the
* Buffer brand (any element kind; only Buffers do). Never throws. */
"buffer.isBranded": { argTypes: [null], result: BOOL },
/** Buffer.from on checked-native strings, bytes, arrays and data-only
* array-like/Buffer-JSON objects. The encoding is a normalized literal;
* non-string inputs ignore it. Copies the input and may throw during
@@ -505,6 +505,8 @@ function scanRuntimeFeatures(mod: IrModule, stopAt?: keyof RuntimeFeatures): Run
}
if (fn.startsWith("dgram.") || fn.startsWith("dns.")) features.dgram = true;
if (fn.startsWith("tlsca.")) features.tlsCa = true;
// Locale case mapping runs the default conversion of the regex unit.
if (fn === "str.localeCase") features.regex = true;
// scr_cipher.c over the mbedTLS archive; DEP0182 reports through the
// process-warning unit, and unknown output encodings render through
// the inspect unit.
+4 -4
View File
@@ -1,4 +1,5 @@
import type { IrExpr, IrFunction, IrModule, IrStmt } from "./ir.js";
import { reparseExpr, reparseStmt } from "./serialize.js";
import { everyExprChild, everyStmtChild, mapExprChildren, mapStmtChildren } from "./traverse.js";
/** Give every occurrence of an executable node its own object.
@@ -41,15 +42,14 @@ export function unshareNodes(mod: IrModule): IrModule {
}
if (affected.size === 0) return mod;
// Rebuilding a function copies every node that has children; a repeated
// childless node gets its own shallow copy at each of its sites there.
// Both kinds of copy keep sharing the type and location objects.
// childless node reads back as a fresh object at each of its sites there.
const expr = (node: IrExpr): IrExpr => {
const copy = mapExprChildren(node, expr, stmt);
return copy !== node || !repeated.has(node) ? copy : { ...node };
return copy !== node || !repeated.has(node) ? copy : reparseExpr(node);
};
const stmt = (node: IrStmt): IrStmt => {
const copy = mapStmtChildren(node, expr, stmt);
return copy !== node || !repeated.has(node) ? copy : { ...node };
return copy !== node || !repeated.has(node) ? copy : reparseStmt(node);
};
return {
...mod,
+5 -1
View File
@@ -5242,7 +5242,11 @@ function validateFunction(
}
function checkLibSpecialCall(e: IrExpr & { kind: "libCall" }): boolean {
if (e.fn === "crypto.randomFillView" || e.fn === "crypto.randomFillViewRest") {
if (
e.fn === "crypto.randomFillView" ||
e.fn === "crypto.randomFillViewRest" ||
e.fn === "crypto.getRandomValues"
) {
// The filled view is the result, at its own element type.
const view = e.args[0]?.type;
if (view?.kind !== "bytes" || !typeEquals(e.type, view)) {
+15 -2
View File
@@ -618,6 +618,12 @@
"code": "SC2020",
"note": "asymmetric-key operations need a public-key stack (bignum, RSA/EC/EdDSA math) and public and private KeyObjects — neither exists in the static runtime, so no faithful lowering can be small; the lowered crypto surface is hashing, randomness, secret KeyObjects, and the introspection statics"
},
{
"id": "node-builtin.crypto.getRandomValues",
"kind": "node-builtin",
"name": "crypto.getRandomValues",
"status": "static"
},
{
"id": "node-builtin.crypto.hash",
"kind": "node-builtin",
@@ -868,6 +874,13 @@
"name": "fs.closeSync",
"status": "static"
},
{
"id": "node-builtin.fs.constants",
"kind": "node-builtin",
"name": "fs.constants",
"status": "static",
"note": "node_constants.cc's fs group under the target libc's macros; members a platform lacks read as undefined"
},
{
"id": "node-builtin.fs.copyFileSync",
"kind": "node-builtin",
@@ -4849,8 +4862,8 @@
"id": "stdlib.number.toPrecision",
"kind": "stdlib",
"name": "number.prototype.toPrecision",
"status": "unsupported",
"code": "SC2012"
"status": "static",
"note": "the lowered call form takes 0 to 1 arguments"
},
{
"id": "stdlib.number.toString",
@@ -0,0 +1,6 @@
{
"order": [
"<repo>/tests/corpus/1112-number-methods.ts"
],
"diags": []
}
@@ -0,0 +1,6 @@
{
"order": [
"<repo>/tests/corpus/5639-union-arm-by-discriminant.ts"
],
"diags": []
}
@@ -0,0 +1,6 @@
{
"order": [
"<repo>/tests/corpus/6080-take-then-flatmap.ts"
],
"diags": []
}
@@ -0,0 +1,6 @@
{
"order": [
"<repo>/tests/corpus/6200-string-annexb-locale-methods.ts"
],
"diags": []
}
@@ -0,0 +1,6 @@
{
"order": [
"<repo>/tests/corpus/6201-number-precision-exponential.ts"
],
"diags": []
}
@@ -0,0 +1,6 @@
{
"order": [
"<repo>/tests/corpus/6202-dyn-string-number-methods.js"
],
"diags": []
}
@@ -0,0 +1,6 @@
{
"order": [
"<repo>/tests/corpus/6203-any-string-number-methods.ts"
],
"diags": []
}
@@ -0,0 +1,6 @@
{
"order": [
"<repo>/tests/corpus/6204-crypto-get-random-values.ts"
],
"diags": []
}
@@ -0,0 +1,6 @@
{
"order": [
"<repo>/tests/corpus/6205-crypto-get-random-values.js"
],
"diags": []
}
@@ -0,0 +1,6 @@
{
"order": [
"<repo>/tests/corpus/6206-buffer-is-buffer-views.ts"
],
"diags": []
}
@@ -0,0 +1,6 @@
{
"order": [
"<repo>/tests/corpus/6207-fs-constants-object.ts"
],
"diags": []
}
@@ -0,0 +1,6 @@
{
"order": [
"<repo>/tests/corpus/6208-fs-constants-require.cjs"
],
"diags": []
}
@@ -0,0 +1,6 @@
{
"order": [
"<repo>/tests/corpus/6209-buffer-module-alias.cjs"
],
"diags": []
}
@@ -0,0 +1,6 @@
{
"order": [
"<repo>/tests/corpus/6210-typeof-global-static-members.js"
],
"diags": []
}
@@ -0,0 +1,6 @@
{
"order": [
"<repo>/tests/corpus/6260-js-var-dyn-identity.js"
],
"diags": []
}
@@ -0,0 +1,6 @@
{
"order": [
"<repo>/tests/corpus/6261-js-destructure-for-of-identity.js"
],
"diags": []
}
@@ -0,0 +1,6 @@
{
"order": [
"<repo>/tests/corpus/6262-js-file-scope-var-identity.js"
],
"diags": []
}
@@ -0,0 +1,6 @@
{
"order": [
"<repo>/tests/corpus/6263-js-factory-return-identity.cjs"
],
"diags": []
}
@@ -0,0 +1,6 @@
{
"order": [
"<repo>/tests/corpus/key-order-late-optional.ts"
],
"diags": []
}
@@ -0,0 +1,6 @@
{
"order": [
"<repo>/tests/corpus/key-order-literal-flows.ts"
],
"diags": []
}
@@ -0,0 +1,6 @@
{
"order": [
"<repo>/tests/corpus/key-order-spread-copies.ts"
],
"diags": []
}
@@ -1,6 +0,0 @@
{
"order": [
"<repo>/tests/diagnostics/retired-engine-1112-number-methods.ts"
],
"diags": []
}
+3
View File
@@ -293,6 +293,9 @@ ScrBytes *scr_bytes_as_buffer(ScrBytes *bytes) {
return scr_bytes_retain(bytes);
}
/* Nothing on an existing path calls this. */
bool scr_bytes_is_buffer(const ScrBytes *bytes) { return bytes->is_buffer; }
void scr_bytes_release(ScrBytes *b) {
if (!b || b->rc == SIZE_MAX) return; /* NULL: an uninitialized `let` local */
if (--b->rc == 0) {
+2
View File
@@ -159,6 +159,8 @@ ScrBytes *scr_bytes_new(ScrBytesElem elem, double n); /* +1 */
/* Shared u8 view over exactly the source view's raw bytes. Borrows; +1. */
ScrBytes *scr_bytes_raw_view(ScrBytes *bytes);
ScrBytes *scr_bytes_as_buffer(ScrBytes *bytes); /* borrows; +1, Buffer factory only */
/* Buffer.isBuffer over a typed view: the view's Buffer brand. Borrows. */
bool scr_bytes_is_buffer(const ScrBytes *bytes);
/* Views retain the root allocation; offsets are relative to its complete
* byte storage, including when src is itself a subarray. */
ScrBytes *scr_bytes_buffer_view(ScrBytes *src, ScrBytesElem elem,
+4
View File
@@ -50,6 +50,10 @@ ScrBytes *scr_crypto_random_fill_rest(ScrBytes *bytes, double offset);
double scr_crypto_random_int(double min, double max);
double scr_crypto_random_int_max(double max);
ScrDyn *scr_crypto_random_fill_sync_dyn(ScrDyn *buf, ScrDyn *offset, ScrDyn *size);
/* crypto.getRandomValues over a view (returned +1) or any value (the
* value itself, +1); NULL with Node's DOMException pending. */
ScrBytes *scr_crypto_get_random_values(ScrBytes *view);
ScrDyn *scr_crypto_get_random_values_dyn(ScrDyn *value);
double scr_crypto_random_fill_args(ScrBytes *view, bool typed_array, double offset, double size,
int form);
void scr_crypto_random_int_refill(void);
+180
View File
@@ -11408,6 +11408,137 @@ ScrStr *scr_num_to_fixed(double x, double fraction_digits) {
return scr_str_new(out, len);
}
/* String.prototype.substr (Annex B.2.2.1) over substring's UTF-16
* clamp: -Infinity and other starts below -size clamp to 0, an Infinity
* length to the end. Comparisons only (no floating-point literal other
* than zero, which would join the constant pool every binary links).
* Nothing on an existing path calls this. */
ScrStr *scr_str_substr(ScrStr *s, double start, double length, bool has_length) {
double size = scr_str_utf16_len(s);
double first = isnan(start) ? 0 : trunc(start);
if (first < 0) {
first = size + first;
if (first < 0) first = 0;
} else if (first > size) {
first = size;
}
double count = has_length ? (isnan(length) ? 0 : trunc(length)) : size;
if (count < 0) count = 0;
double end = first + count;
if (end > size) end = size;
return scr_str_substring(s, first, end);
}
/* The exponent suffix of toPrecision/toExponential: "e+5", "e-7". */
static size_t scr_num_exp_suffix(char *o, int e) {
size_t n = 0;
o[n++] = 'e';
o[n++] = e < 0 ? '-' : '+';
int a = e < 0 ? -e : e;
char rev[8];
int k = 0;
do {
rev[k++] = (char)('0' + a % 10);
a /= 10;
} while (a != 0);
while (k > 0) o[n++] = rev[--k];
return n;
}
/* Number.prototype.toPrecision(precision) (ECMA-262 §21.1.3.5): undefined
* precision is ToString(x); ToIntegerOrInfinity runs (in the caller's
* coercion) before the non-finite arm, which precedes the 1..100 range
* check. Digits are the exact-value, ties-up n of
* scr_f64_precision_digits; exponential form when e < -6 or e >= p.
* Nothing on an existing path calls this. */
ScrStr *scr_num_to_precision(double x, double precision, bool has_precision) {
if (!has_precision || !isfinite(x)) return scr_f64_to_scrstr(x);
double p = isnan(precision) ? 0 : trunc(precision);
if (!(p >= 1 && p <= 100)) {
static const char msg[] = "toPrecision() argument must be between 1 and 100";
scr_throw_error_msg(SCR_ERR_RANGE, msg, sizeof msg - 1);
return NULL;
}
int digits_count = (int)p;
char out[160];
size_t o = 0;
double a = x;
if (x < 0) {
out[o++] = '-';
a = -x;
}
char d[104];
int e = 0;
if (a == 0) {
memset(d, '0', (size_t)digits_count);
d[digits_count] = '\0';
} else {
scr_f64_precision_digits(a, digits_count, d, &e);
}
if (e < -6 || e >= digits_count) {
out[o++] = d[0];
if (digits_count != 1) {
out[o++] = '.';
memcpy(out + o, d + 1, (size_t)(digits_count - 1));
o += (size_t)(digits_count - 1);
}
o += scr_num_exp_suffix(out + o, e);
} else if (e >= 0) {
memcpy(out + o, d, (size_t)(e + 1));
o += (size_t)(e + 1);
if (e + 1 < digits_count) {
out[o++] = '.';
memcpy(out + o, d + e + 1, (size_t)(digits_count - e - 1));
o += (size_t)(digits_count - e - 1);
}
} else {
out[o++] = '0';
out[o++] = '.';
for (int i = 0; i < -(e + 1); i++) out[o++] = '0';
memcpy(out + o, d, (size_t)digits_count);
o += (size_t)digits_count;
}
return scr_str_new(out, o);
}
/* Number.prototype.toExponential(fractionDigits) with a DEFINED argument
* (the undefined form is scr_num_to_exponential's shortest mantissa):
* f + 1 exact-value, ties-up digits, "d.ddde±X". Nothing on an existing
* path calls this. */
ScrStr *scr_num_to_exponential_digits(double x, double fraction_digits) {
if (!isfinite(x)) return scr_f64_to_scrstr(x);
double f = isnan(fraction_digits) ? 0 : trunc(fraction_digits);
if (!(f >= 0 && f <= 100)) {
static const char msg[] = "toExponential() argument must be between 0 and 100";
scr_throw_error_msg(SCR_ERR_RANGE, msg, sizeof msg - 1);
return NULL;
}
int count = (int)f + 1;
char out[160];
size_t o = 0;
double a = x;
if (x < 0) {
out[o++] = '-';
a = -x;
}
char d[104];
int e = 0;
if (a == 0) {
memset(d, '0', (size_t)count);
d[count] = '\0';
} else {
scr_f64_precision_digits(a, count, d, &e);
}
out[o++] = d[0];
if (count != 1) {
out[o++] = '.';
memcpy(out + o, d + 1, (size_t)(count - 1));
o += (size_t)(count - 1);
}
o += scr_num_exp_suffix(out + o, e);
return scr_str_new(out, o);
}
/* Increment a decimal digit string in place. Returns true on overflow —
* the value becomes 1 followed by len zeros (the caller folds the zeros
* into its scale); an EMPTY string increments to "1" the same way (the
@@ -11739,3 +11870,52 @@ ScrDyn *scr_dyn_native_date_from_ms(double ms) {
scr_native_date_release(date);
return result;
}
/* crypto.getRandomValues(typedArray) (WebCrypto, also node:crypto's
* export): integer typed arrays only (Node's TypeMismatchError DOMException
* for float arrays, DataViews and every other value), at most 65536 bytes
* (QuotaExceededError), filled in place and returned. Nothing on an
* existing path calls these. They stay at the end of the unit: clang emits
* a static helper (scr_random_write) where its first emitted caller
* appears, and an earlier caller would move it in every binary that links
* the random fills. */
ScrBytes *scr_crypto_get_random_values(ScrBytes *view) {
static const char mismatch[] = "TypeMismatchError";
static const char mismatch_msg[] = "The data argument must be an integer-type TypedArray";
static const char quota[] = "QuotaExceededError";
static const char quota_msg[] = "The requested length exceeds 65,536 bytes";
if (view->is_data_view || view->elem == SCR_BYTES_F32 || view->elem == SCR_BYTES_F64 ||
view->elem == SCR_BYTES_F16) {
scr_throw_domex(mismatch, mismatch_msg);
return NULL;
}
/* Integer arithmetic: a new floating-point literal would land in the
* merged constant pool every binary links. */
size_t length = view->len * (size_t)scr_bytes_elem_size(view->elem);
if (length > 65536u) {
scr_throw_domex(quota, quota_msg);
return NULL;
}
scr_random_write(view, 0, length);
return scr_bytes_retain(view);
}
ScrDyn *scr_crypto_get_random_values_dyn(ScrDyn *value) {
if (value->kind == SCR_DYN_TYPED_REF) {
ScrDyn *materialized = scr_dyn_typed_ref_materialize(value);
if (!materialized) return NULL;
ScrDyn *result = scr_crypto_get_random_values_dyn(materialized);
scr_dyn_release(materialized);
return result ? scr_dyn_retain(value) : NULL;
}
if (value->kind != SCR_DYN_BYTES) {
static const char mismatch[] = "TypeMismatchError";
static const char mismatch_msg[] = "The data argument must be an integer-type TypedArray";
scr_throw_domex(mismatch, mismatch_msg);
return NULL;
}
ScrBytes *filled = scr_crypto_get_random_values(value->v.bytes);
if (!filled) return NULL;
scr_bytes_release(filled);
return scr_dyn_retain(value);
}
+160
View File
@@ -654,3 +654,163 @@ size_t scr_f64_to_fixed(double x, int f, char *buf) {
*o = '\0';
return (size_t)(o - buf);
}
/* ── toPrecision / toExponential(fractionDigits) digit engine ──────────
* The spec's n and e for p significant digits: the integer n with
* 10^(p-1) <= n < 10^p for which n × 10^(e-p+1) - x is closest to zero,
* ties picking the LARGER n (half-up on the exact binary value). Exact
* big-integer arithmetic over the double's mantissa: the value scaled by
* 10^(p-1-e) is mantissa × 2^(binary_exp + f) × 5^f, and
* round_half_up(v) = floor((floor(2v) + 1) / 2) composes from floor
* divisions alone. Values reach ~1100 bits (5e-324 at 100 digits and
* 1.8e308), so the limbs are wider than toFixed's. Nothing on an existing
* path calls this. */
#define SCR_PREC_LIMBS 40
typedef struct {
uint32_t limb[SCR_PREC_LIMBS]; /* little-endian */
int len;
} ScrPrecInt;
static void scr_prec_normalize(ScrPrecInt *v) {
while (v->len > 1 && v->limb[v->len - 1] == 0) v->len--;
}
static void scr_prec_mul_small(ScrPrecInt *v, uint32_t m) {
uint64_t carry = 0;
for (int i = 0; i < v->len; i++) {
uint64_t p = (uint64_t)v->limb[i] * m + carry;
v->limb[i] = (uint32_t)p;
carry = p >> 32;
}
if (carry != 0) v->limb[v->len++] = (uint32_t)carry;
}
static uint32_t scr_prec_div_small(ScrPrecInt *v, uint32_t d) {
uint64_t rem = 0;
for (int i = v->len - 1; i >= 0; i--) {
uint64_t cur = (rem << 32) | v->limb[i];
v->limb[i] = (uint32_t)(cur / d);
rem = cur % d;
}
scr_prec_normalize(v);
return (uint32_t)rem;
}
static void scr_prec_shl(ScrPrecInt *v, int bits) {
uint32_t out[SCR_PREC_LIMBS] = {0};
int words = bits / 32;
int rem = bits % 32;
for (int i = 0; i < v->len; i++) {
int at = i + words;
out[at] |= v->limb[i] << rem;
if (rem != 0 && at + 1 < SCR_PREC_LIMBS) out[at + 1] |= v->limb[i] >> (32 - rem);
}
int n = v->len + words + 1;
if (n > SCR_PREC_LIMBS) n = SCR_PREC_LIMBS;
memcpy(v->limb, out, sizeof out);
v->len = n;
scr_prec_normalize(v);
}
static void scr_prec_shr(ScrPrecInt *v, int bits) {
int words = bits / 32;
int rem = bits % 32;
if (words >= v->len) {
v->limb[0] = 0;
v->len = 1;
return;
}
int n = v->len - words;
for (int i = 0; i < n; i++) {
uint32_t lo = v->limb[i + words] >> rem;
uint32_t hi = rem != 0 && i + words + 1 < v->len ? v->limb[i + words + 1] << (32 - rem) : 0;
v->limb[i] = lo | hi;
}
v->len = n;
scr_prec_normalize(v);
}
/* round_half_up(a × 10^f) for positive finite a, as a decimal digit
* string written right-aligned ending at `end`; returns the first digit. */
static char *scr_prec_scaled_digits(double a, int f, char *end) {
uint64_t bits;
memcpy(&bits, &a, sizeof bits);
uint64_t mantissa = bits & ((UINT64_C(1) << 52) - 1);
int ieee_exp = (int)((bits >> 52) & 0x7ffu);
int binary_exp;
if (ieee_exp == 0) {
binary_exp = -1074;
} else {
mantissa |= UINT64_C(1) << 52;
binary_exp = ieee_exp - 1023 - 52;
}
ScrPrecInt v = {{(uint32_t)mantissa, (uint32_t)(mantissa >> 32)}, 2};
scr_prec_normalize(&v);
for (int i = 0; i < f; i++) scr_prec_mul_small(&v, 5);
int twos = binary_exp + f + 1; /* the +1 doubles: floor(2v) */
if (twos > 0) scr_prec_shl(&v, twos);
for (int i = 0; i < -f; i++) scr_prec_div_small(&v, 5);
if (twos < 0) scr_prec_shr(&v, -twos);
/* (floor(2v) + 1) / 2 */
uint64_t carry = 1;
for (int i = 0; i < v.len && carry != 0; i++) {
uint64_t s = (uint64_t)v.limb[i] + carry;
v.limb[i] = (uint32_t)s;
carry = s >> 32;
}
if (carry != 0) v.limb[v.len++] = (uint32_t)carry;
scr_prec_shr(&v, 1);
char *p = end;
for (;;) {
uint32_t chunk = scr_prec_div_small(&v, 1000000000u);
bool last = v.len == 1 && v.limb[0] == 0;
for (int i = 0; i < 9 && (chunk != 0 || !last || p == end); i++) {
*--p = (char)('0' + chunk % 10);
chunk /= 10;
}
if (last) break;
}
return p;
}
int scr_f64_precision_digits(double a, int p, char *digits, int *e_out) {
char shortest[18];
int n;
scr_f64_digits(a, shortest, &n);
int e = n - 1;
char buf[400];
char *end = buf + sizeof buf;
for (int attempt = 0; attempt < 3; attempt++) {
char *first = scr_prec_scaled_digits(a, p - 1 - e, end);
int len = (int)(end - first);
if (len == p) {
memcpy(digits, first, (size_t)p);
digits[p] = '\0';
*e_out = e;
return p;
}
if (len > p) {
/* Rounding carried into a new digit (or the estimate was low). */
if (len == p + 1 && first[0] == '1') {
bool zeros = true;
for (int i = 1; i < len; i++) zeros = zeros && first[i] == '0';
if (zeros) {
digits[0] = '1';
memset(digits + 1, '0', (size_t)(p - 1));
digits[p] = '\0';
*e_out = e + 1;
return p;
}
}
e++;
} else {
e--;
}
}
/* Unreachable for finite positive doubles: the estimate is off by at
* most one decade. */
memset(digits, '0', (size_t)p);
digits[p] = '\0';
*e_out = e;
return p;
}
+11
View File
@@ -37,6 +37,12 @@ int scr_decimal_scan(const char *p, size_t n, double *out);
* the length. */
size_t scr_f64_to_fixed(double x, int f, char *buf);
/* toPrecision / toExponential(fractionDigits) digits for POSITIVE finite
* a and 1 <= p <= 101: the spec's p-digit n (ties to the larger n on the
* exact binary value) into digits (NUL-terminated, >= p + 1 bytes) and its
* decimal exponent e (value ~ d.ddd x 10^e) into *e_out. Returns p. */
int scr_f64_precision_digits(double a, int p, char *digits, int *e_out);
/* ToString for template literals / string coercion. Returns +1. */
ScrStr *scr_f64_to_scrstr(double x);
ScrStr *scr_bool_to_scrstr(bool b); /* interned "true"/"false" */
@@ -73,6 +79,11 @@ bool scr_num_is_safe_integer(double x);
ScrStr *scr_num_to_exponential(double x);
ScrStr *scr_num_to_fixed0(double x);
ScrStr *scr_num_to_fixed(double x, double fraction_digits);
/* Number.prototype.toPrecision(precision) and toExponential(fractionDigits)
* with the argument already coerced (has_precision false = undefined).
* Invalid digit counts throw V8's RangeError (NULL). Result +1. */
ScrStr *scr_num_to_precision(double x, double precision, bool has_precision);
ScrStr *scr_num_to_exponential_digits(double x, double fraction_digits);
ScrStr *scr_num_to_string_radix_f64(double value, double radix);
/* Object.is over two numbers — the spec's SameValue on doubles: NaN
+175
View File
@@ -1526,3 +1526,178 @@ ScrDyn *scr_node_constants(const ScrDyn *crypto_table) {
scr_atexit(scr_node_constants_clear);
return scr_dyn_retain(o);
}
/* fs.constants as a value: node_constants.cc's fs group (the set
* node:constants merges above), in its order and under its #ifdef guards,
* on its own cached null-prototype object with read-only members. It keeps
* its own table instead of calling the scr_fs_constants fill: a second
* caller would stop that static from inlining into scr_node_constants and
* change every binary that links node:constants. The names live inside
* the table entries, so the table is one data object section GC drops.
* Nothing on an existing path calls this. */
typedef struct {
char name[32];
double value;
} ScrFsConstant;
#define SCR_FS_C(n) {#n, (double)(n)}
static const ScrFsConstant scr_fs_constant_table[] = {
{"UV_FS_SYMLINK_DIR", 1},
{"UV_FS_SYMLINK_JUNCTION", 2},
#ifdef O_RDONLY
SCR_FS_C(O_RDONLY),
#endif
#ifdef O_WRONLY
SCR_FS_C(O_WRONLY),
#endif
#ifdef O_RDWR
SCR_FS_C(O_RDWR),
#endif
{"UV_DIRENT_UNKNOWN", 0},
{"UV_DIRENT_FILE", 1},
{"UV_DIRENT_DIR", 2},
{"UV_DIRENT_LINK", 3},
{"UV_DIRENT_FIFO", 4},
{"UV_DIRENT_SOCKET", 5},
{"UV_DIRENT_CHAR", 6},
{"UV_DIRENT_BLOCK", 7},
#ifdef S_IFMT
SCR_FS_C(S_IFMT),
#endif
#ifdef S_IFREG
SCR_FS_C(S_IFREG),
#endif
#ifdef S_IFDIR
SCR_FS_C(S_IFDIR),
#endif
#ifdef S_IFCHR
SCR_FS_C(S_IFCHR),
#endif
#ifdef S_IFBLK
SCR_FS_C(S_IFBLK),
#endif
#ifdef S_IFIFO
SCR_FS_C(S_IFIFO),
#endif
#ifdef S_IFLNK
SCR_FS_C(S_IFLNK),
#endif
#ifdef S_IFSOCK
SCR_FS_C(S_IFSOCK),
#endif
#ifdef O_CREAT
SCR_FS_C(O_CREAT),
#endif
#ifdef O_EXCL
SCR_FS_C(O_EXCL),
#endif
{"UV_FS_O_FILEMAP", 0},
{"UV_FS_O_TEMPORARY", 0},
{"UV_FS_O_SHORT_LIVED", 0},
{"UV_FS_O_SEQUENTIAL", 0},
{"UV_FS_O_RANDOM", 0},
#ifdef O_NOCTTY
SCR_FS_C(O_NOCTTY),
#endif
#ifdef O_TRUNC
SCR_FS_C(O_TRUNC),
#endif
#ifdef O_APPEND
SCR_FS_C(O_APPEND),
#endif
#ifdef O_DIRECTORY
SCR_FS_C(O_DIRECTORY),
#endif
#ifdef O_NOATIME
SCR_FS_C(O_NOATIME),
#endif
#ifdef O_NOFOLLOW
SCR_FS_C(O_NOFOLLOW),
#endif
#ifdef O_SYNC
SCR_FS_C(O_SYNC),
#endif
#ifdef O_DSYNC
SCR_FS_C(O_DSYNC),
#endif
#ifdef O_SYMLINK
SCR_FS_C(O_SYMLINK),
#endif
#ifdef O_DIRECT
SCR_FS_C(O_DIRECT),
#endif
#ifdef O_NONBLOCK
SCR_FS_C(O_NONBLOCK),
#endif
#ifdef S_IRWXU
SCR_FS_C(S_IRWXU),
#endif
#ifdef S_IRUSR
SCR_FS_C(S_IRUSR),
#endif
#ifdef S_IWUSR
SCR_FS_C(S_IWUSR),
#endif
#ifdef S_IXUSR
SCR_FS_C(S_IXUSR),
#endif
#ifdef S_IRWXG
SCR_FS_C(S_IRWXG),
#endif
#ifdef S_IRGRP
SCR_FS_C(S_IRGRP),
#endif
#ifdef S_IWGRP
SCR_FS_C(S_IWGRP),
#endif
#ifdef S_IXGRP
SCR_FS_C(S_IXGRP),
#endif
#ifdef S_IRWXO
SCR_FS_C(S_IRWXO),
#endif
#ifdef S_IROTH
SCR_FS_C(S_IROTH),
#endif
#ifdef S_IWOTH
SCR_FS_C(S_IWOTH),
#endif
#ifdef S_IXOTH
SCR_FS_C(S_IXOTH),
#endif
#ifdef F_OK
SCR_FS_C(F_OK),
#endif
#ifdef R_OK
SCR_FS_C(R_OK),
#endif
#ifdef W_OK
SCR_FS_C(W_OK),
#endif
#ifdef X_OK
SCR_FS_C(X_OK),
#endif
{"UV_FS_COPYFILE_EXCL", 1},
{"COPYFILE_EXCL", 1},
{"UV_FS_COPYFILE_FICLONE", 2},
{"COPYFILE_FICLONE", 2},
{"UV_FS_COPYFILE_FICLONE_FORCE", 4},
{"COPYFILE_FICLONE_FORCE", 4},
};
#undef SCR_FS_C
static SCR_TL ScrDyn *scr_fs_constants_value;
static void scr_fs_constants_clear(void) {
ScrDyn *value = scr_fs_constants_value;
scr_fs_constants_value = NULL;
scr_dyn_release(value);
}
ScrDyn *scr_fs_constants_object(void) {
if (scr_fs_constants_value) return scr_dyn_retain(scr_fs_constants_value);
ScrDyn *o = scr_dyn_new_obj_null_proto();
for (size_t i = 0; i < sizeof scr_fs_constant_table / sizeof scr_fs_constant_table[0]; i++)
scr_os_constant(o, scr_fs_constant_table[i].name, scr_fs_constant_table[i].value);
scr_fs_constants_value = o;
scr_atexit(scr_fs_constants_clear);
return scr_dyn_retain(o);
}
+2
View File
@@ -38,6 +38,8 @@ ScrStr *scr_os_cpus_model(double i);
double scr_os_cpus_field(double i, double field);
/* os.constants: the per-thread null-prototype constants object (+1). */
ScrDyn *scr_os_constants(void);
/* fs.constants: the target's null-prototype constants object (cached, +1). */
ScrDyn *scr_fs_constants_object(void);
/* The deprecated node:constants module object (scr_os.c): the os groups,
* fs.constants and the crypto table the borrowed function answers, merged
* and frozen (+1). */
+216
View File
@@ -2020,6 +2020,222 @@ static ScrStr *scr_str_case_conv(const ScrStr *s, int to_lower) {
ScrStr *scr_str_to_lower(const ScrStr *s) { return scr_str_case_conv(s, 1); }
ScrStr *scr_str_to_upper(const ScrStr *s) { return scr_str_case_conv(s, 0); }
/* ── String.prototype.toLocaleLowerCase / toLocaleUpperCase ────────────
* V8 decides the case mapping from the FIRST requested locale only
* (probed: ["en", 5] maps like "en", [5] throws, a non-object primitive
* requests nothing). ICU tailors case mapping for Turkic (tr, az: dotted
* and dotless I), Lithuanian (dot above retention) and Greek uppercasing
* (accent removal); every other language uses the default conversion.
* The Turkic rules are SpecialCasing's: lowercase I → ı unless a
* U+0307 follows (then I + U+0307 → i), İ → i; uppercase i → İ. Tags are
* checked against the unicode_locale_id structure; anything this unit
* cannot decide exactly (malformed tags, whose V8 RangeError texts vary,
* objects, lt, el uppercasing, I before other combining marks) throws a
* 'not supported yet' Error rather than guess. Nothing on an existing
* path calls this. */
static bool scr_tag_alpha(const char *p, size_t n) {
for (size_t i = 0; i < n; i++)
if (!((p[i] >= 'a' && p[i] <= 'z') || (p[i] >= 'A' && p[i] <= 'Z'))) return false;
return true;
}
static bool scr_tag_digit(const char *p, size_t n) {
for (size_t i = 0; i < n; i++)
if (!(p[i] >= '0' && p[i] <= '9')) return false;
return true;
}
static bool scr_tag_alnum(const char *p, size_t n) {
for (size_t i = 0; i < n; i++)
if (!scr_tag_alpha(p + i, 1) && !scr_tag_digit(p + i, 1)) return false;
return true;
}
static bool scr_tag_same(const char *a, size_t an, const char *b, size_t bn) {
if (an != bn) return false;
for (size_t i = 0; i < an; i++) {
char x = a[i] >= 'A' && a[i] <= 'Z' ? (char)(a[i] + 32) : a[i];
char y = b[i] >= 'A' && b[i] <= 'Z' ? (char)(b[i] + 32) : b[i];
if (x != y) return false;
}
return true;
}
/* 0 default, 1 Turkic, 2 Lithuanian, 3 Greek; -1 when the tag is outside
* the structural grammar handled here. */
static int scr_locale_case_rules(const char *tag, size_t len) {
const char *subtags[64];
size_t lens[64];
size_t count = 0;
size_t start = 0;
for (size_t i = 0; i <= len; i++) {
if (i < len && tag[i] != '-') continue;
if (i == start || count == 64) return -1;
subtags[count] = tag + start;
lens[count++] = i - start;
start = i + 1;
}
size_t k = 0;
size_t lang = lens[0];
if (!scr_tag_alpha(subtags[0], lang) || !((lang >= 2 && lang <= 3) || (lang >= 5 && lang <= 8)))
return -1;
k = 1;
if (k < count && lens[k] == 4 && scr_tag_alpha(subtags[k], 4)) k++;
if (k < count && ((lens[k] == 2 && scr_tag_alpha(subtags[k], 2)) ||
(lens[k] == 3 && scr_tag_digit(subtags[k], 3))))
k++;
size_t variants = k;
while (k < count && scr_tag_alnum(subtags[k], lens[k]) &&
((lens[k] >= 5 && lens[k] <= 8) || (lens[k] == 4 && scr_tag_digit(subtags[k], 1)))) {
for (size_t j = variants; j < k; j++)
if (scr_tag_same(subtags[j], lens[j], subtags[k], lens[k])) return -1;
k++;
}
size_t singletons = k;
while (k < count) {
if (lens[k] != 1 || !scr_tag_alnum(subtags[k], 1)) return -1;
for (size_t j = singletons; j < k; j++)
if (lens[j] == 1 && scr_tag_same(subtags[j], 1, subtags[k], 1)) return -1;
bool private_use = subtags[k][0] == 'x' || subtags[k][0] == 'X';
size_t min = private_use ? 1 : 2;
size_t first = ++k;
while (k < count && lens[k] >= min && lens[k] <= 8 && scr_tag_alnum(subtags[k], lens[k]) &&
(private_use || lens[k] > 1))
k++;
if (k == first) return -1;
if (private_use && k != count) return -1;
}
if (lang > 3) return 0;
char l[3] = {0, 0, 0};
for (size_t i = 0; i < lang; i++)
l[i] = subtags[0][i] >= 'A' && subtags[0][i] <= 'Z' ? (char)(subtags[0][i] + 32) : subtags[0][i];
bool two = lang == 2;
if ((two && l[0] == 't' && l[1] == 'r') || (two && l[0] == 'a' && l[1] == 'z') ||
(!two && l[0] == 't' && l[1] == 'u' && l[2] == 'r') ||
(!two && l[0] == 'a' && l[1] == 'z' && l[2] == 'e'))
return 1;
if ((two && l[0] == 'l' && l[1] == 't') || (!two && l[0] == 'l' && l[1] == 'i' && l[2] == 't'))
return 2;
if ((two && l[0] == 'e' && l[1] == 'l') || (!two && l[0] == 'e' && l[1] == 'l' && l[2] == 'l') ||
(!two && l[0] == 'g' && l[1] == 'r' && l[2] == 'e'))
return 3;
return 0;
}
static ScrStr *scr_locale_case_unsupported(bool upper) {
static const char lower_msg[] =
"String.prototype.toLocaleLowerCase with this locale argument is not supported yet";
static const char upper_msg[] =
"String.prototype.toLocaleUpperCase with this locale argument is not supported yet";
/* The unit's existing "SC2020" literal: a separate constant array would
* absorb it (LLVM merges identical constants) and move it out of the
* string section every regex-linking binary keeps. */
if (upper) scr_throw_error_msg_code(SCR_ERR_ERROR, upper_msg, sizeof upper_msg - 1, "SC2020");
else scr_throw_error_msg_code(SCR_ERR_ERROR, lower_msg, sizeof lower_msg - 1, "SC2020");
return NULL;
}
static ScrStr *scr_turkic_case(const ScrStr *s, bool upper) {
ScrJsonBuf b;
scr_jb_init(&b);
for (size_t i = 0; i < s->len; i++) {
unsigned char c = (unsigned char)s->data[i];
if (upper && c == 'i') {
scr_jb_putc(&b, (char)0xC4); /* İ */
scr_jb_putc(&b, (char)0xB0);
continue;
}
if (!upper && c == 'I') {
unsigned char n0 = i + 1 < s->len ? (unsigned char)s->data[i + 1] : 0;
unsigned char n1 = i + 2 < s->len ? (unsigned char)s->data[i + 2] : 0;
if (n0 == 0xCC && n1 == 0x87) { /* I + U+0307 → i */
scr_jb_putc(&b, 'i');
i += 2;
continue;
}
/* Another combining mark (U+0300..U+036F) may precede a U+0307. */
if ((n0 == 0xCC && n1 >= 0x80) || (n0 == 0xCD && n1 >= 0x80 && n1 <= 0xAF)) {
scr_str_release(scr_jb_finish(&b));
return scr_locale_case_unsupported(upper);
}
scr_jb_putc(&b, (char)0xC4); /* ı */
scr_jb_putc(&b, (char)0xB1);
continue;
}
if (!upper && c == 0xC4 && i + 1 < s->len && (unsigned char)s->data[i + 1] == 0xB0) {
scr_jb_putc(&b, 'i'); /* İ → i */
i++;
continue;
}
scr_jb_putc(&b, (char)c);
}
ScrStr *mapped = scr_jb_finish(&b);
ScrStr *result = scr_str_case_conv(mapped, upper ? 0 : 1);
scr_str_release(mapped);
return result;
}
ScrStr *scr_str_locale_case(const ScrStr *s, ScrDyn *locales, bool upper) {
ScrDyn *owned = NULL;
if (locales->kind == SCR_DYN_TYPED_REF) {
owned = scr_dyn_typed_ref_materialize(locales);
if (!owned) return NULL;
locales = owned;
}
const ScrDyn *tag = NULL;
int rules = 0;
switch (locales->kind) {
case SCR_DYN_NULL: {
static const char msg[] = "Cannot convert undefined or null to object";
scr_throw_error_msg(SCR_ERR_TYPE, msg, sizeof msg - 1);
return NULL;
}
case SCR_DYN_UNDEF:
case SCR_DYN_NUM:
case SCR_DYN_BOOL:
case SCR_DYN_SYMBOL:
case SCR_DYN_BIGINT:
break;
case SCR_DYN_STR:
tag = locales;
break;
case SCR_DYN_ARR:
if (locales->v.arr.len == 0) break;
if (locales->v.arr.presence && !locales->v.arr.presence[0]) {
static const char msg[] = "Language ID should be string or object.";
scr_throw_error_msg(SCR_ERR_TYPE, msg, sizeof msg - 1);
scr_dyn_release(owned);
return NULL;
}
tag = locales->v.arr.items[0];
if (tag->kind != SCR_DYN_STR) {
bool primitive = tag->kind == SCR_DYN_UNDEF || tag->kind == SCR_DYN_NULL ||
tag->kind == SCR_DYN_NUM || tag->kind == SCR_DYN_BOOL ||
tag->kind == SCR_DYN_BIGINT || tag->kind == SCR_DYN_SYMBOL;
scr_dyn_release(owned);
if (!primitive) return scr_locale_case_unsupported(upper);
static const char msg[] = "Language ID should be string or object.";
scr_throw_error_msg(SCR_ERR_TYPE, msg, sizeof msg - 1);
return NULL;
}
break;
default:
scr_dyn_release(owned);
return scr_locale_case_unsupported(upper);
}
if (tag) {
rules = scr_locale_case_rules(tag->v.str->data, tag->v.str->len);
if (rules < 0 || rules == 2 || (rules == 3 && upper)) {
scr_dyn_release(owned);
return scr_locale_case_unsupported(upper);
}
}
scr_dyn_release(owned);
if (rules == 1) return scr_turkic_case(s, upper);
return scr_str_case_conv(s, upper ? 0 : 1);
}
static void *scr_normalize_realloc(void *opaque, void *ptr, size_t size) {
(void)opaque;
if (!size) { free(ptr); return NULL; }
+11
View File
@@ -73,6 +73,10 @@ bool scr_str_ends_with_from(ScrStr *s, ScrStr *needle, double end_position);
ScrStr *scr_str_slice(ScrStr *s, double start, double end);
/* substring: clamp-and-swap (negatives clamp to 0, start > end swaps). */
ScrStr *scr_str_substring(ScrStr *s, double start, double end);
/* Annex B substr(start, length): start already ToNumber'd (NaN → 0), a
* negative start counts from the end, length absent (has_length false)
* runs to the end. Returns +1. */
ScrStr *scr_str_substr(ScrStr *s, double start, double length, bool has_length);
/* Scalar-only local slices retain their original owner without copying.
* Bounds and split-surrogate replacements are snapshotted at construction. */
@@ -111,6 +115,13 @@ ScrStr *scr_str_trim(ScrStr *s);
ScrStr *scr_str_to_lower(const ScrStr *s);
ScrStr *scr_str_to_upper(const ScrStr *s);
ScrStr *scr_str_normalize(const ScrStr *s, const ScrStr *form);
/* toLocaleLowerCase/toLocaleUpperCase(locales) (scr_regex.c): undefined,
* empty lists, and primitives other than strings use the default case
* conversion; the first requested tag selects the Turkic (tr, az) rules or
* the default ones. Lithuanian, Greek uppercasing and tags outside the
* structural grammar throw a 'not supported yet' Error; null throws Node's
* TypeError. Borrows both; +1 or NULL with an exception pending. */
ScrStr *scr_str_locale_case(const ScrStr *s, struct ScrDyn *locales, bool upper);
/* NFC over a code point buffer (scr_regex.c, beside normalize): a fresh
* malloc'd buffer the caller frees, its length in *out_len. Borrows input. */
uint32_t *scr_nfc_code_points(const uint32_t *input, size_t length, size_t *out_len);
@@ -1,7 +1,5 @@
// Explicit toFixed is static; toPrecision/toString(radix) execute in the
// island. The receiver marshals by value there and the engine auto-boxes
// it, so `this` binds the number exactly as in JS. All stay byte-exact
// differential.
// Number.prototype.toFixed, toPrecision and toString(radix) on typed
// numbers all compile statically and stay byte-exact differential.
const n = 1234.5678;
console.log(n.toFixed(0), n.toFixed(2), n.toFixed(6));
console.log((0.1 + 0.2).toFixed(17), (-3.7).toFixed(1), (0).toFixed(2));
@@ -0,0 +1,48 @@
// A record that converts into several arms of a discriminated union takes
// the arm its discriminant names. The `map` callback below builds statement
// records whose `value` is an expression literal; that literal also fits the
// shorter `ref` expression, and the statement also fits `brk` (by dropping
// fields), but the `kind` values pick `get` and `set`, as in Node.
type Expr =
| { kind: "get"; obj: Expr | null; field: string }
| { kind: "ref"; obj: Expr | null }
| { kind: "num"; n: number };
type Stmt =
| { kind: "set"; obj: Expr; field: string; value: Expr }
| { kind: "brk"; label?: string };
function fill(target: Expr, names: string[]): Stmt[] {
return names.map((name) => ({
kind: "set",
obj: target,
field: name,
value: { kind: "get", obj: target, field: name },
}));
}
function describe(s: Stmt): string {
switch (s.kind) {
case "set":
return `set ${s.field} = ${s.value.kind}${s.value.kind === "get" ? `.${s.value.field}` : ""} on ${s.obj.kind}`;
case "brk":
return `brk ${s.label ?? "-"}`;
}
}
for (const s of fill({ kind: "num", n: 1 }, ["a", "b"])) console.log(describe(s));
// The same records with the other discriminant value take the other arm.
function pick(kind: "set" | "brk", label: string): Stmt[] {
return [label].map((l) =>
kind === "set"
? { kind: "set" as const, obj: { kind: "num" as const, n: 2 }, field: l, value: { kind: "ref" as const, obj: null } }
: { kind: "brk" as const, label: l },
);
}
console.log(pick("set", "x").map(describe).join("; "));
console.log(pick("brk", "y").map(describe).join("; "));
// A record that fits one arm only, by dropping a field, still takes it.
const extra = { kind: "brk" as const, label: "z", note: 1 };
const stmts: Stmt[] = [extra];
console.log(stmts.map(describe).join("; "));
+19
View File
@@ -0,0 +1,19 @@
// take(n) followed by flatMap counts n source values: the last value
// taken still flattens in full, and nothing is pulled after it.
const log: string[] = [];
function* gen(): Generator<number> { for (let i = 0; i < 4; i++) { log.push("y" + i); yield i; } }
const flush = (): string => log.splice(0).join(" ");
console.log(gen().take(2).flatMap((x) => [x, x + 10]).toArray(), flush());
console.log([0, 1, 2, 3].values().take(2).flatMap((x) => [x, x + 10]).toArray());
console.log(gen().take(3).flatMap((x) => new Set([x, -x])).toArray(), flush());
console.log(gen().take(2).flatMap((x) => [x, x + 10]).take(3).toArray(), flush());
console.log(gen().take(2).flatMap((x) => [x, x + 10]).find((v) => v === 10), flush());
console.log(gen().take(2).flatMap((x) => [x, x + 10]).some((v) => v > 10), flush());
console.log(gen().filter((x) => x > 0).take(2).flatMap((x) => [x, x]).map((v) => v * 2).toArray(), flush());
const out: number[] = [];
for (const v of gen().take(2).flatMap((x) => [x, x + 10])) out.push(v);
console.log(out, flush());
const h = gen().take(1).flatMap((x) => [x, x + 1, x + 2]);
console.log(h.next(), h.next(), h.next(), h.next(), flush());
console.log(gen().take(0).flatMap((x) => [x]).toArray(), flush());
console.log(gen().take(2).flatMap((x) => [x]).flatMap((x) => [x, -x]).toArray(), flush());
@@ -0,0 +1,33 @@
// String.prototype.substr, the locale case pair and the Annex B HTML
// methods on typed strings. substr counts a negative start from the end
// and clamps its length; toLocaleLowerCase/toLocaleUpperCase use the
// default case conversion except for the Turkic languages, decided by the
// first requested locale only; the HTML methods escape '"' in attribute
// values.
const s = "Hello, World";
console.log(s.substr(0), s.substr(7), s.substr(-5), s.substr(-50, 3), s.substr(3, 4));
console.log(JSON.stringify([s.substr(20), s.substr(2, -1), s.substr(NaN, 2), s.substr(1, Infinity)]));
console.log(JSON.stringify([s.substr(-Infinity, 2), s.substr(1.9, 2.9), "😀ab".substr(1, 2)]));
function tail(from: number, length?: number): string {
return s.substr(from, length);
}
console.log(tail(2), tail(2, 3), tail(-3, undefined));
function lower(text: string, locale?: string | string[]): string {
return text.toLocaleLowerCase(locale);
}
function upper(text: string, locale?: string | string[]): string {
return text.toLocaleUpperCase(locale);
}
const sample = "IİiıÍ ΣΑΣ Straße";
for (const locale of [undefined, "en", "en-US", "de-DE", "TR", "tr-TR", "az", "tur", "zh-Hant-TW", "und", "en-u-co-phonebk", "en-x-tr"]) {
console.log(String(locale), lower(sample, locale), upper(sample, locale));
}
console.log(lower(sample, ["tr", "en"]), lower(sample, ["en", "tr"]), lower(sample, []));
console.log(upper("istanbul", ["az"]), lower("DİYARBAKIR"), lower("İx", "tr").length);
console.log("Dž".toLocaleUpperCase(), "ß".toLocaleUpperCase("de"), "ffi".toLocaleUpperCase("en"));
console.log(s.anchor('a"b'), s.big(), s.blink(), s.bold(), s.fixed());
console.log(s.fontcolor("red"), s.fontsize(7), s.italics(), s.link("https://x/?q=\"1\""));
console.log(s.small(), s.strike(), s.sub(), s.sup(), "".anchor(""));
@@ -0,0 +1,34 @@
// Number.prototype.toPrecision and toExponential(fractionDigits): the
// exact binary value rounds half up at the requested digit, exponential
// form takes over when the exponent is below -6 or reaches the precision,
// and the non-finite answers come before the digit-range RangeError.
const values = [0, -0, 1, -1, 2.5, -2.5, 0.5, 1.005, 1.45, 123.456, 0.000001234, 0.0000001234, 1e21, 1e-7, 5e-324, 1.7976931348623157e308, 123456789012345680000, 0.1, 99.99, 9.995, 1e23, -1234.5678];
for (const v of values) {
const p = [1, 2, 3, 5, 10, 21].map((d) => v.toPrecision(d));
const e = [0, 1, 2, 6, 20].map((d) => v.toExponential(d));
console.log(String(v), p.join(" "), e.join(" "));
}
console.log((123.456).toPrecision(), (123.456).toPrecision(undefined), (1 / 3).toPrecision(100));
console.log((1 / 3).toExponential(100), (2 ** 70).toPrecision(25), (2 ** -30).toExponential(30));
console.log(NaN.toPrecision(200), Infinity.toPrecision(0), (-Infinity).toExponential(-1), NaN.toExponential(101));
console.log((15).toPrecision(1.9), (15).toExponential(1.9), (0.00001).toPrecision(1), (1e-6).toPrecision(2));
for (const bad of [0, 101, -1, Infinity]) {
try {
console.log((1).toPrecision(bad));
} catch (e) {
console.log((e as Error).name, (e as Error).message);
}
}
for (const bad of [-1, 101, Infinity]) {
try {
console.log((1).toExponential(bad));
} catch (e) {
console.log((e as Error).name, (e as Error).message);
}
}
let n = 0;
for (let i = 1; i < 2000; i++) {
const x = i / 7 + i * 1e-9;
if (x.toPrecision(7) !== Number(x.toPrecision(7)).toPrecision(7)) n++;
}
console.log("stable", n);
@@ -0,0 +1,92 @@
// String and Number methods on JS-inferred receivers: string receivers run
// the real method (string-width's codePointAt, camelcase's
// toLocaleLowerCase, toml's substr, pretty-bytes' toPrecision); other
// receivers keep JavaScript's answer for the name, an object's own method
// or Node's TypeError; arrays keep the runtime dispatch of shared names.
function width(text) {
let total = 0;
for (const character of text) {
const code = character.codePointAt(0);
total += code >= 0x1100 ? 2 : 1;
}
return total;
}
console.log(width("abc"), width("古"), width("👍x"));
function describe(input) {
return [
input.codePointAt(0),
input.codePointAt(-1),
input.codePointAt("1"),
input.substr(1),
input.substr(-2, 1),
input.toLocaleLowerCase(),
input.toLocaleUpperCase("en-US"),
input.toLocaleLowerCase("tr"),
input.trimLeft(),
input.trimRight(),
input.isWellFormed(),
input.toWellFormed().length,
];
}
console.log(JSON.stringify(describe(" Iİ😀 ")));
console.log(JSON.stringify(describe("\ud800Ab")));
const parsed = JSON.parse('[" I\\u0130\\ud83d\\ude00 ", 1337, 0.000123456, [1, 2, 3]]');
console.log(JSON.stringify(describe(parsed[0])));
function number(value, digits) {
return [value.toPrecision(digits), value.toExponential(digits), value.toPrecision()];
}
console.log(JSON.stringify(number(1337, 3)), JSON.stringify(number(0.000123456, 2)));
console.log(JSON.stringify(number(-1.5, "4")), JSON.stringify(number(2.5, undefined)));
console.log(JSON.stringify(number(parsed[1], 3)), JSON.stringify(number(parsed[2], parsed[1] - 1335)));
function prettyBytes(n) {
const units = ["B", "kB", "MB", "GB"];
const exponent = Math.min(Math.floor(Math.log10(n) / 3), units.length - 1);
return Number((n / 1000 ** exponent).toPrecision(3)) + " " + units[exponent];
}
console.log(prettyBytes(1337), prettyBytes(100), prettyBytes(12345678));
const own = { codePointAt(i) { return "own " + i; }, substr() { return "own substr"; }, toPrecision(d) { return "own " + d; } };
console.log(describeOwn(own));
function describeOwn(value) {
return [value.codePointAt(3), value.substr(), value.toPrecision(2)].join("|");
}
function arrays(list) {
return JSON.stringify([list.at(-1), list.concat([4], 5)]);
}
console.log(arrays([1, 2, 3]), arrays(parsed[3]));
function attempt(f) {
try {
return String(f());
} catch (e) {
return e.name + ": " + e.message;
}
}
function fail(value) {
return [
attempt(() => value.codePointAt(0)),
attempt(() => value.substr(0)),
attempt(() => value.toPrecision(2)),
attempt(() => value.toLocaleLowerCase()),
].join(" / ");
}
for (const value of [42, "str", undefined, null, {}, parsed[3]]) console.log(fail(value));
for (const value of JSON.parse('[42, "str", null, {}, true]')) console.log(fail(value));
console.log(attempt(() => number(1, 200)));
console.log(attempt(() => "x".toLocaleUpperCase(null)));
// A helper whose parameter the checker leaves untyped answers `string |
// undefined` natively; the guarded receiver still runs String methods.
function afterX(segment) {
const index = segment.indexOf("x");
return index === -1 ? undefined : segment.slice(index + 1);
}
function tailCode(text) {
const rest = afterX(text);
if (rest === undefined) return -1;
return [rest.codePointAt(0), rest.substr(1), rest.toUpperCase(), rest.slice(1)].join(",");
}
console.log(tailCode("axé😀"), tailCode("abc"));
@@ -0,0 +1,15 @@
// String and Number methods through 'any'-typed values: string and number
// receivers run the real methods, other values keep JavaScript's answer.
const values: any[] = JSON.parse('["Straße İx", 1234.5678, {"substr": 1}, [1, 2]]');
const [text, num, obj, list] = values;
console.log(text.codePointAt(7), text.substr(-2), text.toLocaleUpperCase(), text.toLocaleLowerCase("az"));
console.log(text.trimRight(), text.isWellFormed(), text.toLocaleLowerCase(["TR"]));
console.log(num.toPrecision(6), num.toExponential(3), num.toPrecision(), num.toExponential());
console.log(list.at(-1), JSON.stringify(list.concat([3])));
for (const call of [() => obj.substr(0), () => num.codePointAt(0), () => text.toPrecision(2)]) {
try {
console.log(call());
} catch (e) {
console.log((e as Error).name + ": " + (e as Error).message);
}
}
@@ -0,0 +1,36 @@
// crypto.getRandomValues through node:crypto's export and webcrypto:
// integer typed arrays fill in place and come back as the same object;
// views over 65536 bytes throw QuotaExceededError. (The global spelling and
// the float/DataView TypeMismatchError are 6205's.)
import { getRandomValues, webcrypto } from "node:crypto";
import * as nodeCrypto from "node:crypto";
const bytes = new Uint8Array(4096);
const same = webcrypto.getRandomValues(bytes);
function nonzero(view: Uint8Array): boolean {
for (let i = 0; i < view.length; i++) if (view[i] !== 0) return true;
return false;
}
function nonzeroWords(view: Uint32Array): boolean {
for (let i = 0; i < view.length; i++) if (view[i] !== 0) return true;
return false;
}
console.log(same === bytes, bytes.length, nonzero(bytes));
const words = new Uint32Array(256);
console.log(getRandomValues(words) === words, nonzeroWords(words));
const shorts = new Int16Array(8);
console.log(webcrypto.getRandomValues(shorts) === shorts, nodeCrypto.getRandomValues(new Uint8ClampedArray(3)).length);
console.log(nodeCrypto.webcrypto.getRandomValues(new Int8Array(65536)).length, getRandomValues(new Uint8Array(0)).length);
const buf = Buffer.alloc(16);
console.log(Buffer.isBuffer(getRandomValues(buf)), buf.subarray(4, 8).length);
function attempt(label: string, f: () => unknown): void {
try {
f();
console.log(label, "ok");
} catch (e) {
console.log(label, (e as DOMException).name, (e as DOMException).code, (e as Error).message, e instanceof DOMException);
}
}
attempt("big", () => getRandomValues(new Uint8Array(65537)));
attempt("big32", () => webcrypto.getRandomValues(new Uint32Array(16385)));
@@ -0,0 +1,38 @@
// nanoid's fillRandom: crypto.getRandomValues over Buffer windows of an
// untyped helper, plus non-view values that throw TypeMismatchError.
import { getRandomValues } from "node:crypto";
const GET_RANDOM_LIMIT = 65536;
function fillRandom(buffer) {
let from = 0;
while (from < buffer.length) {
const to = Math.min(from + GET_RANDOM_LIMIT, buffer.length);
crypto.getRandomValues(buffer.subarray(from, to));
from = to;
}
}
const alphabet = "useandom-26T198340PX75pxJACKVERYMINDBUSHWOLF_GQZbfghjklqvwyzrict";
function nanoid(size = 21) {
const bytes = Buffer.allocUnsafe(size);
fillRandom(bytes);
let id = "";
for (let i = 0; i < size; i++) id += alphabet[bytes[i] & 63];
return id;
}
const id = nanoid();
console.log(id.length, /^[A-Za-z0-9_-]+$/.test(id), nanoid(10).length, nanoid(70000).length);
const filled = globalThis.crypto.getRandomValues(new Uint16Array(3));
console.log(filled.length);
for (const value of [new Float64Array(1), new Float32Array(2), new DataView(new ArrayBuffer(2)), new Uint8Array(65537), [1, 2], "x", 5, {}]) {
try {
crypto.getRandomValues(value);
console.log("ok");
} catch (e) {
console.log(e.name, e.message);
}
}
const untyped = JSON.parse("[1]");
try {
getRandomValues(untyped);
} catch (e) {
console.log(e.name, e.code, e.message);
}
@@ -0,0 +1,16 @@
// Buffer.isBuffer over Uint8Array-typed values reads the view's Buffer
// brand: Buffer factories, Buffer subarrays and slices are Buffers, plain
// typed arrays and copies into them are not.
function check(view: Uint8Array): boolean {
return Buffer.isBuffer(view);
}
const plain = new Uint8Array([1, 2, 3]);
const buffer = Buffer.from(plain);
console.log(check(plain), check(buffer), check(Buffer.alloc(2)), check(Buffer.from("hey").subarray(1)));
console.log(check(plain.subarray(1)), check(new Uint8Array(buffer)), check(buffer.slice(1)), check(Buffer.concat([plain])));
const views: Uint8Array[] = [plain, buffer, new TextEncoder().encode("x"), Buffer.allocUnsafe(1)];
console.log(views.map((v) => Buffer.isBuffer(v)).join(","));
function words(view: Uint32Array): boolean {
return Buffer.isBuffer(view);
}
console.log(words(new Uint32Array(2)), Buffer.from(plain).toString("hex"), Buffer.from(views[2]!).toString());
+19
View File
@@ -0,0 +1,19 @@
// fs.constants as a value and every member: the target platform's flags,
// file-type bits and permission bits, in Node's order, on a null-prototype
// object whose members are read-only. The access-mode bits keep their
// baked literals.
import * as fs from "node:fs";
import { constants } from "node:fs";
// libuv's Windows-only open flags (0 elsewhere) joined the object in later
// Node 24 releases; the rest is the same in every one.
const windowsOnly = ["UV_FS_O_TEMPORARY", "UV_FS_O_SHORT_LIVED", "UV_FS_O_SEQUENTIAL", "UV_FS_O_RANDOM"];
console.log(JSON.stringify(Object.entries(fs.constants).filter(([k]) => !windowsOnly.includes(k))));
console.log(fs.constants.O_RDONLY, fs.constants.O_WRONLY | fs.constants.O_CREAT | fs.constants.O_TRUNC, constants.S_IFMT, constants.S_IFDIR);
console.log(constants.W_OK, fs.constants.F_OK, constants.COPYFILE_EXCL, fs.constants.UV_FS_O_FILEMAP);
const c: unknown = fs.constants;
console.log(typeof c, c === (fs.constants as unknown), Object.getPrototypeOf(c as object) === null, Object.keys(fs.constants).length > 40);
const mode = fs.statSync(".").mode;
console.log((mode & constants.S_IFMT) === constants.S_IFDIR, (mode & fs.constants.S_IRUSR) !== 0);
const descriptor = Object.getOwnPropertyDescriptor(c as object, "O_RDONLY");
console.log(JSON.stringify(descriptor));
@@ -0,0 +1,8 @@
// graceful-fs's reads of require('fs').constants: own-property probes for
// platform-specific flags and member reads through a stored binding.
var fs = require("fs");
var constants = fs.constants;
console.log(typeof constants.O_RDONLY, constants.O_RDWR, Object.prototype.hasOwnProperty.call(constants, "O_SYMLINK") === ("O_SYMLINK" in constants), typeof constants.hasOwnProperty);
console.log(Object.prototype.hasOwnProperty.call(constants, "S_IFREG"), constants.O_EXCL > 0, typeof require("fs").constants.S_IFLNK);
var names = Object.keys(constants).filter(function (k) { return k.indexOf("UV_DIRENT_") === 0; });
console.log(names.join(","), constants.NOT_A_CONSTANT);
+25
View File
@@ -0,0 +1,25 @@
// The buffer module's Buffer through CommonJS bindings — `var Buffer =
// require('buffer').Buffer` and a member of a stored require — is the
// global Buffer constructor, with its statics.
var Buffer = require("buffer").Buffer;
var buffer = require("buffer");
var B2 = buffer.Buffer;
const B3 = require("node:buffer").Buffer;
console.log(Buffer.from("hi").toString("hex"), Buffer.isBuffer(Buffer.alloc(1)), B2.byteLength("héllo"), B2.isBuffer("x"));
console.log(B3.concat([B2.from("a"), Buffer.from([98])]).toString(), B3.alloc(2, 1).toString("base64"));
function toBuffer(chunk) {
return Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
}
console.log(toBuffer("abc").length, toBuffer(Buffer.from("xyz")).toString());
// Buffer.from over a JS-inferred `string | Uint8Array | undefined` result
// dispatches on the runtime kind, Node's TypeError for undefined included.
function make(flag) {
return flag ? "ab" : flag === false ? new Uint8Array([99, 100]) : undefined;
}
console.log(Buffer.from(make(true)).toString(), Buffer.from(make(false)).toString("hex"));
try {
Buffer.from(make(null));
} catch (e) {
console.log(e.name, e.code);
}
@@ -0,0 +1,11 @@
// ky's feature probes: typeof Node's Web-platform constructor globals and
// their static methods answers "function" without materializing the value.
export const supportsAbortController = typeof globalThis.AbortController === "function";
export const supportsAbortSignal =
typeof globalThis.AbortSignal === "function" && typeof globalThis.AbortSignal.any === "function";
console.log(supportsAbortController, supportsAbortSignal);
console.log(typeof AbortSignal.timeout, typeof AbortSignal.abort, typeof globalThis.AbortSignal.any);
console.log(typeof Response.json, typeof Response.error, typeof URL.canParse, typeof Promise.withResolvers);
console.log(typeof AbortSignal, typeof globalThis.URL, typeof Headers, typeof globalThis.TextEncoder, typeof URLSearchParams);
const signal = AbortSignal.abort("stop");
console.log(signal.aborted, signal.reason);
+103
View File
@@ -0,0 +1,103 @@
// JavaScript: a function-scoped `var` initialized with an object that
// already exists (an element of an array built with push, a find result,
// a member of another object) holds that object, not a copy: writes
// through the binding are visible through the array, and `===` holds.
function make(i) {
return { id: i, x: i % 17 };
}
function updateInPlace() {
var all = [];
for (var i = 0; i < 3; i++) all.push(make(i));
var same = 0;
for (var j = 0; j < all.length; j++) {
var o = all[j];
o.x += 1;
if (o === all[j]) same++;
}
console.log(JSON.stringify(all), same);
}
updateInPlace();
function literalElements() {
var all = [];
for (var i = 0; i < 3; i++) all.push({ id: i, x: i });
for (var j = 0; j < all.length; j++) {
var o = all[j];
o.x *= 10;
}
console.log(JSON.stringify(all));
}
literalElements();
function initializerForms(flag) {
var all = [];
all.push({ n: 0, inner: { v: 1 } });
all.push({ n: 1, inner: { v: 2 } });
var found = all.find(function (e) {
return e.n === 1;
});
found.n = 11;
var picked = flag ? all[0] : null;
picked.n = 10;
var either = all[5] || all[0];
either.inner.v += 100;
var inner = all[1].inner;
inner.v = 20;
var last = all.pop();
console.log(JSON.stringify(all), last === found, picked === either);
console.log(JSON.stringify(last));
}
initializerForms(true);
function arraysAndClosures() {
var grid = [];
grid.push([1, 2]);
var row = grid[0];
row[0] = 9;
row.push(3);
var list = [];
list.push({ count: 0 });
var target = list[0];
var bump = function () {
target.count++;
};
bump();
bump();
console.log(JSON.stringify(grid), JSON.stringify(list), target === list[0]);
}
arraysAndClosures();
function redeclaredAndReassigned() {
var all = [];
all.push({ x: 1 });
all.push({ x: 2 });
var o = all[0];
o.x = 10;
var o = all[1];
o.x = 20;
o = { x: 30 };
all.push(o);
o.x = 40;
console.log(JSON.stringify(all), o === all[2]);
}
redeclaredAndReassigned();
const arrow = () => {
var all = [];
all.push({ x: 1 });
var o = all[0];
o.x = 5;
console.log(JSON.stringify(all));
};
arrow();
// An annotated binding keeps its declared type.
function annotated() {
var all = [];
all.push({ x: 1 });
/** @type {{ x: number }} */
var o = all[0];
console.log(o.x);
}
annotated();
@@ -0,0 +1,46 @@
// JavaScript: array destructuring (var, let and const) and `for (var ...
// of ...)` bind the existing element objects of an array built with push,
// so writes through the bindings reach the array's objects.
function destructure() {
var all = [];
all.push({ x: 1 });
all.push({ x: 2 });
var [first] = all;
first.x = 10;
let [, second] = all;
second.x = 20;
const [again] = all;
again.x += 1;
console.log(JSON.stringify(all), first === all[0], again === first, second === all[1]);
}
destructure();
function forVarOf() {
var all = [];
all.push({ x: 1 });
all.push({ x: 2 });
var seen = [];
for (var o of all) {
o.x *= 10;
seen.push(o === all[seen.length]);
}
console.log(JSON.stringify(all), seen.join());
for (let p of all) p.x += 1;
console.log(JSON.stringify(all));
}
forVarOf();
function* tagged(list) {
for (var o of list) {
o.seen = true;
yield o;
}
}
function generators() {
var items = [];
items.push({ seen: false });
items.push({ seen: false });
for (var it of tagged(items)) it.extra = items.indexOf(it);
console.log(JSON.stringify(items));
}
generators();
@@ -0,0 +1,40 @@
// JavaScript: file-scope `var` and `let` bindings initialized with an
// existing object (an element of an array built with push) hold that
// object, including bindings declared in a loop body, by destructuring or
// by for-of, and functions declared above them read the same object.
function show() {
return typeof o === "undefined" ? "unset" : o.x;
}
console.log(show());
var all = [];
all.push({ x: 1 });
all.push({ x: 2 });
var o = all[0];
o.x = 10;
console.log(show(), JSON.stringify(all), o === all[0]);
var same = 0;
for (var j = 0; j < all.length; j++) {
var item = all[j];
item.x += 5;
if (item === all[j]) same++;
}
console.log(JSON.stringify(all), same);
var [head] = all;
head.x = 100;
for (var each of all) {
each.x += 1;
if (each === all[1]) console.log("last", each.x);
}
console.log(JSON.stringify(all), head === o);
let current = all[1];
current.x = 7;
function touch() {
current.x++;
}
touch();
console.log(JSON.stringify(all), current === all[1]);
@@ -0,0 +1,56 @@
// JavaScript: an object a function returns is the object the function
// holds elsewhere (a cache entry, a registry element). Bindings that
// receive it and array or object literals built from those bindings keep
// that object, so writes through them reach the cache and registry.
'use strict';
const cache = { n: 0 };
function get() {
return cache;
}
const registry = [];
function make(name) {
const o = { name: name, hits: 0 };
registry.push(o);
return o;
}
function identity(v) {
return v;
}
function wrapped(i) {
return identity({ id: i, x: i % 17 });
}
function main() {
var a = get();
a.n = 5;
console.log(cache.n, a === cache);
var list = [a];
list[0].n = 6;
var holder = { item: a };
holder.item.n += 1;
console.log(cache.n, list[0] === cache, holder.item === cache);
var x = make("x");
let y = make("y");
const z = make("z");
x.hits++;
y.hits += 2;
z.hits += 3;
var group = [x, y, z];
for (var j = 0; j < group.length; j++) group[j].hits *= 10;
console.log(JSON.stringify(registry), group[0] === registry[0]);
var all = [];
for (var i = 0; i < 4; i++) all.push(wrapped(i));
var sum = 0;
for (var r = 0; r < 3; r++)
for (var k = 0; k < all.length; k++) {
var e = all[k];
e.x += 1;
sum += e.x;
}
console.log(all.length, sum, JSON.stringify(all.slice(0, 2)));
}
main();
+84
View File
@@ -0,0 +1,84 @@
// An optional key that no literal creates is appended when it is first
// assigned, after every key the value was built with, and straight-line
// assignments of several such keys keep their program order, including
// assignments behind conditions. Assigning a key that already exists never
// moves it.
interface Item {
label?: string;
id: number;
done: boolean;
}
function makeItem(id: number): Item {
return { id, done: false };
}
const item = makeItem(1);
item.label = "first";
console.log(Object.keys(item).join(","), JSON.stringify(item));
console.log(item);
const direct: Item = { id: 2, done: true };
direct.label = "second";
console.log(Object.keys(direct).join(","), JSON.stringify(direct));
const untouched = makeItem(3);
console.log(JSON.stringify(untouched), Object.keys(untouched).join(","));
// Two late keys assigned in reverse declaration order.
interface Pos {
id: number;
x?: number;
y?: number;
}
function makePos(): Pos {
return { id: 0 };
}
const pos = makePos();
pos.y = 1;
pos.x = 2;
console.log(Object.keys(pos).join(","), JSON.stringify(pos));
console.log(pos);
pos.id = 3;
console.log(JSON.stringify(pos));
// An options object filled key by key, some keys conditionally.
interface Opts {
verbose?: boolean;
depth?: number;
name?: string;
}
function build(deep: boolean, loud: boolean): Opts {
const o: Opts = {};
o.name = "n";
if (deep) o.depth = 2;
if (loud) o.verbose = true;
return o;
}
for (const deep of [true, false])
for (const loud of [true, false]) console.log(JSON.stringify(build(deep, loud)));
console.log(build(true, true));
const forIn: string[] = [];
for (const k in build(true, true)) forIn.push(k);
console.log(forIn.join(","));
// Late keys on records stored in arrays and Maps.
interface Task {
owner?: string;
title: string;
priority: number;
}
const tasks: Task[] = [];
for (let i = 0; i < 3; i++) tasks.push({ title: "t" + i, priority: i });
for (const t of tasks) if (t.priority > 0) t.owner = "o" + t.priority;
console.log(JSON.stringify(tasks));
const byTitle = new Map<string, Task>();
for (const t of tasks) byTitle.set(t.title, t);
console.log(byTitle);
// An explicit undefined is a present key at its literal position.
interface Maybe {
a?: number;
b: number;
}
function makeMaybe(): Maybe {
return { b: 1, a: undefined };
}
console.log(Object.keys(makeMaybe()).join(","), JSON.stringify(makeMaybe()));
console.log(makeMaybe());
+150
View File
@@ -0,0 +1,150 @@
// Records enumerate their keys in the order their object literals create
// them, even when every literal writes the keys in another order than the
// type declares them and the value is observed far from the literal:
// factory returns, arrays, Maps, nested records, generic factories, union
// arms, records widened to unknown/any, structuredClone, and every
// order-observing surface (keys, values, entries, for...in, JSON.stringify
// with replacers and indentation, inspect, console.log, util.format).
import { format, inspect } from "node:util";
interface Point {
x: number;
y: number;
label: string;
}
function makePoint(i: number): Point {
return { label: "p" + i, y: i * 2, x: i };
}
const p = makePoint(1);
console.log(Object.keys(p).join(","));
console.log(Object.values(p).join(","));
console.log(JSON.stringify(Object.entries(p)));
const forIn: string[] = [];
for (const k in p) forIn.push(k);
console.log(forIn.join(","));
console.log(JSON.stringify(p));
console.log(JSON.stringify(p, null, 2));
console.log(JSON.stringify(p, ["y", "x"]));
const visited: string[] = [];
JSON.stringify(p, (key: string, value: unknown) => {
visited.push(key);
return value;
});
console.log(visited.join(","));
console.log(p);
console.log(inspect(p));
console.log(format("%o|%j|%O", p, p, p));
// Arrays of records: literals pushed in a loop and produced by map.
interface Row {
id: number;
name: string;
score: number;
}
const rows: Row[] = [];
for (let i = 0; i < 3; i++) rows.push({ name: "n" + i, score: i * 10, id: i });
for (const r of rows) console.log(Object.values(r).join("|"));
console.log(JSON.stringify(rows));
const mapped: Row[] = [4, 5].map((i) => ({ name: "m" + i, score: i, id: i * 100 }));
console.log(JSON.stringify(mapped));
console.log(mapped);
// Records stored in a Map, read back, serialized and inspected.
const byName = new Map<string, Row>();
for (const r of rows) byName.set(r.name, r);
byName.set("extra", { name: "extra", score: 9, id: 99 });
console.log(byName);
console.log(JSON.stringify([...byName.values()]));
console.log(Object.keys(byName.get("extra")!).join(","));
// Nested records: every level keeps its own literal's order.
interface Inner {
b: number;
a: number;
}
interface Outer {
inner: Inner;
id: number;
tags: string[];
}
function makeOuter(id: number): Outer {
return { id, tags: ["t" + id], inner: { a: id, b: id + 1 } };
}
console.log(JSON.stringify(makeOuter(1)));
console.log(makeOuter(2));
console.log(JSON.stringify([makeOuter(3), makeOuter(4)], null, 1));
// Generic factories instantiate per type argument.
function boxed<T>(value: T): { value: T; tag: string; count: number } {
return { tag: "box", count: 1, value };
}
console.log(JSON.stringify(boxed(7)));
console.log(JSON.stringify(boxed("s")));
console.log(Object.keys(boxed(true)).join(","));
// Union arms keep their own literal orders.
type Shape = { kind: "circle"; r: number } | { kind: "rect"; w: number; h: number };
function shapeAt(i: number): Shape {
return i % 2 === 0 ? { r: i, kind: "circle" } : { h: i, kind: "rect", w: i + 1 };
}
const shapes: Shape[] = [shapeAt(0), shapeAt(1), shapeAt(2)];
for (const s of shapes) console.log(JSON.stringify(s));
console.log(shapes);
// Widened to unknown and any, and cloned.
const u: unknown = makePoint(3);
console.log(JSON.stringify(u));
const loose: any = makePoint(4);
console.log(Object.keys(loose).join(","));
const cloned = structuredClone(makePoint(5));
console.log(JSON.stringify(cloned), Object.keys(cloned).join(","));
// Integer-like keys still come first, ascending, ahead of the literal order.
interface Indexed {
name: string;
"10": number;
"2": number;
"01": number;
}
function makeIndexed(): Indexed {
return { "01": 1, name: "n", "10": 10, "2": 2 };
}
console.log(JSON.stringify(makeIndexed()));
console.log(Object.keys(makeIndexed()).join(","));
// A literal whose fields are flipped by later assignments: assigning an
// existing key never moves it (the pattern of scriptc's own
// RuntimeFeatures scan).
interface Features {
regex: boolean;
eventTarget: boolean;
warningReplace: boolean;
stdioExtended: boolean;
processEvents: boolean;
emitter: boolean;
}
function scanFeatures(classes: string[], workers: boolean): Features {
const features: Features = {
regex: false,
eventTarget: classes.some((c) => c === "EventTarget"),
processEvents: false,
warningReplace: false,
stdioExtended: false,
emitter: workers,
};
if (classes.length > 1) features.regex = true;
features.stdioExtended = workers;
return features;
}
console.log(JSON.stringify(scanFeatures(["EventTarget", "X"], true)));
console.log(JSON.stringify(scanFeatures([], false)));
// Literals written in declaration order keep it.
interface Ordered {
first: string;
second: number;
third: boolean;
}
const ordered: Ordered = { first: "f", second: 2, third: true };
console.log(JSON.stringify(ordered), Object.keys(ordered).join(","));
+107
View File
@@ -0,0 +1,107 @@
// Copies keep the key order of the values they copy: a spread places the
// source's keys at the spread's position in the source's own order, an
// override keeps the position the spread gave it, and new keys follow.
// Rest bindings, width copies, Object.assign and structuredClone keep the
// source order too, including when the source literal was written in
// another order than its type declares.
interface Source {
a: number;
b: number;
c: number;
}
function makeSource(i: number): Source {
return { c: i, b: i + 1, a: i + 2 };
}
// Spread followed by a new key, and by an override.
interface Extended {
a: number;
b: number;
c: number;
d: number;
}
const extended: Extended = { ...makeSource(1), d: 4 };
console.log(JSON.stringify(extended), Object.keys(extended).join(","));
const overridden: Source = { ...makeSource(2), b: 0 };
console.log(JSON.stringify(overridden));
interface Led {
a: number;
b: number;
c: number;
lead: number;
}
const leading: Led = { lead: 0, ...makeSource(3) };
console.log(JSON.stringify(leading));
// A spread whose overrides change field types builds a new shape through
// the field-by-field desugar (scriptc's own frontend-cache writer).
interface Entry {
ok: true;
llvm: string;
ir: string[] | null;
sidecarJson: string | null;
features: number;
sources: [string, string][];
warnings?: string[];
}
function makeEntry(): Entry {
return { ok: true, llvm: "L", ir: ["x"], sidecarJson: null, features: 1, sources: [] };
}
const entry = makeEntry();
console.log(JSON.stringify({ schema: "v3", input: { ...entry, llvm: "", ir: null } }));
// Same-shape updates keep every key in place.
interface State {
name: string;
count: number;
last: string | undefined;
}
let state: State = { count: 0, last: undefined, name: "s" };
for (let i = 0; i < 3; i++) state = { ...state, count: state.count + 1, last: "i" + i };
console.log(JSON.stringify(state), Object.keys(state).join(","));
console.log({ ...state, name: "shown" });
// Rest bindings copy the remaining keys in the source's order.
const { b, ...rest } = makeSource(5);
console.log(b, JSON.stringify(rest), Object.keys(rest).join(","));
let picked: number;
let restAgain: { a: number; c: number };
({ b: picked, ...restAgain } = makeSource(6));
console.log(picked, JSON.stringify(restAgain), Object.keys(restAgain).join(","));
// Width copies into an optional-widened type and assignment into a fresh
// target.
interface Wider {
a: number;
b: number;
c: number;
note?: string;
}
function widen(s: Source): Wider {
return s;
}
console.log(JSON.stringify(widen(makeSource(7))));
const assigned = Object.assign({ z: 0 }, makeSource(8));
console.log(JSON.stringify(assigned));
console.log(JSON.stringify(structuredClone(makeSource(9))));
// Nested spreads of nested records.
interface Holder {
inner: Source;
id: number;
}
const holder: Holder = { id: 1, inner: makeSource(10) };
const copied: Holder = { ...holder, id: 2 };
console.log(JSON.stringify(copied));
console.log(copied);
// Conditional spreads place their key at the spread's position.
interface Options {
mode: string;
depth?: number;
verbose: boolean;
}
function options(deep: boolean): Options {
return { mode: "m", ...(deep ? { depth: 3 } : {}), verbose: deep };
}
console.log(JSON.stringify(options(true)), JSON.stringify(options(false)));
+3 -3
View File
@@ -20,14 +20,14 @@ console.log(tagged.label);
const view: Labeled = tagged;
console.log(view === tagged);
// Key order: the literal's order differs from the declaration and the
// program observes it.
// Key order: values of one type are built in two orders and the program
// observes them, so the type keeps its declaration order.
interface Point {
x: number;
y: number;
}
const point: Point = { y: 1, x: 2 };
console.log(JSON.stringify(point));
console.log(JSON.stringify(point), JSON.stringify({ x: 3, y: 4 }));
// Text outside the built-in collation compares by code point natively.
console.log(["б", "а"].sort((p, q) => p.localeCompare(q)).join(","), "яблоко".localeCompare("груша"));
+2 -2
View File
@@ -1,9 +1,9 @@
// Native checked values and loose equality share this program with
// island-backed APIs (SC2012) with native compilation refusals.
// native compilation refusals (a default-locale toLocaleString, SC2020).
const v: any = 21;
const doubled = v * 2;
const root = Math.cbrt(81);
const up = (19.99).toPrecision(3);
const up = (19.99).toLocaleString();
const parsed = Number.parseFloat("1.5"); // a string argument compiles statically, like the global
const unknownScore: unknown = 81;
const flags = unknownScore == 81;
+2 -2
View File
@@ -17,9 +17,9 @@ function readName(text: string): string {
return String(data.name);
}
// An operation without a static lowering, on a parameter.
// An operation without a static lowering (the host's default locale), on a parameter.
function precise(n: number): string {
return n.toPrecision(3);
return n.toLocaleString();
}
console.log(readName('{"name":"a"}'), precise(1.5), new Point(1, 2).x);
+5 -4
View File
@@ -4,11 +4,12 @@
// flag — never an ICE, never a link error. Static Math methods include
// the scalar functions and variadic Math.hypot. The .split(string),
// trim/pad variants, parseInt, isNaN, the global parseFloat/isFinite and
// Number.parseFloat/Number.parseInt over exactly-typed arguments, and
// string-pattern replacement compile statically now and no longer appear
// here.)
// Number.parseFloat/Number.parseInt over exactly-typed arguments,
// string-pattern replacement and toPrecision compile statically now and no
// longer appear here; toLocaleString without a locale still has no
// lowering.)
const up = Math.cbrt(2);
const tau = Math.PI * 2;
const price = (19.99).toPrecision(4);
const price = (19.99).toLocaleString();
const n = Number.parseFloat("3.14");
void n;
@@ -5,6 +5,6 @@ scriptc coverage tests/coverage-fixtures/dynamic-mix.ts
in dynamic tier 2 (operate on values typed only at runtime; --explain-dynamic lists them)
blockers:
×1 SC2012 '.toPrecision()' on numbers cannot compile statically
×1 SC2020 'Number.prototype.toLocaleString without a locale' is part of the standard library types but has no scriptc lowering yet
at tests/coverage-fixtures/dynamic-mix.ts:6:12
hint: use a supported API or rewrite this operation for native compilation
hint: the default locale is the host environment's, which a compiled binary cannot carry — pass it explicitly: x.toLocaleString("en-US")
@@ -12,5 +12,5 @@
function <top level> tests/coverage-fixtures/tier-demotions.ts:1:1 (1 site inside)
function precise tests/coverage-fixtures/tier-demotions.ts:21:1 (1 site inside)
binding n in precise tests/coverage-fixtures/tier-demotions.ts:21:18 (1 request)
<- SC2012 operation at tests/coverage-fixtures/tier-demotions.ts:22:10: '.toPrecision()' on numbers cannot compile statically
<- SC2020 operation at tests/coverage-fixtures/tier-demotions.ts:22:10: 'Number.prototype.toLocaleString without a locale' is part of the standard library types but has no scriptc lowering yet
-> function precise turns dynamic
@@ -8,6 +8,6 @@ scriptc coverage tests/coverage-fixtures/tier-demotions.ts
×1 SC1090 assignment to non-variables
at tests/coverage-fixtures/tier-demotions.ts:10:1
hint: this form is valid TypeScript but does not compile yet — rewrite it with a supported equivalent of the construct the message names (https://scriptc.dev/limitations lists the boundary)
×1 SC2012 '.toPrecision()' on numbers cannot compile statically
×1 SC2020 'Number.prototype.toLocaleString without a locale' is part of the standard library types but has no scriptc lowering yet
at tests/coverage-fixtures/tier-demotions.ts:22:10
hint: use a supported API or rewrite this operation for native compilation
hint: the default locale is the host environment's, which a compiled binary cannot carry — pass it explicitly: x.toLocaleString("en-US")
@@ -1,8 +1,8 @@
dynamic-surface.ts:12:15 - error SC2012: '.toPrecision()' on numbers cannot compile statically
dynamic-surface.ts:13:15 - error SC2020: 'Number.prototype.toLocaleString without a locale' is part of the standard library types but has no scriptc lowering yet
11 | const tau = Math.PI * 2;
12 | const price = (19.99).toPrecision(4);
| ^~~~~~~~~~~~~~~~~~~~~~
13 | const n = Number.parseFloat("3.14");
12 | const tau = Math.PI * 2;
13 | const price = (19.99).toLocaleString();
| ^~~~~~~~~~~~~~~~~~~~~~~~
14 | const n = Number.parseFloat("3.14");
hint: use a supported API or rewrite this operation for native compilation
hint: the default locale is the host environment's, which a compiled binary cannot carry — pass it explicitly: x.toLocaleString("en-US")

Some files were not shown because too many files have changed in this diff Show More