Merge main

This commit is contained in:
Chris Tate
2026-10-19 20:27:03 -05:00
416 changed files with 16810 additions and 1872 deletions
+1
View File
@@ -35,6 +35,7 @@ output/
!tests/fixtures/npm/cases/workspace-copied/node_modules/*/dist/
!tests/fixtures/provenance/node_modules/*/dist/
!tests/fixtures/npm-static/node_modules/*/dist/
!tests/corpus/*/node_modules/*/dist/
.scriptc/
*.log
.DS_Store
+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.
@@ -1,4 +1,4 @@
Strings are stored as UTF-8, with lone surrogates kept as their own code units. Length and string methods use UTF-16 semantics, including for lone surrogates, but relational comparisons such as `<` and `>` use code-point order. `split` with a string separator and `replace` or `replaceAll` with a string pattern don't match a lone surrogate against half of a surrogate pair; regular-expression patterns do. Output, file contents, and other encoded text write each lone surrogate as U+FFFD, as Node does. File system paths and network host names that contain a lone surrogate reach the operating system unconverted.
Strings are stored as UTF-8, with lone surrogates kept as their own code units. Length, string methods and relational comparisons such as `<` and `>` use UTF-16 semantics, including for lone surrogates. `split` with a string separator and `replace` or `replaceAll` with a string pattern don't match a lone surrogate against half of a surrogate pair; regular-expression patterns do. Output, file contents, and other encoded text write each lone surrogate as U+FFFD, as Node does. File system paths and network host names that contain a lone surrogate reach the operating system unconverted.
One-argument `localeCompare` follows Node's default root-locale collation for Latin-script text (ASCII, Latin-1 Supplement, Latin Extended-A, combining diacritical marks, and U+2044 FRACTION SLASH): letters order before case and accents, so `"a".localeCompare("B")` is `-1`. Strings containing other characters compare by code point, which can differ from Node's ICU collation; `SC6003` reports such string literals. The locales and options arguments are unsupported.
+50 -50
View File
@@ -328,10 +328,10 @@
"entries": 92,
"apiEntries": 92,
"static": {
"supported": 0,
"partial": 12,
"supported": 1,
"partial": 15,
"refused": 1,
"not-implemented": 79,
"not-implemented": 75,
"by-design": 0,
"unreviewed": 0,
"not-applicable": 0
@@ -854,9 +854,9 @@
"apiEntries": 156,
"static": {
"supported": 2,
"partial": 10,
"partial": 21,
"refused": 0,
"not-implemented": 144,
"not-implemented": 133,
"by-design": 0,
"unreviewed": 0,
"not-applicable": 0
@@ -35693,9 +35693,9 @@
"inherited": true
},
"static": {
"status": "not-implemented",
"detail": "No static compiler lowering is registered for this API yet.",
"verification": "registry-gap"
"status": "partial",
"detail": "Implemented for the documented scriptc module-loader subset.",
"verification": "test-backed"
}
},
{
@@ -36360,9 +36360,9 @@
"inherited": true
},
"static": {
"status": "not-implemented",
"detail": "No static compiler lowering is registered for this API yet.",
"verification": "registry-gap"
"status": "partial",
"detail": "Implemented for the documented scriptc module-loader subset.",
"verification": "test-backed"
}
},
{
@@ -37165,9 +37165,9 @@
"inherited": false
},
"static": {
"status": "not-implemented",
"detail": "No static compiler lowering is registered for this API yet.",
"verification": "registry-gap"
"status": "partial",
"detail": "Implemented for the documented scriptc module-loader subset.",
"verification": "test-backed"
}
},
{
@@ -37303,9 +37303,9 @@
"inherited": true
},
"static": {
"status": "not-implemented",
"detail": "No static compiler lowering is registered for this API yet.",
"verification": "registry-gap"
"status": "supported",
"detail": "Implemented for the documented scriptc module-loader subset.",
"verification": "test-backed"
}
},
{
@@ -80229,9 +80229,9 @@
"inherited": true
},
"static": {
"status": "not-implemented",
"detail": "No static compiler lowering is registered for this API yet.",
"verification": "registry-gap"
"status": "partial",
"detail": "Implemented for the documented scriptc module-loader subset.",
"verification": "test-backed"
}
},
{
@@ -80252,9 +80252,9 @@
"inherited": true
},
"static": {
"status": "not-implemented",
"detail": "No static compiler lowering is registered for this API yet.",
"verification": "registry-gap"
"status": "partial",
"detail": "Implemented for the documented scriptc module-loader subset.",
"verification": "test-backed"
}
},
{
@@ -80275,9 +80275,9 @@
"inherited": true
},
"static": {
"status": "not-implemented",
"detail": "No static compiler lowering is registered for this API yet.",
"verification": "registry-gap"
"status": "partial",
"detail": "Implemented for the documented scriptc module-loader subset.",
"verification": "test-backed"
}
},
{
@@ -80298,9 +80298,9 @@
"inherited": true
},
"static": {
"status": "not-implemented",
"detail": "No static compiler lowering is registered for this API yet.",
"verification": "registry-gap"
"status": "partial",
"detail": "Implemented for the documented scriptc module-loader subset.",
"verification": "test-backed"
}
},
{
@@ -80321,9 +80321,9 @@
"inherited": true
},
"static": {
"status": "not-implemented",
"detail": "No static compiler lowering is registered for this API yet.",
"verification": "registry-gap"
"status": "partial",
"detail": "Implemented for the documented scriptc module-loader subset.",
"verification": "test-backed"
}
},
{
@@ -80344,9 +80344,9 @@
"inherited": true
},
"static": {
"status": "not-implemented",
"detail": "No static compiler lowering is registered for this API yet.",
"verification": "registry-gap"
"status": "partial",
"detail": "Implemented for the documented scriptc module-loader subset.",
"verification": "test-backed"
}
},
{
@@ -80436,9 +80436,9 @@
"inherited": true
},
"static": {
"status": "not-implemented",
"detail": "No static compiler lowering is registered for this API yet.",
"verification": "registry-gap"
"status": "partial",
"detail": "Implemented for the documented scriptc module-loader subset.",
"verification": "test-backed"
}
},
{
@@ -80574,9 +80574,9 @@
"inherited": true
},
"static": {
"status": "not-implemented",
"detail": "No static compiler lowering is registered for this API yet.",
"verification": "registry-gap"
"status": "partial",
"detail": "Implemented for the documented scriptc module-loader subset.",
"verification": "test-backed"
}
},
{
@@ -80735,9 +80735,9 @@
"inherited": true
},
"static": {
"status": "not-implemented",
"detail": "No static compiler lowering is registered for this API yet.",
"verification": "registry-gap"
"status": "partial",
"detail": "Implemented for the documented scriptc module-loader subset.",
"verification": "test-backed"
}
},
{
@@ -80758,9 +80758,9 @@
"inherited": true
},
"static": {
"status": "not-implemented",
"detail": "No static compiler lowering is registered for this API yet.",
"verification": "registry-gap"
"status": "partial",
"detail": "Implemented for the documented scriptc module-loader subset.",
"verification": "test-backed"
}
},
{
@@ -80804,9 +80804,9 @@
"inherited": true
},
"static": {
"status": "not-implemented",
"detail": "No static compiler lowering is registered for this API yet.",
"verification": "registry-gap"
"status": "partial",
"detail": "Implemented for the documented scriptc module-loader subset.",
"verification": "test-backed"
}
},
{
@@ -3,12 +3,12 @@
"nodeVersion": "24.22.0",
"nodeCommit": "f14cd3aa7bf9ca8df2cc5b8bb14beb0f335b1d93",
"summary": {
"taskCount": 3454,
"tierItemCount": 3454,
"taskCount": 3453,
"tierItemCount": 3453,
"tiers": {
"static": {
"audit-partial": 1165,
"verify-gap": 2161,
"audit-partial": 1179,
"verify-gap": 2146,
"replace-refusal": 41,
"classify": 2,
"implement": 85
@@ -17,7 +17,7 @@
"priorities": {
"high": 2287,
"normal": 694,
"low": 473
"low": 472
}
},
"tasks": [
@@ -33201,12 +33201,18 @@
"priority": "high",
"tiers": {
"static": {
"status": "not-implemented",
"action": "verify-gap",
"verification": "registry-gap",
"confidence": "low",
"source": "compiler-unmatched:globals",
"tests": []
"status": "partial",
"action": "audit-partial",
"verification": "test-backed",
"confidence": "high",
"source": "compiler-feature:globals.CryptoKey",
"tests": [
"tests/corpus/6291-webcrypto-hmac-sign-verify.ts",
"tests/corpus/6292-webcrypto-hmac-errors.ts",
"tests/corpus/6293-webcrypto-digest.ts",
"tests/corpus/6294-webcrypto-js-values.mjs",
"tests/corpus/6295-jose-hs256/main.mjs"
]
}
}
},
@@ -33932,12 +33938,18 @@
"priority": "high",
"tiers": {
"static": {
"status": "not-implemented",
"action": "verify-gap",
"verification": "registry-gap",
"confidence": "low",
"source": "compiler-unmatched:globals",
"tests": []
"status": "partial",
"action": "audit-partial",
"verification": "test-backed",
"confidence": "high",
"source": "compiler-feature:globals.SubtleCrypto",
"tests": [
"tests/corpus/6291-webcrypto-hmac-sign-verify.ts",
"tests/corpus/6292-webcrypto-hmac-errors.ts",
"tests/corpus/6293-webcrypto-digest.ts",
"tests/corpus/6294-webcrypto-js-values.mjs",
"tests/corpus/6295-jose-hs256/main.mjs"
]
}
}
},
@@ -34853,12 +34865,16 @@
"priority": "low",
"tiers": {
"static": {
"status": "not-implemented",
"action": "verify-gap",
"verification": "registry-gap",
"confidence": "low",
"source": "compiler-unmatched:globals",
"tests": []
"status": "partial",
"action": "audit-partial",
"verification": "test-backed",
"confidence": "high",
"source": "compiler-feature:globals.navigator",
"tests": [
"tests/fixtures/node-types/navigator.ts",
"tests/harness/project-config.test.ts",
"tests/corpus/6331-jose-root-entry/main.ts"
]
}
}
},
@@ -34987,31 +35003,6 @@
}
}
},
{
"id": "globals:fc0a32e8c26f",
"chapter": "globals",
"label": "userAgent",
"signature": "`userAgent` Type: {string}",
"apiSymbol": "userAgent",
"docsUrl": "https://nodejs.org/docs/v24.22.0/api/globals.html#navigator",
"nodeStability": {
"index": "1.1",
"level": 1,
"text": "Active development. Disable this API with the [`--no-experimental-global-navigator`][] CLI flag.",
"inherited": true
},
"priority": "low",
"tiers": {
"static": {
"status": "not-implemented",
"action": "verify-gap",
"verification": "registry-gap",
"confidence": "low",
"source": "compiler-unmatched:globals",
"tests": []
}
}
},
{
"id": "globals:b412bf9a456d",
"chapter": "globals",
@@ -81024,12 +81015,18 @@
"priority": "high",
"tiers": {
"static": {
"status": "not-implemented",
"action": "verify-gap",
"verification": "registry-gap",
"confidence": "low",
"source": "compiler-unmatched:webcrypto",
"tests": []
"status": "partial",
"action": "audit-partial",
"verification": "test-backed",
"confidence": "high",
"source": "compiler-feature:webcrypto.subtle",
"tests": [
"tests/corpus/6291-webcrypto-hmac-sign-verify.ts",
"tests/corpus/6292-webcrypto-hmac-errors.ts",
"tests/corpus/6293-webcrypto-digest.ts",
"tests/corpus/6294-webcrypto-js-values.mjs",
"tests/corpus/6295-jose-hs256/main.mjs"
]
}
}
},
@@ -81049,12 +81046,16 @@
"priority": "high",
"tiers": {
"static": {
"status": "not-implemented",
"action": "verify-gap",
"verification": "registry-gap",
"confidence": "low",
"source": "compiler-unmatched:webcrypto",
"tests": []
"status": "partial",
"action": "audit-partial",
"verification": "test-backed",
"confidence": "high",
"source": "compiler-feature:webcrypto.CryptoKey",
"tests": [
"tests/corpus/6291-webcrypto-hmac-sign-verify.ts",
"tests/corpus/6294-webcrypto-js-values.mjs",
"tests/corpus/6295-jose-hs256/main.mjs"
]
}
}
},
@@ -81074,12 +81075,16 @@
"priority": "high",
"tiers": {
"static": {
"status": "not-implemented",
"action": "verify-gap",
"verification": "registry-gap",
"confidence": "low",
"source": "compiler-unmatched:webcrypto",
"tests": []
"status": "partial",
"action": "audit-partial",
"verification": "test-backed",
"confidence": "high",
"source": "compiler-feature:webcrypto.cryptoKey.algorithm",
"tests": [
"tests/corpus/6291-webcrypto-hmac-sign-verify.ts",
"tests/corpus/6294-webcrypto-js-values.mjs",
"tests/corpus/6295-jose-hs256/main.mjs"
]
}
}
},
@@ -81099,12 +81104,16 @@
"priority": "high",
"tiers": {
"static": {
"status": "not-implemented",
"action": "verify-gap",
"verification": "registry-gap",
"confidence": "low",
"source": "compiler-unmatched:webcrypto",
"tests": []
"status": "partial",
"action": "audit-partial",
"verification": "test-backed",
"confidence": "high",
"source": "compiler-feature:webcrypto.extractable",
"tests": [
"tests/corpus/6291-webcrypto-hmac-sign-verify.ts",
"tests/corpus/6294-webcrypto-js-values.mjs",
"tests/corpus/6295-jose-hs256/main.mjs"
]
}
}
},
@@ -81124,12 +81133,16 @@
"priority": "high",
"tiers": {
"static": {
"status": "not-implemented",
"action": "verify-gap",
"verification": "registry-gap",
"confidence": "low",
"source": "compiler-unmatched:webcrypto",
"tests": []
"status": "partial",
"action": "audit-partial",
"verification": "test-backed",
"confidence": "high",
"source": "compiler-feature:webcrypto.type",
"tests": [
"tests/corpus/6291-webcrypto-hmac-sign-verify.ts",
"tests/corpus/6294-webcrypto-js-values.mjs",
"tests/corpus/6295-jose-hs256/main.mjs"
]
}
}
},
@@ -81149,12 +81162,16 @@
"priority": "high",
"tiers": {
"static": {
"status": "not-implemented",
"action": "verify-gap",
"verification": "registry-gap",
"confidence": "low",
"source": "compiler-unmatched:webcrypto",
"tests": []
"status": "partial",
"action": "audit-partial",
"verification": "test-backed",
"confidence": "high",
"source": "compiler-feature:webcrypto.usages",
"tests": [
"tests/corpus/6291-webcrypto-hmac-sign-verify.ts",
"tests/corpus/6294-webcrypto-js-values.mjs",
"tests/corpus/6295-jose-hs256/main.mjs"
]
}
}
},
@@ -81249,12 +81266,18 @@
"priority": "high",
"tiers": {
"static": {
"status": "not-implemented",
"action": "verify-gap",
"verification": "registry-gap",
"confidence": "low",
"source": "compiler-unmatched:webcrypto",
"tests": []
"status": "partial",
"action": "audit-partial",
"verification": "test-backed",
"confidence": "high",
"source": "compiler-feature:webcrypto.SubtleCrypto",
"tests": [
"tests/corpus/6291-webcrypto-hmac-sign-verify.ts",
"tests/corpus/6292-webcrypto-hmac-errors.ts",
"tests/corpus/6293-webcrypto-digest.ts",
"tests/corpus/6294-webcrypto-js-values.mjs",
"tests/corpus/6295-jose-hs256/main.mjs"
]
}
}
},
@@ -81399,12 +81422,19 @@
"priority": "high",
"tiers": {
"static": {
"status": "not-implemented",
"action": "verify-gap",
"verification": "registry-gap",
"confidence": "low",
"source": "compiler-unmatched:webcrypto",
"tests": []
"status": "partial",
"action": "audit-partial",
"verification": "test-backed",
"confidence": "high",
"source": "compiler-feature:webcrypto.subtle.digest",
"tests": [
"tests/corpus/6291-webcrypto-hmac-sign-verify.ts",
"tests/corpus/6292-webcrypto-hmac-errors.ts",
"tests/corpus/6293-webcrypto-digest.ts",
"tests/corpus/6294-webcrypto-js-values.mjs",
"tests/corpus/6295-jose-hs256/main.mjs",
"tests/fixtures/node-types/webcrypto-hmac.ts"
]
}
}
},
@@ -81574,12 +81604,19 @@
"priority": "high",
"tiers": {
"static": {
"status": "not-implemented",
"action": "verify-gap",
"verification": "registry-gap",
"confidence": "low",
"source": "compiler-unmatched:webcrypto",
"tests": []
"status": "partial",
"action": "audit-partial",
"verification": "test-backed",
"confidence": "high",
"source": "compiler-feature:webcrypto.subtle.importKey",
"tests": [
"tests/corpus/6291-webcrypto-hmac-sign-verify.ts",
"tests/corpus/6292-webcrypto-hmac-errors.ts",
"tests/corpus/6293-webcrypto-digest.ts",
"tests/corpus/6294-webcrypto-js-values.mjs",
"tests/corpus/6295-jose-hs256/main.mjs",
"tests/fixtures/node-types/webcrypto-hmac.ts"
]
}
}
},
@@ -81599,12 +81636,19 @@
"priority": "high",
"tiers": {
"static": {
"status": "not-implemented",
"action": "verify-gap",
"verification": "registry-gap",
"confidence": "low",
"source": "compiler-unmatched:webcrypto",
"tests": []
"status": "partial",
"action": "audit-partial",
"verification": "test-backed",
"confidence": "high",
"source": "compiler-feature:webcrypto.subtle.sign",
"tests": [
"tests/corpus/6291-webcrypto-hmac-sign-verify.ts",
"tests/corpus/6292-webcrypto-hmac-errors.ts",
"tests/corpus/6293-webcrypto-digest.ts",
"tests/corpus/6294-webcrypto-js-values.mjs",
"tests/corpus/6295-jose-hs256/main.mjs",
"tests/fixtures/node-types/webcrypto-hmac.ts"
]
}
}
},
@@ -81649,12 +81693,19 @@
"priority": "high",
"tiers": {
"static": {
"status": "not-implemented",
"action": "verify-gap",
"verification": "registry-gap",
"confidence": "low",
"source": "compiler-unmatched:webcrypto",
"tests": []
"status": "partial",
"action": "audit-partial",
"verification": "test-backed",
"confidence": "high",
"source": "compiler-feature:webcrypto.subtle.verify",
"tests": [
"tests/corpus/6291-webcrypto-hmac-sign-verify.ts",
"tests/corpus/6292-webcrypto-hmac-errors.ts",
"tests/corpus/6293-webcrypto-digest.ts",
"tests/corpus/6294-webcrypto-js-values.mjs",
"tests/corpus/6295-jose-hs256/main.mjs",
"tests/fixtures/node-types/webcrypto-hmac.ts"
]
}
}
},
@@ -340,10 +340,10 @@
"entries": 94,
"apiEntries": 92,
"static": {
"supported": 0,
"partial": 12,
"supported": 1,
"partial": 15,
"refused": 1,
"not-implemented": 79,
"not-implemented": 75,
"by-design": 0,
"unreviewed": 0,
"not-applicable": 0
@@ -911,9 +911,9 @@
"apiEntries": 156,
"static": {
"supported": 2,
"partial": 10,
"partial": 21,
"refused": 0,
"not-implemented": 144,
"not-implemented": 133,
"by-design": 0,
"unreviewed": 0,
"not-applicable": 0
@@ -52242,8 +52242,16 @@
"tests/corpus/5288-blob-validation.js",
"tests/corpus/5290-blob-dynamic.js",
"tests/corpus/5830-timers-promises-signal.ts",
"tests/corpus/6291-webcrypto-hmac-sign-verify.ts",
"tests/corpus/6292-webcrypto-hmac-errors.ts",
"tests/corpus/6293-webcrypto-digest.ts",
"tests/corpus/6294-webcrypto-js-values.mjs",
"tests/corpus/6295-jose-hs256/main.mjs",
"tests/corpus/6331-jose-root-entry/main.ts",
"tests/corpus/text-codec-values/main.ts",
"tests/harness/fetch-conformance.test.ts"
"tests/fixtures/node-types/navigator.ts",
"tests/harness/fetch-conformance.test.ts",
"tests/harness/project-config.test.ts"
]
},
"anchorSource": "chapter"
@@ -52664,8 +52672,15 @@
"inherited": true
},
"static": {
"status": "not-implemented",
"evidence": "compiler-unmatched:globals"
"status": "partial",
"evidence": "compiler-feature:globals.CryptoKey",
"tests": [
"tests/corpus/6291-webcrypto-hmac-sign-verify.ts",
"tests/corpus/6292-webcrypto-hmac-errors.ts",
"tests/corpus/6293-webcrypto-digest.ts",
"tests/corpus/6294-webcrypto-js-values.mjs",
"tests/corpus/6295-jose-hs256/main.mjs"
]
},
"anchorSource": "exact"
},
@@ -53338,8 +53353,15 @@
"inherited": true
},
"static": {
"status": "not-implemented",
"evidence": "compiler-unmatched:globals"
"status": "partial",
"evidence": "compiler-feature:globals.SubtleCrypto",
"tests": [
"tests/corpus/6291-webcrypto-hmac-sign-verify.ts",
"tests/corpus/6292-webcrypto-hmac-errors.ts",
"tests/corpus/6293-webcrypto-digest.ts",
"tests/corpus/6294-webcrypto-js-values.mjs",
"tests/corpus/6295-jose-hs256/main.mjs"
]
},
"anchorSource": "exact"
},
@@ -54244,8 +54266,13 @@
"inherited": false
},
"static": {
"status": "not-implemented",
"evidence": "compiler-unmatched:globals"
"status": "partial",
"evidence": "compiler-feature:globals.navigator",
"tests": [
"tests/fixtures/node-types/navigator.ts",
"tests/harness/project-config.test.ts",
"tests/corpus/6331-jose-root-entry/main.ts"
]
},
"anchorSource": "exact"
},
@@ -54382,8 +54409,12 @@
"inherited": true
},
"static": {
"status": "not-implemented",
"evidence": "compiler-unmatched:globals"
"status": "supported",
"evidence": "compiler-feature:globals.userAgent",
"tests": [
"tests/fixtures/node-types/navigator.ts",
"tests/harness/project-config.test.ts"
]
},
"anchorSource": "ancestor"
},
@@ -106872,10 +106903,16 @@
"tests/corpus/5443-crypto-random-arguments.js",
"tests/corpus/6204-crypto-get-random-values.ts",
"tests/corpus/6205-crypto-get-random-values.js",
"tests/corpus/6291-webcrypto-hmac-sign-verify.ts",
"tests/corpus/6292-webcrypto-hmac-errors.ts",
"tests/corpus/6293-webcrypto-digest.ts",
"tests/corpus/6294-webcrypto-js-values.mjs",
"tests/corpus/6295-jose-hs256/main.mjs",
"tests/corpus/node24-crypto-kdf-errors.ts",
"tests/corpus/node24-crypto-kdf-options.ts",
"tests/corpus/node24-crypto-kdf.ts",
"tests/corpus/node24-crypto-sha2.ts",
"tests/fixtures/node-types/webcrypto-hmac.ts",
"tests/harness/npm-static.test.ts"
]
},
@@ -110411,8 +110448,15 @@
"inherited": true
},
"static": {
"status": "not-implemented",
"evidence": "compiler-unmatched:webcrypto"
"status": "partial",
"evidence": "compiler-feature:webcrypto.subtle",
"tests": [
"tests/corpus/6291-webcrypto-hmac-sign-verify.ts",
"tests/corpus/6292-webcrypto-hmac-errors.ts",
"tests/corpus/6293-webcrypto-digest.ts",
"tests/corpus/6294-webcrypto-js-values.mjs",
"tests/corpus/6295-jose-hs256/main.mjs"
]
},
"anchorSource": "ancestor"
},
@@ -110434,8 +110478,13 @@
"inherited": true
},
"static": {
"status": "not-implemented",
"evidence": "compiler-unmatched:webcrypto"
"status": "partial",
"evidence": "compiler-feature:webcrypto.CryptoKey",
"tests": [
"tests/corpus/6291-webcrypto-hmac-sign-verify.ts",
"tests/corpus/6294-webcrypto-js-values.mjs",
"tests/corpus/6295-jose-hs256/main.mjs"
]
},
"anchorSource": "exact"
},
@@ -110457,8 +110506,13 @@
"inherited": true
},
"static": {
"status": "not-implemented",
"evidence": "compiler-unmatched:webcrypto"
"status": "partial",
"evidence": "compiler-feature:webcrypto.cryptoKey.algorithm",
"tests": [
"tests/corpus/6291-webcrypto-hmac-sign-verify.ts",
"tests/corpus/6294-webcrypto-js-values.mjs",
"tests/corpus/6295-jose-hs256/main.mjs"
]
},
"anchorSource": "exact"
},
@@ -110480,8 +110534,13 @@
"inherited": true
},
"static": {
"status": "not-implemented",
"evidence": "compiler-unmatched:webcrypto"
"status": "partial",
"evidence": "compiler-feature:webcrypto.extractable",
"tests": [
"tests/corpus/6291-webcrypto-hmac-sign-verify.ts",
"tests/corpus/6294-webcrypto-js-values.mjs",
"tests/corpus/6295-jose-hs256/main.mjs"
]
},
"anchorSource": "ancestor"
},
@@ -110503,8 +110562,13 @@
"inherited": true
},
"static": {
"status": "not-implemented",
"evidence": "compiler-unmatched:webcrypto"
"status": "partial",
"evidence": "compiler-feature:webcrypto.type",
"tests": [
"tests/corpus/6291-webcrypto-hmac-sign-verify.ts",
"tests/corpus/6294-webcrypto-js-values.mjs",
"tests/corpus/6295-jose-hs256/main.mjs"
]
},
"anchorSource": "ancestor"
},
@@ -110526,8 +110590,13 @@
"inherited": true
},
"static": {
"status": "not-implemented",
"evidence": "compiler-unmatched:webcrypto"
"status": "partial",
"evidence": "compiler-feature:webcrypto.usages",
"tests": [
"tests/corpus/6291-webcrypto-hmac-sign-verify.ts",
"tests/corpus/6294-webcrypto-js-values.mjs",
"tests/corpus/6295-jose-hs256/main.mjs"
]
},
"anchorSource": "ancestor"
},
@@ -110618,8 +110687,15 @@
"inherited": true
},
"static": {
"status": "not-implemented",
"evidence": "compiler-unmatched:webcrypto"
"status": "partial",
"evidence": "compiler-feature:webcrypto.SubtleCrypto",
"tests": [
"tests/corpus/6291-webcrypto-hmac-sign-verify.ts",
"tests/corpus/6292-webcrypto-hmac-errors.ts",
"tests/corpus/6293-webcrypto-digest.ts",
"tests/corpus/6294-webcrypto-js-values.mjs",
"tests/corpus/6295-jose-hs256/main.mjs"
]
},
"anchorSource": "exact"
},
@@ -110756,8 +110832,16 @@
"inherited": true
},
"static": {
"status": "not-implemented",
"evidence": "compiler-unmatched:webcrypto"
"status": "partial",
"evidence": "compiler-feature:webcrypto.subtle.digest",
"tests": [
"tests/corpus/6291-webcrypto-hmac-sign-verify.ts",
"tests/corpus/6292-webcrypto-hmac-errors.ts",
"tests/corpus/6293-webcrypto-digest.ts",
"tests/corpus/6294-webcrypto-js-values.mjs",
"tests/corpus/6295-jose-hs256/main.mjs",
"tests/fixtures/node-types/webcrypto-hmac.ts"
]
},
"anchorSource": "exact"
},
@@ -110917,8 +111001,16 @@
"inherited": true
},
"static": {
"status": "not-implemented",
"evidence": "compiler-unmatched:webcrypto"
"status": "partial",
"evidence": "compiler-feature:webcrypto.subtle.importKey",
"tests": [
"tests/corpus/6291-webcrypto-hmac-sign-verify.ts",
"tests/corpus/6292-webcrypto-hmac-errors.ts",
"tests/corpus/6293-webcrypto-digest.ts",
"tests/corpus/6294-webcrypto-js-values.mjs",
"tests/corpus/6295-jose-hs256/main.mjs",
"tests/fixtures/node-types/webcrypto-hmac.ts"
]
},
"anchorSource": "exact"
},
@@ -110940,8 +111032,16 @@
"inherited": true
},
"static": {
"status": "not-implemented",
"evidence": "compiler-unmatched:webcrypto"
"status": "partial",
"evidence": "compiler-feature:webcrypto.subtle.sign",
"tests": [
"tests/corpus/6291-webcrypto-hmac-sign-verify.ts",
"tests/corpus/6292-webcrypto-hmac-errors.ts",
"tests/corpus/6293-webcrypto-digest.ts",
"tests/corpus/6294-webcrypto-js-values.mjs",
"tests/corpus/6295-jose-hs256/main.mjs",
"tests/fixtures/node-types/webcrypto-hmac.ts"
]
},
"anchorSource": "exact"
},
@@ -110986,8 +111086,16 @@
"inherited": true
},
"static": {
"status": "not-implemented",
"evidence": "compiler-unmatched:webcrypto"
"status": "partial",
"evidence": "compiler-feature:webcrypto.subtle.verify",
"tests": [
"tests/corpus/6291-webcrypto-hmac-sign-verify.ts",
"tests/corpus/6292-webcrypto-hmac-errors.ts",
"tests/corpus/6293-webcrypto-digest.ts",
"tests/corpus/6294-webcrypto-js-values.mjs",
"tests/corpus/6295-jose-hs256/main.mjs",
"tests/fixtures/node-types/webcrypto-hmac.ts"
]
},
"anchorSource": "exact"
},
@@ -179,11 +179,19 @@
"stdlib.set": ["tests/corpus/typed-collection-iterators.ts", "tests/corpus/typed-collection-iterator-references.ts", "tests/corpus/checked-collection-iterators.ts"],
"stdlib.headers": ["tests/harness/fetch-conformance.test.ts"],
"stdlib.request": ["tests/harness/fetch-conformance.test.ts"],
"stdlib.webcrypto.subtle": ["tests/corpus/6291-webcrypto-hmac-sign-verify.ts", "tests/corpus/6292-webcrypto-hmac-errors.ts", "tests/corpus/6293-webcrypto-digest.ts", "tests/corpus/6294-webcrypto-js-values.mjs", "tests/corpus/6295-jose-hs256/main.mjs"],
"stdlib.webcrypto.CryptoKey": ["tests/corpus/6291-webcrypto-hmac-sign-verify.ts", "tests/corpus/6294-webcrypto-js-values.mjs", "tests/corpus/6295-jose-hs256/main.mjs"],
"stdlib.response": ["tests/harness/fetch-conformance.test.ts"],
"stdlib.readable-stream": ["tests/harness/fetch-conformance.test.ts"],
"stdlib.abort": ["tests/harness/fetch-conformance.test.ts"]
},
"features": [
{ "chapter": "webcrypto", "symbols": ["subtle.importKey", "subtle.sign", "subtle.verify", "subtle.digest"], "status": "partial", "evidence": ["tests/corpus/6291-webcrypto-hmac-sign-verify.ts", "tests/corpus/6292-webcrypto-hmac-errors.ts", "tests/corpus/6293-webcrypto-digest.ts", "tests/corpus/6294-webcrypto-js-values.mjs", "tests/corpus/6295-jose-hs256/main.mjs", "tests/fixtures/node-types/webcrypto-hmac.ts"] },
{ "chapter": "webcrypto", "signatures": ["Class: `SubtleCrypto`", "`subtle` Type: {SubtleCrypto}"], "status": "partial", "evidence": ["tests/corpus/6291-webcrypto-hmac-sign-verify.ts", "tests/corpus/6292-webcrypto-hmac-errors.ts", "tests/corpus/6293-webcrypto-digest.ts", "tests/corpus/6294-webcrypto-js-values.mjs", "tests/corpus/6295-jose-hs256/main.mjs"] },
{ "chapter": "webcrypto", "signatures": ["Class: `CryptoKey`", "`cryptoKey.algorithm`", "`extractable` Type: {boolean}", "`type` Type: {string} One of `'secret'`, `'private'`, or `'public'`.", "`usages` Type: {string\\[]}"], "status": "partial", "evidence": ["tests/corpus/6291-webcrypto-hmac-sign-verify.ts", "tests/corpus/6294-webcrypto-js-values.mjs", "tests/corpus/6295-jose-hs256/main.mjs"] },
{ "chapter": "globals", "signatures": ["`navigator`"], "status": "partial", "evidence": ["tests/fixtures/node-types/navigator.ts", "tests/harness/project-config.test.ts", "tests/corpus/6331-jose-root-entry/main.ts"] },
{ "chapter": "globals", "signatures": ["`userAgent` Type: {string}"], "status": "supported", "evidence": ["tests/fixtures/node-types/navigator.ts", "tests/harness/project-config.test.ts"] },
{ "chapter": "globals", "signatures": ["Class: `CryptoKey`", "Class: `SubtleCrypto`"], "status": "partial", "evidence": ["tests/corpus/6291-webcrypto-hmac-sign-verify.ts", "tests/corpus/6292-webcrypto-hmac-errors.ts", "tests/corpus/6293-webcrypto-digest.ts", "tests/corpus/6294-webcrypto-js-values.mjs", "tests/corpus/6295-jose-hs256/main.mjs"] },
{ "chapter": "errors", "parent": "Class: `SystemError`", "symbols": ["code", "errno", "syscall", "path", "dest"], "status": "partial", "evidence": ["tests/corpus/caught-system-error-fields.ts", "tests/fixtures/node-types/terminal-error-fields.ts", "tests/harness/project-config.test.ts"] },
{"chapter": "events", "symbols": ["EventTarget", "eventTarget.addEventListener", "eventTarget.removeEventListener", "eventTarget.dispatchEvent"], "status": "partial", "evidence": ["tests/corpus/5130-event-target-dispatch.js", "tests/corpus/5131-event-target-listener-errors.js", "tests/corpus/5132-event-target-typed.ts", "tests/corpus/5133-abort-signal-event-target.ts", "tests/corpus/5135-event-target-warnings.js", "tests/corpus/5240-event-target-subclass.ts", "tests/corpus/5242-event-target-subclass-hierarchy.ts", "tests/corpus/5243-event-target-subclass-dynamic.ts", "tests/corpus/5244-event-target-subclass.js", "tests/corpus/5245-event-target-subclass-signals.ts", "tests/corpus/5248-event-target-subclass-warnings.ts"]},
{"chapter": "events", "symbols": ["Event"], "status": "partial", "evidence": ["tests/corpus/5130-event-target-dispatch.js", "tests/corpus/5132-event-target-typed.ts", "tests/corpus/5241-event-subclass.ts", "tests/corpus/5242-event-target-subclass-hierarchy.ts", "tests/corpus/5244-event-target-subclass.js", "tests/corpus/5247-event-target-subclass-lifetimes.ts"]},
+111 -6
View File
@@ -511,14 +511,119 @@ declare var DOMException: {
readonly prototype: DOMException;
};
/* Capability-gated globals harness code probes behind feature guards
* (globalThis.crypto under a hasCrypto check, the SQLite-backed storage
* pair): typed `unknown` — no lowering exists, the guard is expected to
* be false in a compiled binary, and any reached use fences per site. */
declare var crypto: unknown;
/* WebCrypto's HMAC slice (scr_webcrypto.c): crypto.subtle's importKey for
* raw HMAC keys, HMAC sign/verify and the SHA-1/SHA-2 digests, typed as
* exactly that surface (the declarations mirror @types/node's webcrypto
* shapes). Every other SubtleCrypto method and crypto member is absent,
* so a use is an honest type error. CryptoKey values are native handles
* whose four members read through the handle. */
declare var crypto: Crypto;
/* The Crypto and SubtleCrypto constructors (feature probes only) stay
* `unknown`; the values keep this declaration order. */
declare var Crypto: unknown;
declare var CryptoKey: unknown;
declare var CryptoKey: {
prototype: CryptoKey;
new (): CryptoKey;
};
declare var SubtleCrypto: unknown;
type KeyUsage =
| "encrypt"
| "decrypt"
| "sign"
| "verify"
| "deriveKey"
| "deriveBits"
| "encapsulateBits"
| "decapsulateBits"
| "encapsulateKey"
| "decapsulateKey"
| "wrapKey"
| "unwrapKey";
type KeyType = "private" | "public" | "secret";
interface Algorithm {
name: string;
}
type AlgorithmIdentifier = Algorithm | string;
type HashAlgorithmIdentifier = AlgorithmIdentifier;
interface HmacImportParams extends Algorithm {
hash: HashAlgorithmIdentifier;
length?: number;
}
interface KeyAlgorithm {
name: string;
}
interface HmacKeyAlgorithm extends KeyAlgorithm {
hash: KeyAlgorithm;
length: number;
}
interface CryptoKey {
readonly algorithm: KeyAlgorithm;
readonly extractable: boolean;
readonly type: KeyType;
readonly usages: KeyUsage[];
}
type KeyFormat =
| "jwk"
| "pkcs8"
| "raw"
| "raw-private"
| "raw-public"
| "raw-secret"
| "raw-seed"
| "spki";
interface JsonWebKey {
alg?: string;
crv?: string;
d?: string;
dp?: string;
dq?: string;
e?: string;
ext?: boolean;
k?: string;
key_ops?: string[];
kty?: string;
n?: string;
p?: string;
q?: string;
qi?: string;
use?: string;
x?: string;
y?: string;
}
/* The 'jwk' form type-checks like @types/node's; the compiler refuses it
* (the JsonWebKey import is not implemented). */
interface SubtleCrypto {
digest(algorithm: AlgorithmIdentifier, data: BufferSource): Promise<ArrayBuffer>;
importKey(
format: "jwk",
keyData: JsonWebKey,
algorithm: AlgorithmIdentifier | HmacImportParams,
extractable: boolean,
keyUsages: readonly KeyUsage[],
): Promise<CryptoKey>;
importKey(
format: Exclude<KeyFormat, "jwk">,
keyData: BufferSource,
algorithm: AlgorithmIdentifier | HmacImportParams,
extractable: boolean,
keyUsages: readonly KeyUsage[],
): Promise<CryptoKey>;
sign(algorithm: AlgorithmIdentifier, key: CryptoKey, data: BufferSource): Promise<ArrayBuffer>;
verify(
algorithm: AlgorithmIdentifier,
key: CryptoKey,
signature: BufferSource,
data: BufferSource,
): Promise<boolean>;
}
interface Crypto {
readonly subtle: SubtleCrypto;
}
/* Capability-gated globals harness code probes behind feature guards
* (the SQLite-backed storage pair): typed `unknown` — no lowering exists,
* the guard is expected to be false in a compiled binary, and any reached
* use fences per site. */
declare var localStorage: unknown;
declare var sessionStorage: unknown;
@@ -16,6 +16,8 @@ export function executableLinkFeatures(mod: IrModule): NativeLinkFeatures {
fileHandle: features.fileHandle,
fetch: features.fetch,
...(features.eventTarget ? { eventTarget: true } : {}),
...(features.warningReplace ? { warningReplace: true } : {}),
...(features.stdioExtended ? { stdioExtended: true } : {}),
zlib: features.zlib,
assert: features.assert,
inspect: features.inspect,
@@ -41,5 +43,6 @@ export function executableLinkFeatures(mod: IrModule): NativeLinkFeatures {
tls: features.tls,
tlsCa: features.tlsCa,
cipher: features.cipher,
webcrypto: features.webcrypto,
};
}
@@ -53,6 +53,7 @@ export async function compilePackedLibrary(
nodeTest: false,
tls: false,
cipher: false,
webcrypto: false,
tlsCa: false,
};
const selection = await loadRuntimePack({
+101 -51
View File
@@ -3672,13 +3672,30 @@ export class LlEmitter {
}
defs.push(...helper.define(this.sizeType, FN_ATTRS, scanInline), ``);
}
// Ordering also decides inline when the first bytes differ: byte order
// is the plain comparison's order, and the UTF-16 order whenever both
// bytes are ASCII. Callers only test the sign of the result.
for (const fn of ["scr_str_cmp", "scr_str_cmp_u16"]) {
if (!this.decls.has(`declare i32 @${fn}(ptr, ptr)`)) continue;
// Ordering decides inline from the first differing bytes. The bytes
// before them are equal, so both strings are at the same position of a
// character: either both bytes continue characters with the same lead
// byte, or both start a character. Byte order is the UTF-16 order for
// two continuation bytes, and for two lead bytes when one starts a
// character below U+D000, whose UTF-16 unit is below every unit of a
// character with a lead byte of at least 0xED. Between lead bytes of at
// least 0xED, a supplementary character (0xF0..0xF4, a high surrogate
// unit) orders below U+E000..U+FFFF (0xEE, 0xEF), and a lead byte of
// 0xED (U+D000..U+D7FF or a lone surrogate) takes the runtime.
// Differing words without a byte of at least 0xED decide in byte
// order; other differing words decide by their first differing byte,
// and strings shorter than a word by their first byte. Otherwise a
// proven-ASCII string on either side orders by bytes (scr_str_cmp), and
// other pairs call scr_str_cmp_u16_bytes, which only reads the two
// strings, so LLVM can merge repeated comparisons of one pair. Callers
// only test the sign of the result.
if (this.decls.has(`declare i32 @scr_str_cmp_u16(ptr, ptr)`)) {
const fn = "scr_str_cmp_u16";
this.declare("declare i64 @llvm.bswap.i64(i64)");
const utf16 = fn === "scr_str_cmp_u16";
this.declare("declare i64 @llvm.cttz.i64(i64, i1 immarg)");
this.declare("declare i8 @llvm.umin.i8(i8, i8)");
this.declare("declare i32 @scr_str_cmp(ptr, ptr)");
this.declare("declare i32 @scr_str_cmp_u16_bytes(ptr, ptr)");
defs.push(
`define internal i32 @${fn.replace(/^scr_/, "sc_")}(ptr %a, ptr %b) ${FN_ATTRS} {`,
`entry:`,
@@ -3693,8 +3710,7 @@ export class LlEmitter {
` %bempty = icmp eq ${this.sizeType} %blen, 0`,
` %empty = or i1 %aempty, %bempty`,
` br i1 %empty, label %order, label %words`,
// Both at least 8 bytes: the first differing byte of one word pair.
// Byte-swapped little-endian words compare in byte order.
// Both at least 8 bytes: the first word of each.
`words:`,
` %along = icmp uge ${this.sizeType} %alen, 8`,
` %blong = icmp uge ${this.sizeType} %blen, 8`,
@@ -3706,22 +3722,7 @@ export class LlEmitter {
` %aw = load i64, ptr %awp, align 1`,
` %bw = load i64, ptr %bwp, align 1`,
` %wdiffer = icmp ne i64 %aw, %bw`,
` br i1 %wdiffer, label %wdiff, label %wsame`,
`wdiff:`,
...(utf16
? [
` %wbits = or i64 %aw, %bw`,
` %whigh = and i64 %wbits, -9187201950435737472`,
` %wascii = icmp eq i64 %whigh, 0`,
` br i1 %wascii, label %wordorder, label %order`,
]
: [` br label %wordorder`]),
`wordorder:`,
` %asw = call i64 @llvm.bswap.i64(i64 %aw)`,
` %bsw = call i64 @llvm.bswap.i64(i64 %bw)`,
` %wbelow = icmp ult i64 %asw, %bsw`,
` %wsign = select i1 %wbelow, i32 -1, i32 1`,
` ret i32 %wsign`,
` br i1 %wdiffer, label %differ, label %wsame`,
// Equal first words and one length of at most 16 bytes: the word
// ending at that length covers the rest, and any difference lies
// past the equal first word.
@@ -3737,42 +3738,89 @@ export class LlEmitter {
` %at = load i64, ptr %atp, align 1`,
` %bt = load i64, ptr %btp, align 1`,
` %tdiffer = icmp ne i64 %at, %bt`,
` br i1 %tdiffer, label %tdiff, label %equal`,
`tdiff:`,
...(utf16
? [
` %tbits = or i64 %at, %bt`,
` %thigh = and i64 %tbits, -9187201950435737472`,
` %tascii = icmp eq i64 %thigh, 0`,
` br i1 %tascii, label %tailorder, label %order`,
]
: [` br label %tailorder`]),
`tailorder:`,
` %ats = call i64 @llvm.bswap.i64(i64 %at)`,
` %bts = call i64 @llvm.bswap.i64(i64 %bt)`,
` %tbelow = icmp ult i64 %ats, %bts`,
` %tsign = select i1 %tbelow, i32 -1, i32 1`,
` ret i32 %tsign`,
` br i1 %tdiffer, label %differ, label %equal`,
`first:`,
` %adata = getelementptr inbounds %ScrStr, ptr %a, i32 1`,
` %bdata = getelementptr inbounds %ScrStr, ptr %b, i32 1`,
` %x = load i8, ptr %adata`,
` %y = load i8, ptr %bdata`,
` %differ = icmp ne i8 %x, %y`,
...(utf16
? [
` %bits = or i8 %x, %y`,
` %ascii = icmp sge i8 %bits, 0`,
` %decide = and i1 %differ, %ascii`,
]
: [` %decide = and i1 %differ, true`]),
` br i1 %decide, label %byte, label %order`,
` %fdiffer = icmp ne i8 %x, %y`,
` br i1 %fdiffer, label %bytes, label %order`,
// Two differing words. When neither has a byte of at least 0xED
// (a byte b is when its high bit and bit 7 of (b & 0x7f) + 0x13 are
// set), byte-swapped little-endian words compare in byte order.
// Otherwise the lowest set byte of their xor is the first differing
// byte.
`differ:`,
` %dx = phi i64 [ %aw, %word ], [ %at, %tail ]`,
` %dy = phi i64 [ %bw, %word ], [ %bt, %tail ]`,
` %xl = and i64 %dx, 9187201950435737471`,
` %yl = and i64 %dy, 9187201950435737471`,
` %xa = add i64 %xl, 1374463283923456787`,
` %ya = add i64 %yl, 1374463283923456787`,
` %xh = and i64 %dx, %xa`,
` %yh = and i64 %dy, %ya`,
` %hh = or i64 %xh, %yh`,
` %whigh = and i64 %hh, -9187201950435737472`,
` %dlow = icmp eq i64 %whigh, 0`,
` br i1 %dlow, label %wordorder, label %locate`,
`wordorder:`,
` %xsw = call i64 @llvm.bswap.i64(i64 %dx)`,
` %ysw = call i64 @llvm.bswap.i64(i64 %dy)`,
` %wbelow = icmp ult i64 %xsw, %ysw`,
` %wsign = select i1 %wbelow, i32 -1, i32 1`,
` ret i32 %wsign`,
`locate:`,
` %dxor = xor i64 %dx, %dy`,
` %dtz = call i64 @llvm.cttz.i64(i64 %dxor, i1 true)`,
` %dshift = and i64 %dtz, 56`,
` %dxs = lshr i64 %dx, %dshift`,
` %dys = lshr i64 %dy, %dshift`,
` %dxb = trunc i64 %dxs to i8`,
` %dyb = trunc i64 %dys to i8`,
` br label %bytes`,
// The first differing bytes.
`bytes:`,
` %bx = phi i8 [ %x, %first ], [ %dxb, %locate ]`,
` %by = phi i8 [ %y, %first ], [ %dyb, %locate ]`,
` %bmin = call i8 @llvm.umin.i8(i8 %bx, i8 %by)`,
` %blow = icmp ult i8 %bmin, -19`,
` br i1 %blow, label %byte, label %high`,
// Both at least 0xED. A lone surrogate (0xED) takes the runtime;
// supplementary lead bytes (0xF0..0xF4) order below 0xEE and 0xEF
// once 0x10 is subtracted from them.
`high:`,
` %xed = icmp eq i8 %bx, -19`,
` %yed = icmp eq i8 %by, -19`,
` %ed = or i1 %xed, %yed`,
` br i1 %ed, label %order, label %planes`,
`planes:`,
` %xsupp = icmp uge i8 %bx, -16`,
` %ysupp = icmp uge i8 %by, -16`,
` %xlow = add i8 %bx, -16`,
` %ylow = add i8 %by, -16`,
` %xk = select i1 %xsupp, i8 %xlow, i8 %bx`,
` %yk = select i1 %ysupp, i8 %ylow, i8 %by`,
` %kbelow = icmp ult i8 %xk, %yk`,
` %ksign = select i1 %kbelow, i32 -1, i32 1`,
` ret i32 %ksign`,
`byte:`,
` %below = icmp ult i8 %x, %y`,
` %below = icmp ult i8 %bx, %by`,
` %sign = select i1 %below, i32 -1, i32 1`,
` ret i32 %sign`,
`order:`,
` %c = call i32 @${fn}(ptr %a, ptr %b)`,
` %acw.p = getelementptr inbounds %ScrStr, ptr %a, i32 0, i32 2`,
` %bcw.p = getelementptr inbounds %ScrStr, ptr %b, i32 0, i32 2`,
` %acw = load i32, ptr %acw.p`,
` %bcw = load i32, ptr %bcw.p`,
` %cws = or i32 %acw, %bcw`,
` %anyascii = icmp slt i32 %cws, 0`,
` br i1 %anyascii, label %memorder, label %units`,
`memorder:`,
` %m = call i32 @scr_str_cmp(ptr %a, ptr %b)`,
` ret i32 %m`,
`units:`,
` %c = call i32 @scr_str_cmp_u16_bytes(ptr %a, ptr %b) nounwind willreturn memory(argmem: read)`,
` ret i32 %c`,
`equal:`,
` ret i32 0`,
@@ -5882,6 +5930,8 @@ export class LlEmitter {
// Process streams use a numeric descriptor internally, including zero
// for stdin. They remain JavaScript objects in boolean contexts.
case "procStream":
// A module namespace object is always present.
case "moduleNs":
return "true";
case "array":
case "record":
@@ -379,7 +379,29 @@ export function emitOperatorExpr(
const fn = libm[e.op];
if (fn === undefined) throw new LlvmUnsupportedError(`bin:${e.op}`, e.loc);
host.declare(`declare double @${fn}(double, double)`);
B.line(`${t} = call double @${fn}(double ${l.name}, double ${r.name})`);
// libm's pow answers 1 for pow(±1, ±Infinity) and pow(1, NaN), where
// `**` answers NaN (and LLVM folds pow(1.0, x) to 1.0): an inline
// guard selects NaN for a base of ±1 with a non-finite exponent and
// keeps the direct pow call. A finite literal exponent, or a literal
// base other than ±1, cannot meet those cases and needs no guard.
if (fn === "pow" && !libmPowAgrees(e.left, e.right)) {
host.declare(`declare double @llvm.fabs.f64(double)`);
const power = B.tmp();
B.line(`${power} = call double @pow(double ${l.name}, double ${r.name})`);
const base = B.tmp();
const unit = B.tmp();
const magnitude = B.tmp();
const finite = B.tmp();
const reject = B.tmp();
B.line(`${base} = call double @llvm.fabs.f64(double ${l.name})`);
B.line(`${unit} = fcmp oeq double ${base}, 1.0`);
B.line(`${magnitude} = call double @llvm.fabs.f64(double ${r.name})`);
B.line(`${finite} = fcmp olt double ${magnitude}, 0x7FF0000000000000`);
B.line(`${reject} = select i1 ${finite}, i1 false, i1 ${unit}`);
B.line(`${t} = select i1 ${reject}, double 0x7FF8000000000000, double ${power}`);
} else {
B.line(`${t} = call double @${fn}(double ${l.name}, double ${r.name})`);
}
}
return { name: t, type: e.type };
}
@@ -602,7 +624,7 @@ export function emitStringExpr(
}
case "strCmp": {
const [l, r] = emitStringInputs(host, [e.left, e.right]);
const fn = e.utf16 === true ? "scr_str_cmp_u16" : "scr_str_cmp";
const fn = "scr_str_cmp_u16";
host.declare(`declare i32 @${fn}(ptr, ptr)`);
const c = B.tmp();
const t = B.tmp();
@@ -1106,3 +1128,19 @@ export function emitRecordLitFields(
host.initField(ptr, type, inline, v);
}
}
/** Whether libm pow matches `**` for these operands: every case except a
* base of ±1 with a NaN or infinite exponent agrees. */
function libmPowAgrees(base: IrExpr, exponent: IrExpr): boolean {
const power = numericLiteral(exponent);
if (power !== null && Number.isFinite(power)) return true;
const value = numericLiteral(base);
return value !== null && !Number.isNaN(value) && Math.abs(value) !== 1;
}
function numericLiteral(expr: IrExpr): number | null {
if (expr.kind === "numLit") return expr.value;
if (expr.kind === "unary" && expr.op === "-" && expr.operand.kind === "numLit")
return -expr.operand.value;
return null;
}
@@ -835,6 +835,7 @@ export function emitLibCall(host: LlvmEmitterContext, e: LibCallExpr): LlValue {
case "util":
case "bigint":
case "crypto":
case "webcrypto":
case "buffer":
case "bytes":
case "tlsca":
@@ -546,15 +546,24 @@ export function emitProcessLibCall(host: LlvmEmitterContext, e: LibCallExpr): Ll
B.line(`call void @scr_stdin_on_end(ptr ${cb.name}, i1 ${once.name})`);
return { name: "", type: e.type };
}
// A string listener takes the setEncoding-decoded chunk.
const stringData = e.fn === "stdin.onData" && cbT.params[0]?.kind === "string";
const adapter =
e.fn === "stdin.onData"
? cbT.params.length === 0
? "scr_stdin_data_thunk0"
: "scr_stdin_data_thunk_bytes"
: stringData
? "scr_stdin_data_thunk_str"
: "scr_stdin_data_thunk_bytes"
: cbT.params.length === 0
? "scr_child_err_thunk0"
: "scr_child_err_thunk_error";
const sym = e.fn === "stdin.onData" ? "scr_stdin_on_data" : "scr_stdin_on_error";
const sym =
e.fn === "stdin.onData"
? stringData
? "scr_stdin_on_data_str"
: "scr_stdin_on_data"
: "scr_stdin_on_error";
host.declare(`declare void @${adapter}(ptr, ptr)`);
host.declare(`declare void @${sym}(ptr, ptr, i1 zeroext)`);
B.line(`call void @${sym}(ptr ${cb.name}, ptr @${adapter}, i1 ${once.name})`);
@@ -588,6 +588,11 @@ export const LIB_FN_SYMS: Record<string, string> = {
"crypto.generateKeySync": "scr_crypto_generate_key_sync",
"crypto.keyObjectInput": "scr_crypto_key_object_input",
"crypto.isKeyObject": "scr_crypto_is_key_object",
"webcrypto.importKey": "scr_webcrypto_import_key",
"webcrypto.sign": "scr_webcrypto_sign",
"webcrypto.verify": "scr_webcrypto_verify",
"webcrypto.digest": "scr_webcrypto_digest",
"webcrypto.isCryptoKey": "scr_webcrypto_is_crypto_key",
"crypto.randomUUIDv7": "scr_crypto_random_uuid_v7",
"crypto.pbkdf2": "scr_crypto_pbkdf2",
"crypto.hkdf": "scr_crypto_hkdf",
@@ -1336,6 +1341,7 @@ export const LIB_FN_SYMS: Record<string, string> = {
"process.emitWarningText": "scr_process_emit_warning_text",
"process.deprecationFlag": "scr_process_deprecation_flag",
"process.setDeprecationFlag": "scr_process_set_deprecation_flag",
"process.setEmitWarning": "scr_process_set_emit_warning",
"process.onUncaughtException": "scr_process_on_uncaught_exception",
"process.offUncaughtException": "scr_process_off_uncaught_exception",
"process.onUnhandledRejection": "scr_process_on_unhandled_rejection",
@@ -1471,6 +1477,10 @@ export const USES_TIMERS_LIB_FNS = new Set<string>([
"process.emitWarning",
// Decipheriv.setAuthTag reports DEP0182 on the next tick.
"crypto.cipherNew",
// WebCrypto results settle through the deferred crypto queue.
"webcrypto.sign",
"webcrypto.verify",
"webcrypto.digest",
"process.emitWarningText",
// The legacy url entries queue DEP0169/DEP0170 on the next tick.
"url.legacyParse",
@@ -432,7 +432,6 @@ test("borrowed literals keep UTF-16 string comparison without temporary owners",
op: "<",
left: str("a"),
right: str("b"),
utf16: true,
type: BOOL,
loc,
});
@@ -443,16 +442,14 @@ test("borrowed literals keep UTF-16 string comparison without temporary owners",
});
test("string comparisons decide identity, lengths and leading bytes before the runtime", () => {
const order = (utf16: boolean) =>
fn(`order${utf16 ? 16 : 8}`, [], {
kind: "strCmp",
op: "<",
left: str("apple"),
right: str("apricot"),
utf16,
type: BOOL,
loc,
});
const order = fn("order", [], {
kind: "strCmp",
op: "<",
left: str("apple"),
right: str("apricot"),
type: BOOL,
loc,
});
const same = fn("same", ["a", "b"], {
kind: "strEq",
left: { kind: "varRef", localId: "a", type: STRING, loc },
@@ -462,23 +459,28 @@ test("string comparisons decide identity, lengths and leading bytes before the r
loc,
});
for (const bits of [32, 64] as const) {
const ir = emitLlvmModule(mod(order(false), order(true), same), { pointerBits: bits });
const ir = emitLlvmModule(mod(order, same), { pointerBits: bits });
const helper = (name: string) =>
new RegExp(`^define internal [^\\n]*@${name}(?:\\.i32)?\\([^]*?^}`, "m").exec(ir)![0];
const eq = helper("sc_str_eq");
expect(eq).toContain("icmp eq ptr %a, %b");
expect(eq).toMatch(/icmp eq i(32|64) %alen, %blen/);
expect(eq).toContain("call zeroext i1 @scr_str_eq(ptr %a, ptr %b)");
const bytes = helper("sc_str_cmp");
expect(bytes).toContain("@llvm.bswap.i64");
expect(bytes).not.toContain("%ascii");
expect(bytes).toContain("call i32 @scr_str_cmp(ptr %a, ptr %b)");
const units = helper("sc_str_cmp_u16");
expect(units).toContain("%wascii = icmp eq i64 %whigh, 0");
expect(units).toContain("%tascii = icmp eq i64 %thigh, 0");
expect(bytes).toContain("%tailable = and i1 %samelen, %short");
expect(units).toContain("%ascii = icmp sge i8 %bits, 0");
expect(units).toContain("call i32 @scr_str_cmp_u16(ptr %a, ptr %b)");
expect(units).toContain("%dx = phi i64 [ %aw, %word ], [ %at, %tail ]");
expect(units).toContain("%dlow = icmp eq i64 %whigh, 0");
expect(units).toContain("@llvm.bswap.i64");
expect(units).toContain("%dtz = call i64 @llvm.cttz.i64(i64 %dxor, i1 true)");
expect(units).toContain("%bx = phi i8 [ %x, %first ], [ %dxb, %locate ]");
expect(units).toContain("%blow = icmp ult i8 %bmin, -19");
expect(units).toContain("%kbelow = icmp ult i8 %xk, %yk");
expect(units).toContain("%below = icmp ult i8 %bx, %by");
expect(units).toContain("%tailable = and i1 %samelen, %short");
expect(units).toContain("%anyascii = icmp slt i32 %cws, 0");
expect(units).toContain("call i32 @scr_str_cmp(ptr %a, ptr %b)");
expect(units).toContain(
"call i32 @scr_str_cmp_u16_bytes(ptr %a, ptr %b) nounwind willreturn memory(argmem: read)",
);
}
});
@@ -25,6 +25,12 @@ export interface NativeLinkFeatures {
fileHandle: boolean;
fetch: boolean;
eventTarget?: boolean;
/** The program assigns process.emitWarning: links the
* SCR_WARNING_REPLACE variant of the dyn-async unit. */
warningReplace?: boolean;
/** The program uses process.stdin.setEncoding, string 'data' listeners
* or stdio 'resize' events: links the SCR_STDIO_EXTENDED events unit. */
stdioExtended?: boolean;
zlib: boolean;
assert: boolean;
inspect: boolean;
@@ -54,6 +60,9 @@ export interface NativeLinkFeatures {
* archive. */
cipher: boolean;
tlsCa: boolean;
/** crypto.subtle importKey/sign/verify/digest and CryptoKey values:
* scr_webcrypto.c. */
webcrypto: boolean;
}
export interface NativeLinkInfo {
@@ -118,6 +118,10 @@ export interface CcOptions {
* native fetch AbortSignal built on the same engine: compiles
* scr_event_target.c. */
eventTarget?: boolean;
/** SCR_WARNING_REPLACE: process.emitWarning replacement support. */
warningReplace?: boolean;
/** SCR_STDIO_EXTENDED: stdin encoding, string data listeners, resize. */
stdioExtended?: boolean;
/** The program uses the process-events surface (signal/exit listeners,
* stdin events, for-await over stdin — moduleUsesProcessEvents on the
* IR): compiles scr_events.c into the binary. Event-free binaries keep
@@ -203,6 +207,8 @@ export interface CcOptions {
/** crypto.createCipheriv/createDecipheriv: compiles scr_cipher.c and links
* the mbedTLS archive (shared with tls). */
cipher?: boolean;
/** crypto.subtle importKey/sign/verify/digest: compiles scr_webcrypto.c. */
webcrypto?: boolean;
/** Internal compiler hook: called only after a strict native artifact hit
* or a successful stable build has installed `outPath`. The executable
* frontend cache uses it to publish its stamp after native dependencies
@@ -445,6 +445,8 @@ export async function compileCInternal(
: [optimization === "dev" ? "-O0" : "-O2"]),
...executableSectionFlags.compile,
...(opts.textDecoderLegacy ? ["-DSCR_TEXT_DECODER_LEGACY"] : []),
...(opts.warningReplace ? ["-DSCR_WARNING_REPLACE"] : []),
...(opts.stdioExtended ? ["-DSCR_STDIO_EXTENDED"] : []),
...(opts.youngNursery ? ["-DSCR_YIN"] : []),
"-fno-math-errno",
// The runtime object model uses type-punned C: a hierarchy
@@ -579,6 +581,7 @@ export async function compileCInternal(
...(opts.watch ? [rt(join(rtDir, "scr_watch.c"))] : []),
...(opts.foreignFfi ? [rt(join(rtDir, "scr_ffi_queue.c"))] : []),
...(opts.nodeTest ? [rt(join(rtDir, "scr_test.c"))] : []),
...(opts.webcrypto ? [rt(join(rtDir, "scr_webcrypto.c"))] : []),
// The CA-store unit rides its own gate OR the tls one: scr_tls.c
// references its default-set override unconditionally.
...(tlsCa ? [rt(join(rtDir, "scr_tls_ca.c"))] : []),
@@ -645,6 +648,8 @@ export async function compileCInternal(
: [optimization === "dev" ? "-O0" : "-O2"]),
...executableSectionFlags.compile,
...(opts.textDecoderLegacy ? ["-DSCR_TEXT_DECODER_LEGACY"] : []),
...(opts.warningReplace ? ["-DSCR_WARNING_REPLACE"] : []),
...(opts.stdioExtended ? ["-DSCR_STDIO_EXTENDED"] : []),
...(opts.youngNursery ? ["-DSCR_YIN"] : []),
"-fno-math-errno",
"-fno-strict-aliasing", // the emitted object model type-puns — see buildArgs
@@ -83,6 +83,8 @@ export interface RuntimeFeatureSet extends NativeLinkFeatures {
youngNursery: boolean;
eventTarget: boolean;
dynTier: boolean;
warningReplace: boolean;
stdioExtended: boolean;
nativeFetch: boolean;
netEffective: boolean;
httpEffective: boolean;
@@ -271,6 +273,8 @@ export function effectiveRuntimeFeatures(
youngNursery: features.youngNursery === true,
eventTarget: features.eventTarget === true,
dynTier: features.dynTier === true,
warningReplace: features.warningReplace === true,
stdioExtended: features.stdioExtended === true,
nativeFetch,
netEffective,
httpEffective,
@@ -119,6 +119,7 @@ const features = {
tls: false,
tlsCa: false,
cipher: false,
webcrypto: false,
};
test.each([
@@ -56,6 +56,7 @@ const BASE: NativeLinkFeatures = {
tls: false,
tlsCa: false,
cipher: false,
webcrypto: false,
};
async function fixture(target: NativeTargetSpec = MACOS_ARM64_TARGET) {
@@ -61,6 +61,7 @@ export async function warmNativeCaches(
nodeTest: false,
tls: false,
cipher: false,
webcrypto: false,
tlsCa: false,
},
});
@@ -11,7 +11,8 @@ afterAll(() => rmSync(dir, { recursive: true, force: true }));
test("unchanged and comment-only reruns replay the verdict; code edits relower", () => {
const entry = join(dir, "main.ts");
const program = `const pairs: [number, number][] = [[1, 2]];\nconst [[a, b]] = pairs;\nconsole.log(a + b);\nconst { length } = [1, 2] as number[];\nconsole.log(length);\n`;
// The blocker: an object pattern over a union source (arrays alone compile).
const program = `const pairs: [number, number][] = [[1, 2]];\nconst [[a, b]] = pairs;\nconsole.log(a + b);\nconst { length } = (a > 9 ? [1, 2] : "ab") as number[] | string;\nconsole.log(length);\n`;
writeFileSync(entry, program);
const entries = new Map<string, string>();
const store: VerdictStore = {
@@ -57,6 +57,7 @@ const native: EarlyExecutableNativeFeatures = {
tls: false,
tlsCa: false,
cipher: false,
webcrypto: false,
};
async function fixture(): Promise<{
@@ -56,6 +56,8 @@ export interface EarlyExecutableNativeFeatures {
fileHandle: boolean;
fetch: boolean;
eventTarget?: boolean;
warningReplace?: boolean;
stdioExtended?: boolean;
zlib: boolean;
assert: boolean;
inspect: boolean;
@@ -81,6 +83,7 @@ export interface EarlyExecutableNativeFeatures {
tls: boolean;
cipher: boolean;
tlsCa: boolean;
webcrypto: boolean;
}
interface EarlyExecutableCacheStamp {
@@ -194,6 +197,7 @@ const BOOLEAN_NATIVE_KEYS = [
"tls",
"tlsCa",
"cipher",
"webcrypto",
] as const satisfies readonly (keyof EarlyExecutableNativeFeatures)[];
function validNativeFeatures(value: unknown): value is EarlyExecutableNativeFeatures {
@@ -109,7 +109,7 @@ function markerNamed(
if (!MARKERS.has(written)) return null;
let member = written;
const symbol = checker.getSymbolAtLocation(ident);
const decl = symbol ? checker.declarationsOf(symbol)[0] : undefined;
const decl = symbol ? symbol.declarations[0] : undefined;
if (decl === undefined) return null;
let importDecl: ts.Node | undefined;
if (ts.isImportSpecifier(decl) && !qualified) {
@@ -18,7 +18,7 @@ export function experimentalImportOf(
): ExperimentalMember | null {
const checker = program.getTypeChecker();
const symbol = checker.getSymbolAtLocation(ident);
const decl = symbol ? checker.declarationsOf(symbol)[0] : undefined;
const decl = symbol ? symbol.declarations[0] : undefined;
if (decl === undefined || !ts.isImportSpecifier(decl)) return null;
const importDecl = decl.parent?.parent?.parent;
if (
@@ -67,7 +67,7 @@ export function lifetimeRegionMemberOf(
): "run" | "release" | null {
const checker = program.getTypeChecker();
const symbol = checker.getSymbolAtLocation(access.name);
const decl = symbol ? checker.declarationsOf(symbol)[0] : undefined;
const decl = symbol ? symbol.declarations[0] : undefined;
if (decl === undefined || !ts.isMethodDeclaration(decl)) return null;
const owner = decl.parent;
if (
@@ -91,7 +91,7 @@ export function internTableMemberOf(
): InternTableMember | null {
const checker = program.getTypeChecker();
const symbol = checker.getSymbolAtLocation(access.name);
const decl = symbol ? checker.declarationsOf(symbol)[0] : undefined;
const decl = symbol ? symbol.declarations[0] : undefined;
if (
decl === undefined ||
!(
@@ -129,7 +129,7 @@ export function threadSlotsMemberOf(
): ThreadSlotsMember | null {
const checker = program.getTypeChecker();
const symbol = checker.getSymbolAtLocation(access.name);
const decl = symbol ? checker.declarationsOf(symbol)[0] : undefined;
const decl = symbol ? symbol.declarations[0] : undefined;
if (decl === undefined || !ts.isMethodDeclaration(decl)) return null;
const owner = decl.parent;
if (
+7 -10
View File
@@ -8,7 +8,7 @@ import { moduleSourceCandidates } from "./module-source-candidates.js";
export function isNodeGlobal(program: ts.Program, expression: ts.Identifier): boolean {
const checker = program.getTypeChecker();
const symbol = checker.getSymbolAtLocation(expression);
const declarations = symbol ? checker.declarationsOf(symbol) : [];
const declarations = symbol ? symbol.declarations : [];
return (
declarations.length > 0 &&
declarations.every((declaration) => {
@@ -45,7 +45,7 @@ export function isBuiltinMemberImport(
): boolean {
const checker = program.getTypeChecker();
const symbol = checker.getSymbolAtLocation(ident);
const decl = symbol ? checker.declarationsOf(symbol)[0] : undefined;
const decl = symbol ? symbol.declarations[0] : undefined;
if (decl !== undefined && ts.isImportSpecifier(decl)) {
const importDecl = decl.parent?.parent?.parent;
if (ts.isImportDeclaration(importDecl) && ts.isStringLiteral(importDecl.moduleSpecifier)) {
@@ -60,7 +60,7 @@ export function isBuiltinMemberImport(
}
if (symbol !== undefined && (symbol.flags & ts.SymbolFlags.Alias) !== 0) {
const target = checker.getAliasedSymbol(symbol);
const targetDecl = checker.declarationsOf(target)[0];
const targetDecl = target.declarations[0];
if (targetDecl !== undefined && targetDecl.getSourceFile().isDeclarationFile) {
for (
let parent: ts.Node | undefined = targetDecl.parent;
@@ -88,7 +88,7 @@ export function isBuiltinNamespaceImport(
): boolean {
const checker = program.getTypeChecker();
const symbol = checker.getSymbolAtLocation(ident);
const decl = symbol ? checker.declarationsOf(symbol)[0] : undefined;
const decl = symbol ? symbol.declarations[0] : undefined;
let importDecl: ts.ImportDeclaration | undefined;
if (decl !== undefined && ts.isNamespaceImport(decl)) {
const candidate = decl.parent?.parent;
@@ -108,12 +108,9 @@ function constInitializer(program: ts.Program, expr: ts.Expression): ts.Expressi
if (!ts.isIdentifier(current)) return null;
const symbol = program.getTypeChecker().getSymbolAtLocation(current);
const decl = symbol
? program
.getTypeChecker()
.declarationsOf(symbol)
.find((candidate): candidate is ts.VariableDeclaration =>
ts.isVariableDeclaration(candidate),
)
? symbol.declarations.find((candidate): candidate is ts.VariableDeclaration =>
ts.isVariableDeclaration(candidate),
)
: undefined;
if (
decl === undefined ||
@@ -25,15 +25,13 @@ export function inferredJsDiagnosticSuppressed(
const symbol = checker.getSymbolAtLocation(node.name);
if (
symbol &&
checker
.declarationsOf(symbol)
.some(
(decl) =>
ts.isPropertyDeclaration(decl) &&
!decl.type &&
!decl.initializer &&
!/@type\b/.test(file.text.slice(decl.pos, decl.getStart(file))),
)
symbol.declarations.some(
(decl) =>
ts.isPropertyDeclaration(decl) &&
!decl.type &&
!decl.initializer &&
!/@type\b/.test(file.text.slice(decl.pos, decl.getStart(file))),
)
) {
// Bare JS fields exist with value undefined before the first write;
// native storage includes that arm even when inference omits it.
@@ -43,7 +41,7 @@ export function inferredJsDiagnosticSuppressed(
}
if (diagnostic.code === 2554 && ts.isCallExpression(node)) {
const signature = checker.getResolvedSignature(node);
const declaration = signature && checker.signatureDeclaration(signature);
const declaration = signature && signature.declaration;
if (
declaration &&
isJsSourceFileName(declaration.getSourceFile().fileName) &&
@@ -83,9 +81,7 @@ export function inferredJsDiagnosticSuppressed(
checker.getTypeOfSymbolAtLocation(throwMethod, node.expression),
);
const declaration =
throwSignatures.length === 1
? checker.signatureDeclaration(throwSignatures[0]!)
: undefined;
throwSignatures.length === 1 ? throwSignatures[0]!.declaration : undefined;
// A sole throw cannot return an invalid IteratorResult. Avoid inferring
// termination through arbitrary control flow or explicit return types.
if (
@@ -176,7 +176,7 @@ export function assertInstanceOf(
): AssertInstanceOptions | null {
if (!ts.isIdentifier(expr)) return null;
const symbol = lowerer.checker.getSymbolAtLocation(expr);
const decl = symbol ? lowerer.checker.declarationsOf(symbol)[0] : undefined;
const decl = symbol ? symbol.declarations[0] : undefined;
if (decl === undefined || !ts.isVariableDeclaration(decl)) return null;
return staticAssertInstanceDecl(lowerer, decl.name, decl.initializer);
}
@@ -0,0 +1,78 @@
/* Type assertions of the global object.
*
* `globalThis as { [KEY]?: State }` is the process-global state idiom: a
* module asserts the global object to a record type and reads and writes
* its members, and every other module that asserts it sees the same
* members. Node evaluates the assertion to the global object itself.
*
* The global object is a checked-dynamic value, and a checked-dynamic value
* asserted to a record or array type takes the checked extraction
* (dynCheck), which builds a new native value. For the global object that
* copy is always wrong: a write through it never reached the global object,
* and a view bound before another module's write never saw it. So an
* assertion of the global object keeps the checked-dynamic value itself: a
* view whose member reads and writes go to the global object, and bindings
* of it hold that object.
*
* Other checked-dynamic operands keep the checked extraction: a native value
* held as `unknown` unwraps by identity there, and a checked-dynamic tree is
* copied at the documented checked-value boundary.
*/
import * as ts from "../ts7/adapter.js";
import type { IrType } from "../../ir/ir.js";
import type { Lowerer } from "./lowerer.js";
import { stdlibGlobalNameOf } from "./surfaces.js";
/** A target whose checked extraction builds a new mutable value. */
export function extractionCopiesReference(lowerer: Lowerer, type: IrType): boolean {
const seen = new Set<string>();
const copies = (t: IrType): boolean => {
if (t.kind === "record" || t.kind === "array") return true;
if (t.kind !== "union" || seen.has(t.unionId)) return false;
seen.add(t.unionId);
return lowerer.unions.get(t.unionId)?.arms.some(copies) ?? false;
};
return copies(type);
}
/** The asserted expression without parentheses, `!` and nested
* assertions (`globalThis as unknown as T`). */
function assertedSource(expr: ts.Expression): ts.Expression {
let e = expr;
while (
ts.isParenthesizedExpression(e) ||
ts.isNonNullExpression(e) ||
ts.isAsExpression(e) ||
ts.isTypeAssertion(e) ||
ts.isSatisfiesExpression(e)
)
e = e.expression;
return e;
}
/** `expr` asserts the global object (`globalThis`, `global`). */
export function assertsGlobalObject(
lowerer: Lowerer,
expr: ts.AsExpression | ts.TypeAssertion,
): boolean {
const name = stdlibGlobalNameOf(lowerer, assertedSource(expr.expression));
return name === "globalThis" || name === "global";
}
/** The assertion a binding initializer is, when it keeps the global object:
* bindings of it hold that object rather than a native value of the
* asserted type. A target with no native layout keeps the object as well
* (the assertion lowers to the checked-dynamic value either way). */
export function keptIdentityAssertion(
lowerer: Lowerer,
initializer: ts.Expression,
): ts.AsExpression | ts.TypeAssertion | undefined {
let e = initializer;
while (ts.isParenthesizedExpression(e)) e = e.expression;
if (!ts.isAsExpression(e) && !ts.isTypeAssertion(e)) return undefined;
if (!assertsGlobalObject(lowerer, e)) return undefined;
const target = lowerer.mapTypeOf(lowerer.checker.getTypeFromTypeNode(e.type));
return !target || target.kind === "dyn" || extractionCopiesReference(lowerer, target)
? e
: undefined;
}
@@ -114,7 +114,7 @@ export function nullishValueUnitOf(
const cached = lowerer.nullishBindings.get(sym);
if (cached !== undefined) return cached;
lowerer.nullishBindings.set(sym, null); // cycle guard: self-referential probes answer non-qualifying
const decl = lowerer.checker.valueDeclarationOf(sym);
const decl = sym.valueDeclaration;
// Statement-position declarators only: a for-loop head (`for (let x =
// null as any; ...)`) declares a LOCAL with per-iteration semantics —
// lowerVarDeclList's contract requires a lowered statement for it, so
@@ -0,0 +1,52 @@
/** A builtin module function used as a JavaScript value when it has no
* value adapter (lowerNativeCallableValue). Node hands the program a real
* function; the compiled value is one function per builtin that throws the
* named fence when called. Identity flows (`fn === fs.readFileSync`, Set
* membership) still hold, `typeof` answers "function" and inspection
* prints `[Function: name]`, where a string token would answer "string"
* and turn a call into an ordinary "is not a function" TypeError. */
import * as ts from "../ts7/adapter.js";
import { DYN, type IrExpr, type SrcLoc, funcOf } from "../../ir/ir.js";
import { unsupportedDiag } from "../../diagnostics/diagnostic.js";
import type { Lowerer } from "./lowerer.js";
export function builtinFunctionFenceValue(
lowerer: Lowerer,
bi: { module: string; member: string },
node: ts.Identifier,
loc: SrcLoc,
): IrExpr {
const display = `${bi.module}.${bi.member}`;
const name = `%builtin.fence.${display}`;
if (!lowerer.liftedFns.some((fn) => fn.name === name)) {
const diag = unsupportedDiag(
"SC1090",
loc,
`library functions as values ('${display}' has no function-value lowering; call it directly)`,
);
lowerer.runtimeFences.push(diag);
const line = ts.getLineAndCharacterOfPosition(node.getSourceFile(), loc.start).line + 1;
lowerer.liftedFns.push({
name,
params: [],
locals: [],
returnType: DYN,
body: [
{
kind: "runtimeFence",
code: diag.code,
message: `${diag.message} [${diag.code} at ${loc.file}:${line}]`,
loc,
},
],
loc,
});
}
return {
kind: "dynFrom",
value: { kind: "closure", fnName: name, captures: [], type: funcOf([], DYN), loc },
fnName: bi.member,
type: DYN,
loc,
};
}
@@ -34,7 +34,7 @@ export function lowerDiagnosticsSubscriber(lowerer: Lowerer, node: ts.Expression
// has no other first-class story.
if (ts.isIdentifier(node) && node.text === "setImmediate") {
const sym = lowerer.checker.getSymbolAtLocation(node);
const decls = sym ? lowerer.checker.declarationsOf(sym) : [];
const decls = sym ? sym.declarations : [];
if (decls.length > 0 && decls.every((d) => lowerer.isStdlibFile(d.getSourceFile()))) {
return {
kind: "libCall",
@@ -93,7 +93,7 @@ export function builtinClassAliasOwnsNoSlot(lowerer: Lowerer, symbol: ts.Symbol)
if (builtinClassAliasOf(lowerer, symbol) === null) return false;
const binding =
symbol.flags & ts.SymbolFlags.Alias ? lowerer.checker.getAliasedSymbol(symbol) : symbol;
const decl = lowerer.checker.valueDeclarationOf(binding);
const decl = binding.valueDeclaration;
if (decl === undefined || !ts.isVariableDeclaration(decl)) return false;
if (builtinMemberRequireDecl(decl.name, decl.initializer)) return false;
if (streamClassAliasDecl(lowerer, decl.name, decl.initializer)) return false;
@@ -189,7 +189,7 @@ export function sameBuiltinClass(a: BuiltinClassRef, b: BuiltinClassRef): boolea
* can name a class (an identifier or a property access), or null. */
function constInitializerOf(lowerer: Lowerer, symbol: ts.Symbol): ts.Expression | null {
if ((symbol.flags & ts.SymbolFlags.BlockScopedVariable) === 0) return null;
const decl = lowerer.checker.valueDeclarationOf(symbol);
const decl = symbol.valueDeclaration;
if (
decl === undefined ||
!ts.isVariableDeclaration(decl) ||
@@ -138,9 +138,7 @@ export function zlibStreamModeOf(
symbol.flags & ts.SymbolFlags.Alias ? lowerer.checker.getAliasedSymbol(symbol) : symbol;
const mode = ZLIB_STREAM_MODES.get(target.name);
if (mode === undefined) return null;
return lowerer.checker
.declarationsOf(target)
.some((d) => ts.isClassDeclaration(d) && isZlibFallbackDecl(lowerer, d))
return target.declarations.some((d) => ts.isClassDeclaration(d) && isZlibFallbackDecl(lowerer, d))
? mode
: null;
}
@@ -161,15 +159,13 @@ function isZlibFallbackDecl(lowerer: Lowerer, d: ts.Node): boolean {
export function isZlibStreamMember(lowerer: Lowerer, access: ts.PropertyAccessExpression): boolean {
const symbol = lowerer.checker.getSymbolAtLocation(access.name);
if (!symbol) return false;
return lowerer.checker
.declarationsOf(symbol)
.some(
(d) =>
d.parent !== undefined &&
ts.isClassDeclaration(d.parent) &&
d.parent.name?.text === "ZlibBase" &&
isZlibFallbackDecl(lowerer, d.parent),
);
return symbol.declarations.some(
(d) =>
d.parent !== undefined &&
ts.isClassDeclaration(d.parent) &&
d.parent.name?.text === "ZlibBase" &&
isZlibFallbackDecl(lowerer, d.parent),
);
}
/** `stream.bytesWritten` on a zlib stream. */
@@ -106,7 +106,7 @@ export function builtinConstantsDestructureDecl(
* its baked literal value. Null for every other binding. */
export function builtinConstantBindingOf(lowerer: Lowerer, ident: ts.Identifier): IrExpr | null {
const symbol = lowerer.checker.getSymbolAtLocation(ident);
const decl = symbol ? lowerer.checker.declarationsOf(symbol)[0] : undefined;
const decl = symbol ? symbol.declarations[0] : undefined;
if (!decl || !ts.isBindingElement(decl) || decl.dotDotDotToken || decl.initializer) return null;
if (!ts.isObjectBindingPattern(decl.parent)) return null;
const varDecl = decl.parent.parent;
@@ -879,7 +879,7 @@ export function isCipherType(lowerer: Lowerer, type: ts.Type): boolean {
return (
!!symbol &&
CIPHER_TYPE_NAMES.has(symbol.name) &&
lowerer.checker.declarationsOf(symbol).some((d) => lowerer.isStdlibFile(d.getSourceFile()))
symbol.declarations.some((d) => lowerer.isStdlibFile(d.getSourceFile()))
);
}
@@ -19,9 +19,9 @@ export function isExecExceptionType(lowerer: Lowerer, t: ts.Type): boolean {
const sym = t.getSymbol();
return (
sym?.name === "ExecException" &&
lowerer.checker
.declarationsOf(sym)
.some((d) => ts.isInterfaceDeclaration(d) && lowerer.isStdlibFile(d.getSourceFile()))
sym.declarations.some(
(d) => ts.isInterfaceDeclaration(d) && lowerer.isStdlibFile(d.getSourceFile()),
)
);
}
@@ -34,15 +34,13 @@ function isFsFallbackDecl(lowerer: Lowerer, d: ts.Node): boolean {
export function isFsStreamMember(lowerer: Lowerer, access: ts.PropertyAccessExpression): boolean {
const symbol = lowerer.checker.getSymbolAtLocation(access.name);
if (!symbol) return false;
return lowerer.checker
.declarationsOf(symbol)
.some(
(d) =>
d.parent !== undefined &&
ts.isClassDeclaration(d.parent) &&
(d.parent.name?.text === "ReadStream" || d.parent.name?.text === "WriteStream") &&
isFsFallbackDecl(lowerer, d.parent),
);
return symbol.declarations.some(
(d) =>
d.parent !== undefined &&
ts.isClassDeclaration(d.parent) &&
(d.parent.name?.text === "ReadStream" || d.parent.name?.text === "WriteStream") &&
isFsFallbackDecl(lowerer, d.parent),
);
}
/** bytesRead / bytesWritten / pending / path reads. Null for other
@@ -156,9 +154,9 @@ export function fsStreamClassOf(
symbol: ts.Symbol | null | undefined,
): "ReadStream" | "WriteStream" | null {
if (!symbol || (symbol.name !== "ReadStream" && symbol.name !== "WriteStream")) return null;
const declared = lowerer.checker
.declarationsOf(symbol)
.some((d) => ts.isClassDeclaration(d) && isFsFallbackDecl(lowerer, d));
const declared = symbol.declarations.some(
(d) => ts.isClassDeclaration(d) && isFsFallbackDecl(lowerer, d),
);
return declared ? symbol.name : null;
}
@@ -504,9 +504,7 @@ function jsonNullishArgument(lowerer: Lowerer, node: ts.Expression): boolean {
const symbol = lowerer.checker.getSymbolAtLocation(node);
return (
symbol !== undefined &&
lowerer.checker
.declarationsOf(symbol)
.every((decl) => lowerer.isStdlibFile(decl.getSourceFile()))
symbol.declarations.every((decl) => lowerer.isStdlibFile(decl.getSourceFile()))
);
}
@@ -28,7 +28,7 @@ export function builtinImportOf(
ts.isShorthandPropertyAssignment(ident.parent) && ident.parent.name === ident
? lowerer.checker.getShorthandAssignmentValueSymbol(ident.parent)
: lowerer.checker.getSymbolAtLocation(ident);
const decl = symbol ? lowerer.checker.declarationsOf(symbol)[0] : undefined;
const decl = symbol ? symbol.declarations[0] : undefined;
if (!decl) return null;
// The CommonJS twin of the named import: a destructured require
// binding (`const { readFileSync } = require("fs")`, renames via
@@ -123,7 +123,7 @@ export function builtinImportOf(
// keys the same tables under the builtin's own member name.
if (symbol !== undefined && (symbol.flags & ts.SymbolFlags.Alias) !== 0) {
const target = lowerer.checker.getAliasedSymbol(symbol);
const tdecl = lowerer.checker.declarationsOf(target)[0];
const tdecl = target.declarations[0];
if (tdecl !== undefined && tdecl.getSourceFile().isDeclarationFile) {
for (
let p: ts.Node | undefined = tdecl.parent;
@@ -153,7 +153,7 @@ export function esmImportBindingKind(
ident: ts.Identifier,
): "namespace" | "named" | null {
const symbol = lowerer.checker.getSymbolAtLocation(ident);
const decl = symbol ? lowerer.checker.declarationsOf(symbol)[0] : undefined;
const decl = symbol ? symbol.declarations[0] : undefined;
if (decl === undefined) return null;
if (ts.isNamespaceImport(decl)) return "namespace";
return ts.isImportSpecifier(decl) ? "named" : null;
@@ -290,7 +290,7 @@ export function createRequireCalleeFileOf(
}
if (!ts.isIdentifier(e)) return null;
const symbol = lowerer.checker.getSymbolAtLocation(e);
const decl = symbol ? lowerer.checker.declarationsOf(symbol)[0] : undefined;
const decl = symbol ? symbol.declarations[0] : undefined;
if (!decl || !ts.isVariableDeclaration(decl) || decl.initializer === undefined) return null;
return createRequireBindingDecl(lowerer, decl.name, decl.initializer)
? decl.getSourceFile()
@@ -391,7 +391,7 @@ export function createRequireProgramModuleDecl(
* binding poison with the same module-naming diagnostic. */
export function fencedBuiltinImportOf(lowerer: Lowerer, ident: ts.Identifier): string | null {
const symbol = lowerer.checker.getSymbolAtLocation(ident);
const decl = symbol ? lowerer.checker.declarationsOf(symbol)[0] : undefined;
const decl = symbol ? symbol.declarations[0] : undefined;
if (!decl) return null;
// The CommonJS twins: `const x = require("net")` and
// `const { createServer } = require("net")` — the require statement
@@ -0,0 +1,60 @@
/* Node 24's `navigator` global (lib/internal/navigator.js), the part
* libraries read to tell runtimes apart: `typeof navigator` is "object"
* and `navigator.userAgent` is `Node.js/<major>`. jose reads exactly
* that at module load (jwks/remote.js). Both fold to string literals, so
* nothing links. The other members (hardwareConcurrency, language,
* languages, platform, locks) and the object as a value keep a named
* refusal. */
import * as ts from "../../ts7/adapter.js";
import { strLit } from "../../../ir/build.js";
import type { IrExpr } from "../../../ir/ir.js";
import { isJsSourceFile, locOf } from "../../program.js";
import type { Lowerer } from "../lowerer.js";
/** True when `node` names Node's global `navigator`: the @types/node
* declaration, or, in a JavaScript file, the free name no declaration
* binds (Node defines it on the global object). */
export function isNavigatorGlobal(lowerer: Lowerer, node: ts.Expression): boolean {
let expr = node;
while (ts.isParenthesizedExpression(expr)) expr = expr.expression;
if (!ts.isIdentifier(expr) || expr.text !== "navigator") return false;
if (lowerer.isStdlibGlobal(expr, "navigator")) return true;
return (
isJsSourceFile(expr.getSourceFile()) && lowerer.checker.getSymbolAtLocation(expr) === undefined
);
}
/** `navigator.userAgent` (`Node.js/<major>` of `nodeVersion`, the pinned
* release builtins/process.ts names), or the named refusal for another
* member. Null when the receiver is not the global. */
export function lowerNavigatorMember(
lowerer: Lowerer,
access: ts.PropertyAccessExpression,
nodeVersion: string,
): IrExpr | null {
if (!isNavigatorGlobal(lowerer, access.expression)) return null;
if (access.name.text === "userAgent") {
return strLit(`Node.js/${nodeVersion.split(".")[0]}`, locOf(access));
}
return refuseNavigator(
lowerer,
access,
`navigator.${access.name.text}`,
lowerer.checker.getSymbolAtLocation(access.name),
);
}
/** The refusal for the `navigator` object itself or an unlowered member. */
export function refuseNavigator(
lowerer: Lowerer,
node: ts.Node,
what: string,
symbol?: ts.Symbol,
): never {
return lowerer.noLowering(
what,
node,
"of Node's navigator global only typeof navigator and navigator.userAgent compile",
symbol ?? null,
);
}
@@ -26,6 +26,7 @@ import {
funcOf,
} from "../../../ir/ir.js";
import { lowerBuiltinLoaderValue } from "../lower-builtin-values.js";
import { canonicalBuiltinModule } from "../../builtin-modules.js";
import { lowerProcessIpcSend, ipcMessageListener, ipcDisconnectListener } from "./ipc.js";
import { lowerDiagnosticsSubscriber, lowerTracingArguments } from "./async-context.js";
@@ -148,6 +149,18 @@ export function lowerProcessStreamProperty(
loc: locOf(expr),
};
}
// `if (process.stdin.setRawMode)`: Node's stdin has the method only
// when it is a TTY. The runtime's stream object answers the same
// presence (the method or undefined); calls keep their direct lowering.
if (direct === "stdin" && member === "setRawMode") {
return {
kind: "dynKeyGet",
value: lowerer.lowerExpr(expr.expression),
key: strLit(member, locOf(expr.name)),
type: DYN,
loc: locOf(expr),
};
}
if (member !== "isTTY" && member !== "columns" && member !== "rows") return null;
let recv: ts.Expression = expr.expression;
while (ts.isParenthesizedExpression(recv) || ts.isAsExpression(recv) || ts.isTypeAssertion(recv))
@@ -364,6 +377,11 @@ export function lowerProcessProperty(
if (member === "versions") {
return { kind: "libCall", fn: "process.versions", args: [], type: DYN, loc };
}
// process.features as a value (`"sea" in process.features`): an object
// holding none of the flags, consistent with the member reads above.
if (member === "features") {
return { kind: "dynObjLit", type: DYN, loc };
}
// process.noDeprecation / process.throwDeprecation: the values the
// program assigned (undefined before that — no Node flags reach a
// compiled binary).
@@ -627,6 +645,73 @@ export function lowerProcessMethodCall(
}
return { kind: "libCall", fn: "process.stdinSetRawMode", args: [mode], type: VOID, loc };
}
// process.stdout.on("resize", cb) (and once/off/removeListener/
// addListener, stderr too): the terminal resize event, registered on the
// runtime's process stream object (a SIGWINCH listener that emits when a
// TTY's size changed; it never keeps the loop alive). Statement position:
// Node returns the stream.
if (
["on", "once", "off", "removeListener", "addListener"].includes(access.name.text) &&
ts.isPropertyAccessExpression(access.expression) &&
call.arguments.nodes.length === 2 &&
!call.arguments.nodes.some(ts.isSpreadElement) &&
ts.isExpressionStatement(call.parent)
) {
const stream = lowerer.stdlibGlobalMember(access.expression, "process");
const evT = lowerer.typeOf(call.arguments.nodes[0]!);
if (
(stream === "stdout" || stream === "stderr") &&
evT.isStringLiteralType() &&
evT.value === "resize"
) {
const loc = locOf(call);
const receiverLocal = lowerer.declareHiddenLocal("%stdioReceiver", DYN);
const receiver = varRef(receiverLocal.id, DYN, loc);
const method =
access.name.text === "off"
? "removeListener"
: access.name.text === "addListener"
? "on"
: access.name.text;
return {
kind: "seqExpr",
stmts: [
{
kind: "varDecl",
localId: receiverLocal.id,
init: {
kind: "libCall",
fn: "process.stdio",
args: [numLit(stream === "stdout" ? 1 : 2, loc)],
type: DYN,
loc,
},
loc,
},
],
result: {
kind: "dynCall",
callee: {
kind: "dynKeyGet",
value: receiver,
key: strLit(method, locOf(access.name)),
type: DYN,
loc,
},
receiver,
calleeName: access.getText(),
args: [
lowerer.lowerExprExpecting(call.arguments.nodes[0]!, DYN),
lowerer.lowerExprExpecting(call.arguments.nodes[1]!, DYN),
],
type: DYN,
loc,
},
type: DYN,
loc,
};
}
}
// process.stdin.on/once("data" | "end" | "error", cb): the piped-stdin
// event slice. A 'data' listener keeps the event loop alive until EOF
// (Node's flowing stdin); 'end'/'error' listeners alone do not. `once`
@@ -678,11 +763,18 @@ export function lowerProcessMethodCall(
const param = cb.type.params[0];
const onceArg: IrExpr = { kind: "boolLit", value: once, type: BOOL, loc };
if (event === "data") {
if (param !== undefined && !(param.kind === "bytes" && param.elem === "u8")) {
// A string parameter receives the chunks process.stdin.setEncoding
// decodes (pi-tui's terminal input); the runtime refuses a delivery
// while no encoding is set, where Node would pass a Buffer.
if (
param !== undefined &&
!(param.kind === "bytes" && param.elem === "u8") &&
param.kind !== "string"
) {
lowerer.unsupported(
"SC1090",
call.arguments.nodes[1]!,
`data listeners whose parameter is not 'Uint8Array' (got '${lowerer.fmt(param)}')`,
`data listeners whose parameter is not 'Uint8Array' or 'string' (got '${lowerer.fmt(param)}')`,
);
}
return { kind: "libCall", fn: "stdin.onData", args: [cb, onceArg], type: VOID, loc };
@@ -1537,3 +1629,36 @@ function processCredentialCall(member: string, loc: SrcLoc): IrExpr {
loc,
};
}
/** `process.getBuiltinModule("node:os")`: a string-literal id naming a
* supported builtin whose call the checker types as that module's
* namespace (@types/node's BuiltInModule map) answers the namespace value
* `import * as ns` binds, so its members key the builtin tables at their
* use sites. The receiver may be cast (`(process as P)`) and the call
* optional: Node 24's process always has the member, and the cast changes
* no runtime value. Other ids keep the dynamic loader (undefined for
* unknown ids, Node's argument errors). */
export function lowerProcessBuiltinModuleNamespace(
lowerer: Lowerer,
call: ts.CallExpression,
): IrExpr | null {
const callee = call.expression;
if (!ts.isPropertyAccessExpression(callee) || callee.name.text !== "getBuiltinModule")
return null;
const id = call.arguments.nodes[0];
if (call.arguments.nodes.length !== 1 || !id || !ts.isStringLiteralLike(id)) return null;
let receiver: ts.Expression = callee.expression;
while (
ts.isParenthesizedExpression(receiver) ||
ts.isAsExpression(receiver) ||
ts.isSatisfiesExpression(receiver) ||
ts.isNonNullExpression(receiver)
)
receiver = receiver.expression;
if (!lowerer.isStdlibGlobal(receiver, "process")) return null;
const module = canonicalBuiltinModule(id.text);
if (module === null) return null;
const type = lowerer.mapTypeOf(lowerer.checker.getNonNullableType(lowerer.typeOf(call)));
if (type?.kind !== "moduleNs" || type.moduleId !== `builtin:${module}`) return null;
return { kind: "moduleNsRef", moduleId: type.moduleId, type, loc: locOf(call) };
}
@@ -46,7 +46,7 @@ export function isReadlineTyped(lowerer: Lowerer, node: ts.Expression): boolean
const t = lowerer.checker.getTypeAtLocation(node);
const sym = t.getAliasSymbol() ?? t.getSymbol();
if (sym?.name !== "Interface") return false;
return lowerer.checker.declarationsOf(sym).some((d) => {
return sym.declarations.some((d) => {
if (!ts.isClassDeclaration(d) && !ts.isInterfaceDeclaration(d)) return false;
if (!lowerer.isStdlibFile(d.getSourceFile())) return false;
return isReadlineModule(ambientModuleOf(d));
@@ -107,7 +107,7 @@ function isReadlineUiTyped(lowerer: Lowerer, node: ts.Expression): boolean {
const t = lowerer.checker.getTypeAtLocation(node);
const sym = t.getAliasSymbol() ?? t.getSymbol();
if (sym?.name !== "Readline") return false;
return lowerer.checker.declarationsOf(sym).some((d) => {
return sym.declarations.some((d) => {
if (!ts.isClassDeclaration(d) && !ts.isInterfaceDeclaration(d)) return false;
if (!lowerer.isStdlibFile(d.getSourceFile())) return false;
const spec = ambientModuleOf(d);
@@ -11,11 +11,9 @@ export function isBuiltinClassInstance(
const type = lowerer.checker.getTypeAtLocation(node);
const symbol = type.getAliasSymbol() ?? type.getSymbol();
if (symbol?.name !== name) return false;
return lowerer.checker
.declarationsOf(symbol)
.some(
(declaration) =>
(ts.isClassDeclaration(declaration) || ts.isInterfaceDeclaration(declaration)) &&
lowerer.isStdlibFile(declaration.getSourceFile()),
);
return symbol.declarations.some(
(declaration) =>
(ts.isClassDeclaration(declaration) || ts.isInterfaceDeclaration(declaration)) &&
lowerer.isStdlibFile(declaration.getSourceFile()),
);
}
@@ -4,7 +4,19 @@ import { type Lowerer } from "../lowerer.js";
import { locOf } from "../../program.js";
import { lowerOptionalArgument, lowerStringSearchArgument } from "../optional-arguments.js";
import { lowerToNumberArgument } from "../lower-exprs.js";
import { F64, type IrExpr, STRING, arrayOf, isUnitType, typeEquals } from "../../../ir/ir.js";
import {
BOOL,
DYN,
F64,
type IrExpr,
type IrStmt,
NULL_T,
STRING,
arrayOf,
bytesOf,
isUnitType,
typeEquals,
} from "../../../ir/ir.js";
/** `String.fromCharCode/fromCodePoint(...codes)` on THE String global: every argument
* lowers as a number and packs into ONE f64[] array-literal argument
@@ -17,6 +29,8 @@ export function lowerStringStaticCall(
access: ts.PropertyAccessExpression,
): IrExpr | null {
if (call.questionDotToken) return null;
const applied = lowerFromCharCodeApply(lowerer, call, access);
if (applied) return applied;
const member = lowerer.stdlibGlobalMember(access, "String");
if (member !== "fromCharCode" && member !== "fromCodePoint" && member !== "raw") return null;
const loc = locOf(call);
@@ -108,6 +122,99 @@ export function lowerStringStaticCall(
return { kind: "libCall", fn: `string.${member}`, args: [packed], type: STRING, loc };
}
/** `String.fromCharCode.apply(thisArg, codes)`, the btoa chunking idiom
* (`String.fromCharCode.apply(null, bytes.subarray(i, j))`): the receiver
* is ignored, and a typed array or number array forwards to the packed
* fromCharCode like a spread. An untyped list reads a Uint8Array the same
* way at run time; any other value keeps Function.prototype.apply's
* generic array-like call. Null for every other call. */
function lowerFromCharCodeApply(
lowerer: Lowerer,
call: ts.CallExpression,
access: ts.PropertyAccessExpression,
): IrExpr | null {
if (access.name.text !== "apply" || access.questionDotToken) return null;
const target = access.expression;
if (
!ts.isPropertyAccessExpression(target) ||
lowerer.stdlibGlobalMember(target, "String") !== "fromCharCode"
) {
return null;
}
if (call.arguments.nodes.length !== 2 || call.arguments.nodes.some(ts.isSpreadElement))
return null;
const listNode = call.arguments.nodes[1]!;
const checkerT = lowerer.typeOf(listNode);
const untyped = (checkerT.flags & (ts.TypeFlags.Any | ts.TypeFlags.Unknown)) !== 0;
const listT = untyped ? DYN : lowerer.mapTypeOf(checkerT);
if (
listT === null ||
(listT.kind !== "bytes" && listT.kind !== "dyn" && !typeEquals(listT, arrayOf(F64)))
) {
return null;
}
const loc = locOf(call);
const thisArg = lowerer.lowerExprExpecting(call.arguments.nodes[0]!, DYN);
const stmts: IrStmt[] = [{ kind: "exprStmt", expr: thisArg, loc: thisArg.loc }];
if (listT.kind !== "dyn") {
const list = lowerer.lowerExprExpecting(listNode, listT);
const result: IrExpr = {
kind: "libCall",
fn: "string.fromCharCode",
args: [list],
type: STRING,
loc,
};
return { kind: "seqExpr", stmts, result, type: STRING, loc };
}
const listLocal = lowerer.declareHiddenLocal("%charCodes", DYN);
stmts.push({
kind: "varDecl",
localId: listLocal.id,
init: lowerer.lowerExprExpecting(listNode, DYN),
loc,
});
const list = (): IrExpr => ({ kind: "varRef", localId: listLocal.id, type: DYN, loc });
const bytes = bytesOf("u8");
const generic: IrExpr = {
kind: "dynCheck",
value: {
kind: "dynInvoke",
recv: lowerer.lowerExprExpecting(target, DYN),
method: "apply",
calleeName: "String.fromCharCode.apply",
args: [
{
kind: "dynFrom",
value: { kind: "unitLit", unit: "null", type: NULL_T, loc },
type: DYN,
loc,
},
list(),
],
type: DYN,
loc,
},
type: STRING,
loc,
};
const result: IrExpr = {
kind: "ternary",
cond: { kind: "dynTest", test: "bytes", bytesElem: "u8", value: list(), type: BOOL, loc },
then: {
kind: "libCall",
fn: "string.fromCharCode",
args: [{ kind: "dynCheck", value: list(), type: bytes, loc }],
type: STRING,
loc,
},
else_: generic,
type: STRING,
loc,
};
return { kind: "seqExpr", stmts, result, type: STRING, loc };
}
/** `s.lastIndexOf(searchValue?, position?)` on string receivers, using UTF-16
* indices. An omitted search value searches for "undefined". Omitted or undefined positions clamp to the string's end;
* MAX_SAFE_INTEGER has the same effect for every representable string.
@@ -361,13 +361,11 @@ export function storedTextCodecClassOf(
): TextCodecCtor["cls"] | null {
const sym = lowerer.typeOf(expr).getSymbol();
if (!sym || (sym.name !== "TextEncoder" && sym.name !== "TextDecoder")) return null;
return lowerer.checker
.declarationsOf(sym)
.some(
(d) =>
(ts.isInterfaceDeclaration(d) || ts.isClassDeclaration(d)) &&
lowerer.isStdlibFile(d.getSourceFile()),
)
return sym.declarations.some(
(d) =>
(ts.isInterfaceDeclaration(d) || ts.isClassDeclaration(d)) &&
lowerer.isStdlibFile(d.getSourceFile()),
)
? sym.name
: null;
}
@@ -155,9 +155,9 @@ function isTimerHandleTyped(
const t = lowerer.checker.getTypeAtLocation(node);
const sym = t.getAliasSymbol() ?? t.getSymbol();
if (sym?.name !== name) return false;
return lowerer.checker
.declarationsOf(sym)
.some((d) => ts.isInterfaceDeclaration(d) && lowerer.isStdlibFile(d.getSourceFile()));
return sym.declarations.some(
(d) => ts.isInterfaceDeclaration(d) && lowerer.isStdlibFile(d.getSourceFile()),
);
}
function isTimeoutTyped(lowerer: Lowerer, node: ts.Expression): boolean {
return isTimerHandleTyped(lowerer, node, "Timeout");
@@ -0,0 +1,130 @@
import * as ts from "../../ts7/adapter.js";
import { type Lowerer } from "../lowerer.js";
import { isJsSourceFile, locOf } from "../../program.js";
import { BOOL, DYN, type IrExpr, type IrLibFn } from "../../../ir/ir.js";
import { lowerDynObjectLiteral } from "../expressions/object-literals.js";
import { stdlibGlobalNameOf } from "../surfaces.js";
import { WEBCRYPTO_DYN_TYPES } from "../../type-mapper.js";
/* WebCrypto's HMAC slice (scr_webcrypto.c): `crypto.subtle.importKey`,
* `sign`, `verify` and `digest` called on the crypto global (also through
* globalThis) lower to runtime entry points that take the argument vector
* as one dyn array and answer a promise, so Web IDL conversion, algorithm
* normalization and every error stay Node-shaped at run time. CryptoKey
* values are native handles; their type maps to dyn and their members
* are handle reads. Other SubtleCrypto methods are refused by name. */
const SUBTLE_LOWERED: ReadonlyMap<string, IrLibFn> = new Map([
["importKey", "webcrypto.importKey"],
["sign", "webcrypto.sign"],
["verify", "webcrypto.verify"],
["digest", "webcrypto.digest"],
]);
const SUBTLE_HINT =
"the native WebCrypto surface is crypto.subtle.importKey('raw', ...) for HMAC keys, sign and verify with HMAC, and digest with SHA-1, SHA-256, SHA-384 or SHA-512, each called directly on crypto.subtle";
/** Whether `node` is `crypto.subtle` on the WebCrypto global. */
function isSubtleReceiver(lowerer: Lowerer, node: ts.Expression): boolean {
while (ts.isParenthesizedExpression(node)) node = node.expression;
return (
ts.isPropertyAccessExpression(node) &&
!node.questionDotToken &&
node.name.text === "subtle" &&
stdlibGlobalNameOf(lowerer, node.expression) === "crypto"
);
}
/** `crypto.subtle.<method>(...args)`: the lowered methods become their
* libCall over the argument vector; every other method is refused. */
export function lowerWebCryptoCall(lowerer: Lowerer, call: ts.CallExpression): IrExpr | null {
const callee = call.expression;
if (!ts.isPropertyAccessExpression(callee) || !isSubtleReceiver(lowerer, callee.expression)) {
return null;
}
const member = callee.name.text;
const fn = SUBTLE_LOWERED.get(member);
if (fn === undefined) lowerer.noLowering(`crypto.subtle.${member}`, callee, SUBTLE_HINT);
if (callee.questionDotToken || call.questionDotToken) {
lowerer.noLowering(`optional calls of crypto.subtle.${member}`, call, SUBTLE_HINT);
}
if (call.arguments.nodes.some(ts.isSpreadElement)) {
lowerer.noLowering(`crypto.subtle.${member} with spread arguments`, call, SUBTLE_HINT);
}
// JsonWebKey imports are not implemented: a literal 'jwk' format is
// refused here (the runtime refuses a computed one).
const format = call.arguments.nodes[0];
if (
member === "importKey" &&
format !== undefined &&
ts.isStringLiteralLike(format) &&
format.text === "jwk"
) {
lowerer.noLowering("crypto.subtle.importKey with the 'jwk' key format", format, SUBTLE_HINT);
}
const loc = locOf(call);
const args = call.arguments.nodes.map((arg) =>
ts.isObjectLiteralExpression(arg)
? lowerDynObjectLiteral(lowerer, arg)
: lowerer.lowerExprExpecting(arg, DYN),
);
return {
kind: "libCall",
fn,
args: [{ kind: "dynArrLit", elems: args, type: DYN, loc }],
type: { kind: "promise", inner: member === "verify" ? BOOL : DYN },
loc,
};
}
/** `v instanceof CryptoKey` against the global. */
export function lowerCryptoKeyInstanceof(
lowerer: Lowerer,
expr: ts.BinaryExpression,
): IrExpr | null {
if (stdlibGlobalNameOf(lowerer, expr.right) !== "CryptoKey") return null;
const value = lowerer.coerceInto(expr.left, lowerer.lowerExpr(expr.left), DYN);
return {
kind: "libCall",
fn: "webcrypto.isCryptoKey",
args: [value],
type: BOOL,
loc: locOf(expr),
};
}
/** TypeScript reads of `crypto.subtle` and its members outside a direct
* call: the global's dynamic object does not carry the lowered methods,
* so a detached read is refused rather than answering a value without
* them. JavaScript keeps the dynamic object's reads. */
export function fenceWebCryptoMember(
lowerer: Lowerer,
access: ts.PropertyAccessExpression | ts.ElementAccessExpression,
use: "read" | "call",
): IrExpr | null {
if (use !== "read" || !ts.isPropertyAccessExpression(access)) return null;
if (isJsSourceFile(access.getSourceFile())) return null;
if (isSubtleReceiver(lowerer, access)) {
lowerer.noLowering("crypto.subtle as a value", access, SUBTLE_HINT);
}
if (isSubtleReceiver(lowerer, access.expression)) {
lowerer.noLowering(`crypto.subtle.${access.name.text} as a value`, access, SUBTLE_HINT);
}
return null;
}
/** Whether a checker type is one of the WebCrypto interfaces reached
* through the crypto global's members (SubtleCrypto, CryptoKey and the
* algorithm dictionaries). While `crypto` was typed `unknown` those member
* reads were untyped, so JavaScript destructurings of them keep the
* module-global path untyped bindings take. `Crypto` itself is excluded:
* the global was `unknown`, which already mapped to dyn. */
export function isWebCryptoMemberType(lowerer: Lowerer, type: ts.Type): boolean {
const symbol = type.getAliasSymbol() ?? type.getSymbol();
return (
symbol !== undefined &&
symbol.name !== "Crypto" &&
WEBCRYPTO_DYN_TYPES.has(symbol.name) &&
lowerer.isStdlibSymbol(symbol)
);
}
@@ -175,7 +175,7 @@ function isWorkerThreadsClass(lowerer: Lowerer, node: ts.Expression, name: strin
const t = lowerer.checker.getTypeAtLocation(node);
const sym = t.getSymbol();
if (sym?.name !== name) return false;
return lowerer.checker.declarationsOf(sym).some((d) => {
return sym.declarations.some((d) => {
if (!ts.isClassDeclaration(d) && !ts.isInterfaceDeclaration(d)) return false;
if (!lowerer.isStdlibFile(d.getSourceFile())) return false;
let up: ts.Node | undefined = d.parent;
@@ -408,7 +408,15 @@ export function paramShapes(
return { type: mapped, mode: "rest" };
}
if (declParam.initializer) return defaultParameterShape(lowerer, declParam, mapped);
if (declParam.questionToken && !lowerer.bareUndefinedArmedUnion(mapped)) {
// A checked-value slot holds undefined directly, so an omitted
// argument needs no union arm (paramShape's declared-signature rule;
// an erased type parameter and a class-implemented interface view
// both map here).
if (
declParam.questionToken &&
!lowerer.bareUndefinedArmedUnion(mapped) &&
mapped.kind !== "dyn"
) {
lowerer.unsupported(
"SC1090",
declParam,
@@ -878,8 +886,7 @@ export function lambdaSignature(
const ctxSigs = ctxType
? lowerer.checker.getCallSignatures(lowerer.checker.getNonNullableType(ctxType))
: [];
const ctxDecl =
ctxSigs.length === 1 ? lowerer.checker.signatureDeclaration(ctxSigs[0]!) : undefined;
const ctxDecl = ctxSigs.length === 1 ? ctxSigs[0]!.declaration : undefined;
const ctxRetRaw =
ctxDecl && !ctxDecl.getSourceFile().isDeclarationFile
? lowerer.mapTypeOf(lowerer.checker.getReturnTypeOfSignature(ctxSigs[0]!))
@@ -37,7 +37,7 @@ export function isCjsExportsIdentifier(lowerer: Lowerer, node: ts.Expression): b
if (!ts.isIdentifier(node) || node.text !== "exports") return false;
if (!isCjsJsFile(node.getSourceFile(), lowerer.program)) return false;
const symbol = lowerer.checker.getSymbolAtLocation(node);
const decls = symbol ? lowerer.checker.declarationsOf(symbol) : [];
const decls = symbol ? symbol.declarations : [];
return !decls.some(
(d) =>
!d.getSourceFile().isDeclarationFile &&
@@ -349,7 +349,7 @@ function functionUsesThis(fn: ts.FunctionLikeDeclaration): boolean {
/** A function declaration, or a variable initialized with a function or
* arrow expression, that never reads `this`. */
function symbolIsThisFreeFunction(lowerer: Lowerer, sym: ts.Symbol | undefined): boolean {
const decls = sym ? lowerer.checker.declarationsOf(sym) : [];
const decls = sym ? sym.declarations : [];
const fn = decls.find(ts.isFunctionDeclaration);
if (fn !== undefined) return !functionUsesThis(fn);
const v = decls.find(ts.isVariableDeclaration);
@@ -353,7 +353,7 @@ export function cjsExportValueByName(
if (sym.flags & ts.SymbolFlags.Alias) sym = lowerer.checker.getAliasedSymbol(sym);
const direct = lowerer.globalsBySymbol.get(sym);
if (direct) return { kind: "varRef", localId: direct.id, type: direct.type, loc: locOf(blame) };
for (const d of lowerer.checker.declarationsOf(sym)) {
for (const d of sym.declarations) {
const byDecl = lowerer.globalsByDeclNode.get(d);
if (byDecl) return { kind: "varRef", localId: byDecl.id, type: byDecl.type, loc: locOf(blame) };
// Alias plumbing: a shorthand/identifier table entry or a member
@@ -132,7 +132,7 @@ export function tscInteropBindingOf(
): TscInteropBinding | null {
if (!ts.isIdentifier(expr)) return null;
const sym = lowerer.checker.getSymbolAtLocation(expr);
const decl = sym ? lowerer.checker.declarationsOf(sym).find(ts.isVariableDeclaration) : undefined;
const decl = sym ? sym.declarations.find(ts.isVariableDeclaration) : undefined;
return decl ? tscInteropBindingOfDecl(lowerer, decl) : null;
}
@@ -1,4 +1,5 @@
import { dynUndefinedExpr, varRef } from "../../ir/build.js";
import { constructedValue } from "./constructor-returns.js";
import {
BOOL,
DYN,
@@ -389,13 +390,13 @@ export class ClassConstructionDispatch {
if (!args) continue;
const type: IrType = { kind: "classval", className: name };
const callee = varRef("p.0", DYN, loc);
const value: IrExpr = {
const value = constructedValue(lowerer, info, {
kind: "newValue",
callee: { kind: "dynCheck", value: callee, type, loc },
args,
type: { kind: "object", className: name },
loc,
};
});
const body: IrStmt[] = [
{ kind: "return", value: lowerer.coerceToExpected(value, DYN), loc },
];
@@ -1,4 +1,5 @@
import { dynUndefinedExpr, helperLoc, nodeThrowExpr, varRef } from "../../ir/build.js";
import { constructedValue } from "./constructor-returns.js";
import { eventTargetApiMembers, eventTargetInheritedMember } from "./lower-event-target.js";
import {
BOOL,
@@ -2883,7 +2884,7 @@ export class ClassDynamicDispatch {
if (info.symbolMethods)
for (const [symbol, name] of info.symbolMethods) symbolEntries.push([symbol, name]);
for (const [symbol, name] of symbolEntries) {
const declaration = lowerer.checker.valueDeclarationOf(symbol);
const declaration = symbol.valueDeclaration;
if (
declaration &&
ts.isVariableDeclaration(declaration) &&
@@ -3164,13 +3165,13 @@ export class ClassDynamicDispatch {
}
if (!supported || !info.decl) continue;
const cls = classValueRef(lowerer, info, info.decl);
const result: IrExpr = {
const result = constructedValue(lowerer, info, {
kind: "new",
className: name,
args,
type: { kind: "object", className: name },
loc,
};
});
this.constructDispatch.body.unshift({
kind: "if",
cond: {
@@ -93,7 +93,7 @@ export function collectClassMethodMutations(
value = unwrap(value);
if (ts.isIdentifier(value)) {
const symbol = lowerer.checker.getSymbolAtLocation(value);
const decl = symbol && lowerer.checker.declarationsOf(symbol)[0];
const decl = symbol && symbol.declarations[0];
if (
!decl ||
!ts.isVariableDeclaration(decl) ||
@@ -162,7 +162,7 @@ export function collectClassMethodMutations(
const literalLoopKeys = (key: ts.Expression): (string | number)[] | null => {
if (!ts.isIdentifier(key)) return null;
const symbol = lowerer.checker.getSymbolAtLocation(key);
const decl = symbol && lowerer.checker.declarationsOf(symbol)[0];
const decl = symbol && symbol.declarations[0];
if (!decl || !ts.isVariableDeclaration(decl) || !ts.isIdentifier(decl.name)) return null;
const list = decl.parent;
if (!ts.isVariableDeclarationList(list) || !(list.flags & ts.NodeFlags.Const)) return null;
@@ -413,7 +413,7 @@ export function collectClassMethodMutations(
const symbol = ts.isPropertyAccessExpression(access)
? lowerer.checker.getSymbolAtLocation(access.name)
: lowerer.checker.getPropertyOfType(lowerer.typeOf(access.expression), name);
if (symbol && lowerer.checker.declarationsOf(symbol).some(ts.isMethodDeclaration))
if (symbol && symbol.declarations.some(ts.isMethodDeclaration))
lowerer.prototypeMethodAccesses.set(name, access);
}
lowerer.checker.prefetchSymbolRoots([...globals, ...reflection]);
@@ -480,8 +480,7 @@ export function collectClassMethodMutations(
}
const type = lowerer.checker.getDeclaredTypeOfSymbol(symbol);
const property = lowerer.checker.getPropertyOfType(type, name);
if (!property || !lowerer.checker.declarationsOf(property).some(ts.isMethodDeclaration))
continue;
if (!property || !property.declarations.some(ts.isMethodDeclaration)) continue;
if (cls.typeParameters !== undefined && cls.typeParameters.nodes.length > 0) {
owners = null;
break;
@@ -99,7 +99,7 @@ function ownedBinding(lowerer: Lowerer, id: ts.Identifier): boolean {
if (!symbol) return false;
const cached = lowerer.protocolOwnedBindings.get(symbol);
if (cached !== undefined) return cached;
const decl = lowerer.checker.valueDeclarationOf(symbol);
const decl = symbol.valueDeclaration;
let owned =
decl !== undefined &&
decl.getSourceFile() === id.getSourceFile() &&
@@ -191,7 +191,7 @@ function protocolSignature(
const recvTs = lowerer.typeOf(access.expression);
const property = lowerer.checker.getPropertyOfType(recvTs, access.name.text);
if (!property) return null;
const declarations = lowerer.checker.declarationsOf(property);
const declarations = property.declarations;
if (
declarations.length === 0 ||
!declarations.every(
@@ -200,7 +200,7 @@ function protocolSignature(
)
return null;
const signature = lowerer.checker.getResolvedSignature(call);
const declaration = signature && lowerer.checker.signatureDeclaration(signature);
const declaration = signature && signature.declaration;
if (
!signature ||
!declaration ||
@@ -490,7 +490,7 @@ export function lowerProtocolPropertyRead(
if (!lowerer.classImplementedProtocol(receiverType)) return null;
const name = access.name.text;
const property = lowerer.checker.getPropertyOfType(receiverType, name);
const declarations = property ? lowerer.checker.declarationsOf(property) : [];
const declarations = property ? property.declarations : [];
if (declarations.length === 0 || !declarations.every((decl) => ts.isPropertySignature(decl)))
return null;
const result = lowerer.mapTypeOf(lowerer.typeOf(access));
@@ -37,15 +37,13 @@ export class ClassProtocols {
const instance = symbol ? this.checker.getDeclaredTypeOfSymbol(symbol) : undefined;
for (const property of instance ? this.checker.getPropertiesOfType(instance) : []) {
if (
!this.checker
.declarationsOf(property)
.some(
(member) =>
ts.isMethodDeclaration(member) &&
!member.modifiers?.nodes.some(
(modifier) => modifier.kind === ts.SyntaxKind.StaticKeyword,
),
)
!property.declarations.some(
(member) =>
ts.isMethodDeclaration(member) &&
!member.modifiers?.nodes.some(
(modifier) => modifier.kind === ts.SyntaxKind.StaticKeyword,
),
)
)
continue;
const entries = this.methods.get(property.name) ?? [];
@@ -289,7 +287,7 @@ export class ClassProtocols {
for (const part of parts) {
if ((part.flags & ts.TypeFlags.TypeParameter) === 0) continue;
const symbol = part.getSymbol();
for (const declaration of symbol ? this.checker.declarationsOf(symbol) : [])
for (const declaration of symbol ? symbol.declarations : [])
if (ts.isTypeParameterDeclaration(declaration) && declaration.parent !== undefined)
facts.genericObservers.add(declaration.parent);
}
@@ -501,7 +499,7 @@ export class ClassProtocols {
if (context !== undefined && this.acceptsAnyValue(context)) return true;
if (facts.genericObservers.size === 0) return false;
const signature = this.checker.getResolvedSignature(arg.call);
const declaration = signature && this.checker.signatureDeclaration(signature);
const declaration = signature && signature.declaration;
return (
declaration !== undefined &&
(facts.genericObservers.has(declaration) ||
@@ -520,13 +518,10 @@ export class ClassProtocols {
.some(
(property) =>
names.has(property.name) &&
this.checker
.declarationsOf(property)
.some(
(declaration) =>
ts.isGetAccessorDeclaration(declaration) ||
ts.isSetAccessorDeclaration(declaration),
),
property.declarations.some(
(declaration) =>
ts.isGetAccessorDeclaration(declaration) || ts.isSetAccessorDeclaration(declaration),
),
);
}
@@ -580,9 +575,7 @@ export class ClassProtocols {
return (
symbol !== undefined &&
(symbol.flags & ts.SymbolFlags.Class) !== 0 &&
this.checker
.declarationsOf(symbol)
.some((declaration) => !declaration.getSourceFile().isDeclarationFile) &&
symbol.declarations.some((declaration) => !declaration.getSourceFile().isDeclarationFile) &&
// `typeof C` (the class value) shares the class symbol.
this.checker.getConstructSignatures(type).length === 0
);
@@ -603,13 +596,10 @@ export class ClassProtocols {
// A program interface a class matches by its methods (protocol below).
return (
this.methods.size > 0 &&
this.checker
.declarationsOf(symbol)
.some(
(declaration) =>
ts.isInterfaceDeclaration(declaration) &&
!declaration.getSourceFile().isDeclarationFile,
) &&
symbol.declarations.some(
(declaration) =>
ts.isInterfaceDeclaration(declaration) && !declaration.getSourceFile().isDeclarationFile,
) &&
this.protocol(type)
);
}
@@ -624,7 +614,7 @@ export class ClassProtocols {
if (this.checker.getConstructSignatures(type).length > 0) return false;
const symbol = type.getSymbol();
if (symbol === undefined || (symbol.flags & ts.SymbolFlags.Class) !== 0) return false;
const declarations = this.checker.declarationsOf(symbol);
const declarations = symbol.declarations;
return (
declarations.length > 0 &&
declarations.every((declaration) => !declaration.getSourceFile().isDeclarationFile)
@@ -712,13 +702,10 @@ export class ClassProtocols {
if (symbol && protocols.has(symbol)) return true;
if (
!symbol ||
!this.checker
.declarationsOf(symbol)
.some(
(declaration) =>
ts.isInterfaceDeclaration(declaration) &&
!declaration.getSourceFile().isDeclarationFile,
)
!symbol.declarations.some(
(declaration) =>
ts.isInterfaceDeclaration(declaration) && !declaration.getSourceFile().isDeclarationFile,
)
)
return false;
for (const property of properties) {
@@ -555,7 +555,7 @@ function prototypeProtectedKeys(
} else if (name.startsWith("%symbol:")) {
const entries: [ts.Symbol, string][] = owner.symbolMethods ? [...owner.symbolMethods] : [];
const entry = entries.find(([, field]) => field === name);
const declaration = entry ? lowerer.checker.valueDeclarationOf(entry[0]) : undefined;
const declaration = entry ? entry[0].valueDeclaration : undefined;
if (
!declaration ||
!ts.isVariableDeclaration(declaration) ||
@@ -1,8 +1,115 @@
import { BOOL, type IrExpr, type IrStmt, type IrType } from "../../ir/ir.js";
import * as ts from "../ts7/adapter.js";
import { BOOL, typeKey, type IrExpr, type IrStmt, type IrType } from "../../ir/ir.js";
import { nodeThrowExpr, varRef } from "../../ir/build.js";
import { checkedClassAssertion } from "./class-assertions.js";
import type { Lowerer } from "./lowerer.js";
/** The classes a narrowed read of a `staticClass` value can hold when the
* checker typed it as the subclass `narrowed`, as a class union, or null
* when `narrowed` is the only one.
*
* TypeScript reduces a union of type-parameter intersections by structural
* subtyping. With `<T extends Node>`, `node instanceof Identifier || node
* instanceof PrivateIdentifier` narrows `node` to `T & PrivateIdentifier`
* when Identifier has every member PrivateIdentifier has, so the narrowed
* read can hold an Identifier. Class identity is nominal in compiled code:
* such a read holds the narrowed class or any class under `staticClass`
* that is assignable to it without extending it. A narrowing outside a
* type-parameter intersection keeps every class it names. */
export function structuralNarrowing(
lowerer: Lowerer,
node: ts.Node,
staticClass: string,
narrowed: IrType & { kind: "object" },
): (IrType & { kind: "union" }) | null {
if (!intersectsTypeParameter(lowerer.checker.getTypeAtLocation(node))) return null;
const key = `${staticClass}:${narrowed.className}`;
let peers = lowerer.structuralClassPeers.get(key);
if (peers === undefined) {
peers = assignableClassPeers(lowerer, staticClass, narrowed.className);
lowerer.structuralClassPeers.set(key, peers);
}
if (peers.length === 0) return null;
const arms: IrType[] = [
narrowed,
...peers.map((className) => ({ kind: "object" as const, className })),
];
arms.sort((a, b) => (typeKey(a) < typeKey(b) ? -1 : 1));
return { kind: "union", unionId: lowerer.unions.intern(arms) };
}
/** The type a member access dispatches on when its receiver `node` is a
* narrowed read of a type-parameter intersection, or null to use the
* checker's type: the class union structuralNarrowing reads, or the
* receiver's own class when this instantiation's class is unrelated to the
* narrowed one but assignable to it (with `T = Identifier`, the reduced
* `T & PrivateIdentifier` still holds an Identifier). */
export function structuralReadType(lowerer: Lowerer, node: ts.Expression): IrType | null {
if (!ts.isIdentifier(node) && !ts.isPropertyAccessExpression(node)) return null;
if (!intersectsTypeParameter(lowerer.checker.getTypeAtLocation(node))) return null;
const narrowed = lowerer.mapTypeOf(lowerer.typeOf(node));
if (narrowed?.kind !== "object") return null;
const stored = ts.isIdentifier(node) ? lowerer.peekLocal(node)?.type : undefined;
const symbol = lowerer.checker.getSymbolAtLocation(ts.isIdentifier(node) ? node : node.name);
const declared =
stored ?? (symbol ? lowerer.mapTypeOf(lowerer.checker.getTypeOfSymbol(symbol)) : null);
if (declared?.kind !== "object" || declared.className === narrowed.className) return null;
if (lowerer.isSubclassOf(narrowed.className, declared.className))
return structuralNarrowing(lowerer, node, declared.className, narrowed);
if (lowerer.isSubclassOf(declared.className, narrowed.className)) return null;
const declaredType = classInstanceType(lowerer, declared.className);
const narrowedType = classInstanceType(lowerer, narrowed.className);
return declaredType !== null &&
narrowedType !== null &&
lowerer.checker.isTypeAssignableTo(declaredType, narrowedType)
? declared
: null;
}
function classInstanceType(lowerer: Lowerer, className: string): ts.Type | null {
const decl = lowerer.classes.get(className)?.decl;
if (!decl?.name || decl.typeParameters?.nodes.length) return null;
const symbol = lowerer.checker.getSymbolAtLocation(decl.name);
return symbol ? lowerer.checker.getDeclaredTypeOfSymbol(symbol) : null;
}
function intersectsTypeParameter(type: ts.Type): boolean {
try {
if (type.isUnionType()) return ts.constituentTypes(type).some(intersectsTypeParameter);
return (
(type.flags & ts.TypeFlags.Intersection) !== 0 &&
ts.constituentTypes(type).some((part) => (part.flags & ts.TypeFlags.TypeParameter) !== 0)
);
} catch (e) {
if (ts.isUnreadableConstituentsPanic(e)) return false;
throw e;
}
}
/** The outermost classes under `staticClass`, other than `target` and its
* subclasses, whose instance type is assignable to `target`'s. */
function assignableClassPeers(lowerer: Lowerer, staticClass: string, target: string): string[] {
const targetType = classInstanceType(lowerer, target);
if (targetType === null) return [];
const assignable: string[] = [];
for (const className of lowerer.classes.keys()) {
if (
className === target ||
!lowerer.isSubclassOf(className, staticClass) ||
lowerer.isSubclassOf(className, target)
)
continue;
const type = classInstanceType(lowerer, className);
if (type !== null && lowerer.checker.isTypeAssignableTo(type, targetType))
assignable.push(className);
}
// A subclass of an assignable class is one of its instances already.
return assignable.filter(
(className) =>
!assignable.some((other) => other !== className && lowerer.isSubclassOf(className, other)),
);
}
/** A base-class slot stays one pointer after a guard selects several
* subclasses. Recover the tagged view from runtime membership without
* copying the object or guessing which of the remaining layouts it has. */
@@ -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 };
}
@@ -1,3 +1,4 @@
import { constructedValue } from "../constructor-returns.js";
import { dynUndefinedExpr, varRef } from "../../../ir/build.js";
import type { FieldLift } from "./structural-plans.js";
import { buildArrayConversion } from "./builders.js";
@@ -106,6 +107,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");
@@ -524,7 +535,13 @@ export function recordClassWidthHelper(
params: [{ localId: "r.0", name: "r", type: fromT }],
returnType: toT,
locals: [{ id: "r.0", name: "r", type: fromT, mutable: true }],
body: [{ kind: "return", value: { kind: "new", className, args, type: toT, loc }, loc }],
body: [
{
kind: "return",
value: constructedValue(lowerer, info, { kind: "new", className, args, type: toT, loc }),
loc,
},
],
loc,
});
return name;
@@ -603,6 +620,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 +632,7 @@ function buildRecordProjection(
body: [
{
kind: "return",
value: {
value: copyOf({
kind: "recordLit",
fields: target.fields.map((field) => {
const conversion = plan.get(field.name)!;
@@ -661,7 +682,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,
@@ -13,12 +13,7 @@ export function concreteCollectionNarrow(
const symbol = narrowed.getSymbol();
const name = symbol?.name;
if (name !== "Map" && name !== "Set") return null;
if (
!symbol ||
!lowerer.checker
.declarationsOf(symbol)
.some((decl) => lowerer.isStdlibFile(decl.getSourceFile()))
)
if (!symbol || !symbol.declarations.some((decl) => lowerer.isStdlibFile(decl.getSourceFile())))
return null;
const args = lowerer.checker.getTypeArguments(narrowed as ts.TypeReference);
if (args.length === 0 || !args.every((arg) => (arg.flags & ts.TypeFlags.Any) !== 0)) return null;
@@ -0,0 +1,522 @@
/** Constructors that `return` a value.
*
* JavaScript's `new C()` answers the constructor's returned value when it
* is an object (functions included) and the constructed `this` otherwise.
* A primitive or bare return therefore keeps the ordinary lowering, which
* evaluates the operand and drops it.
*
* A return whose value is an instance of the constructor's own class keeps
* the class representation (`if (Single.instance) return
* Single.instance;`). The return stores that instance in a per-class
* result slot and every direct `new C()` takes it from there. Any other
* object has no representation as a `C` instance and refuses at the return
* statement, deferred to run time in JavaScript like other statement
* fences. A JavaScript value that is an object only at run time (`any`)
* is tested when it returns: a primitive answers the constructed
* instance, an instance of the class takes the result slot, and any other
* object refuses.
*
* `super()` from a subclass refuses: Node would rebind the subclass's
* `this` to the returned instance. In JavaScript the refusal is a run-time
* test after the call: an instance other than the subclass's own `this`
* refuses, and the constructed instance itself (`return this.set(r, g,
* b)`, where `set` answers `this`) continues. */
import * as ts from "../ts7/adapter.js";
import { varRef } from "../../ir/build.js";
import {
BOOL,
DYN,
type IrExpr,
type IrGlobal,
type IrStmt,
type IrType,
STRING,
typeKey,
VOID,
} from "../../ir/ir.js";
import { unsupportedDiag } from "../../diagnostics/diagnostic.js";
import { isJsSourceFile, locOf } from "../program.js";
import type { ClassInfo } from "./lower-classes.js";
import type { Lowerer } from "./lowerer.js";
type ReturnKind = "instance" | "object" | "mixed";
export interface ConstructorReturnPlan {
/** Returns whose value may be an object, by kind. Primitive and `this`
* returns are absent: they keep the ordinary lowering. */
returns: Map<ts.ReturnStatement, ReturnKind>;
/** The result slot (`C | undefined`), present when some return yields an
* instance of the class itself. */
slot: IrGlobal | null;
}
const PRIMITIVE_FLAGS =
ts.TypeFlags.Primitive | ts.TypeFlags.EnumLike | ts.TypeFlags.Void | ts.TypeFlags.Never;
/** Types whose values may be primitives or objects: open types, type
* variables and intersections (a branded `string & { tag }` is a string). */
const OPEN_FLAGS =
ts.TypeFlags.AnyOrUnknown | ts.TypeFlags.InstantiableNonPrimitive | ts.TypeFlags.Intersection;
function unparenthesized(node: ts.Expression): ts.Expression {
let e = node;
while (ts.isParenthesizedExpression(e)) e = e.expression;
return e;
}
/** Whether a value of this checker type can be an object: "never",
* "always", or "maybe" (open types and primitive/object unions). */
function objectness(type: ts.Type): "never" | "always" | "maybe" {
const parts = type.isUnionType() ? ts.constituentTypes(type) : [type];
let primitive = false;
let object = false;
for (const part of parts) {
if ((part.flags & OPEN_FLAGS) !== 0) return "maybe";
if ((part.flags & PRIMITIVE_FLAGS) !== 0) primitive = true;
else object = true;
}
if (!object) return "never";
return primitive ? "maybe" : "always";
}
/** The constructor's own `return <expr>` statements: nested functions,
* arrows and classes have their own returns. */
function ownValueReturns(ctor: ts.ConstructorDeclaration): ts.ReturnStatement[] {
const out: ts.ReturnStatement[] = [];
const visit = (node: ts.Node): void => {
if (ts.isFunctionLike(node) || ts.isClassLikeDeclaration(node)) return;
if (ts.isReturnStatement(node) && node.expression !== undefined) out.push(node);
node.forEachChild(visit);
};
ctor.body?.forEachChild(visit);
return out;
}
/** A return inside a `try` with a `finally` block: the finally block runs
* after the result is stored and may throw past it. */
function insideFinallyTry(node: ts.Node, ctor: ts.ConstructorDeclaration): boolean {
for (let p = node.parent; p !== undefined && p !== ctor; p = p.parent) {
if (ts.isTryStatement(p) && p.finallyBlock !== undefined && !nodeWithin(node, p.finallyBlock))
return true;
}
return false;
}
function nodeWithin(node: ts.Node, ancestor: ts.Node): boolean {
return node.pos >= ancestor.pos && node.end <= ancestor.end;
}
function instanceType(info: ClassInfo): IrType {
return { kind: "object", className: info.def.name };
}
function isInstanceOf(lowerer: Lowerer, info: ClassInfo, type: IrType | null): boolean {
if (type === null) return false;
if (type.kind === "object") return type.className === info.def.name;
if (type.kind !== "union") return false;
const arms = lowerer.unions.get(type.unionId)?.arms ?? [];
return (
arms.length === 2 &&
arms.some((arm) => arm.kind === "object" && arm.className === info.def.name) &&
arms.some((arm) => arm.kind === "undefinedT")
);
}
/** Whether an expression always yields the receiver itself: `this`, or a
* call `this.m(...)` / `super.m(...)` whose every implementation the
* receiver can reach (the statically resolved method and, for `this`
* calls, each subclass override) returns only `this`, a primitive, or
* nothing (`return this.set(r, g, b)` with a chainable setter). Such a
* return answers the constructed instance. Methods already being checked
* count as returning `this` (their other returns decide). */
function returnsThis(
lowerer: Lowerer,
info: ClassInfo,
node: ts.Expression,
visiting: Set<ts.Node> = new Set(),
): boolean {
let e = node;
while (ts.isParenthesizedExpression(e) || ts.isAsExpression(e) || ts.isNonNullExpression(e))
e = e.expression;
if (e.kind === ts.SyntaxKind.ThisKeyword) return true;
if (!ts.isCallExpression(e)) return false;
const callee = unparenthesized(e.expression);
if (
!ts.isPropertyAccessExpression(callee) ||
callee.questionDotToken !== undefined ||
!ts.isIdentifier(callee.name)
)
return false;
const receiver = unparenthesized(callee.expression).kind;
if (receiver !== ts.SyntaxKind.ThisKeyword && receiver !== ts.SyntaxKind.SuperKeyword)
return false;
const name = callee.name.text;
const symbol = lowerer.checker.getSymbolAtLocation(callee.name);
if (symbol === undefined) return false;
const methods: ts.Node[] = [...symbol.declarations];
if (receiver === ts.SyntaxKind.ThisKeyword) {
const pending = [...info.subclasses];
while (pending.length > 0) {
const sub = pending.pop()!;
pending.push(...sub.subclasses);
for (const member of sub.decl?.members.nodes ?? []) {
if (member.name === undefined || !ts.isIdentifier(member.name) || member.name.text !== name)
continue;
if (!ts.isMethodDeclaration(member)) return false;
methods.push(member);
}
}
}
if (methods.length === 0) return false;
for (const method of methods) {
if (!ts.isMethodDeclaration(method) || method.body === undefined) return false;
if (visiting.has(method)) continue;
visiting.add(method);
for (const ret of ownReturns(method.body)) {
if (ret.expression === undefined) continue;
if (objectness(lowerer.typeOf(ret.expression)) === "never") continue;
if (!returnsThis(lowerer, info, ret.expression, visiting)) return false;
}
}
return true;
}
/** A function body's own return statements (nested functions, arrows and
* classes have their own). */
function ownReturns(body: ts.Node): ts.ReturnStatement[] {
const out: ts.ReturnStatement[] = [];
const visit = (node: ts.Node): void => {
if (ts.isFunctionLike(node) || ts.isClassLikeDeclaration(node)) return;
if (ts.isReturnStatement(node)) out.push(node);
node.forEachChild(visit);
};
body.forEachChild(visit);
return out;
}
export function constructorReturnPlan(
lowerer: Lowerer,
info: ClassInfo,
): ConstructorReturnPlan | null {
const byClass = lowerer.constructorReturnPlans;
const known = byClass.get(info.def.name);
if (known !== undefined) return known;
const ctor = info.ctor;
let plan: ConstructorReturnPlan | null = null;
if (ctor?.body !== undefined && info.decl !== null) {
const returns = new Map<ts.ReturnStatement, ReturnKind>();
for (const stmt of ownValueReturns(ctor)) {
const value = unparenthesized(stmt.expression!);
if (returnsThis(lowerer, info, value)) continue;
const type = lowerer.typeOf(value);
const shape = objectness(type);
if (shape === "never") continue;
// Local classes construct through their own environment, which has
// no result slot.
const instance =
info.localClass === undefined &&
!insideFinallyTry(stmt, ctor) &&
isInstanceOf(lowerer, info, lowerer.mapTypeOf(type));
returns.set(stmt, instance ? "instance" : shape === "always" ? "object" : "mixed");
}
if (returns.size > 0) {
let slot: IrGlobal | null = null;
// A JavaScript value typed only at run time may hold an instance of
// the class too (`if (C.instance) return C.instance;`).
const runtimeInstance =
info.localClass === undefined &&
isJsSourceFile(ctor.getSourceFile()) &&
[...returns].some(([stmt, kind]) => kind === "mixed" && !insideFinallyTry(stmt, ctor));
if ([...returns.values()].includes("instance") || runtimeInstance) {
slot = {
id: `%g.${info.def.name}.%constructorResult`,
name: `${info.def.name}.%constructorResult`,
type: lowerer.withUndefinedArm(instanceType(info)),
mutable: true,
};
lowerer.globalsList.push(slot);
}
plan = { returns, slot };
}
}
byClass.set(info.def.name, plan);
return plan;
}
function foreignObjectFeature(info: ClassInfo): string {
const display = info.def.name.replace(/^%|^%m\d+\./, "");
return `returning an object other than an instance of the class from a constructor (Node's 'new ${display}()' answers the returned object, which has no representation as a '${display}' instance)`;
}
/** `return <expr>` inside a class constructor whose value may be an
* object. Null when the statement keeps the ordinary lowering. */
export function lowerConstructorReturn(lowerer: Lowerer, stmt: ts.ReturnStatement): IrStmt | null {
const info = lowerer.currentClass;
if (info === null || info.ctor === null || stmt.expression === undefined) return null;
let fn: ts.Node | undefined = stmt.parent;
while (fn !== undefined && !ts.isFunctionLike(fn) && !ts.isClassLikeDeclaration(fn))
fn = fn.parent;
if (fn !== info.ctor) return null;
const plan = constructorReturnPlan(lowerer, info);
const kind = plan?.returns.get(stmt);
if (plan === null || kind === undefined) return null;
const loc = locOf(stmt);
if (kind === "instance") {
const value = lowerer.coerceInto(
stmt.expression,
lowerer.lowerExpr(stmt.expression),
plan.slot!.type,
);
return {
kind: "block",
body: [
{ kind: "assign", localId: plan.slot!.id, value, loc },
{ kind: "return", value: null, loc },
],
loc,
};
}
const feature = foreignObjectFeature(info);
if (kind === "object" || !isJsSourceFile(stmt.getSourceFile()))
lowerer.unsupported("SC1090", stmt, feature);
// A JavaScript value that is an object only sometimes: a primitive
// still answers the constructed instance, an object refuses.
const value = lowerer.coerceToExpected(lowerer.lowerExpr(stmt.expression), DYN);
if (value.type.kind !== "dyn") lowerer.unsupported("SC1090", stmt, feature);
const held = lowerer.declareHiddenLocal("%returned", DYN);
const read = varRef(held.id, DYN, loc);
const typeOf: IrExpr = { kind: "libCall", fn: "dyn.typeof", args: [read], type: STRING, loc };
const typeIs = (name: string): IrExpr => ({
kind: "strEq",
negated: false,
left: typeOf,
right: { kind: "strLit", value: name, type: STRING, loc },
type: BOOL,
loc,
});
const diag = unsupportedDiag("SC1090", loc, feature);
lowerer.runtimeFences.push(diag);
const sf = stmt.getSourceFile();
const line = ts.getLineAndCharacterOfPosition(sf, loc.start).line + 1;
return {
kind: "block",
body: [
{ kind: "varDecl", localId: held.id, init: value, loc },
{
kind: "if",
// null answers "object" and is falsy; every object is truthy.
cond: {
kind: "logical",
op: "||",
left: typeIs("function"),
right: {
kind: "logical",
op: "&&",
left: typeIs("object"),
right: lowerer.ensureBool(read, stmt.expression),
type: BOOL,
loc,
},
type: BOOL,
loc,
},
then: [
...(plan.slot !== null && !insideFinallyTry(stmt, info.ctor)
? [
{
kind: "if" as const,
cond: {
kind: "libCall" as const,
fn: "dyn.typedRefIs" as const,
args: [
read,
{
kind: "strLit" as const,
value: typeKey(instanceType(info)),
type: STRING,
loc,
},
],
type: BOOL,
loc,
},
then: [
{
kind: "assign" as const,
localId: plan.slot.id,
value: lowerer.coerceToExpected(
lowerer.coerceToExpected(read, instanceType(info)),
plan.slot.type,
),
loc,
},
{ kind: "return" as const, value: null, loc },
],
else_: null,
loc,
},
]
: []),
{
kind: "runtimeFence",
code: diag.code,
message: `${diag.message} [${diag.code} at ${loc.file}:${line}]`,
loc,
},
],
else_: null,
loc,
},
{ kind: "return", value: null, loc },
],
loc,
};
}
/** The value of a construction of exactly class `C`: the constructed
* instance, or the instance the constructor returned. Unchanged for
* classes without a result slot. The slot is read and cleared by a
* per-class helper, so every construction site (direct `new`, class
* values, the checked dynamic dispatchers) shares one protocol. */
export function constructedValue(lowerer: Lowerer, info: ClassInfo, constructed: IrExpr): IrExpr {
const slot = constructorReturnPlan(lowerer, info)?.slot;
if (slot === null || slot === undefined) return constructed;
const loc = constructed.loc;
const type = instanceType(info);
const name = `%${info.def.name}.%constructed`;
if (!lowerer.liftedFns.some((fn) => fn.name === name)) {
const instance = { id: "instance.0", name: "instance", type, mutable: false };
const returned = { id: "returned.0", name: "returned", type: slot.type, mutable: false };
lowerer.liftedFns.push({
name,
params: [{ localId: instance.id, name: instance.name, type }],
returnType: type,
locals: [instance, returned],
body: [
{ kind: "varDecl", localId: returned.id, init: varRef(slot.id, slot.type, loc), loc },
{ kind: "assign", localId: slot.id, value: lowerer.wrappedUndefined(slot.type, loc)!, loc },
{
kind: "return",
value: {
kind: "orDefault",
left: varRef(returned.id, slot.type, loc),
right: varRef(instance.id, type, loc),
type,
loc,
},
loc,
},
],
loc,
});
}
return { kind: "call", callee: name, args: [constructed], type, loc };
}
/** A construction through a class value whose static class is `info`: the
* value holds `info` itself or a descendant. Descendants of a class with a
* result slot refuse at their `super()`, so only `info`'s own slot can
* apply; a descendant with its own slot has no static home here. */
export function constructedFromValue(
lowerer: Lowerer,
info: ClassInfo,
constructed: IrExpr,
blame: ts.Node,
): IrExpr {
const pending = [...info.subclasses];
while (pending.length > 0) {
const sub = pending.pop()!;
pending.push(...sub.subclasses);
const plan = constructorReturnPlan(lowerer, sub);
if (plan === null || plan.slot === null) continue;
const display = sub.def.name.replace(/^%|^%m\d+\./, "");
lowerer.unsupported(
"SC1090",
blame,
`constructing through a class value that may hold '${display}', whose constructor can return an existing instance`,
);
}
return constructedValue(lowerer, info, constructed);
}
/** `super()` into a class with a result slot, from the constructor of
* class `sub`: `call` runs the base constructor over `self` (the
* subclass's `this`, upcast to the base). Node rebinds the subclass's
* `this` to an instance the base constructor returns, which has no
* representation. A TypeScript subclass refuses. In JavaScript the base
* stores the instance it returns in its slot, and a per-subclass helper
* reads and clears the slot after the call: an instance other than `self`
* refuses at run time. Unchanged for bases without a result slot. */
export function superIntoResultSlot(
lowerer: Lowerer,
base: ClassInfo,
sub: ClassInfo,
call: IrStmt,
self: IrExpr,
blame: ts.Node,
): IrStmt {
const plan = constructorReturnPlan(lowerer, base);
if (plan === null || plan.slot === null) return call;
const slot = plan.slot;
const display = base.def.name.replace(/^%|^%m\d+\./, "");
const feature = `extending a class whose constructor can return an existing instance ('super()' would rebind 'this' to the instance '${display}' returns)`;
if (!isJsSourceFile(blame.getSourceFile())) {
lowerer.unsupported("SC1090", blame, feature);
return call;
}
const loc = locOf(blame);
const type = instanceType(base);
const name = `%${sub.def.name}.%superResult`;
if (!lowerer.liftedFns.some((fn) => fn.name === name)) {
const diag = unsupportedDiag("SC1090", loc, feature);
lowerer.runtimeFences.push(diag);
const line = ts.getLineAndCharacterOfPosition(blame.getSourceFile(), loc.start).line + 1;
const instance = { id: "instance.0", name: "instance", type, mutable: false };
const returned = { id: "returned.0", name: "returned", type: slot.type, mutable: false };
lowerer.liftedFns.push({
name,
params: [{ localId: instance.id, name: instance.name, type }],
returnType: VOID,
locals: [instance, returned],
body: [
{ kind: "varDecl", localId: returned.id, init: varRef(slot.id, slot.type, loc), loc },
{ kind: "assign", localId: slot.id, value: lowerer.wrappedUndefined(slot.type, loc)!, loc },
{
kind: "if",
cond: {
kind: "bin",
op: "!==",
left: {
kind: "orDefault",
left: varRef(returned.id, slot.type, loc),
right: varRef(instance.id, type, loc),
type,
loc,
},
right: varRef(instance.id, type, loc),
type: BOOL,
loc,
},
then: [
{
kind: "runtimeFence",
code: diag.code,
message: `${diag.message} [${diag.code} at ${loc.file}:${line}]`,
loc,
},
],
else_: null,
loc,
},
{ kind: "return", value: null, loc },
],
loc,
});
}
const check: IrStmt = {
kind: "exprStmt",
expr: { kind: "call", callee: name, args: [self], type: VOID, loc },
loc,
};
return { kind: "block", body: [call, check], loc };
}
@@ -555,9 +555,9 @@ export function lowerBytesMethodCall(
const nameSym = lowerer.checker.getSymbolAtLocation(access.name);
return (
nameSym !== undefined &&
lowerer.checker
.declarationsOf(nameSym)
.some((d) => ts.isInterfaceDeclaration(d.parent) && d.parent.name.text === "Buffer")
nameSym.declarations.some(
(d) => ts.isInterfaceDeclaration(d.parent) && d.parent.name.text === "Buffer",
)
);
})();
const method = name === "subarray" || declaredOnBuffer ? "subarray" : "slice";
@@ -581,13 +581,11 @@ export function lowerBytesMethodCall(
const symbol = lowerer.checker.getSymbolAtLocation(access.name);
return (
symbol !== undefined &&
lowerer.checker
.declarationsOf(symbol)
.some(
(declaration) =>
ts.isInterfaceDeclaration(declaration.parent) &&
declaration.parent.name.text === "Buffer",
)
symbol.declarations.some(
(declaration) =>
ts.isInterfaceDeclaration(declaration.parent) &&
declaration.parent.name.text === "Buffer",
)
);
})()
) {
@@ -647,9 +645,9 @@ export function lowerBytesMethodCall(
const nameSym = lowerer.checker.getSymbolAtLocation(access.name);
return (
nameSym !== undefined &&
lowerer.checker
.declarationsOf(nameSym)
.some((d) => ts.isInterfaceDeclaration(d.parent) && d.parent.name.text === "Buffer")
nameSym.declarations.some(
(d) => ts.isInterfaceDeclaration(d.parent) && d.parent.name.text === "Buffer",
)
);
})();
if (receiverIr.elem !== "u8" || !declaredOnBuffer) {
@@ -725,9 +723,9 @@ export function lowerBytesMethodCall(
const nameSym = lowerer.checker.getSymbolAtLocation(access.name);
return (
nameSym !== undefined &&
lowerer.checker
.declarationsOf(nameSym)
.some((d) => ts.isInterfaceDeclaration(d.parent) && d.parent.name.text === "Buffer")
nameSym.declarations.some(
(d) => ts.isInterfaceDeclaration(d.parent) && d.parent.name.text === "Buffer",
)
);
})();
if (declOnBuffer && receiverIr.elem === "u8") {
@@ -1274,16 +1272,14 @@ function knownBufferProducer(
const symbol = lowerer.checker.getSymbolAtLocation(node);
if (!symbol || seen.has(symbol)) return false;
seen.add(symbol);
return lowerer.checker
.declarationsOf(symbol)
.some(
(decl) =>
ts.isVariableDeclaration(decl) &&
ts.isVariableDeclarationList(decl.parent) &&
(decl.parent.flags & ts.NodeFlags.Const) !== 0 &&
decl.initializer !== undefined &&
knownBufferProducer(lowerer, decl.initializer, seen),
);
return symbol.declarations.some(
(decl) =>
ts.isVariableDeclaration(decl) &&
ts.isVariableDeclarationList(decl.parent) &&
(decl.parent.flags & ts.NodeFlags.Const) !== 0 &&
decl.initializer !== undefined &&
knownBufferProducer(lowerer, decl.initializer, seen),
);
}
if (!ts.isCallExpression(node)) return false;
const callee = node.expression;
@@ -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);
@@ -298,7 +298,7 @@ function isHelperObject(lowerer: Lowerer, expr: ts.Expression): boolean {
if (isStageCall(lowerer, expr)) return true;
if (!ts.isIdentifier(expr)) return false;
const symbol = lowerer.resolveValueSymbol(expr);
const decl = symbol ? lowerer.checker.valueDeclarationOf(symbol) : undefined;
const decl = symbol ? symbol.valueDeclaration : undefined;
if (
!decl ||
!ts.isVariableDeclaration(decl) ||
@@ -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,
]),
];
};
}
@@ -41,6 +41,10 @@ export interface CycleInitFlag {
* own module's early calls is checked opportunistically: whatever cannot
* be checked keeps the unchecked behavior instead of refusing. */
cycle: boolean;
/** A pointer-guarded binding declared without an initializer: its
* declaration initializes it (to undefined, which the pointer cannot
* hold), so the flag alone decides its writes (markUninitializedWrites). */
uninitialized?: true;
/** A class binding: its references are the class's own IR forms and its
* static field storage (`global` is the flag itself). */
className?: string;
@@ -107,6 +111,30 @@ export function markCycleEarlyBindings(lowerer: Lowerer): void {
registerInitFlag(lowerer, binding, global, false);
}
}
// `let f: T;` with a pointer-guarded slot: once the declaration ran the
// binding is initialized while the slot is still empty, so a write
// checks the flag (where code can run before the declaration) instead of
// the empty pointer. `f = () => f(...)` is the shape.
for (const [id, entry] of lowerer.uninitializedLexicalGlobals) {
const global = lowerer.globalsList.find((g) => g.id === id);
if (global === undefined) continue;
if (!lowerer.cycleInitFlags.has(id)) {
registerInitFlag(
lowerer,
{
symbol: entry.symbol,
name: entry.name,
kind: "lexical",
module: entry.module,
reference: entry.name,
settled: new Set(),
},
global,
false,
);
}
lowerer.cycleInitFlags.get(id)!.uninitialized = true;
}
}
/** A class that its module's early-running code can reach before the class
@@ -304,6 +332,22 @@ export function checkCycleBindingAccesses(lowerer: Lowerer, mod: IrModule): void
const fileOfInit = new Map<string, ts.SourceFile>();
for (const [sf, name] of lowerer.initNameOf) fileOfInit.set(name, sf);
for (const fn of mod.functions) rewriteFunction(lowerer, fn, fileOfInit.get(fn.name));
markUninitializedWrites(lowerer, mod);
}
/** Writes to a binding declared without an initializer that run after its
* declaration store into an initialized binding whose slot may still be
* empty: the pointer guard must not refuse them. Writes that can run
* earlier carry the flag check rewriteFunction inserted. */
function markUninitializedWrites(lowerer: Lowerer, mod: IrModule): void {
const ids = new Set<string>();
for (const [id, flag] of lowerer.cycleInitFlags) if (flag.uninitialized) ids.add(id);
if (ids.size === 0) return;
const mark = (s: IrStmt): IrStmt =>
s.kind === "assign" && !s.initializes && ids.has(s.localId) ? { ...s, initializes: true } : s;
const rewriteExpr = (e: IrExpr): IrExpr => mapExprChildren(e, rewriteExpr, rewriteStmt);
const rewriteStmt = (s: IrStmt): IrStmt => mark(mapStmtChildren(s, rewriteExpr, rewriteStmt));
for (const fn of mod.functions) fn.body = fn.body.map(rewriteStmt);
}
function rewriteFunction(
@@ -444,6 +488,7 @@ function rewriteFunction(
kind: "assign",
localId: s.localId,
value: { kind: "varRef", localId: t, type: s.value.type, loc: s.loc },
...(flag.uninitialized ? { initializes: true as const } : {}),
loc: s.loc,
};
return {
@@ -286,7 +286,7 @@ function registryInitializer(
)
return true;
const symbol = lowerer.resolveValueSymbol(callee);
const declaration = symbol && lowerer.checker.valueDeclarationOf(symbol);
const declaration = symbol && symbol.valueDeclaration;
if (!symbol || !declaration || !bindingNeverReassigned(lowerer, symbol, declaration))
return false;
if (ts.isFunctionDeclaration(declaration)) return true;
@@ -346,7 +346,7 @@ function registryInitializer(
}
if (packageSource && ts.isPropertyAccessExpression(node)) {
const symbol = lowerer.checker.getSymbolAtLocation(node.name);
const declaration = symbol && lowerer.checker.valueDeclarationOf(symbol);
const declaration = symbol && symbol.valueDeclaration;
const owner = ts.isIdentifier(node.expression)
? lowerer.resolveValueSymbol(node.expression)
: undefined;
@@ -394,7 +394,7 @@ function registryInitializer(
)
return true;
const symbol = lowerer.resolveValueSymbol(node);
const functionDeclaration = symbol && lowerer.checker.valueDeclarationOf(symbol);
const functionDeclaration = symbol && symbol.valueDeclaration;
if (
functionDeclaration &&
ts.isFunctionDeclaration(functionDeclaration) &&
@@ -449,7 +449,7 @@ function registryOwnerIsData(
symbol: ts.Symbol,
statement: ts.Statement,
): boolean {
const declaration = lowerer.checker.valueDeclarationOf(symbol);
const declaration = symbol.valueDeclaration;
if (
!declaration ||
!ts.isVariableDeclaration(declaration) ||
@@ -23,6 +23,7 @@ import {
} from "../../diagnostics/diagnostic.js";
import { isCollationCovered } from "./collation-repertoire.js";
import type { Lowerer } from "./lowerer.js";
import { keptIdentityAssertion } from "./assertion-identity.js";
/** How an identity-sensitive binding is used after a copy could replace it. */
interface IdentityUse {
@@ -179,7 +180,7 @@ export function scanDivergences(
if (!candidates.some((fn) => identityUsesOf(fn).some((u) => u !== null))) continue;
const signature = safely(() => checker.getResolvedSignature(call));
if (signature === undefined) continue;
const decl = safely(() => checker.signatureDeclaration(signature));
const decl = safely(() => signature.declaration);
if (decl === undefined || !candidates.includes(decl as ts.FunctionLikeDeclaration)) continue;
const fn = decl as ts.FunctionLikeDeclaration;
const uses = identityUsesOf(fn);
@@ -217,6 +218,8 @@ export function scanDivergences(
}
for (const { decl, source, type } of castDecls) {
// An assertion of the global object keeps that object: no copy.
if (keptIdentityAssertion(lowerer, decl.initializer!) !== undefined) continue;
const name = (decl.name as ts.Identifier).text;
const scope = enclosingScope(decl);
if (scope === undefined) continue;
@@ -232,11 +235,9 @@ export function scanDivergences(
const left = assignment.left as ts.Identifier;
const symbol = safely(() => checker.getSymbolAtLocation(left));
if (symbol === undefined) continue;
const decl = checker
.declarationsOf(symbol)
.find(
(d): d is ts.VariableDeclaration => ts.isVariableDeclaration(d) && d.type !== undefined,
);
const decl = symbol.declarations.find(
(d): d is ts.VariableDeclaration => ts.isVariableDeclaration(d) && d.type !== undefined,
);
if (decl === undefined) continue;
const scope = enclosingScope(decl);
if (scope === undefined) continue;
@@ -287,7 +288,7 @@ export function scanDivergences(
if (ts.isObjectLiteralExpression(inner)) lit = inner;
else if (ts.isIdentifier(inner)) {
const symbol = safely(() => checker.getSymbolAtLocation(inner));
const decl = symbol === undefined ? undefined : checker.valueDeclarationOf(symbol);
const decl = symbol === undefined ? undefined : symbol.valueDeclaration;
if (
decl !== undefined &&
ts.isVariableDeclaration(decl) &&
@@ -373,8 +374,7 @@ export function scanDivergences(
const name = calleeName(call);
if (name === null || !fnsByName.has(name)) continue;
const signature = safely(() => checker.getResolvedSignature(call));
const decl =
signature === undefined ? undefined : safely(() => checker.signatureDeclaration(signature));
const decl = signature === undefined ? undefined : safely(() => signature.declaration);
const sites = decl === undefined ? undefined : downcasts.get(decl);
if (sites === undefined) continue;
for (const site of sites) {
@@ -423,7 +423,7 @@ function dateStringLiterals(checker: ts.TypeChecker, arg: ts.Expression): ts.Exp
if (stringLiteralText(arg) !== null) return [arg];
if (!ts.isIdentifier(arg)) return [];
const symbol = safely(() => checker.getSymbolAtLocation(arg));
const decl = symbol === undefined ? undefined : checker.valueDeclarationOf(symbol);
const decl = symbol === undefined ? undefined : symbol.valueDeclaration;
const list = decl?.parent;
const loop = list?.parent;
if (
@@ -438,7 +438,7 @@ function dateStringLiterals(checker: ts.TypeChecker, arg: ts.Expression): ts.Exp
let iterated: ts.Expression = loop.expression;
if (ts.isIdentifier(iterated)) {
const source = safely(() => checker.getSymbolAtLocation(iterated));
const init = source === undefined ? undefined : checker.valueDeclarationOf(source);
const init = source === undefined ? undefined : source.valueDeclaration;
if (init === undefined || !ts.isVariableDeclaration(init) || init.initializer === undefined)
return [];
if (init.parent === undefined || (init.parent.flags & ts.NodeFlags.Const) === 0) return [];
@@ -743,7 +743,7 @@ function isProgramClassType(checker: ts.TypeChecker, type: ts.Type): boolean {
return (
symbol !== undefined &&
(symbol.flags & ts.SymbolFlags.Class) !== 0 &&
checker.declarationsOf(symbol).some((d) => !d.getSourceFile().isDeclarationFile)
symbol.declarations.some((d) => !d.getSourceFile().isDeclarationFile)
);
}
@@ -756,7 +756,7 @@ function isPlainRecordType(checker: ts.TypeChecker, type: ts.Type): boolean {
const symbol = type.getSymbol();
if (symbol !== undefined && (symbol.flags & ts.SymbolFlags.Class) !== 0) return false;
// Library-declared object types (Map, Date, Promise, ...) are not records.
const decls = symbol === undefined ? [] : checker.declarationsOf(symbol);
const decls = symbol === undefined ? [] : symbol.declarations;
if (decls.some((d) => d.getSourceFile().isDeclarationFile)) return false;
return true;
}
@@ -769,7 +769,7 @@ function namedObjectDeclaration(checker: ts.TypeChecker, type: ts.Type): ts.Node
const symbol = alias ?? type.getSymbol();
if (symbol === undefined) return null;
if ((symbol.flags & ts.SymbolFlags.Class) !== 0) return null;
const decl = checker.declarationsOf(symbol)[0];
const decl = symbol.declarations[0];
if (decl === undefined || decl.getSourceFile().isDeclarationFile) return null;
return decl;
}
@@ -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,
@@ -810,9 +823,7 @@ function classDeclarationOfTypeNode(
if (!ts.isTypeReferenceNode(node) || !ts.isIdentifier(node.typeName)) return null;
const symbol = lowerer.checker.getSymbolAtLocation(node.typeName);
if (symbol === undefined || (symbol.flags & ts.SymbolFlags.Class) === 0) return null;
const decl = lowerer.checker
.declarationsOf(symbol)
.find((d) => ts.isClassDeclaration(d) || ts.isClassExpression(d));
const decl = symbol.declarations.find((d) => ts.isClassDeclaration(d) || ts.isClassExpression(d));
if (decl === undefined || decl.getSourceFile().isDeclarationFile) return null;
return decl as ts.ClassLikeDeclaration;
}
@@ -823,9 +834,7 @@ function classDeclarationOfType(
): ts.ClassLikeDeclaration | null {
const symbol = type.getSymbol();
if (symbol === undefined || (symbol.flags & ts.SymbolFlags.Class) === 0) return null;
const decl = checker
.declarationsOf(symbol)
.find((d) => ts.isClassDeclaration(d) || ts.isClassExpression(d));
const decl = symbol.declarations.find((d) => ts.isClassDeclaration(d) || ts.isClassExpression(d));
if (decl === undefined || decl.getSourceFile().isDeclarationFile) return null;
return decl as ts.ClassLikeDeclaration;
}
@@ -850,9 +859,9 @@ function classExtends(
(symbol.flags & ts.SymbolFlags.Alias) !== 0
? lowerer.checker.getAliasedSymbol(symbol)
: symbol;
const decl = lowerer.checker
.declarationsOf(resolved)
.find((d) => ts.isClassDeclaration(d) || ts.isClassExpression(d));
const decl = resolved.declarations.find(
(d) => ts.isClassDeclaration(d) || ts.isClassExpression(d),
);
cur = decl === undefined ? null : (decl as ts.ClassLikeDeclaration);
}
return false;
@@ -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 };
@@ -3047,7 +3062,7 @@ export function lowerShorthandValue(
return lowerer.maybeNarrow({ kind: "varRef", localId: g.id, type: g.type, loc }, propName);
}
const sig = lowerer.fnSigsBySymbol.get(resolved);
const decl = lowerer.checker.declarationsOf(resolved)[0];
const decl = resolved.declarations[0];
if (
sig &&
decl &&
@@ -1,3 +1,4 @@
import { mapAsyncGeneratorAnnotation } from "../type-mapper.js";
import { dynUndefinedExpr } from "../../ir/build.js";
import * as ts from "../ts7/adapter.js";
import type { Lowerer } from "./lowerer.js";
@@ -146,6 +147,12 @@ export function declaredReturnType(
// (`.action(() => { throw ... })`). `never` VALUES stay unmapped.
if (retTsType.flags & ts.TypeFlags.Never) return VOID;
let mappedReturn = lowerer.mapTypeOf(retTsType);
if (
mappedReturn === null &&
decl.asteriskToken &&
decl.modifiers?.nodes.some((modifier) => modifier.kind === ts.SyntaxKind.AsyncKeyword)
)
mappedReturn = mapAsyncGeneratorAnnotation(retTsType, lowerer.typeCtx);
if (
isJsSourceFile(decl.getSourceFile()) &&
decl.modifiers?.nodes.some((modifier) => modifier.kind === ts.SyntaxKind.AsyncKeyword) &&
@@ -187,9 +187,9 @@ function requireStableGenericBinding(
name: string,
): void {
const isConst = (ts.getCombinedNodeFlags(decl) & ts.NodeFlags.Const) !== 0;
const redeclared = lowerer.checker
.declarationsOf(symbol)
.some((d) => d !== decl && ts.isVariableDeclaration(d) && d.initializer !== undefined);
const redeclared = symbol.declarations.some(
(d) => d !== decl && ts.isVariableDeclaration(d) && d.initializer !== undefined,
);
if (!isConst && (redeclared || !bindingNeverReassigned(lowerer, symbol, decl))) {
lowerer.unsupported(
"SC1090",
@@ -266,7 +266,7 @@ export function genericCallInstance(
// honest story (monomorphize per implementation signature with the
// reconcile bridge, like the non-generic path).
{
const rdecl = rsig ? lowerer.checker.signatureDeclaration(rsig) : undefined;
const rdecl = rsig ? rsig.declaration : undefined;
if (
rdecl &&
(ts.isFunctionDeclaration(rdecl) || ts.isMethodDeclaration(rdecl)) &&
@@ -451,7 +451,7 @@ function localClassOfResult(
): ts.ClassLikeDeclaration | null {
const symbol = type.getSymbol();
if (!symbol || !(symbol.flags & ts.SymbolFlags.Class)) return null;
const decl = lowerer.checker.valueDeclarationOf(symbol);
const decl = symbol.valueDeclaration;
if (!decl || !(ts.isClassExpression(decl) || ts.isClassDeclaration(decl))) return null;
for (let parent = decl.parent; parent && !ts.isSourceFile(parent); parent = parent.parent)
if (parent === info.decl) return decl;
@@ -1549,9 +1549,9 @@ function implicitDeclaredReturn(lowerer: Lowerer, info: GenericFnInfo): IrType |
const symbol = lowerer.resolveValueSymbol(node.expression);
if (
symbol &&
lowerer.checker
.declarationsOf(symbol)
.some((decl) => ts.isVariableDeclaration(decl) && jsBindingHasOpenWrites(lowerer, decl))
symbol.declarations.some(
(decl) => ts.isVariableDeclaration(decl) && jsBindingHasOpenWrites(lowerer, decl),
)
)
openReturn = true;
return undefined;
@@ -2041,9 +2041,9 @@ export function implicitLocalFnNodeOf(
const sym = lowerer.checker.getSymbolAtLocation(decl.name);
if (!sym) return null;
const isConst = (ts.getCombinedNodeFlags(decl) & ts.NodeFlags.Const) !== 0;
const redeclared = lowerer.checker
.declarationsOf(sym)
.some((d) => d !== decl && ts.isVariableDeclaration(d) && d.initializer !== undefined);
const redeclared = sym.declarations.some(
(d) => d !== decl && ts.isVariableDeclaration(d) && d.initializer !== undefined,
);
if (redeclared) return null;
if (!isConst && !bindingNeverReassigned(lowerer, sym, decl)) return null;
// The capture scan: instances are module functions with no frame.
@@ -2059,7 +2059,7 @@ export function implicitLocalFnNodeOf(
}
if (ts.isIdentifier(n)) {
const s = lowerer.checker.getSymbolAtLocation(n);
const d = s ? lowerer.checker.valueDeclarationOf(s) : undefined;
const d = s ? s.valueDeclaration : undefined;
if (
d &&
d.getSourceFile() === decl.getSourceFile() &&
@@ -2146,7 +2146,7 @@ export function objLitGenericFnNodeOf(
fnNode: ts.MethodDeclaration | ts.FunctionExpression | ts.ArrowFunction;
literal: ts.ObjectLiteralExpression;
} | null {
const decl = lowerer.checker.valueDeclarationOf(propSym);
const decl = propSym.valueDeclaration;
if (!decl) return null;
if (ts.isMethodDeclaration(decl) && ts.isObjectLiteralExpression(decl.parent)) {
return decl.typeParameters !== undefined && decl.body !== undefined
@@ -2420,7 +2420,7 @@ export function requireObjLitGenericReceiver(
);
if (!ts.isIdentifier(recv)) fenceReceiver();
const recvSym = lowerer.resolveValueSymbol(recv);
const recvDecl = recvSym ? lowerer.checker.valueDeclarationOf(recvSym) : undefined;
const recvDecl = recvSym ? recvSym.valueDeclaration : undefined;
if (
!recvDecl ||
!ts.isVariableDeclaration(recvDecl) ||
@@ -190,9 +190,9 @@ export function lowerValueReturningConstructorNew(
if (symbol !== null && (symbol.flags & ts.SymbolFlags.Alias) !== 0)
symbol = lowerer.checker.getAliasedSymbol(symbol);
const decl = symbol
? lowerer.checker
.declarationsOf(symbol)
.find((node): node is ts.ClassDeclaration => ts.isClassDeclaration(node) && isDynamic(node))
? symbol.declarations.find(
(node): node is ts.ClassDeclaration => ts.isClassDeclaration(node) && isDynamic(node),
)
: undefined;
if (decl === undefined) return null;
const loc = locOf(expr);
@@ -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);
}
}
@@ -870,7 +870,7 @@ export function lowerAssertDirectCall(
const module = lowerer.builtinNamespaceModuleOf(callee);
if (module !== "assert" && module !== "assert/strict") return null;
const calleeSym = lowerer.checker.getSymbolAtLocation(callee);
const decl = calleeSym ? lowerer.checker.declarationsOf(calleeSym)[0] : undefined;
const decl = calleeSym ? calleeSym.declarations[0] : undefined;
if (decl && ts.isNamespaceImport(decl)) {
lowerer.unsupported(
"SC1013",
@@ -20,7 +20,7 @@ import {
VOID,
} from "../../ir/ir.js";
import { locOf } from "../program.js";
import { extractIteratorValue } from "./lower-generators.js";
import { extractIteratorValue, type GenType } from "./lower-generators.js";
import { streamSidesOf } from "./lower-stream.js";
import { staticReadableStreamElementType } from "./lower-stdlib.js";
import { genResultRecord } from "../type-mapper.js";
@@ -282,6 +282,43 @@ function classAsyncIteratorPlan(
};
}
/** `for await (const x of obj)` where obj's class declares (or inherits)
* `async *[Symbol.asyncIterator]()`: the loop opens the iterator with that
* method call, evaluated once, and then drives the async generator it
* returns exactly like `for await (const x of obj[Symbol.asyncIterator]())`
* (pi's EventStream). Null for other sources. */
export function classAsyncGeneratorIterator(
lowerer: Lowerer,
expression: ts.Expression,
): (IrExpr & { type: GenType }) | null {
const sourceType = lowerer.mapTypeOf(lowerer.typeOf(expression));
if (sourceType?.kind !== "object") return null;
const info = lowerer.classes.get(sourceType.className);
const open = info ? lowerer.findMethodOn(info, "sym:asyncIterator") : null;
if (
!open ||
open.sig.abstract === true ||
open.sig.params.length !== 0 ||
open.sig.ret.kind !== "generator" ||
!open.sig.ret.async
)
return null;
const retT = open.sig.ret;
const source = lowerer.lowerExpr(expression);
if (source.type.kind !== "object") return null;
return {
...lowerer.accessorCall(
sourceType.className,
"sym:asyncIterator",
source,
[],
retT,
locOf(expression),
),
type: retT,
};
}
export function checkForAwaitBinding(
lowerer: Lowerer,
stmt: ts.ForOfStatement,
@@ -132,13 +132,11 @@ export function isNativeBuiltinValueInitializer(
const symbol = lowerer.resolveValueSymbol(expr);
if (!symbol || seen.has(symbol)) return false;
seen.add(symbol);
return lowerer.checker
.declarationsOf(symbol)
.some(
(decl) =>
ts.isVariableDeclaration(decl) &&
isNativeBuiltinValueInitializer(lowerer, decl.initializer, seen),
);
return symbol.declarations.some(
(decl) =>
ts.isVariableDeclaration(decl) &&
isNativeBuiltinValueInitializer(lowerer, decl.initializer, seen),
);
}
/** The stored Array.isArray function shares the direct call's native test. */
@@ -1,4 +1,5 @@
import { guardNullProto, nullProtoRecordShape } from "./null-prototype.js";
import { refuseModuleMemberThis } from "./module-member-this.js";
import { lowerUnionEquality, tagEqualityMayMissAlias } from "./strict-equality.js";
import { lowerHashStreamCall } from "./builtins/hash-stream.js";
import { lowerResponseStaticJson } from "./lower-stdlib.js";
@@ -150,7 +151,10 @@ import {
lowerPerfHooksCall,
perfEntryTag,
} from "./builtins/performance.js";
import { lowerProcStreamMethodCall } from "./builtins/process.js";
import {
lowerProcStreamMethodCall,
lowerProcessBuiltinModuleNamespace,
} from "./builtins/process.js";
import { recordMayKeepSource, throughRecordSource } from "./record-sources.js";
import { lowerReflectCall } from "./builtins/reflection.js";
import {
@@ -168,6 +172,7 @@ import { isSafeToDiscard } from "./expressions/evaluation-safety.js";
import { tryLowerExpression } from "./expressions/try-lower-expression.js";
import { classToNumberConversion } from "./class-to-primitive.js";
import { hasRuntimeStatics } from "./class-runtime-statics.js";
import { structuralReadType } from "./class-unions.js";
import {
httpClientFnBindingOf,
lowerCompatReqStreamOptionalCall,
@@ -425,6 +430,15 @@ export function lowerCall(lowerer: Lowerer, expr: ts.CallExpression): IrExpr {
return { kind: "libCall", fn: "http2.streamNoop", args: [], type: VOID, loc };
}
// `process.getBuiltinModule("node:os")` typed as the module namespace,
// including pi's cast-and-probe spelling
// `(process as P).getBuiltinModule?.("node:os")` (the member always
// exists on Node 24's process object).
{
const namespace = lowerProcessBuiltinModuleNamespace(lowerer, expr);
if (namespace) return namespace;
}
// Optional-chain call forms: `f?.()` (the token on the call) and
// `a?.m()` (the token on the member access). The handled markers keep
// the chain lowering's re-entrant dispatch from looping.
@@ -685,12 +699,23 @@ export function lowerCall(lowerer: Lowerer, expr: ts.CallExpression): IrExpr {
};
// The declared result is the value's own type (the generic's T):
// validate the dyn copy back into it when the type can be checked;
// dyn-typed and unmappable results stay dyn values (JS files).
// dyn-typed and unmappable results stay dyn values (JS files). An
// empty record (`{}` inferred from a JavaScript `payload = {}`
// default) is the checker's supertype of whatever arrives, not a
// layout: checking into it would drop every member of the copy.
const resultT = lowerer.mapTypeOf(lowerer.typeOf(expr));
const resultShape =
resultT?.kind === "record" ? lowerer.shapes.get(resultT.shapeId) : undefined;
const emptyRecord =
resultShape !== undefined &&
!resultShape.tuple &&
resultShape.indexValue === undefined &&
resultShape.fields.length === 0;
if (
resultT !== null &&
resultT.kind !== "dyn" &&
resultT.kind !== "void" &&
!emptyRecord &&
canDynCheckTo(
resultT,
(id) => lowerer.shapes.get(id),
@@ -1159,7 +1184,7 @@ export function lowerCall(lowerer: Lowerer, expr: ts.CallExpression): IrExpr {
lowerer.noteEdge(sig.name);
const args = lowerer.completeArgs(expr.arguments.nodes, sig.params, loc, expr);
const symbol = lowerer.resolveValueSymbol(expr.expression);
const declaration = symbol ? lowerer.checker.valueDeclarationOf(symbol) : undefined;
const declaration = symbol ? symbol.valueDeclaration : undefined;
// A bare call supplies undefined as this, even inside a callback
// currently carrying a receiver. The value-call ABI saves and
// restores that ambient receiver around the function invocation.
@@ -1254,6 +1279,7 @@ export function lowerCall(lowerer: Lowerer, expr: ts.CallExpression): IrExpr {
if (facadeServed) return facadeServed;
const memberSym = lowerer.checker.getSymbolAtLocation(nsMember);
if (memberSym) fenceEarlyNsMemberRef(lowerer, expr.expression, memberSym);
refuseModuleMemberThis(lowerer, expr, nsMember);
const generic = lowerer.genericFnOf(nsMember);
if (generic) return lowerer.lowerGenericCall(expr, generic);
const sig = lowerer.fnSigOf(nsMember);
@@ -1317,6 +1343,7 @@ export function lowerCall(lowerer: Lowerer, expr: ts.CallExpression): IrExpr {
if (!ts.isIdentifier(nameId)) {
lowerer.unsupported("SC1090", nameId, "private-named module members");
}
refuseModuleMemberThis(lowerer, expr, nameId);
const generic = lowerer.genericFnOf(nameId);
if (generic) return lowerer.lowerGenericCall(expr, generic);
const sig = lowerer.fnSigOf(nameId);
@@ -3537,9 +3564,9 @@ function lowerDynReceiverMethodCall(
// Those are not stored members of a checked-dynamic object, so
// dynKeyGet cannot stand in for JavaScript's prototype lookup. Only
// an authored declaration can justify the own-member path.
const authored = lowerer.checker
.declarationsOf(prop)
.some((decl) => !lowerer.isStdlibFile(decl.getSourceFile()));
const authored = prop.declarations.some(
(decl) => !lowerer.isStdlibFile(decl.getSourceFile()),
);
return (
authored &&
lowerer.checker.getCallSignatures(lowerer.checker.getTypeOfSymbol(prop)).length > 0
@@ -3640,9 +3667,9 @@ function lowerDynReceiverMethodCall(
return true;
return (
prop !== undefined &&
lowerer.checker
.declarationsOf(prop)
.some((decl) => ts.isPropertyDeclaration(decl) || ts.isPropertySignature(decl))
prop.declarations.some(
(decl) => ts.isPropertyDeclaration(decl) || ts.isPropertySignature(decl),
)
);
})();
// Protocol methods keep their static signature: instances of the
@@ -3677,8 +3704,14 @@ function lowerDynReceiverMethodCall(
// own or inherited member exactly like a non-prototype name (chalk's
// `chalk.red.bold(...)` reads a prototype getter of a function), so
// only a string receiver keeps the refusal, deferred to run time.
// The same holds when only the MEMBER is checker-untyped: TypeScript
// importing chalk's JavaScript source types the default export as a
// plain function, so `chalk.bold` reads as `any` on a typed receiver.
const stringOnlyName =
checkerUntyped && !ownCallable && DYN_STRING_PROTO_ONLY_NAMES.has(access.name.text);
(checkerUntyped ||
(lowerer.typeOf(access).flags & (ts.TypeFlags.Any | ts.TypeFlags.Unknown)) !== 0) &&
!ownCallable &&
DYN_STRING_PROTO_ONLY_NAMES.has(access.name.text);
if (DYN_PROTO_METHOD_NAMES.has(access.name.text) && !ownCallable && !stringOnlyName) return null;
// Optional forms (`obj.cb?.()`, `obj?.cb()`) belong to the chain
// machinery's short-circuit semantics — not modeled here yet.
@@ -4297,9 +4330,7 @@ function lowerCaughtPredicateCall(
const callee = call.expression;
if (!ts.isIdentifier(callee)) return null;
const symbol = lowerer.resolveValueSymbol(callee);
const decl = symbol
? lowerer.checker.declarationsOf(symbol).find(ts.isFunctionDeclaration)
: undefined;
const decl = symbol ? symbol.declarations.find(ts.isFunctionDeclaration) : undefined;
if (!symbol || !decl || !decl.body) return null;
if (!decl.type || !ts.isTypePredicateNode(decl.type)) return null;
if (decl.parameters.nodes.length !== 1) return null;
@@ -4868,7 +4899,7 @@ function implicitReceiverOverloadReturn(
if (!member) return null;
const signatures = lowerer.checker.getCallSignatures(lowerer.checker.getTypeOfSymbol(member));
const matches = signatures.filter((signature) => {
const decl = lowerer.checker.signatureDeclaration(signature);
const decl = signature.declaration;
if (!decl || !(ts.isFunctionDeclaration(decl) || ts.isMethodDeclaration(decl)) || decl.body) {
return false;
}
@@ -4920,7 +4951,7 @@ function reconcileOverloadReturn(
call: IrExpr,
): IrExpr {
const rsig = lowerer.checker.getResolvedSignature(expr);
const rdecl = rsig ? lowerer.checker.signatureDeclaration(rsig) : undefined;
const rdecl = rsig ? rsig.declaration : undefined;
// Nominal covariant overrides share the base's return ABI, but retain
// their declared subclass result at the call site. Unannotated fluent
// JS overrides use the same bridge for their proven receiver result.
@@ -6619,6 +6650,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) {
@@ -7861,7 +7893,7 @@ function lowerObjectStaticCall(
? lowerer.checker.getSymbolAtLocation(argNode)
: undefined;
const namespace = symbol
? lowerer.checker.declarationsOf(symbol).some((decl) => ts.isNamespaceImport(decl))
? symbol.declarations.some((decl) => ts.isNamespaceImport(decl))
: false;
const names = namespace ? [...UTIL_TYPES_KEYS, "default"].sort() : UTIL_TYPES_KEYS;
const loc = locOf(call);
@@ -8099,6 +8131,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) {
@@ -8504,7 +8537,7 @@ function isOverloadedMemberAccess(lowerer: Lowerer, access: ts.PropertyAccessExp
function resolvedOverloadResultType(lowerer: Lowerer, call: ts.CallExpression): IrType {
const { checker } = lowerer;
const signature = checker.getResolvedSignature(call);
const decl = signature ? checker.signatureDeclaration(signature) : undefined;
const decl = signature ? signature.declaration : undefined;
const declared = decl ? checker.getSignatureFromDeclaration(decl) : undefined;
const source = declared && declared.getTypeParameters().length === 0 ? declared : signature;
const result = source ? lowerer.mapTypeOf(checker.getReturnTypeOfSignature(source)) : null;
@@ -8696,7 +8729,7 @@ const SP_BRAND_METHODS = new Set([
function arrowSignatureOf(lowerer: Lowerer, node: ts.Expression): ts.Signature | null {
const signatures = lowerer.checker.getCallSignatures(lowerer.typeOf(node));
if (signatures.length !== 1) return null;
const declaration = lowerer.checker.signatureDeclaration(signatures[0]!);
const declaration = signatures[0]!.declaration;
return declaration !== undefined && ts.isArrowFunction(declaration) ? signatures[0]! : null;
}
@@ -8949,7 +8982,7 @@ function lowerObjLitGenericMethodCall(
const recvT = lowerer.typeOf(access.expression);
const propSym = lowerer.checker.getPropertyOfType(recvT, name);
if (!propSym) return null;
const propertyDecl = lowerer.checker.valueDeclarationOf(propSym);
const propertyDecl = propSym.valueDeclaration;
if (
propertyDecl &&
ts.isShorthandPropertyAssignment(propertyDecl) &&
@@ -8973,13 +9006,11 @@ function lowerObjLitGenericMethodCall(
// claimed compilable ones; a class that failed collection keeps its
// own diagnostics and the generic method-call fence downstream).
if (
lowerer.checker
.declarationsOf(propSym)
.some(
(d) =>
d.parent !== undefined &&
(ts.isClassDeclaration(d.parent) || ts.isClassExpression(d.parent)),
)
propSym.declarations.some(
(d) =>
d.parent !== undefined &&
(ts.isClassDeclaration(d.parent) || ts.isClassExpression(d.parent)),
)
) {
return null;
}
@@ -9046,9 +9077,9 @@ function lowerObjLitGenericMethodCall(
// receiver's runtime class must be provable — name that discipline
// instead of the object-literal wording when the method lives on an
// interface.
const onInterface = lowerer.checker
.declarationsOf(propSym)
.some((d) => d.parent !== undefined && ts.isInterfaceDeclaration(d.parent));
const onInterface = propSym.declarations.some(
(d) => d.parent !== undefined && ts.isInterfaceDeclaration(d.parent),
);
// A receiver held as a checked-dynamic value (a class-backed interface)
// dispatches by name to the runtime object's member, which a generic
// method answers with its erased instance; the result converts to this
@@ -9102,7 +9133,9 @@ export function lowerObjectMethodCall(
access: ts.PropertyAccessExpression,
): IrExpr | null {
if (lowerer.chainBlocked(access, call)) return null;
let mappedReceiver = lowerer.mapTypeOf(lowerer.typeOf(access.expression));
let mappedReceiver =
structuralReadType(lowerer, access.expression) ??
lowerer.mapTypeOf(lowerer.typeOf(access.expression));
// EventEmitter's static side (the class, a value of it, or the events
// module, which is the class) keeps its static surface on the emitter
// dispatch (`EventEmitter.setMaxListeners(n)`), which tells the statics
@@ -9716,6 +9749,44 @@ function unionObjectMethodHelper(
if (existing) return existing;
const name = `%union.call.${lowerer.unionCallHelpers.size}`;
lowerer.unionCallHelpers.set(key, name);
// A probe that catches a refusal while the body lowers must not leave
// the name registered for a helper that was never emitted.
try {
lowerer.liftedFns.push(
unionObjectMethodFunction(
lowerer,
name,
sourceCall,
receiverT,
method,
plans,
shapes,
resultT,
loc,
),
);
} catch (e) {
lowerer.unionCallHelpers.delete(key);
throw e;
}
return name;
}
function unionObjectMethodFunction(
lowerer: Lowerer,
name: string,
sourceCall: ts.CallExpression,
receiverT: IrType & { kind: "union" },
method: string,
plans: {
arm: IrType & { kind: "object" };
info: ClassInfo;
found: NonNullable<ReturnType<Lowerer["findMethodOn"]>>;
}[],
shapes: readonly ParamShape[],
resultT: IrType,
loc: SrcLoc,
): IrFunction {
const receiver = varRef("this.0", receiverT, loc);
const argRefs = shapes.map((shape, i) => varRef(`a.${i}`, shape.type, loc));
const branch = (plan: (typeof plans)[number]): IrStmt[] => {
@@ -9790,8 +9861,7 @@ function unionObjectMethodHelper(
type: param.type,
mutable: false,
}));
lowerer.liftedFns.push({ name, params, returnType: resultT, locals, body, loc });
return name;
return { name, params, returnType: resultT, locals, body, loc };
}
/** The argument's lowered type for a message (what the static form saw),
@@ -1,5 +1,10 @@
import { lowerValueReturningConstructorNew } from "./js-dynamic-objects.js";
import { dynUndefinedExpr, nodeThrowExpr, varRef } from "../../ir/build.js";
import {
constructedFromValue,
constructedValue,
superIntoResultSlot,
} from "./constructor-returns.js";
import { checkedIterableSpread } from "./checked-iterable-spread.js";
import { lowerReadlineUiNew } from "./builtins/readline.js";
import { checkedClassUnionAssertion } from "./class-assertions.js";
@@ -86,7 +91,7 @@ import {
isNodeTypesPath,
locOf,
} from "../program.js";
import { PoisonError, dynFallbackType, newFnCtx, own } from "./lowerer.js";
import { PoisonError, dynFallbackType, jsFuncNameOf, newFnCtx, own } from "./lowerer.js";
import { isAssertClassNew } from "./assert-instances.js";
import { lowerArrayConstructor } from "./containers/array-construction.js";
import { lowerMapSeedNew, lowerSetSeedNew } from "./containers/collection-methods.js";
@@ -178,6 +183,39 @@ export function returnsOnlyThis(member: ts.MethodDeclaration): boolean {
return valid;
}
/** returnsOnlyThis, also accepting a comma expression whose last operand
* is `this` (the minifier's `return a(), this.x = v, this`, jose's fluent
* claim setters). Used only to narrow a call's result back to the
* receiver's class. */
export function returnsOnlyThisValue(member: ts.MethodDeclaration): boolean {
const body = member.body;
const last = body?.statements?.nodes.at(-1);
if (!body || !last || !ts.isReturnStatement(last)) return false;
let valid = true;
const visit = (node: ts.Node): void => {
if (ts.isFunctionLike(node)) return;
if (ts.isReturnStatement(node)) {
let value = node.expression;
for (;;) {
while (value && ts.isParenthesizedExpression(value)) value = value.expression;
if (
value &&
ts.isBinaryExpression(value) &&
value.operatorToken.kind === ts.SyntaxKind.CommaToken
) {
value = value.right;
continue;
}
break;
}
if (!value || value.kind !== ts.SyntaxKind.ThisKeyword) valid = false;
}
node.forEachChild(visit);
};
body.forEachChild(visit);
return valid;
}
function functionLocalClass(decl: ts.ClassLikeDeclaration): boolean {
for (
let parent: ts.Node | undefined = decl.parent;
@@ -221,14 +259,12 @@ export function storedClassValueType(
const symbol = lowerer.resolveValueSymbol(expression);
const declaration =
symbol &&
lowerer.checker
.declarationsOf(symbol)
.find(
(decl) =>
ts.isVariableDeclaration(decl) &&
decl.initializer &&
(ts.getCombinedNodeFlags(decl) & ts.NodeFlags.Const) !== 0,
);
symbol.declarations.find(
(decl) =>
ts.isVariableDeclaration(decl) &&
decl.initializer &&
(ts.getCombinedNodeFlags(decl) & ts.NodeFlags.Const) !== 0,
);
if (declaration && ts.isVariableDeclaration(declaration) && declaration.initializer) {
const value = storedClassValueType(lowerer, declaration.initializer, visiting);
if (value?.kind === "classval") return value;
@@ -243,7 +279,7 @@ export function storedClassValueType(
const mapped = lowerer.mapTypeOf(type);
if (mapped?.kind === "classval" && !lowerer.classes.has(mapped.className)) {
for (const signature of lowerer.checker.getConstructSignatures(type)) {
const constructor = lowerer.checker.signatureDeclaration(signature);
const constructor = signature.declaration;
const declaration =
constructor && ts.isConstructorDeclaration(constructor) ? constructor.parent : undefined;
if (
@@ -284,14 +320,12 @@ export function storedClassValueType(
const symbol = lowerer.resolveValueSymbol(callee);
const declaration =
symbol &&
lowerer.checker
.declarationsOf(symbol)
.find(
(decl) =>
ts.isVariableDeclaration(decl) &&
decl.initializer &&
(ts.getCombinedNodeFlags(decl) & ts.NodeFlags.Const) !== 0,
);
symbol.declarations.find(
(decl) =>
ts.isVariableDeclaration(decl) &&
decl.initializer &&
(ts.getCombinedNodeFlags(decl) & ts.NodeFlags.Const) !== 0,
);
if (declaration && ts.isVariableDeclaration(declaration) && declaration.initializer) {
callee = declaration.initializer;
continue;
@@ -353,7 +387,7 @@ export function storedClassValueType(
// Cache the module-level closure so its captures and lifted bodies are
// exactly the ones used when the declaration executes.
for (const signature of lowerer.checker.getConstructSignatures(type)) {
const declaration = lowerer.checker.signatureDeclaration(signature);
const declaration = signature.declaration;
const cls = declaration && ts.isConstructorDeclaration(declaration) ? declaration.parent : null;
if (!cls || (!ts.isClassExpression(cls) && !ts.isClassDeclaration(cls))) continue;
let factory: ts.ArrowFunction | ts.FunctionExpression | undefined;
@@ -725,7 +759,7 @@ export interface GenericClassInfo {
* or the ElementAccessExpression itself). Null when no declaration has
* that shape (well-known-symbol members like `[Symbol.iterator]`). */
function lateBoundKeySymOf(lowerer: Lowerer, p: ts.Symbol): ts.Symbol | null {
for (const d of lowerer.checker.declarationsOf(p)) {
for (const d of p.declarations) {
const access =
ts.isBinaryExpression(d) && ts.isElementAccessExpression(d.left)
? d.left
@@ -990,7 +1024,7 @@ export function builtinStreamInfoOf(
// member to the stdlib class symbol. The declaration itself is
// alias plumbing (streamClassAliasDecl — both declaration walks
// skip it).
const decl = lowerer.checker.valueDeclarationOf(symbol);
const decl = symbol.valueDeclaration;
if (
decl &&
ts.isVariableDeclaration(decl) &&
@@ -1012,7 +1046,7 @@ export function builtinStreamInfoOf(
if (rec.lib === symbol.name) irName = name;
}
if (!irName) return null;
const declared = lowerer.checker.declarationsOf(symbol).some((d) => {
const declared = symbol.declarations.some((d) => {
if (!ts.isClassDeclaration(d)) return false;
if (isNodeTypesPath(d.getSourceFile().fileName)) return false;
let node: ts.Node | undefined = d.parent;
@@ -1034,15 +1068,13 @@ function jsFieldInitializerType(lowerer: Lowerer, type: IrType, expression: ts.E
const symbol = lowerer.checker.getSymbolAtLocation(expression);
if (
symbol &&
lowerer.checker
.declarationsOf(symbol)
.some(
(decl) =>
ts.isParameter(decl) &&
((ts.isConstructorDeclaration(decl.parent) &&
!checkedJsDocumentedParameter(lowerer, decl)) ||
(lowerer.typeOf(expression).flags & (ts.TypeFlags.Any | ts.TypeFlags.Unknown)) !== 0),
)
symbol.declarations.some(
(decl) =>
ts.isParameter(decl) &&
((ts.isConstructorDeclaration(decl.parent) &&
!checkedJsDocumentedParameter(lowerer, decl)) ||
(lowerer.typeOf(expression).flags & (ts.TypeFlags.Any | ts.TypeFlags.Unknown)) !== 0),
)
)
return DYN;
}
@@ -1102,7 +1134,7 @@ function jsFieldInitializerType(lowerer: Lowerer, type: IrType, expression: ts.E
const symbol = lowerer.checker.getSymbolAtLocation(part.name);
if (
symbol &&
lowerer.checker.declarationsOf(symbol).some((decl) => {
symbol.declarations.some((decl) => {
const member = ts.isBinaryExpression(decl) ? decl.left : decl;
return (
ts.isPropertyAccessExpression(member) &&
@@ -1244,13 +1276,10 @@ function returnsWidenedJsField(
: undefined;
if (
symbol &&
lowerer.checker
.declarationsOf(symbol)
.some(
(declaration) =>
ts.isMethodDeclaration(declaration) &&
lowerer.checkedVirtualJsMethods.has(declaration),
)
symbol.declarations.some(
(declaration) =>
ts.isMethodDeclaration(declaration) && lowerer.checkedVirtualJsMethods.has(declaration),
)
)
return true;
}
@@ -1263,14 +1292,12 @@ function returnsWidenedJsField(
const symbol = lowerer.checker.getSymbolAtLocation(expression.name);
if (
symbol &&
lowerer.checker
.declarationsOf(symbol)
.some(
(declaration) =>
(ts.isGetAccessorDeclaration(declaration) ||
ts.isSetAccessorDeclaration(declaration)) &&
lowerer.checkedVirtualJsAccessors.has(declaration),
)
symbol.declarations.some(
(declaration) =>
(ts.isGetAccessorDeclaration(declaration) ||
ts.isSetAccessorDeclaration(declaration)) &&
lowerer.checkedVirtualJsAccessors.has(declaration),
)
)
return true;
}
@@ -1303,11 +1330,11 @@ function returnsWidenedJsField(
* (genResultRecord's list, including scalar-backed Date values). */
function undefArmedFieldType(lowerer: Lowerer, p: ts.Symbol): IrType | null {
const t = lowerer.checker.getTypeOfSymbol(p);
const site = lowerer.checker.declarationsOf(p)[0];
const site = p.declarations[0];
let mapped = lowerer.mapTypeOf(t) ?? (site ? dynFallbackType(lowerer, site, t) : null);
if (mapped && site) mapped = jsOpenObjectType(site, mapped, lowerer.shapes, lowerer.unions);
if (!mapped || mapped.kind === "void") return null;
for (const declaration of lowerer.checker.declarationsOf(p)) {
for (const declaration of p.declarations) {
const assignment = ts.isBinaryExpression(declaration) ? declaration : declaration.parent;
if (
assignment &&
@@ -1354,7 +1381,7 @@ export function builtinEmitterInfoOf(
// follow the member off the module namespace. The declaration
// itself is alias plumbing (builtinMemberRequireDecl — both
// declaration walks skip it).
const decl = lowerer.checker.valueDeclarationOf(symbol);
const decl = symbol.valueDeclaration;
if (
decl !== undefined &&
ts.isVariableDeclaration(decl) &&
@@ -1370,7 +1397,7 @@ export function builtinEmitterInfoOf(
return null;
}
if (symbol.name !== "EventEmitter") return null;
const declared = lowerer.checker.declarationsOf(symbol).some((d) => {
const declared = symbol.declarations.some((d) => {
if (!ts.isClassDeclaration(d) && !ts.isInterfaceDeclaration(d)) return false;
let node: ts.Node | undefined = d.parent;
while (node) {
@@ -1539,7 +1566,7 @@ function collectClassShapeDeferring(lowerer: Lowerer, decl: ts.ClassDeclaration)
const baseSymbol =
reference && ts.isIdentifier(reference) ? lowerer.resolveValueSymbol(reference) : undefined;
const baseDeclaration = baseSymbol
? lowerer.checker.declarationsOf(baseSymbol).find(ts.isClassDeclaration)
? baseSymbol.declarations.find(ts.isClassDeclaration)
: undefined;
if (
baseDeclaration &&
@@ -1886,8 +1913,7 @@ export function collectClassShapeInner(
return true;
}
const signatures = lowerer.checker.getCallSignatures(lowerer.typeOf(expression));
const source =
signatures.length === 1 ? lowerer.checker.signatureDeclaration(signatures[0]!) : undefined;
const source = signatures.length === 1 ? signatures[0]!.declaration : undefined;
if (
!source ||
!isJsSourceFile(source.getSourceFile()) ||
@@ -2023,12 +2049,9 @@ export function collectClassShapeInner(
// what actually blocks it).
const rebinds =
memberSym !== undefined &&
lowerer.checker
.declarationsOf(memberSym)
.filter(
(d) =>
ts.isBinaryExpression(d) && d.operatorToken.kind === ts.SyntaxKind.EqualsToken,
).length > 1;
memberSym.declarations.filter(
(d) => ts.isBinaryExpression(d) && d.operatorToken.kind === ts.SyntaxKind.EqualsToken,
).length > 1;
if (rebinds) {
lowerer.unsupported(
"SC1090",
@@ -4015,14 +4038,12 @@ export function collectClassShapeInner(
if (fields.has(p.name) || methods.has(p.name)) continue;
if (hasAccessor(p.name)) continue;
if (base && (base.fields.has(p.name) || lowerer.findMethodOn(base, p.name))) continue;
const site = lowerer.checker
.declarationsOf(p)
.find(
(d) =>
ts.isPropertyAccessExpression(d) ||
ts.isBinaryExpression(d) ||
ts.isElementAccessExpression(d),
);
const site = p.declarations.find(
(d) =>
ts.isPropertyAccessExpression(d) ||
ts.isBinaryExpression(d) ||
ts.isElementAccessExpression(d),
);
if (!site) continue;
// Late-bound properties: the ones the scan above collected are
// real fields under their printable names — skip. The rest keep
@@ -4726,8 +4747,7 @@ export function analyzeClassDecoration(lowerer: Lowerer, info: ClassInfo): void
: null;
if (ambientCallee !== null) {
const sym = lowerer.resolveValueSymbol(ambientCallee);
const vdecl =
sym && !lowerer.isStdlibSymbol(sym) ? lowerer.checker.declarationsOf(sym)[0] : undefined;
const vdecl = sym && !lowerer.isStdlibSymbol(sym) ? sym.declarations[0] : undefined;
const ambientVar =
vdecl !== undefined &&
ts.isVariableDeclaration(vdecl) &&
@@ -5747,7 +5767,7 @@ export function exactClassOfReceiver(lowerer: Lowerer, expr: ts.Expression): Cla
// static write would create an own property in JS. The general
// class-value write fence answers instead.
if (direct) return direct.classDecorators?.valueGlobalId !== undefined ? null : direct;
const decl = lowerer.checker.valueDeclarationOf(symbol);
const decl = symbol.valueDeclaration;
if (
!decl ||
!ts.isVariableDeclaration(decl) ||
@@ -5795,7 +5815,7 @@ export function propertyAssignedClassInfoOf(
symbol.flags & ts.SymbolFlags.Alias ? lowerer.checker.getAliasedSymbol(symbol) : symbol;
const registered = lowerer.classBySymbol.get(resolved);
if (registered) return registered;
const decls = lowerer.checker.declarationsOf(resolved);
const decls = resolved.declarations;
// The class expression's OWN symbol (tsgo's answer through CJS export
// aliases): the declaration is the expression itself. Its top-level
// assignment statement must be the binding's ONLY producer. The
@@ -6259,7 +6279,7 @@ function capturesEnclosingScope(
if (captured) return;
if (ts.isIdentifier(node)) {
const symbol = lowerer.checker.getSymbolAtLocation(node);
const decl = symbol ? lowerer.checker.valueDeclarationOf(symbol) : undefined;
const decl = symbol ? symbol.valueDeclaration : undefined;
// The enclosing function's own name is the same binding for every
// evaluation.
if (decl && decl !== scope && inside(decl, scope) && !inside(decl, member)) captured = true;
@@ -6665,7 +6685,7 @@ export function collectInheritedJsFieldWrites(
for (const base of lowerer.checker.getBaseTypes(target)) {
for (const [name, expressions] of writes) {
const property = lowerer.checker.getPropertyOfType(base, name);
for (const declaration of property ? lowerer.checker.declarationsOf(property) : []) {
for (const declaration of property ? property.declarations : []) {
if (ts.isMethodDeclaration(declaration) || ts.isAccessor(declaration)) continue;
let owner: ts.Node | undefined = declaration.parent;
while (owner && !ts.isClassDeclaration(owner) && !ts.isClassExpression(owner))
@@ -6724,7 +6744,7 @@ export function collectVirtualJsMethods(lowerer: Lowerer, files: readonly ts.Sou
for (const member of accessors) {
if (!ts.isIdentifier(member.name)) continue;
const property = lowerer.checker.getPropertyOfType(base, member.name.text);
for (const declaration of property ? lowerer.checker.declarationsOf(property) : []) {
for (const declaration of property ? property.declarations : []) {
if (
(ts.isGetAccessorDeclaration(declaration) ||
ts.isSetAccessorDeclaration(declaration)) &&
@@ -6767,7 +6787,7 @@ export function collectVirtualJsMethods(lowerer: Lowerer, files: readonly ts.Sou
for (const member of members) {
if (!ts.isIdentifier(member.name)) continue;
const property = lowerer.checker.getPropertyOfType(base, member.name.text);
for (const declaration of property ? lowerer.checker.declarationsOf(property) : []) {
for (const declaration of property ? property.declarations : []) {
if (
ts.isMethodDeclaration(declaration) &&
isJsSourceFile(declaration.getSourceFile()) &&
@@ -6877,7 +6897,7 @@ export function exactInstanceClassOf(lowerer: Lowerer, expr: ts.Expression): Cla
if (direct) return direct;
if (!ts.isIdentifier(e)) return null;
const symbol = lowerer.resolveValueSymbol(e);
const decl = symbol ? lowerer.checker.valueDeclarationOf(symbol) : undefined;
const decl = symbol ? symbol.valueDeclaration : undefined;
if (
!decl ||
!ts.isVariableDeclaration(decl) ||
@@ -6988,19 +7008,43 @@ export function lowerClassGenericMethodCall(
}
: upcastTo(lowerer, receiver, declarer.def.name);
const args = lowerer.completeArgs(call.arguments.nodes, instance.params, loc, call);
return {
const result: IrExpr = {
kind: "call",
callee: instance.name,
args: [thisArg, ...args],
type: instance.returnType,
loc,
};
// A fluent JavaScript method (every return ends in `this`, jose's claim
// setters) answers the receiver: the instance returns the declarer's
// class, a used result's checker type is the receiver's subclass.
if (
result.type.kind === "object" &&
!ts.isExpressionStatement(call.parent) &&
ts.isMethodDeclaration(info.decl) &&
isJsSourceFile(info.decl.getSourceFile()) &&
returnsOnlyThisValue(info.decl)
) {
return lowerer.maybeNarrow(result, call);
}
return result;
}
/** Wraps a derived-class expression in an upcast when the target base
* class differs (a no-op reinterpret at runtime; keeps IR types exact). */
export function upcastTo(lowerer: Lowerer, expr: IrExpr, className: string): IrExpr {
if (expr.type.kind === "object" && expr.type.className !== className) {
// A value lowered at an ANCESTOR of the class the site needs (a
// fluent `this` result whose lowering kept the method's declaring
// class, the receiver of the next call in a chain): the checker
// vouches for the subclass, so the bridge is the verified downcast,
// never an upcast the validator rejects (an internal error before).
if (
!lowerer.isSubclassOf(expr.type.className, className) &&
lowerer.isSubclassOf(className, expr.type.className)
) {
return { kind: "downcast", value: expr, type: { kind: "object", className }, loc: expr.loc };
}
return { kind: "upcast", value: expr, type: { kind: "object", className }, loc: expr.loc };
}
return expr;
@@ -8303,7 +8347,7 @@ export function superCallStmt(
)[0]!;
}
lowerer.noteSuperConstructorEdge(`%${base.def.name}.constructor`);
return {
const call: IrStmt = {
kind: "exprStmt",
expr: {
kind: "call",
@@ -8314,6 +8358,14 @@ export function superCallStmt(
},
loc,
};
return superIntoResultSlot(
lowerer,
base,
info,
call,
lowerer.upcastTo({ ...thisRef }, base.def.name),
info.ctor ?? info.decl!,
);
}
/** `super.method(args)`: the base chain's implementation, called
@@ -8711,21 +8763,21 @@ function lowerProgramClassNew(
: lowerer.completeArgs(expr.arguments?.nodes ?? [], info.ctorParams, loc, expr);
for (let current: ClassInfo | null = info; current; current = current.base) {
if (current.def.baseValueGlobal)
return {
return constructedValue(lowerer, info, {
kind: "newValue",
callee: classValueRef(lowerer, info, expr),
args,
type: { kind: "object", className: info.def.name },
loc,
};
});
}
return {
return constructedValue(lowerer, info, {
kind: "new",
className: info.def.name,
args,
type: { kind: "object", className: info.def.name },
loc,
};
});
}
function assignedThisFieldType(lowerer: Lowerer, expr: ts.NewExpression): IrType | null {
@@ -8767,7 +8819,7 @@ export function lowerNew(lowerer: Lowerer, expr: ts.NewExpression): IrExpr {
let intlConstructor = expr.expression;
if (ts.isIdentifier(intlConstructor)) {
const symbol = lowerer.resolveValueSymbol(intlConstructor);
const declaration = symbol ? lowerer.checker.valueDeclarationOf(symbol) : undefined;
const declaration = symbol ? symbol.valueDeclaration : undefined;
if (declaration && ts.isVariableDeclaration(declaration) && declaration.initializer)
intlConstructor = declaration.initializer;
}
@@ -8791,9 +8843,7 @@ export function lowerNew(lowerer: Lowerer, expr: ts.NewExpression): IrExpr {
const symbol = lowerer.resolveValueSymbol(expr.expression);
const declaration =
symbol &&
lowerer.checker
.declarationsOf(symbol)
.find((node) => ts.isFunctionDeclaration(node) && node.body !== undefined);
symbol.declarations.find((node) => ts.isFunctionDeclaration(node) && node.body !== undefined);
if (declaration && ts.isFunctionDeclaration(declaration) && declaration.body) {
const last = declaration.body.statements.nodes.at(-1);
if (
@@ -9026,13 +9076,18 @@ export function lowerNew(lowerer: Lowerer, expr: ts.NewExpression): IrExpr {
loc,
expr,
);
return {
kind: "newValue",
callee,
args,
type: { kind: "object", className: info.def.name },
loc,
};
return constructedFromValue(
lowerer,
info,
{
kind: "newValue",
callee,
args,
type: { kind: "object", className: info.def.name },
loc,
},
expr,
);
}
}
}
@@ -9659,7 +9714,7 @@ export function lowerNew(lowerer: Lowerer, expr: ts.NewExpression): IrExpr {
const a0 = args[0]!;
if (ts.isIdentifier(a0) && a0.text === "setImmediate") {
const sym = lowerer.checker.getSymbolAtLocation(a0);
const decls = sym ? lowerer.checker.declarationsOf(sym) : [];
const decls = sym ? sym.declarations : [];
if (decls.length > 0 && decls.every((d) => lowerer.isStdlibFile(d.getSourceFile()))) {
// The settled value is the undefined dyn value — the result
// is promise<dyn> whatever T the checker inferred for the
@@ -9955,13 +10010,18 @@ export function lowerNew(lowerer: Lowerer, expr: ts.NewExpression): IrExpr {
loc,
expr,
);
const built: IrExpr = {
kind: "newValue",
callee,
args,
type: { kind: "object", className: info.def.name },
loc,
};
const built = constructedFromValue(
lowerer,
info,
{
kind: "newValue",
callee,
args,
type: { kind: "object", className: info.def.name },
loc,
},
expr,
);
// A runtime class slot widened from a union (`[Readable, Writable]`'s
// elements hold EventEmitter values): the checker types the result as
// the union of the arms' instances, which the checked class view
@@ -10028,7 +10088,15 @@ export function lowerNew(lowerer: Lowerer, expr: ts.NewExpression): IrExpr {
((callee.type.kind === "func" || callee.type.kind === "classval") &&
lowerer.dynConvertible(callee.type)))
) {
return lowerCheckedConstructor(lowerer, expr, lowerer.coerceToExpected(callee, DYN));
let boxed = lowerer.coerceToExpected(callee, DYN);
// A plain function's box carries its name: the construction's
// prototype records it as `constructor`, which util.inspect prints
// (`F { a: 2 }`).
if (boxed.kind === "dynFrom" && boxed.fnName === undefined && callee.type.kind === "func") {
const name = jsFuncNameOf(expr.expression, lowerer);
if (name !== null) boxed = { ...boxed, fnName: name };
}
return lowerCheckedConstructor(lowerer, expr, boxed);
}
lowerer.unsupported(
"SC1090",
@@ -162,7 +162,7 @@ export function rejectComptimeCaptures(
const symbol = ts.isShorthandPropertyAssignment(parent)
? lowerer.checker.getShorthandAssignmentValueSymbol(parent)
: lowerer.checker.getSymbolAtLocation(node);
const decls = symbol ? lowerer.checker.declarationsOf(symbol) : undefined;
const decls = symbol ? symbol.declarations : undefined;
if (symbol && decls && decls.length > 0) {
const ownLocal = decls.every(
(d) => d.getSourceFile() === sf && d.pos >= cb.pos && d.end <= cb.end,
@@ -73,7 +73,7 @@ export function lowerEnumAccess(
if (ts.isPropertyAccessExpression(expr)) {
const sym = lowerer.checker.getSymbolAtLocation(expr.name);
if (sym && sym.flags & ts.SymbolFlags.EnumMember) {
const decl = lowerer.checker.valueDeclarationOf(sym);
const decl = sym.valueDeclaration;
if (decl && ts.isEnumMember(decl) && effectFreeReceiver(expr.expression)) {
return lowerEnumMemberRead(lowerer, expr, decl);
}
@@ -86,9 +86,9 @@ export function lowerEnumAccess(
if (!ts.isIdentifier(recv)) return null;
const recvSym = lowerer.resolveValueSymbol(recv);
if (!recvSym || !(recvSym.flags & ts.SymbolFlags.Enum)) return null;
const decls = lowerer.checker
.declarationsOf(recvSym)
.filter((d): d is ts.EnumDeclaration => ts.isEnumDeclaration(d));
const decls = recvSym.declarations.filter((d): d is ts.EnumDeclaration =>
ts.isEnumDeclaration(d),
);
if (decls.length === 0) return null;
let key = expr.argumentExpression;
while (ts.isParenthesizedExpression(key)) key = key.expression;
@@ -222,7 +222,7 @@ function foldConstIndex(lowerer: Lowerer, e: ts.Expression): number | null {
if (ts.isPropertyAccessExpression(x)) {
const sym = lowerer.checker.getSymbolAtLocation(x.name);
if (sym && sym.flags & ts.SymbolFlags.EnumMember) {
const decl = lowerer.checker.valueDeclarationOf(sym);
const decl = sym.valueDeclaration;
if (decl && ts.isEnumMember(decl)) {
const v = lowerer.checker.getConstantValue(decl);
if (typeof v === "number") return v;
@@ -420,7 +420,7 @@ function isErrorMonitorRef(lowerer: Lowerer, arg: ts.Expression): boolean {
// binding's type in JavaScript; its source property still names it.
if (!ts.isIdentifier(arg)) return false;
const symbol = lowerer.resolveValueSymbol(arg);
const declaration = symbol ? lowerer.checker.valueDeclarationOf(symbol) : undefined;
const declaration = symbol ? symbol.valueDeclaration : undefined;
if (!declaration || !ts.isBindingElement(declaration)) return false;
const pattern = declaration.parent;
const variable = pattern?.parent;
@@ -475,7 +475,7 @@ function staticStringPrefixOf(
const symbol = lowerer.resolveValueSymbol(expression);
if (!symbol || seen.has(symbol)) return null;
seen.add(symbol);
const declaration = lowerer.checker.valueDeclarationOf(symbol);
const declaration = symbol.valueDeclaration;
if (
declaration &&
ts.isVariableDeclaration(declaration) &&
@@ -509,7 +509,7 @@ function staticStringSuffixOf(
const symbol = lowerer.resolveValueSymbol(expression);
if (!symbol || seen.has(symbol)) return null;
seen.add(symbol);
const declaration = lowerer.checker.valueDeclarationOf(symbol);
const declaration = symbol.valueDeclaration;
if (
declaration &&
ts.isVariableDeclaration(declaration) &&
@@ -97,7 +97,7 @@ function expandoFnSymbolOf(lowerer: Lowerer, recv: ts.Expression): ts.Symbol | n
// existing member paths stay put.
const sym = lowerer.resolveValueSymbol(recv);
if (!sym) return null;
const decl = lowerer.checker.valueDeclarationOf(sym);
const decl = sym.valueDeclaration;
if (!decl || decl.getSourceFile().isDeclarationFile) return null;
if (isJsSourceFile(decl.getSourceFile())) return null;
if (ts.isFunctionDeclaration(decl)) {
@@ -202,7 +202,7 @@ export function collectExpandoMembers(lowerer: Lowerer, sf: ts.SourceFile): void
const memberName =
typeof w.key === "string"
? w.key
: `sym%${w.key.name}%${lowerer.checker.declarationsOf(w.key)[0]?.getStart() ?? 0}`;
: `sym%${w.key.name}%${w.key.declarations[0]?.getStart() ?? 0}`;
const g: IrGlobal = {
id: `%g.${tag}${fnName}%.${memberName}`,
name: `${fnName}.${memberName}`,
@@ -239,7 +239,7 @@ function nsOwnedMember(
const nameNode = ts.isPropertyAccessExpression(access) ? access.name : access.argumentExpression;
const sym = lowerer.checker.getSymbolAtLocation(nameNode);
if (!sym) return false;
for (const d of lowerer.checker.declarationsOf(sym)) {
for (const d of sym.declarations) {
for (
let p: ts.Node | undefined = d.parent;
p !== undefined && !ts.isSourceFile(p);
@@ -6,13 +6,14 @@ import { lowerEventObjectProperty, lowerEventObjectPropertyWrite } from "./lower
import { lowerDisposableStackInstanceOf } from "./builtins/disposable-stack.js";
import { lowerEventStaticProperty } from "./builtins/numbers.js";
import { lowerUnionEquality, tagEqualityMayMissAlias } from "./strict-equality.js";
import { isOptionalProcessStreamProperty } from "./builtins/process.js";
import { isOptionalProcessStreamProperty, NODE_COMPAT_VERSION } from "./builtins/process.js";
import { lowerWorkerMetadata } from "./builtins/workers.js";
import { esmImportBindingKind } from "./builtins/module-bindings.js";
import { lowerReadlineProperty } from "./builtins/readline.js";
import { lowerBlobInstanceof } from "./builtins/blob.js";
import { lowerWebAssemblyTypeof, refuseWebAssemblyUse } from "./builtins/webassembly.js";
import { lowerKeyObjectInstanceof } from "./builtins/key-object.js";
import { lowerCryptoKeyInstanceof } from "./builtins/webcrypto.js";
import { lowerNetAddressInstanceof, lowerNetAddressStaticRead } from "./builtins/net-address.js";
import { isExecExceptionType, refuseExecExceptionCode } from "./builtins/errors.js";
import { STATS_TYPE_TESTS } from "./builtins/filesystem.js";
@@ -33,6 +34,7 @@ import { literalUnionArm } from "../union-discriminants.js";
* (FieldTarget). */
import { isFsStreamMember, lowerFsStreamProperty } from "./builtins/fs-streams.js";
import * as ts from "../ts7/adapter.js";
import { builtinFunctionFenceValue } from "./builtin-function-fences.js";
import {
isZlibStreamMember,
lowerZlibInstanceOf,
@@ -68,7 +70,12 @@ import {
} from "./maybe-promise.js";
import { checkedClassAssertion, checkedClassUnionAssertion } from "./class-assertions.js";
import { checkedArmExtraction } from "./coercions/unions.js";
import { narrowClassUnion, narrowStoredClassValue } from "./class-unions.js";
import {
narrowClassUnion,
narrowStoredClassValue,
structuralNarrowing,
structuralReadType,
} from "./class-unions.js";
import { lowerUnionFieldRead, lowerUnionFieldWrite } from "./expressions/union-fields.js";
import { captureContextArguments, declareContextLocal } from "./function-context.js";
import { OBJECT_CALLABLE_VALUES } from "./surfaces.js";
@@ -148,6 +155,7 @@ import {
unsupportedDiag,
} from "../../diagnostics/diagnostic.js";
import { PoisonError, dynFallbackType, jsFuncNameOf, neverTaintedJsType, own } from "./lowerer.js";
import { assertsGlobalObject, extractionCopiesReference } from "./assertion-identity.js";
import { lowerCollectionSpread } from "./containers/collection-methods.js";
import { lowerHelperReturnCall } from "./containers/iterator-helpers.js";
import {
@@ -215,6 +223,7 @@ import { errorPropertyRead, errorPropertyWrite, errorToStringCall } from "./erro
import { lowerStreamProperty, lowerStreamStateProperty, streamSidesOf } from "./lower-stream.js";
import { unionWideningTags } from "../../ir/analysis.js";
import { isSafeToDiscard, isSafeToRepeat } from "./expressions/evaluation-safety.js";
import { isNavigatorGlobal, lowerNavigatorMember, refuseNavigator } from "./builtins/navigator.js";
import { globalSymbolKey } from "./expressions/global-symbols.js";
import { lowerShortCircuitAssignment } from "./expressions/nullish-assignment.js";
import {
@@ -345,23 +354,41 @@ export function abstractPropertyDeclOf(
): boolean {
const sym = lowerer.checker.getSymbolAtLocation(expr.name);
if (!sym) return false;
return lowerer.checker
.declarationsOf(sym)
.some(
(d) =>
ts.isPropertyDeclaration(d) &&
ts.getModifiers(d)?.some((m) => m.kind === ts.SyntaxKind.AbstractKeyword) === true,
);
return sym.declarations.some(
(d) =>
ts.isPropertyDeclaration(d) &&
ts.getModifiers(d)?.some((m) => m.kind === ts.SyntaxKind.AbstractKeyword) === true,
);
}
/** A field declared by node:util's parseArgs checked-dynamic type family.
* Narrowing a ParseArgsToken exposes one anonymous union arm, so its alias
* identity no longer reaches mapType; declaration ancestry is the stable
* provenance check. */
/** The Uint8Array base64/hex prototype methods (ES2026) Node 24 lacks. */
const UINT8ARRAY_BASE64_METHODS: ReadonlySet<string> = new Set([
"toBase64",
"toHex",
"setFromBase64",
"setFromHex",
]);
/** `Uint8Array.prototype.<base64/hex method>` read off the global. */
function isUint8ArrayBase64Probe(lowerer: Lowerer, expr: ts.PropertyAccessExpression): boolean {
if (!UINT8ARRAY_BASE64_METHODS.has(expr.name.text)) return false;
const proto = expr.expression;
return (
ts.isPropertyAccessExpression(proto) &&
!proto.questionDotToken &&
proto.name.text === "prototype" &&
stdlibGlobalNameOf(lowerer, proto.expression) === "Uint8Array"
);
}
function isParseArgsDynProperty(lowerer: Lowerer, expr: ts.PropertyAccessExpression): boolean {
const sym = lowerer.checker.getSymbolAtLocation(expr.name);
if (!sym) return false;
return lowerer.checker.declarationsOf(sym).some((d) => {
return sym.declarations.some((d) => {
if (!lowerer.isStdlibFile(d.getSourceFile())) return false;
let inParseArgsType = false;
for (let node: ts.Node | undefined = d.parent; node; node = node.parent) {
@@ -389,7 +416,7 @@ function isStatFsDynProperty(lowerer: Lowerer, expr: ts.PropertyAccessExpression
const symbol = lowerer.checker.getSymbolAtLocation(expr.name);
return (
!!symbol &&
lowerer.checker.declarationsOf(symbol).some((declaration) => {
symbol.declarations.some((declaration) => {
if (!lowerer.isStdlibFile(declaration.getSourceFile())) return false;
const parent = declaration.parent;
if (!ts.isInterfaceDeclaration(parent)) return false;
@@ -646,6 +673,8 @@ function lowerExprInner(lowerer: Lowerer, expr: ts.Expression): IrExpr {
return { kind: "strLit", value: "object", type: STRING, loc };
if (lowerer.isStdlibGlobal(expr.expression, "fetch"))
return { kind: "strLit", value: "function", type: STRING, loc };
if (isNavigatorGlobal(lowerer, expr.expression))
return { kind: "strLit", value: "object", type: STRING, loc };
const performance = lowerPerfHooksTypeof(lowerer, expr.expression);
if (performance) return performance;
if (isNodeModuleValue(lowerer, expr.expression)) {
@@ -708,7 +737,7 @@ function lowerExprInner(lowerer: Lowerer, expr: ts.Expression): IrExpr {
lowerer.isStdlibMember(tested)
) {
const sym = lowerer.checker.getSymbolAtLocation(tested.name);
const decls = sym ? lowerer.checker.declarationsOf(sym) : [];
const decls = sym ? sym.declarations : [];
if (
decls.some(isBuiltinModuleValueDecl) &&
decls.every((decl) => builtinModuleExportParent(decl) !== undefined)
@@ -997,7 +1026,7 @@ function lowerExprInner(lowerer: Lowerer, expr: ts.Expression): IrExpr {
recordTextCodecClass(lowerer.shapes.get(local.type.shapeId)!) !== null
) {
const symbol = lowerer.resolveValueSymbol(expr);
const decl = symbol ? lowerer.checker.valueDeclarationOf(symbol) : undefined;
const decl = symbol ? symbol.valueDeclaration : undefined;
if (
decl &&
ts.isVariableDeclaration(decl) &&
@@ -1368,6 +1397,9 @@ function lowerExprInner(lowerer: Lowerer, expr: ts.Expression): IrExpr {
// the same identity-token story as stdlib globals above (the
// harness adds worker_threads.Worker to its identity Set).
if (isJsSourceFile(expr.getSourceFile())) {
// A function member is a function value that fences when called.
if (builtinModuleFnOf(lowerer, bi.module, bi.member))
return builtinFunctionFenceValue(lowerer, bi, expr, loc);
return {
kind: "strLit",
value: `[builtin ${bi.module}.${bi.member}]`,
@@ -1581,7 +1613,7 @@ function lowerExprInner(lowerer: Lowerer, expr: ts.Expression): IrExpr {
// is above); only user-file declares do.
{
const sym = lowerer.resolveValueSymbol(expr);
const decl = sym ? lowerer.checker.declarationsOf(sym)[0] : undefined;
const decl = sym ? sym.declarations[0] : undefined;
if (
decl !== undefined &&
ts.isVariableDeclaration(decl) &&
@@ -1693,6 +1725,14 @@ function lowerExprInner(lowerer: Lowerer, expr: ts.Expression): IrExpr {
const ns = lowerNsIdentifierValue(lowerer, expr);
if (ns) return ns;
}
if (isNavigatorGlobal(lowerer, expr)) {
refuseNavigator(
lowerer,
expr,
"the navigator object as a value",
lowerer.checker.getSymbolAtLocation(expr),
);
}
// Preflight guarantees no unresolved identifiers; anything else here
// is a blocked declaration's binding (the SC2004 cascade) or a
// binding form we don't model yet.
@@ -1956,6 +1996,10 @@ function lowerExprInner(lowerer: Lowerer, expr: ts.Expression): IrExpr {
if (ts.isConditionalExpression(expr)) return lowerTernary(lowerer, expr);
if (ts.isPropertyAccessExpression(expr)) {
if (!expr.questionDotToken) {
const navigator = lowerNavigatorMember(lowerer, expr, NODE_COMPAT_VERSION);
if (navigator) return navigator;
}
// `require("./x").member` over a program module's export table: the
// module's %init at this position, then the member read exactly like
// a `const m = require("./x")` binding's (cjs-inline-require.ts).
@@ -2154,6 +2198,10 @@ function lowerExprInner(lowerer: Lowerer, expr: ts.Expression): IrExpr {
) {
return dynUndefinedExpr(loc);
}
// `Uint8Array.prototype.toBase64` and its siblings, the base64/hex
// feature probes libraries such as jose make: the methods are newer
// than Node 24, whose Uint8Array prototype answers undefined for them.
if (isUint8ArrayBase64Probe(lowerer, expr)) return dynUndefinedExpr(loc);
if (expr.questionDotToken && expr.name.text === "electron") {
const processProperty = lowerer.lowerProcessProperty(expr);
if (processProperty) return processProperty;
@@ -2644,13 +2692,11 @@ function lowerExprInner(lowerer: Lowerer, expr: ts.Expression): IrExpr {
(recvSym?.name === "IteratorResult" ||
recvSym?.name === "IteratorYieldResult" ||
recvSym?.name === "IteratorReturnResult") &&
lowerer.checker
.declarationsOf(recvSym)
.some(
(d) =>
(ts.isInterfaceDeclaration(d) || ts.isTypeAliasDeclaration(d)) &&
lowerer.isStdlibFile(d.getSourceFile()),
)
recvSym.declarations.some(
(d) =>
(ts.isInterfaceDeclaration(d) || ts.isTypeAliasDeclaration(d)) &&
lowerer.isStdlibFile(d.getSourceFile()),
)
) {
const recv = lowerer.lowerExpr(expr.expression);
if (recv.type.kind === "dyn")
@@ -3344,15 +3390,13 @@ export function maybeNarrow(lowerer: Lowerer, expr: IrExpr, node: ts.Node): IrEx
const symbol = lowerer.checker.getSymbolAtLocation(node.name);
if (
symbol &&
lowerer.checker
.declarationsOf(symbol)
.some(
(decl) =>
ts.isPropertyDeclaration(decl) &&
!decl.initializer &&
!decl.type &&
isJsSourceFile(decl.getSourceFile()),
)
symbol.declarations.some(
(decl) =>
ts.isPropertyDeclaration(decl) &&
!decl.initializer &&
!decl.type &&
isJsSourceFile(decl.getSourceFile()),
)
) {
// Inference describes later writes, not the initial undefined.
// A use that requires the inferred type must validate the value.
@@ -3420,48 +3464,44 @@ export function maybeNarrow(lowerer: Lowerer, expr: IrExpr, node: ts.Node): IrEx
const symbol = lowerer.resolveValueSymbol(node);
if (
symbol &&
lowerer.checker
.declarationsOf(symbol)
.some(
(decl) =>
ts.isParameter(decl) &&
ts.isMethodDeclaration(decl.parent) &&
lowerer.checkedVirtualJsMethods.has(decl.parent),
)
symbol.declarations.some(
(decl) =>
ts.isParameter(decl) &&
ts.isMethodDeclaration(decl.parent) &&
lowerer.checkedVirtualJsMethods.has(decl.parent),
)
)
return expr;
if (
symbol &&
lowerer.checker
.declarationsOf(symbol)
.some((decl) => ts.isParameter(decl) && ts.isConstructorDeclaration(decl.parent))
symbol.declarations.some(
(decl) => ts.isParameter(decl) && ts.isConstructorDeclaration(decl.parent),
)
)
return expr;
if (
symbol &&
lowerer.checker
.declarationsOf(symbol)
.some((decl) => ts.isVariableDeclaration(decl) && jsBindingHasOpenWrites(lowerer, decl))
symbol.declarations.some(
(decl) => ts.isVariableDeclaration(decl) && jsBindingHasOpenWrites(lowerer, decl),
)
)
return expr;
if (
symbol &&
lowerer.checker
.declarationsOf(symbol)
.some(
(decl) =>
ts.isParameter(decl) &&
decl.initializer &&
!decl.type &&
!/@(?:param|type)\b/.test(
decl.getSourceFile().text.slice(decl.parent?.pos ?? decl.pos, decl.getStart()),
),
)
symbol.declarations.some(
(decl) =>
ts.isParameter(decl) &&
decl.initializer &&
!decl.type &&
!/@(?:param|type)\b/.test(
decl.getSourceFile().text.slice(decl.parent?.pos ?? decl.pos, decl.getStart()),
),
)
)
return expr;
if (
symbol &&
lowerer.checker.declarationsOf(symbol).some((decl) => {
symbol.declarations.some((decl) => {
if (!ts.isParameter(decl) || decl.type) return false;
const fn = decl.parent;
if (!ts.isFunctionExpression(fn) && !ts.isArrowFunction(fn)) return false;
@@ -3534,6 +3574,9 @@ export function maybeNarrow(lowerer: Lowerer, expr: IrExpr, node: ts.Node): IrEx
narrowed.className !== expr.type.className &&
lowerer.isSubclassOf(narrowed.className, expr.type.className)
) {
const reduced = structuralNarrowing(lowerer, node, expr.type.className, narrowed);
const union = reduced ? narrowClassUnion(lowerer, expr, reduced) : null;
if (union) return union;
if (lowerer.narrowing.untrusted(node, narrowed)) {
const checked = checkedClassAssertion(lowerer, expr, narrowed, expr.loc);
if (checked) return checked;
@@ -7087,6 +7130,10 @@ export function lowerElementAccess(lowerer: Lowerer, expr: ts.ElementAccessExpre
}
}
}
if (key.type.kind === "f64") {
const safe = lowerOptionalArrayIndexRead(lowerer, expr, value, key);
if (safe) return safe;
}
if (key.type.kind === "string" || key.type.kind === "f64") {
const lit = ts.isStringLiteral(expr.argumentExpression)
? expr.argumentExpression.text
@@ -8048,6 +8095,22 @@ export function lowerAbsenceProbe(lowerer: Lowerer, node: ts.Expression): IrExpr
}
return null;
}
// `m[2]` on a JavaScript `RegExpExecArray | null` that a previous
// statement ruled non-null without narrowing it (jose's secs()).
if (
receiverT?.kind === "union" &&
isJsSourceFile(expr.getSourceFile()) &&
!expr.questionDotToken &&
!hasOptionalChainGuard(expr.expression)
) {
const arms = lowerer.unions.get(receiverT.unionId)?.arms ?? [];
const present = arms.filter((arm) => !isUnitType(arm));
if (present.length !== 1 || present[0]!.kind !== "array") return null;
const value = lowerer.lowerExpr(expr.expression);
const index = lowerer.lowerExpr(expr.argumentExpression);
if (index.type.kind !== "f64") return null;
return lowerOptionalArrayIndexRead(lowerer, expr, value, index);
}
if (receiverT?.kind !== "array") return null;
const arr = lowerer.lowerExpr(expr.expression);
if (arr.type.kind !== "array") return null;
@@ -9624,6 +9687,11 @@ export function lowerAsExpression(
return inner;
}
}
// The global object: the checked extraction would build a separate
// value, so the assertion keeps the object and its member operations
// reach it (assertion-identity.ts).
if (extractionCopiesReference(lowerer, target) && assertsGlobalObject(lowerer, expr))
return inner;
if (
target.kind === "void" ||
!canDynCheckTo(
@@ -11826,6 +11894,25 @@ function lowerLogicalPair(lowerer: Lowerer, expr: ts.BinaryExpression, left: IrE
}
}
}
// `&&` over an always-truthy plain left operand (an array, object or
// function value) whose result union has no arm for it: every JS
// object is truthy, so `list && list[0]` evaluates the left for its
// effects and answers the right. Coercing the left into that union
// would trap at run time where Node answers the right operand.
if (
op === ts.SyntaxKind.AmpersandAmpersandToken &&
target?.kind === "union" &&
REF_TRUTHY_KINDS.has(left.type.kind) &&
lowerer.armTag(target.unionId, left.type) < 0
) {
return {
kind: "seqExpr",
stmts: [{ kind: "exprStmt", expr: left, loc }],
result: lowerer.coerceInto(expr.right, right, target),
type: target,
loc,
};
}
if (target?.kind === "union") {
// `&&` whose left does NOT fit the result union: the checker
// built that result by DROPPING left arms which are always
@@ -12336,7 +12423,7 @@ function aliasTypeofArmOf(
const a = strip(a0);
if (!ts.isIdentifier(a)) continue;
const aliasSym = lowerer.resolveValueSymbol(a);
const aliasDecl = aliasSym ? lowerer.checker.valueDeclarationOf(aliasSym) : undefined;
const aliasDecl = aliasSym ? aliasSym.valueDeclaration : undefined;
if (
!aliasSym ||
!aliasDecl ||
@@ -12350,7 +12437,7 @@ function aliasTypeofArmOf(
if (!ts.isIdentifier(opnd)) continue;
if (!bindingNeverReassigned(lowerer, aliasSym, aliasDecl)) continue;
const valSym = lowerer.resolveValueSymbol(opnd);
const valDecl = valSym ? lowerer.checker.valueDeclarationOf(valSym) : undefined;
const valDecl = valSym ? valSym.valueDeclaration : undefined;
if (!valSym || !valDecl) continue;
// v1: identifier PARAMETERS only — initialized at function entry,
// before any alias can capture their typeof.
@@ -12632,9 +12719,7 @@ function lowerReplacedExportsRead(
* rare enough to keep fenced). */
function cjsExportAccessorRead(lowerer: Lowerer, ident: ts.Identifier): IrExpr | null {
const symbol = lowerer.resolveValueSymbol(ident);
const getter = symbol
? lowerer.checker.declarationsOf(symbol).find(ts.isGetAccessorDeclaration)
: undefined;
const getter = symbol ? symbol.declarations.find(ts.isGetAccessorDeclaration) : undefined;
if (!getter) return null;
if (!ts.isObjectLiteralExpression(getter.parent) || !isCjsExportTableLiteral(getter.parent)) {
return null;
@@ -12912,6 +12997,8 @@ export function lowerInstanceOf(lowerer: Lowerer, expr: ts.BinaryExpression, loc
if (blob) return blob;
const keyObject = lowerKeyObjectInstanceof(lowerer, expr);
if (keyObject) return keyObject;
const cryptoKey = lowerCryptoKeyInstanceof(lowerer, expr);
if (cryptoKey) return cryptoKey;
const netAddress = lowerNetAddressInstanceof(lowerer, expr);
if (netAddress) return netAddress;
}
@@ -14528,7 +14615,7 @@ function staticRegexPatternOf(lowerer: Lowerer, e: ts.Expression): string | null
* destructured bindings, and unresolvable names. */
function constInitializerOf(lowerer: Lowerer, ident: ts.Identifier): ts.Expression | null {
const sym = lowerer.resolveValueSymbol(ident);
const decl = sym ? lowerer.checker.valueDeclarationOf(sym) : undefined;
const decl = sym ? sym.valueDeclaration : undefined;
if (decl === undefined || !ts.isVariableDeclaration(decl) || decl.initializer === undefined)
return null;
if (!ts.isVariableDeclarationList(decl.parent) || (decl.parent.flags & ts.NodeFlags.Const) === 0)
@@ -14644,7 +14731,7 @@ function matchProducerRegexOf(lowerer: Lowerer, e: ts.Expression): ts.Expression
// The for-of binding over a matchAll drain: `for (const m of
// s.matchAll(re))` (or over a stored const rows).
const sym = lowerer.resolveValueSymbol(expr);
const decl = sym ? lowerer.checker.valueDeclarationOf(sym) : undefined;
const decl = sym ? sym.valueDeclaration : undefined;
if (
decl !== undefined &&
ts.isVariableDeclaration(decl) &&
@@ -15091,13 +15178,11 @@ export function lowerFieldRead(lowerer: Lowerer, expr: ts.PropertyAccessExpressi
isGenericCallableMemberType(lowerer.checker.getTypeOfSymbol(propSym), lowerer.checker) &&
// CLASS members keep the class-path fences (a poisoned class's own
// diagnostics, the bound-method fence above).
!lowerer.checker
.declarationsOf(propSym)
.some(
(d) =>
d.parent !== undefined &&
(ts.isClassDeclaration(d.parent) || ts.isClassExpression(d.parent)),
)
!propSym.declarations.some(
(d) =>
d.parent !== undefined &&
(ts.isClassDeclaration(d.parent) || ts.isClassExpression(d.parent)),
)
) {
const found = objLitGenericFnNodeOf(lowerer, propSym);
if (!found) {
@@ -15187,7 +15272,9 @@ export function lowerUnionProperty(
expr: ts.PropertyAccessExpression,
): IrExpr | null {
if (lowerer.chainBlocked(expr)) return null;
const receiverIr = lowerer.mapTypeOf(lowerer.typeOf(expr.expression));
const receiverIr =
structuralReadType(lowerer, expr.expression) ??
lowerer.mapTypeOf(lowerer.typeOf(expr.expression));
if (receiverIr?.kind !== "union") return null;
// Lower the receiver first: its runtime value can still carry the full
// binding union even when the checker has removed several variants.
@@ -15394,6 +15481,62 @@ export function lowerUnionProperty(
);
}
/** A number-keyed read in JavaScript on an array that may be undefined or
* null outside an optional chain (`m[1]` on a `RegExpExecArray | null`
* that an earlier statement ruled non-null without narrowing it): the
* array arm is an ordinary JavaScript element read, so a hole, a present
* undefined (a regex group that did not participate) or an invalid index
* answers undefined, as lowerSafeIndexRead does for a plain array. The
* unit arms keep the keyed read's answer. The receiver and key evaluate
* once, in source order. Null when the union is not exactly one array arm
* beside unit arms; optional-chain tails and TypeScript keep the keyed
* read. */
function lowerOptionalArrayIndexRead(
lowerer: Lowerer,
expr: ts.ElementAccessExpression,
value: IrExpr,
key: IrExpr,
): IrExpr | null {
if (
!isJsSourceFile(expr.getSourceFile()) ||
expr.questionDotToken ||
hasOptionalChainGuard(expr.expression)
)
return null;
if (value.type.kind !== "union") return null;
const unionId = value.type.unionId;
const arms = lowerer.unions.get(unionId)?.arms;
if (!arms) return null;
const present = arms.filter((arm) => !isUnitType(arm));
const arm = present[0];
if (present.length !== 1 || arm?.kind !== "array" || arm.elem.kind === "void") return null;
const tag = arms.indexOf(arm);
const loc = locOf(expr);
const stmts: IrStmt[] = [];
const stable = (e: IrExpr, name: string): IrExpr => {
if (e.kind === "varRef" || e.kind === "numLit") return e;
const local = lowerer.declareHiddenLocal(name, e.type);
stmts.push({ kind: "varDecl", localId: local.id, init: e, loc });
return varRef(local.id, e.type, loc);
};
if (lowerUnionKeyedRead(lowerer, expr, unionId, value, key, null) === null) return null;
const recv = stable(value, "%indexReceiver");
const index = stable(key, "%indexKey");
const keyed = lowerUnionKeyedRead(lowerer, expr, unionId, recv, index, null)!;
const narrowed: IrExpr = { kind: "unionNarrow", unionId, tag, value: recv, type: arm, loc };
const read = lowerSafeIndexRead(lowerer, narrowed, index, loc);
if (read === null) return null;
const result: IrExpr = {
kind: "ternary",
cond: { kind: "unionIsTag", unionId, tag, value: recv, negated: false, type: BOOL, loc },
then: lowerer.coerceToExpected(read, keyed.type),
else_: keyed,
type: keyed.type,
loc,
};
return stmts.length === 0 ? result : { kind: "seqExpr", stmts, result, type: keyed.type, loc };
}
/** The unionDisc generalization: a keyed read on a union receiver whose
* arms answer DIFFERENT (but joinable) types. Each arm contributes its
* declared answer — an array element's type (number keys), a declared field's type (literal keys), an
@@ -15619,14 +15762,12 @@ function classFieldTarget(
);
if (
property &&
lowerer.checker
.declarationsOf(property)
.some(
(declaration) =>
ts.isPropertyAccessExpression(declaration) ||
ts.isBinaryExpression(declaration) ||
ts.isElementAccessExpression(declaration),
)
property.declarations.some(
(declaration) =>
ts.isPropertyAccessExpression(declaration) ||
ts.isBinaryExpression(declaration) ||
ts.isElementAccessExpression(declaration),
)
) {
return {
container: "dynamic",
@@ -15830,7 +15971,7 @@ export function fieldTarget(
// fences: JS finds the prototype function, never an overflow miss.
const canonicalized = (): boolean => {
if (!nameSym) return false;
const nameDecls = lowerer.checker.declarationsOf(nameSym);
const nameDecls = nameSym.declarations;
if (!nameDecls.length) return false;
if (!nameDecls.every((d) => ts.isPropertySignature(d))) return false;
const declared = lowerer.mapTypeOf(lowerer.typeOf(access));
@@ -15903,7 +16044,7 @@ export function uniqueSymbolKeyOf(
// which a runtime-identity symbol value can never satisfy.
if (!(t.flags & (ts.TypeFlags.UniqueESSymbol | ts.TypeFlags.ESSymbol))) return null;
const sym = lowerer.resolveValueSymbol(key);
const decl = sym ? lowerer.checker.valueDeclarationOf(sym) : undefined;
const decl = sym ? sym.valueDeclaration : undefined;
if (!sym || !decl || !ts.isVariableDeclaration(decl)) return null;
if (!(ts.getCombinedNodeFlags(decl) & ts.NodeFlags.Const)) return null;
// Module level only: a unique-symbol const inside a FUNCTION is a
@@ -16980,7 +17121,7 @@ function jsFunctionExpandoMember(lowerer: Lowerer, access: ts.PropertyAccessExpr
const recvType = lowerer.typeOf(access.expression);
if (lowerer.checker.getCallSignatures(recvType).length === 0) return false;
const member = lowerer.checker.getPropertyOfType(recvType, access.name.text);
const decls = member ? lowerer.checker.declarationsOf(member) : [];
const decls = member ? member.declarations : [];
return (
decls.length > 0 &&
decls.every((d) => {
@@ -59,7 +59,7 @@ function declareCallReceiver(
export function isTopLevelFnSymbol(lowerer: Lowerer, ident: ts.Identifier): boolean {
const symbol = lowerer.resolveValueSymbol(ident);
if (symbol && lowerer.staticCallables.get(symbol)?.kind === "declared-function") return true;
const decl = symbol ? lowerer.checker.declarationsOf(symbol)[0] : undefined;
const decl = symbol ? symbol.declarations[0] : undefined;
return (
!!decl &&
ts.isFunctionDeclaration(decl) &&
@@ -203,14 +203,12 @@ function isGeneratorMember(lowerer: Lowerer, access: ts.PropertyAccessExpression
const member = lowerer.checker.getSymbolAtLocation(access.name);
return (
member !== undefined &&
lowerer.checker
.declarationsOf(member)
.some(
(d) =>
ts.isInterfaceDeclaration(d.parent) &&
(d.parent.name.text === "Generator" || d.parent.name.text === "AsyncGenerator") &&
lowerer.isStdlibFile(d.getSourceFile()),
)
member.declarations.some(
(d) =>
ts.isInterfaceDeclaration(d.parent) &&
(d.parent.name.text === "Generator" || d.parent.name.text === "AsyncGenerator") &&
lowerer.isStdlibFile(d.getSourceFile()),
)
);
}
@@ -268,28 +266,24 @@ export function isSettledIteratorValueRead(
const member = lowerer.checker.getSymbolAtLocation(node.name);
if (
!member ||
!lowerer.checker
.declarationsOf(member)
.some(
(d) =>
ts.isPropertySignature(d) &&
ts.isInterfaceDeclaration(d.parent) &&
(d.parent.name.text === "IteratorYieldResult" ||
d.parent.name.text === "IteratorReturnResult") &&
lowerer.isStdlibFile(d.getSourceFile()),
)
!member.declarations.some(
(d) =>
ts.isPropertySignature(d) &&
ts.isInterfaceDeclaration(d.parent) &&
(d.parent.name.text === "IteratorYieldResult" ||
d.parent.name.text === "IteratorReturnResult") &&
lowerer.isStdlibFile(d.getSourceFile()),
)
)
return false;
const receiver = lowerer.typeOf(node.expression);
const stdlib = (sym: ts.Symbol | undefined): boolean =>
sym !== undefined &&
lowerer.checker
.declarationsOf(sym)
.some(
(d) =>
(ts.isInterfaceDeclaration(d) || ts.isTypeAliasDeclaration(d)) &&
lowerer.isStdlibFile(d.getSourceFile()),
);
sym.declarations.some(
(d) =>
(ts.isInterfaceDeclaration(d) || ts.isTypeAliasDeclaration(d)) &&
lowerer.isStdlibFile(d.getSourceFile()),
);
const alias = receiver.getAliasSymbol();
const symbol = receiver.getSymbol();
if (
@@ -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 },
@@ -1768,7 +1773,7 @@ function directCallableInspect(lowerer: Lowerer, node: ts.Expression, loc: SrcLo
const base = cls.base ? ` extends ${cls.base.def.name.replace(/^%/, "")}` : "";
return strLit(`[class ${cls.def.name}${base}]`, loc);
}
const decl = lowerer.checker.declarationsOf(sym)[0];
const decl = sym.declarations[0];
if (!decl) return null;
if (ts.isFunctionDeclaration(decl) && decl.name) {
return strLit(`[Function: ${decl.name.text}]`, loc);
@@ -223,7 +223,7 @@ export function mixinFnOfCallee(lowerer: Lowerer, callee: ts.Expression): MixinF
let sym = lowerer.checker.getSymbolAtLocation(callee) ?? null;
if (sym && sym.flags & ts.SymbolFlags.Alias) sym = lowerer.checker.getAliasedSymbol(sym);
if (!sym) return null;
const decls = lowerer.checker.declarationsOf(sym);
const decls = sym.declarations;
if (decls.length !== 1 || decls[0] === undefined) return null; // overloads / merged declarations
const d = decls[0];
if (ts.isFunctionDeclaration(d)) return mixinFnShapeOf(lowerer, d);
@@ -348,7 +348,7 @@ export function mixinResultBindingClassOf(
sym: ts.Symbol | null | undefined,
): ClassInfo | null {
if (!sym) return null;
const decls = lowerer.checker.declarationsOf(sym);
const decls = sym.declarations;
if (decls.length !== 1 || decls[0] === undefined || !ts.isVariableDeclaration(decls[0]))
return null;
const decl = decls[0];
@@ -541,7 +541,7 @@ export function mixinIntersectionInstanceType(lowerer: Lowerer, widened: ts.Type
const plainParts: string[] = [];
for (const part of parts) {
const sym = part.getSymbol();
const decl = sym ? lowerer.checker.valueDeclarationOf(sym) : undefined;
const decl = sym ? sym.valueDeclaration : undefined;
const node =
decl &&
(ts.isClassDeclaration(decl) || ts.isClassExpression(decl)) &&
@@ -93,6 +93,7 @@ import {
import { isPromisifyCall, registerBuiltinCallableAlias } from "./builtins/callable-aliases.js";
import { registerBuiltinClassAlias } from "./builtins/class-values.js";
import { stripTypeCasts } from "./builtins/arguments.js";
import { isWebCryptoMemberType } from "./builtins/webcrypto.js";
import {
bindingContextualGenericFnNodeOf,
bindingGenericFnAliasInfoOf,
@@ -145,6 +146,7 @@ import { workerTargetPaths } from "../worker-target.js";
import { isNativeProxyInitializer } from "./expressions/native-proxy.js";
import { handleEventReaderPrelude } from "./lower-event-target.js";
import { jsHoldsDynObject } from "./lower-exprs.js";
import { keptIdentityAssertion } from "./assertion-identity.js";
/** One file's declarations, split for collection and init-body lowering. */
export interface FileParts {
@@ -652,9 +654,7 @@ function collectDynamicImports(lowerer: Lowerer, sf: ts.SourceFile): void {
const modSym = lowerer.checker.getSymbolAtLocation(node.arguments.nodes[0]);
const ownModule =
modSym !== undefined &&
lowerer.checker
.declarationsOf(modSym)
.some((d) => ts.isSourceFile(d) && !d.isDeclarationFile);
modSym.declarations.some((d) => ts.isSourceFile(d) && !d.isDeclarationFile);
const builtin = canonicalBuiltinModule(spec);
const res = ownModule
? ({ kind: "program-module" } as const)
@@ -692,7 +692,7 @@ export function collectJsonImports(lowerer: Lowerer, parts: FileParts[]): void {
const nameSym = lowerer.checker.getSymbolAtLocation(clause.name);
if (!nameSym || !(nameSym.flags & ts.SymbolFlags.Alias)) continue;
const target = lowerer.checker.getAliasedSymbol(nameSym);
const jsonSf = lowerer.checker.declarationsOf(target)[0]?.getSourceFile();
const jsonSf = target.declarations[0]?.getSourceFile();
if (!jsonSf || !jsonSf.fileName.endsWith(".json")) continue;
try {
const tsType = lowerer.typeOf(clause.name);
@@ -1054,7 +1054,7 @@ function checkedGlobalReference(lowerer: Lowerer, expression: ts.Expression): bo
)
return false;
const root = lowerer.checker.getSymbolAtLocation(expression.expression);
const namespace = root && lowerer.checker.declarationsOf(root).some(ts.isNamespaceImport);
const namespace = root && root.declarations.some(ts.isNamespaceImport);
return (
(namespace || lowerer.cjsLocalModuleBindingOf(expression.expression)) &&
lowerer.globalOf(expression.name)?.type.kind === "dyn"
@@ -1262,7 +1262,7 @@ export function collectGlobals(
if (!symbol) continue;
if (symbol.flags & ts.SymbolFlags.Alias) {
const target = lowerer.checker.getAliasedSymbol(symbol);
const vd = lowerer.checker.valueDeclarationOf(target);
const vd = target.valueDeclaration;
const mutableVar =
vd !== undefined &&
ts.isVariableDeclaration(vd) &&
@@ -1371,8 +1371,7 @@ export function collectGlobals(
if (vG) {
if (!vG.mutable) {
lowerer.globalsBySymbol.set(symbol, vG);
for (const d of lowerer.checker.declarationsOf(symbol))
lowerer.globalsByDeclNode.set(d, vG);
for (const d of symbol.declarations) lowerer.globalsByDeclNode.set(d, vG);
return;
}
const g: IrGlobal = {
@@ -1382,8 +1381,7 @@ export function collectGlobals(
mutable: false,
};
lowerer.globalsBySymbol.set(symbol, g);
for (const d of lowerer.checker.declarationsOf(symbol))
lowerer.globalsByDeclNode.set(d, g);
for (const d of symbol.declarations) lowerer.globalsByDeclNode.set(d, g);
lowerer.globalsList.push(g);
return;
}
@@ -1393,8 +1391,7 @@ export function collectGlobals(
lowerer.globalsBySymbol.set(symbol, g);
// Importer aliases resolve to a DISTINCT late-bound symbol with
// the same declaration — key the node too (globalOf's fallback).
for (const d of lowerer.checker.declarationsOf(symbol))
lowerer.globalsByDeclNode.set(d, g);
for (const d of symbol.declarations) lowerer.globalsByDeclNode.set(d, g);
lowerer.globalsList.push(g);
} catch (e) {
if (!(e instanceof PoisonError)) throw e;
@@ -1537,7 +1534,7 @@ export function collectGlobals(
)
continue;
const target = lowerer.checker.getAliasedSymbol(aliasSym);
const vd = lowerer.checker.valueDeclarationOf(target);
const vd = target.valueDeclaration;
const mutableVar =
vd !== undefined &&
ts.isVariableDeclaration(vd) &&
@@ -1921,7 +1918,8 @@ export function collectGlobals(
decl.initializer &&
!decl.type &&
!hasJsTypeAnnotation(decl) &&
!lowerer.mapTypeOf(lowerer.typeOf(nameNode))
(!lowerer.mapTypeOf(lowerer.typeOf(nameNode)) ||
isWebCryptoMemberType(lowerer, lowerer.typeOf(nameNode)))
) {
const symbol = lowerer.checker.getSymbolAtLocation(nameNode);
if (symbol && !lowerer.globalsBySymbol.has(symbol)) {
@@ -1972,6 +1970,30 @@ export function collectGlobals(
}
continue;
}
// A file-scope binding of an assertion of the global object
// (`const state = globalThis as { [KEY]?: State }`): the binding
// holds that object, so writes through it reach the object other
// modules read (assertion-identity.ts).
const asserted = decl.initializer
? keptIdentityAssertion(lowerer, decl.initializer)
: undefined;
if (nameNode === decl.name && !decl.type && asserted) {
const symbol = lowerer.checker.getSymbolAtLocation(nameNode);
if (symbol && !lowerer.globalsBySymbol.has(symbol)) {
const g: IrGlobal = {
id: `%g.${tag}${nsPrefix}${nameNode.text}`,
name: nameNode.text,
type: DYN,
mutable: isLet,
source: bindingSource(nameNode),
...(!isVarDeclared(decl) ? { tdz: true as const } : {}),
};
lowerer.globalsBySymbol.set(symbol, g);
lowerer.globalsList.push(g);
if (isVarDeclared(decl)) noteVarGlobalEntryInit(lowerer, sf, g);
}
continue;
}
// A JS file-scope evolving ARRAY (`const mustCallChecks = [];`
// — test/common's exit-accounting ledger): the strict type
// (any[]) has no mapping, but the VALUE is the dyn array the
@@ -2250,8 +2272,7 @@ export function collectGlobals(
ts.isCallExpression(decl.initializer)
? lowerer.checker.getResolvedSignature(decl.initializer)
: undefined;
const callDeclaration =
callSignature && lowerer.checker.signatureDeclaration(callSignature);
const callDeclaration = callSignature && callSignature.declaration;
const callType =
callDeclaration && isJsSourceFile(callDeclaration.getSourceFile())
? lowerer.mapTypeOf(lowerer.typeOf(nameNode))
@@ -2518,6 +2539,11 @@ export function collectGlobals(
};
lowerer.globalsBySymbol.set(symbol, g);
lowerer.globalsList.push(g);
// `let f: T;` initializes the binding (to undefined) where the
// declaration runs: later writes are ordinary stores, which the
// pointer guard alone would refuse (markCycleEarlyBindings).
if (g.tdz && !decl.initializer)
lowerer.uninitializedLexicalGlobals.set(g.id, { name: nameNode, module: sf, symbol });
// Mutable checked-dynamic LET globals ride the same entry
// init: a closure called above the declaration statement
// reads the dyn undefined instead of faulting on NULL — the
@@ -2879,11 +2905,16 @@ export function lowerFileInit(
// a function called above the declaration reads `undefined`, exactly
// Node); checked-dynamic var globals hold the dyn undefined the same
// way. After the guard: a cache-hit revisit must not reset them.
// A union slot the body retypes checked-dynamic (lowerVarDecl: a
// JavaScript var initialized with an existing checked-dynamic object)
// gets the dyn undefined once the body has lowered.
const unionEntryInits: { at: number; g: IrGlobal }[] = [];
for (const g of lowerer.varGlobalEntryInits.get(sf) ?? []) {
if (g.type.kind !== "union" && g.type.kind !== "dyn") continue;
const wrapped =
g.type.kind === "dyn" ? dynUndefinedExpr(loc0) : lowerer.unassignedSlotInit(g.type, loc0);
if (wrapped) {
if (g.type.kind === "union") unionEntryInits.push({ at: header.length, g });
// A let global whose early reads are checked (initFlag) only
// gets its storage filled here: this is not the source write, so
// it must neither run the TDZ check nor set the flag.
@@ -2978,6 +3009,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);
@@ -96,7 +96,7 @@ function nsSymbolRuntimeKind(
if (!(sym.flags & (ts.SymbolFlags.ValueModule | ts.SymbolFlags.NamespaceModule))) return null;
let sawTypeOnly = false;
let sawNamespace = false;
for (const d of lowerer.checker.declarationsOf(sym)) {
for (const d of sym.declarations) {
if (!ts.isModuleDeclaration(d)) continue;
sawNamespace = true;
let body = d.body;
@@ -189,7 +189,7 @@ export function nsPathPrefix(node: ts.Node, modifierDecl?: ts.Node): string {
* null. The walk stops at class/function boundaries: a static member of a
* class INSIDE a namespace is the class's business, not the namespace's. */
function nsBlockKindOfSymbol(lowerer: Lowerer, sym: ts.Symbol): "flattened" | "typeOnly" | null {
for (const d of lowerer.checker.declarationsOf(sym)) {
for (const d of sym.declarations) {
// TYPE declarations don't make a member a namespace VALUE: a class
// static `C.B` whose name also carries `namespace C { export
// interface B }` is the static's business — value references consult
@@ -253,7 +253,7 @@ function moduleNsSourceFileOf(lowerer: Lowerer, e: ts.Expression): ts.SourceFile
}
if (!sym) return null;
if (sym.flags & ts.SymbolFlags.Alias) sym = lowerer.checker.getAliasedSymbol(sym);
for (const d of lowerer.checker.declarationsOf(sym)) {
for (const d of sym.declarations) {
if (
sym.flags & ts.SymbolFlags.ValueModule &&
ts.isSourceFile(d) &&
@@ -322,7 +322,7 @@ export function ambientNsRootOf(lowerer: Lowerer, e: ts.Expression): ts.Identifi
if (!sym || !(sym.flags & (ts.SymbolFlags.ValueModule | ts.SymbolFlags.NamespaceModule)))
return null;
if (lowerer.isStdlibSymbol(sym)) return null;
const decls = lowerer.checker.declarationsOf(sym);
const decls = sym.declarations;
if (decls.length === 0) return null;
for (const d of decls) {
// A namespace declaration, or the fundule/clazzdule merge partners
@@ -427,7 +427,7 @@ export function ambientUndefVarRootOf(lowerer: Lowerer, e: ts.Expression): ts.Id
if (lowerer.trapBindings.has(sym)) return root;
if (lowerer.isStdlibSymbol(sym)) return null;
if (ambientUndefinedFnSymbolOf(lowerer, root) !== null) return root;
const decls = lowerer.checker.declarationsOf(sym);
const decls = sym.declarations;
if (decls.length === 0) return null;
for (const d of decls) {
if (!ts.isVariableDeclaration(d) || d.initializer !== undefined) return null;
@@ -608,7 +608,7 @@ export function ambientUndefinedFnSymbolOf(
if (!sym || !(sym.flags & ts.SymbolFlags.Function)) return null;
if (lowerer.isStdlibSymbol(sym)) return null;
let sawFn = false;
for (const d of lowerer.checker.declarationsOf(sym)) {
for (const d of sym.declarations) {
if (ts.isInterfaceDeclaration(d) || ts.isTypeAliasDeclaration(d)) continue; // type-world merge partners
if (ts.isFunctionDeclaration(d)) {
if (d.body) return null; // an implementation exists — not ambient-undefined
@@ -656,7 +656,7 @@ function initExecutingPosition(node: ts.Node): boolean {
function earliestSameFileDeclStart(lowerer: Lowerer, sym: ts.Symbol, sf: ts.SourceFile): number {
let earliest = Infinity;
for (const d of lowerer.checker.declarationsOf(sym)) {
for (const d of sym.declarations) {
if (d.getSourceFile() === sf) earliest = Math.min(earliest, d.getStart());
}
return earliest;
@@ -669,7 +669,7 @@ function earliestSameFileDeclStart(lowerer: Lowerer, sym: ts.Symbol, sf: ts.Sour
export function fenceEarlyAliasUse(lowerer: Lowerer, ident: ts.Identifier, refNode: ts.Node): void {
const sym = lowerer.checker.getSymbolAtLocation(ident);
if (!sym || !(sym.flags & ts.SymbolFlags.Alias)) return;
const aliasDecl = lowerer.checker.declarationsOf(sym).find(ts.isImportEqualsDeclaration);
const aliasDecl = sym.declarations.find(ts.isImportEqualsDeclaration);
if (!aliasDecl || ts.isExternalModuleReference(aliasDecl.moduleReference)) return;
if (!initExecutingPosition(refNode)) return;
const sf = refNode.getSourceFile();
@@ -766,7 +766,7 @@ export function fenceCrossBlockNsRef(
) {
if (lowerer.nsBlocks.has(q)) refBlocks.push(q);
}
for (const d of lowerer.checker.declarationsOf(symbol)) {
for (const d of symbol.declarations) {
for (
let q: ts.Node | undefined = d.parent;
q !== undefined && !ts.isSourceFile(q);
@@ -42,7 +42,7 @@ function cjsGlobal(lowerer: Lowerer, node: ts.Expression, name: "module" | "requ
if (!ts.isIdentifier(node) || node.text !== name) return false;
if (lowerer.isStdlibGlobal(node, name)) return true;
const symbol = lowerer.checker.getSymbolAtLocation(node);
const decls = symbol ? lowerer.checker.declarationsOf(symbol) : [];
const decls = symbol ? symbol.declarations : [];
const sourceShadow = decls.some(
(decl) =>
!decl.getSourceFile().isDeclarationFile &&
@@ -204,16 +204,14 @@ function nodeModuleType(lowerer: Lowerer, node: ts.Expression): boolean {
if (
symbol &&
(symbol.name === "Module" || symbol.name === "ScriptcModule") &&
lowerer.checker
.declarationsOf(symbol)
.some((decl) => lowerer.isStdlibFile(decl.getSourceFile()))
symbol.declarations.some((decl) => lowerer.isStdlibFile(decl.getSourceFile()))
) {
return true;
}
const property = lowerer.checker.getPropertyOfType(type, "filename");
const declaredByModule =
property &&
lowerer.checker.declarationsOf(property).some((decl) => {
property.declarations.some((decl) => {
for (let parent: ts.Node | undefined = decl.parent; parent; parent = parent.parent) {
if (
(ts.isInterfaceDeclaration(parent) || ts.isClassDeclaration(parent)) &&
@@ -5051,7 +5051,7 @@ function clientFnRefOf(
* `const` declaration, or a parameter/let whose enclosing scope contains
* no assignment or ++/-- targeting it. */
function neverReassigned(lowerer: Lowerer, sym: ts.Symbol): boolean {
const decl = lowerer.checker.declarationsOf(sym)[0];
const decl = sym.declarations[0];
if (!decl) return false;
if (
ts.isVariableDeclaration(decl) &&

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