feat(compiler): bootstrap the C emitter natively

- Compile the production C emitter and reproduce its output across native generations.
- Support the union operations, recursive spreads, and compression options needed by the emitter.
- Fix temporary ownership and verify the bootstrap and shared call operands with both backends and sanitizers.
This commit is contained in:
Chris Tate
2026-09-27 21:45:26 -05:00
committed by GitHub
parent a1ec135d79
commit cb94453647
44 changed files with 1627 additions and 367 deletions
+1
View File
@@ -47,6 +47,7 @@ const results = await Promise.all(jobs); // number[] — compiles
- `fs.openSync` accepts inline bitwise OR expressions of `fs.constants.O_RDONLY`, `O_WRONLY`, `O_RDWR`, `O_CREAT`, `O_EXCL`, `O_NOFOLLOW`, `O_NONBLOCK`, `O_TRUNC`, and `O_APPEND`, with an optional creation mode. Computed numeric flags remain fenced because their bit values differ by target. `fstatSync`, `fchmodSync`, `fsyncSync`, and `linkSync` compile at direct call sites. On Windows, a numeric open with `O_NOFOLLOW` and `fchmodSync` throw `ENOSYS` because the CRT cannot enforce those contracts; programs requiring them should use a POSIX target. - `fs.openSync` accepts inline bitwise OR expressions of `fs.constants.O_RDONLY`, `O_WRONLY`, `O_RDWR`, `O_CREAT`, `O_EXCL`, `O_NOFOLLOW`, `O_NONBLOCK`, `O_TRUNC`, and `O_APPEND`, with an optional creation mode. Computed numeric flags remain fenced because their bit values differ by target. `fstatSync`, `fchmodSync`, `fsyncSync`, and `linkSync` compile at direct call sites. On Windows, a numeric open with `O_NOFOLLOW` and `fchmodSync` throw `ENOSYS` because the CRT cannot enforce those contracts; programs requiring them should use a POSIX target.
- Numeric `process.exitCode = value` writes in statement position set the implicit exit status; `process.exit()` with no argument uses that status. Reading or resetting `process.exitCode`, and assignments used as values, remain fenced. - Numeric `process.exitCode = value` writes in statement position set the implicit exit status; `process.exit()` with no argument uses that status. Reading or resetting `process.exitCode`, and assignments used as values, remain fenced.
- Map keys and Set elements support numbers and strings by value, and records, class instances, arrays, symbols, and server handles by identity. Unions consisting only of these identity types also compile. Other key types, mixed scalar/reference unions, nullable key unions, and key conversions that copy an existing reference remain fenced. - Map keys and Set elements support numbers and strings by value, and records, class instances, arrays, symbols, and server handles by identity. Unions consisting only of these identity types also compile. Other key types, mixed scalar/reference unions, nullable key unions, and key conversions that copy an existing reference remain fenced.
- Static `zlib.deflateSync`, `deflateRawSync`, and `gzipSync` accept default options or an inline `{ level: n }` with an integer literal from `-1` through `9`. Other compression options and runtime-valued levels remain fenced.
**The `any`/`unknown` boundary** **The `any`/`unknown` boundary**
@@ -108750,7 +108750,8 @@
"source": "surface-manifest:node-builtin.zlib.crc32", "source": "surface-manifest:node-builtin.zlib.crc32",
"tests": [ "tests": [
"tests/corpus/1404-zlib-crypto-bytes.ts", "tests/corpus/1404-zlib-crypto-bytes.ts",
"tests/corpus/2921-zlib-callbacks.ts" "tests/corpus/2921-zlib-callbacks.ts",
"tests/corpus/3115-zlib-static-compression-levels.ts"
] ]
}, },
"dynamic": { "dynamic": {
@@ -109303,7 +109304,8 @@
"source": "surface-manifest:node-builtin.zlib.deflate", "source": "surface-manifest:node-builtin.zlib.deflate",
"tests": [ "tests": [
"tests/corpus/1404-zlib-crypto-bytes.ts", "tests/corpus/1404-zlib-crypto-bytes.ts",
"tests/corpus/2921-zlib-callbacks.ts" "tests/corpus/2921-zlib-callbacks.ts",
"tests/corpus/3115-zlib-static-compression-levels.ts"
] ]
}, },
"dynamic": { "dynamic": {
@@ -109341,7 +109343,8 @@
"source": "surface-manifest:node-builtin.zlib.deflateSync", "source": "surface-manifest:node-builtin.zlib.deflateSync",
"tests": [ "tests": [
"tests/corpus/1404-zlib-crypto-bytes.ts", "tests/corpus/1404-zlib-crypto-bytes.ts",
"tests/corpus/2921-zlib-callbacks.ts" "tests/corpus/2921-zlib-callbacks.ts",
"tests/corpus/3115-zlib-static-compression-levels.ts"
] ]
}, },
"dynamic": { "dynamic": {
@@ -109379,7 +109382,8 @@
"source": "surface-manifest:node-builtin.zlib.deflateRaw", "source": "surface-manifest:node-builtin.zlib.deflateRaw",
"tests": [ "tests": [
"tests/corpus/1404-zlib-crypto-bytes.ts", "tests/corpus/1404-zlib-crypto-bytes.ts",
"tests/corpus/2921-zlib-callbacks.ts" "tests/corpus/2921-zlib-callbacks.ts",
"tests/corpus/3115-zlib-static-compression-levels.ts"
] ]
}, },
"dynamic": { "dynamic": {
@@ -109417,7 +109421,8 @@
"source": "surface-manifest:node-builtin.zlib.deflateRawSync", "source": "surface-manifest:node-builtin.zlib.deflateRawSync",
"tests": [ "tests": [
"tests/corpus/1404-zlib-crypto-bytes.ts", "tests/corpus/1404-zlib-crypto-bytes.ts",
"tests/corpus/2921-zlib-callbacks.ts" "tests/corpus/2921-zlib-callbacks.ts",
"tests/corpus/3115-zlib-static-compression-levels.ts"
] ]
}, },
"dynamic": { "dynamic": {
@@ -109455,7 +109460,8 @@
"source": "surface-manifest:node-builtin.zlib.gunzip", "source": "surface-manifest:node-builtin.zlib.gunzip",
"tests": [ "tests": [
"tests/corpus/1404-zlib-crypto-bytes.ts", "tests/corpus/1404-zlib-crypto-bytes.ts",
"tests/corpus/2921-zlib-callbacks.ts" "tests/corpus/2921-zlib-callbacks.ts",
"tests/corpus/3115-zlib-static-compression-levels.ts"
] ]
}, },
"dynamic": { "dynamic": {
@@ -109493,7 +109499,8 @@
"source": "surface-manifest:node-builtin.zlib.gunzipSync", "source": "surface-manifest:node-builtin.zlib.gunzipSync",
"tests": [ "tests": [
"tests/corpus/1404-zlib-crypto-bytes.ts", "tests/corpus/1404-zlib-crypto-bytes.ts",
"tests/corpus/2921-zlib-callbacks.ts" "tests/corpus/2921-zlib-callbacks.ts",
"tests/corpus/3115-zlib-static-compression-levels.ts"
] ]
}, },
"dynamic": { "dynamic": {
@@ -109531,7 +109538,8 @@
"source": "surface-manifest:node-builtin.zlib.gzip", "source": "surface-manifest:node-builtin.zlib.gzip",
"tests": [ "tests": [
"tests/corpus/1404-zlib-crypto-bytes.ts", "tests/corpus/1404-zlib-crypto-bytes.ts",
"tests/corpus/2921-zlib-callbacks.ts" "tests/corpus/2921-zlib-callbacks.ts",
"tests/corpus/3115-zlib-static-compression-levels.ts"
] ]
}, },
"dynamic": { "dynamic": {
@@ -109569,7 +109577,8 @@
"source": "surface-manifest:node-builtin.zlib.gzipSync", "source": "surface-manifest:node-builtin.zlib.gzipSync",
"tests": [ "tests": [
"tests/corpus/1404-zlib-crypto-bytes.ts", "tests/corpus/1404-zlib-crypto-bytes.ts",
"tests/corpus/2921-zlib-callbacks.ts" "tests/corpus/2921-zlib-callbacks.ts",
"tests/corpus/3115-zlib-static-compression-levels.ts"
] ]
}, },
"dynamic": { "dynamic": {
@@ -109607,7 +109616,8 @@
"source": "surface-manifest:node-builtin.zlib.inflate", "source": "surface-manifest:node-builtin.zlib.inflate",
"tests": [ "tests": [
"tests/corpus/1404-zlib-crypto-bytes.ts", "tests/corpus/1404-zlib-crypto-bytes.ts",
"tests/corpus/2921-zlib-callbacks.ts" "tests/corpus/2921-zlib-callbacks.ts",
"tests/corpus/3115-zlib-static-compression-levels.ts"
] ]
}, },
"dynamic": { "dynamic": {
@@ -109645,7 +109655,8 @@
"source": "surface-manifest:node-builtin.zlib.inflateSync", "source": "surface-manifest:node-builtin.zlib.inflateSync",
"tests": [ "tests": [
"tests/corpus/1404-zlib-crypto-bytes.ts", "tests/corpus/1404-zlib-crypto-bytes.ts",
"tests/corpus/2921-zlib-callbacks.ts" "tests/corpus/2921-zlib-callbacks.ts",
"tests/corpus/3115-zlib-static-compression-levels.ts"
] ]
}, },
"dynamic": { "dynamic": {
@@ -109683,7 +109694,8 @@
"source": "surface-manifest:node-builtin.zlib.inflateRaw", "source": "surface-manifest:node-builtin.zlib.inflateRaw",
"tests": [ "tests": [
"tests/corpus/1404-zlib-crypto-bytes.ts", "tests/corpus/1404-zlib-crypto-bytes.ts",
"tests/corpus/2921-zlib-callbacks.ts" "tests/corpus/2921-zlib-callbacks.ts",
"tests/corpus/3115-zlib-static-compression-levels.ts"
] ]
}, },
"dynamic": { "dynamic": {
@@ -109721,7 +109733,8 @@
"source": "surface-manifest:node-builtin.zlib.inflateRawSync", "source": "surface-manifest:node-builtin.zlib.inflateRawSync",
"tests": [ "tests": [
"tests/corpus/1404-zlib-crypto-bytes.ts", "tests/corpus/1404-zlib-crypto-bytes.ts",
"tests/corpus/2921-zlib-callbacks.ts" "tests/corpus/2921-zlib-callbacks.ts",
"tests/corpus/3115-zlib-static-compression-levels.ts"
] ]
}, },
"dynamic": { "dynamic": {
@@ -109759,7 +109772,8 @@
"source": "surface-manifest:node-builtin.zlib.unzip", "source": "surface-manifest:node-builtin.zlib.unzip",
"tests": [ "tests": [
"tests/corpus/1404-zlib-crypto-bytes.ts", "tests/corpus/1404-zlib-crypto-bytes.ts",
"tests/corpus/2921-zlib-callbacks.ts" "tests/corpus/2921-zlib-callbacks.ts",
"tests/corpus/3115-zlib-static-compression-levels.ts"
] ]
}, },
"dynamic": { "dynamic": {
@@ -109797,7 +109811,8 @@
"source": "surface-manifest:node-builtin.zlib.unzipSync", "source": "surface-manifest:node-builtin.zlib.unzipSync",
"tests": [ "tests": [
"tests/corpus/1404-zlib-crypto-bytes.ts", "tests/corpus/1404-zlib-crypto-bytes.ts",
"tests/corpus/2921-zlib-callbacks.ts" "tests/corpus/2921-zlib-callbacks.ts",
"tests/corpus/3115-zlib-static-compression-levels.ts"
] ]
}, },
"dynamic": { "dynamic": {
@@ -110237,7 +110252,8 @@
"source": "surface-manifest:node-builtin.zlib.deflate", "source": "surface-manifest:node-builtin.zlib.deflate",
"tests": [ "tests": [
"tests/corpus/1404-zlib-crypto-bytes.ts", "tests/corpus/1404-zlib-crypto-bytes.ts",
"tests/corpus/2921-zlib-callbacks.ts" "tests/corpus/2921-zlib-callbacks.ts",
"tests/corpus/3115-zlib-static-compression-levels.ts"
] ]
}, },
"dynamic": { "dynamic": {
@@ -110275,7 +110291,8 @@
"source": "surface-manifest:node-builtin.zlib.deflateSync", "source": "surface-manifest:node-builtin.zlib.deflateSync",
"tests": [ "tests": [
"tests/corpus/1404-zlib-crypto-bytes.ts", "tests/corpus/1404-zlib-crypto-bytes.ts",
"tests/corpus/2921-zlib-callbacks.ts" "tests/corpus/2921-zlib-callbacks.ts",
"tests/corpus/3115-zlib-static-compression-levels.ts"
] ]
}, },
"dynamic": { "dynamic": {
@@ -110313,7 +110330,8 @@
"source": "surface-manifest:node-builtin.zlib.deflateRaw", "source": "surface-manifest:node-builtin.zlib.deflateRaw",
"tests": [ "tests": [
"tests/corpus/1404-zlib-crypto-bytes.ts", "tests/corpus/1404-zlib-crypto-bytes.ts",
"tests/corpus/2921-zlib-callbacks.ts" "tests/corpus/2921-zlib-callbacks.ts",
"tests/corpus/3115-zlib-static-compression-levels.ts"
] ]
}, },
"dynamic": { "dynamic": {
@@ -110351,7 +110369,8 @@
"source": "surface-manifest:node-builtin.zlib.deflateRawSync", "source": "surface-manifest:node-builtin.zlib.deflateRawSync",
"tests": [ "tests": [
"tests/corpus/1404-zlib-crypto-bytes.ts", "tests/corpus/1404-zlib-crypto-bytes.ts",
"tests/corpus/2921-zlib-callbacks.ts" "tests/corpus/2921-zlib-callbacks.ts",
"tests/corpus/3115-zlib-static-compression-levels.ts"
] ]
}, },
"dynamic": { "dynamic": {
@@ -110389,7 +110408,8 @@
"source": "surface-manifest:node-builtin.zlib.gunzip", "source": "surface-manifest:node-builtin.zlib.gunzip",
"tests": [ "tests": [
"tests/corpus/1404-zlib-crypto-bytes.ts", "tests/corpus/1404-zlib-crypto-bytes.ts",
"tests/corpus/2921-zlib-callbacks.ts" "tests/corpus/2921-zlib-callbacks.ts",
"tests/corpus/3115-zlib-static-compression-levels.ts"
] ]
}, },
"dynamic": { "dynamic": {
@@ -110427,7 +110447,8 @@
"source": "surface-manifest:node-builtin.zlib.gunzipSync", "source": "surface-manifest:node-builtin.zlib.gunzipSync",
"tests": [ "tests": [
"tests/corpus/1404-zlib-crypto-bytes.ts", "tests/corpus/1404-zlib-crypto-bytes.ts",
"tests/corpus/2921-zlib-callbacks.ts" "tests/corpus/2921-zlib-callbacks.ts",
"tests/corpus/3115-zlib-static-compression-levels.ts"
] ]
}, },
"dynamic": { "dynamic": {
@@ -110465,7 +110486,8 @@
"source": "surface-manifest:node-builtin.zlib.gzip", "source": "surface-manifest:node-builtin.zlib.gzip",
"tests": [ "tests": [
"tests/corpus/1404-zlib-crypto-bytes.ts", "tests/corpus/1404-zlib-crypto-bytes.ts",
"tests/corpus/2921-zlib-callbacks.ts" "tests/corpus/2921-zlib-callbacks.ts",
"tests/corpus/3115-zlib-static-compression-levels.ts"
] ]
}, },
"dynamic": { "dynamic": {
@@ -110503,7 +110525,8 @@
"source": "surface-manifest:node-builtin.zlib.gzipSync", "source": "surface-manifest:node-builtin.zlib.gzipSync",
"tests": [ "tests": [
"tests/corpus/1404-zlib-crypto-bytes.ts", "tests/corpus/1404-zlib-crypto-bytes.ts",
"tests/corpus/2921-zlib-callbacks.ts" "tests/corpus/2921-zlib-callbacks.ts",
"tests/corpus/3115-zlib-static-compression-levels.ts"
] ]
}, },
"dynamic": { "dynamic": {
@@ -110541,7 +110564,8 @@
"source": "surface-manifest:node-builtin.zlib.inflate", "source": "surface-manifest:node-builtin.zlib.inflate",
"tests": [ "tests": [
"tests/corpus/1404-zlib-crypto-bytes.ts", "tests/corpus/1404-zlib-crypto-bytes.ts",
"tests/corpus/2921-zlib-callbacks.ts" "tests/corpus/2921-zlib-callbacks.ts",
"tests/corpus/3115-zlib-static-compression-levels.ts"
] ]
}, },
"dynamic": { "dynamic": {
@@ -110579,7 +110603,8 @@
"source": "surface-manifest:node-builtin.zlib.inflateSync", "source": "surface-manifest:node-builtin.zlib.inflateSync",
"tests": [ "tests": [
"tests/corpus/1404-zlib-crypto-bytes.ts", "tests/corpus/1404-zlib-crypto-bytes.ts",
"tests/corpus/2921-zlib-callbacks.ts" "tests/corpus/2921-zlib-callbacks.ts",
"tests/corpus/3115-zlib-static-compression-levels.ts"
] ]
}, },
"dynamic": { "dynamic": {
@@ -110617,7 +110642,8 @@
"source": "surface-manifest:node-builtin.zlib.inflateRaw", "source": "surface-manifest:node-builtin.zlib.inflateRaw",
"tests": [ "tests": [
"tests/corpus/1404-zlib-crypto-bytes.ts", "tests/corpus/1404-zlib-crypto-bytes.ts",
"tests/corpus/2921-zlib-callbacks.ts" "tests/corpus/2921-zlib-callbacks.ts",
"tests/corpus/3115-zlib-static-compression-levels.ts"
] ]
}, },
"dynamic": { "dynamic": {
@@ -110655,7 +110681,8 @@
"source": "surface-manifest:node-builtin.zlib.inflateRawSync", "source": "surface-manifest:node-builtin.zlib.inflateRawSync",
"tests": [ "tests": [
"tests/corpus/1404-zlib-crypto-bytes.ts", "tests/corpus/1404-zlib-crypto-bytes.ts",
"tests/corpus/2921-zlib-callbacks.ts" "tests/corpus/2921-zlib-callbacks.ts",
"tests/corpus/3115-zlib-static-compression-levels.ts"
] ]
}, },
"dynamic": { "dynamic": {
@@ -110693,7 +110720,8 @@
"source": "surface-manifest:node-builtin.zlib.unzip", "source": "surface-manifest:node-builtin.zlib.unzip",
"tests": [ "tests": [
"tests/corpus/1404-zlib-crypto-bytes.ts", "tests/corpus/1404-zlib-crypto-bytes.ts",
"tests/corpus/2921-zlib-callbacks.ts" "tests/corpus/2921-zlib-callbacks.ts",
"tests/corpus/3115-zlib-static-compression-levels.ts"
] ]
}, },
"dynamic": { "dynamic": {
@@ -110731,7 +110759,8 @@
"source": "surface-manifest:node-builtin.zlib.unzipSync", "source": "surface-manifest:node-builtin.zlib.unzipSync",
"tests": [ "tests": [
"tests/corpus/1404-zlib-crypto-bytes.ts", "tests/corpus/1404-zlib-crypto-bytes.ts",
"tests/corpus/2921-zlib-callbacks.ts" "tests/corpus/2921-zlib-callbacks.ts",
"tests/corpus/3115-zlib-static-compression-levels.ts"
] ]
}, },
"dynamic": { "dynamic": {
@@ -125817,7 +125817,8 @@
"evidence": "derived:descendants", "evidence": "derived:descendants",
"tests": [ "tests": [
"tests/corpus/1404-zlib-crypto-bytes.ts", "tests/corpus/1404-zlib-crypto-bytes.ts",
"tests/corpus/2921-zlib-callbacks.ts" "tests/corpus/2921-zlib-callbacks.ts",
"tests/corpus/3115-zlib-static-compression-levels.ts"
] ]
}, },
"dynamic": { "dynamic": {
@@ -126427,7 +126428,8 @@
"evidence": "surface-manifest:node-builtin.zlib.crc32", "evidence": "surface-manifest:node-builtin.zlib.crc32",
"tests": [ "tests": [
"tests/corpus/1404-zlib-crypto-bytes.ts", "tests/corpus/1404-zlib-crypto-bytes.ts",
"tests/corpus/2921-zlib-callbacks.ts" "tests/corpus/2921-zlib-callbacks.ts",
"tests/corpus/3115-zlib-static-compression-levels.ts"
] ]
}, },
"dynamic": { "dynamic": {
@@ -126896,7 +126898,8 @@
"evidence": "surface-manifest:node-builtin.zlib.deflate", "evidence": "surface-manifest:node-builtin.zlib.deflate",
"tests": [ "tests": [
"tests/corpus/1404-zlib-crypto-bytes.ts", "tests/corpus/1404-zlib-crypto-bytes.ts",
"tests/corpus/2921-zlib-callbacks.ts" "tests/corpus/2921-zlib-callbacks.ts",
"tests/corpus/3115-zlib-static-compression-levels.ts"
] ]
}, },
"dynamic": { "dynamic": {
@@ -126930,7 +126933,8 @@
"evidence": "surface-manifest:node-builtin.zlib.deflateSync", "evidence": "surface-manifest:node-builtin.zlib.deflateSync",
"tests": [ "tests": [
"tests/corpus/1404-zlib-crypto-bytes.ts", "tests/corpus/1404-zlib-crypto-bytes.ts",
"tests/corpus/2921-zlib-callbacks.ts" "tests/corpus/2921-zlib-callbacks.ts",
"tests/corpus/3115-zlib-static-compression-levels.ts"
] ]
}, },
"dynamic": { "dynamic": {
@@ -126964,7 +126968,8 @@
"evidence": "surface-manifest:node-builtin.zlib.deflateRaw", "evidence": "surface-manifest:node-builtin.zlib.deflateRaw",
"tests": [ "tests": [
"tests/corpus/1404-zlib-crypto-bytes.ts", "tests/corpus/1404-zlib-crypto-bytes.ts",
"tests/corpus/2921-zlib-callbacks.ts" "tests/corpus/2921-zlib-callbacks.ts",
"tests/corpus/3115-zlib-static-compression-levels.ts"
] ]
}, },
"dynamic": { "dynamic": {
@@ -126998,7 +127003,8 @@
"evidence": "surface-manifest:node-builtin.zlib.deflateRawSync", "evidence": "surface-manifest:node-builtin.zlib.deflateRawSync",
"tests": [ "tests": [
"tests/corpus/1404-zlib-crypto-bytes.ts", "tests/corpus/1404-zlib-crypto-bytes.ts",
"tests/corpus/2921-zlib-callbacks.ts" "tests/corpus/2921-zlib-callbacks.ts",
"tests/corpus/3115-zlib-static-compression-levels.ts"
] ]
}, },
"dynamic": { "dynamic": {
@@ -127032,7 +127038,8 @@
"evidence": "surface-manifest:node-builtin.zlib.gunzip", "evidence": "surface-manifest:node-builtin.zlib.gunzip",
"tests": [ "tests": [
"tests/corpus/1404-zlib-crypto-bytes.ts", "tests/corpus/1404-zlib-crypto-bytes.ts",
"tests/corpus/2921-zlib-callbacks.ts" "tests/corpus/2921-zlib-callbacks.ts",
"tests/corpus/3115-zlib-static-compression-levels.ts"
] ]
}, },
"dynamic": { "dynamic": {
@@ -127066,7 +127073,8 @@
"evidence": "surface-manifest:node-builtin.zlib.gunzipSync", "evidence": "surface-manifest:node-builtin.zlib.gunzipSync",
"tests": [ "tests": [
"tests/corpus/1404-zlib-crypto-bytes.ts", "tests/corpus/1404-zlib-crypto-bytes.ts",
"tests/corpus/2921-zlib-callbacks.ts" "tests/corpus/2921-zlib-callbacks.ts",
"tests/corpus/3115-zlib-static-compression-levels.ts"
] ]
}, },
"dynamic": { "dynamic": {
@@ -127100,7 +127108,8 @@
"evidence": "surface-manifest:node-builtin.zlib.gzip", "evidence": "surface-manifest:node-builtin.zlib.gzip",
"tests": [ "tests": [
"tests/corpus/1404-zlib-crypto-bytes.ts", "tests/corpus/1404-zlib-crypto-bytes.ts",
"tests/corpus/2921-zlib-callbacks.ts" "tests/corpus/2921-zlib-callbacks.ts",
"tests/corpus/3115-zlib-static-compression-levels.ts"
] ]
}, },
"dynamic": { "dynamic": {
@@ -127134,7 +127143,8 @@
"evidence": "surface-manifest:node-builtin.zlib.gzipSync", "evidence": "surface-manifest:node-builtin.zlib.gzipSync",
"tests": [ "tests": [
"tests/corpus/1404-zlib-crypto-bytes.ts", "tests/corpus/1404-zlib-crypto-bytes.ts",
"tests/corpus/2921-zlib-callbacks.ts" "tests/corpus/2921-zlib-callbacks.ts",
"tests/corpus/3115-zlib-static-compression-levels.ts"
] ]
}, },
"dynamic": { "dynamic": {
@@ -127168,7 +127178,8 @@
"evidence": "surface-manifest:node-builtin.zlib.inflate", "evidence": "surface-manifest:node-builtin.zlib.inflate",
"tests": [ "tests": [
"tests/corpus/1404-zlib-crypto-bytes.ts", "tests/corpus/1404-zlib-crypto-bytes.ts",
"tests/corpus/2921-zlib-callbacks.ts" "tests/corpus/2921-zlib-callbacks.ts",
"tests/corpus/3115-zlib-static-compression-levels.ts"
] ]
}, },
"dynamic": { "dynamic": {
@@ -127202,7 +127213,8 @@
"evidence": "surface-manifest:node-builtin.zlib.inflateSync", "evidence": "surface-manifest:node-builtin.zlib.inflateSync",
"tests": [ "tests": [
"tests/corpus/1404-zlib-crypto-bytes.ts", "tests/corpus/1404-zlib-crypto-bytes.ts",
"tests/corpus/2921-zlib-callbacks.ts" "tests/corpus/2921-zlib-callbacks.ts",
"tests/corpus/3115-zlib-static-compression-levels.ts"
] ]
}, },
"dynamic": { "dynamic": {
@@ -127236,7 +127248,8 @@
"evidence": "surface-manifest:node-builtin.zlib.inflateRaw", "evidence": "surface-manifest:node-builtin.zlib.inflateRaw",
"tests": [ "tests": [
"tests/corpus/1404-zlib-crypto-bytes.ts", "tests/corpus/1404-zlib-crypto-bytes.ts",
"tests/corpus/2921-zlib-callbacks.ts" "tests/corpus/2921-zlib-callbacks.ts",
"tests/corpus/3115-zlib-static-compression-levels.ts"
] ]
}, },
"dynamic": { "dynamic": {
@@ -127270,7 +127283,8 @@
"evidence": "surface-manifest:node-builtin.zlib.inflateRawSync", "evidence": "surface-manifest:node-builtin.zlib.inflateRawSync",
"tests": [ "tests": [
"tests/corpus/1404-zlib-crypto-bytes.ts", "tests/corpus/1404-zlib-crypto-bytes.ts",
"tests/corpus/2921-zlib-callbacks.ts" "tests/corpus/2921-zlib-callbacks.ts",
"tests/corpus/3115-zlib-static-compression-levels.ts"
] ]
}, },
"dynamic": { "dynamic": {
@@ -127304,7 +127318,8 @@
"evidence": "surface-manifest:node-builtin.zlib.unzip", "evidence": "surface-manifest:node-builtin.zlib.unzip",
"tests": [ "tests": [
"tests/corpus/1404-zlib-crypto-bytes.ts", "tests/corpus/1404-zlib-crypto-bytes.ts",
"tests/corpus/2921-zlib-callbacks.ts" "tests/corpus/2921-zlib-callbacks.ts",
"tests/corpus/3115-zlib-static-compression-levels.ts"
] ]
}, },
"dynamic": { "dynamic": {
@@ -127338,7 +127353,8 @@
"evidence": "surface-manifest:node-builtin.zlib.unzipSync", "evidence": "surface-manifest:node-builtin.zlib.unzipSync",
"tests": [ "tests": [
"tests/corpus/1404-zlib-crypto-bytes.ts", "tests/corpus/1404-zlib-crypto-bytes.ts",
"tests/corpus/2921-zlib-callbacks.ts" "tests/corpus/2921-zlib-callbacks.ts",
"tests/corpus/3115-zlib-static-compression-levels.ts"
] ]
}, },
"dynamic": { "dynamic": {
@@ -128164,7 +128180,8 @@
"evidence": "surface-manifest:node-builtin.zlib.deflate", "evidence": "surface-manifest:node-builtin.zlib.deflate",
"tests": [ "tests": [
"tests/corpus/1404-zlib-crypto-bytes.ts", "tests/corpus/1404-zlib-crypto-bytes.ts",
"tests/corpus/2921-zlib-callbacks.ts" "tests/corpus/2921-zlib-callbacks.ts",
"tests/corpus/3115-zlib-static-compression-levels.ts"
] ]
}, },
"dynamic": { "dynamic": {
@@ -128198,7 +128215,8 @@
"evidence": "surface-manifest:node-builtin.zlib.deflateSync", "evidence": "surface-manifest:node-builtin.zlib.deflateSync",
"tests": [ "tests": [
"tests/corpus/1404-zlib-crypto-bytes.ts", "tests/corpus/1404-zlib-crypto-bytes.ts",
"tests/corpus/2921-zlib-callbacks.ts" "tests/corpus/2921-zlib-callbacks.ts",
"tests/corpus/3115-zlib-static-compression-levels.ts"
] ]
}, },
"dynamic": { "dynamic": {
@@ -128232,7 +128250,8 @@
"evidence": "surface-manifest:node-builtin.zlib.deflateRaw", "evidence": "surface-manifest:node-builtin.zlib.deflateRaw",
"tests": [ "tests": [
"tests/corpus/1404-zlib-crypto-bytes.ts", "tests/corpus/1404-zlib-crypto-bytes.ts",
"tests/corpus/2921-zlib-callbacks.ts" "tests/corpus/2921-zlib-callbacks.ts",
"tests/corpus/3115-zlib-static-compression-levels.ts"
] ]
}, },
"dynamic": { "dynamic": {
@@ -128266,7 +128285,8 @@
"evidence": "surface-manifest:node-builtin.zlib.deflateRawSync", "evidence": "surface-manifest:node-builtin.zlib.deflateRawSync",
"tests": [ "tests": [
"tests/corpus/1404-zlib-crypto-bytes.ts", "tests/corpus/1404-zlib-crypto-bytes.ts",
"tests/corpus/2921-zlib-callbacks.ts" "tests/corpus/2921-zlib-callbacks.ts",
"tests/corpus/3115-zlib-static-compression-levels.ts"
] ]
}, },
"dynamic": { "dynamic": {
@@ -128300,7 +128320,8 @@
"evidence": "surface-manifest:node-builtin.zlib.gunzip", "evidence": "surface-manifest:node-builtin.zlib.gunzip",
"tests": [ "tests": [
"tests/corpus/1404-zlib-crypto-bytes.ts", "tests/corpus/1404-zlib-crypto-bytes.ts",
"tests/corpus/2921-zlib-callbacks.ts" "tests/corpus/2921-zlib-callbacks.ts",
"tests/corpus/3115-zlib-static-compression-levels.ts"
] ]
}, },
"dynamic": { "dynamic": {
@@ -128334,7 +128355,8 @@
"evidence": "surface-manifest:node-builtin.zlib.gunzipSync", "evidence": "surface-manifest:node-builtin.zlib.gunzipSync",
"tests": [ "tests": [
"tests/corpus/1404-zlib-crypto-bytes.ts", "tests/corpus/1404-zlib-crypto-bytes.ts",
"tests/corpus/2921-zlib-callbacks.ts" "tests/corpus/2921-zlib-callbacks.ts",
"tests/corpus/3115-zlib-static-compression-levels.ts"
] ]
}, },
"dynamic": { "dynamic": {
@@ -128368,7 +128390,8 @@
"evidence": "surface-manifest:node-builtin.zlib.gzip", "evidence": "surface-manifest:node-builtin.zlib.gzip",
"tests": [ "tests": [
"tests/corpus/1404-zlib-crypto-bytes.ts", "tests/corpus/1404-zlib-crypto-bytes.ts",
"tests/corpus/2921-zlib-callbacks.ts" "tests/corpus/2921-zlib-callbacks.ts",
"tests/corpus/3115-zlib-static-compression-levels.ts"
] ]
}, },
"dynamic": { "dynamic": {
@@ -128402,7 +128425,8 @@
"evidence": "surface-manifest:node-builtin.zlib.gzipSync", "evidence": "surface-manifest:node-builtin.zlib.gzipSync",
"tests": [ "tests": [
"tests/corpus/1404-zlib-crypto-bytes.ts", "tests/corpus/1404-zlib-crypto-bytes.ts",
"tests/corpus/2921-zlib-callbacks.ts" "tests/corpus/2921-zlib-callbacks.ts",
"tests/corpus/3115-zlib-static-compression-levels.ts"
] ]
}, },
"dynamic": { "dynamic": {
@@ -128436,7 +128460,8 @@
"evidence": "surface-manifest:node-builtin.zlib.inflate", "evidence": "surface-manifest:node-builtin.zlib.inflate",
"tests": [ "tests": [
"tests/corpus/1404-zlib-crypto-bytes.ts", "tests/corpus/1404-zlib-crypto-bytes.ts",
"tests/corpus/2921-zlib-callbacks.ts" "tests/corpus/2921-zlib-callbacks.ts",
"tests/corpus/3115-zlib-static-compression-levels.ts"
] ]
}, },
"dynamic": { "dynamic": {
@@ -128470,7 +128495,8 @@
"evidence": "surface-manifest:node-builtin.zlib.inflateSync", "evidence": "surface-manifest:node-builtin.zlib.inflateSync",
"tests": [ "tests": [
"tests/corpus/1404-zlib-crypto-bytes.ts", "tests/corpus/1404-zlib-crypto-bytes.ts",
"tests/corpus/2921-zlib-callbacks.ts" "tests/corpus/2921-zlib-callbacks.ts",
"tests/corpus/3115-zlib-static-compression-levels.ts"
] ]
}, },
"dynamic": { "dynamic": {
@@ -128504,7 +128530,8 @@
"evidence": "surface-manifest:node-builtin.zlib.inflateRaw", "evidence": "surface-manifest:node-builtin.zlib.inflateRaw",
"tests": [ "tests": [
"tests/corpus/1404-zlib-crypto-bytes.ts", "tests/corpus/1404-zlib-crypto-bytes.ts",
"tests/corpus/2921-zlib-callbacks.ts" "tests/corpus/2921-zlib-callbacks.ts",
"tests/corpus/3115-zlib-static-compression-levels.ts"
] ]
}, },
"dynamic": { "dynamic": {
@@ -128538,7 +128565,8 @@
"evidence": "surface-manifest:node-builtin.zlib.inflateRawSync", "evidence": "surface-manifest:node-builtin.zlib.inflateRawSync",
"tests": [ "tests": [
"tests/corpus/1404-zlib-crypto-bytes.ts", "tests/corpus/1404-zlib-crypto-bytes.ts",
"tests/corpus/2921-zlib-callbacks.ts" "tests/corpus/2921-zlib-callbacks.ts",
"tests/corpus/3115-zlib-static-compression-levels.ts"
] ]
}, },
"dynamic": { "dynamic": {
@@ -128572,7 +128600,8 @@
"evidence": "surface-manifest:node-builtin.zlib.unzip", "evidence": "surface-manifest:node-builtin.zlib.unzip",
"tests": [ "tests": [
"tests/corpus/1404-zlib-crypto-bytes.ts", "tests/corpus/1404-zlib-crypto-bytes.ts",
"tests/corpus/2921-zlib-callbacks.ts" "tests/corpus/2921-zlib-callbacks.ts",
"tests/corpus/3115-zlib-static-compression-levels.ts"
] ]
}, },
"dynamic": { "dynamic": {
@@ -128606,7 +128635,8 @@
"evidence": "surface-manifest:node-builtin.zlib.unzipSync", "evidence": "surface-manifest:node-builtin.zlib.unzipSync",
"tests": [ "tests": [
"tests/corpus/1404-zlib-crypto-bytes.ts", "tests/corpus/1404-zlib-crypto-bytes.ts",
"tests/corpus/2921-zlib-callbacks.ts" "tests/corpus/2921-zlib-callbacks.ts",
"tests/corpus/3115-zlib-static-compression-levels.ts"
] ]
}, },
"dynamic": { "dynamic": {
+1 -1
View File
@@ -72,7 +72,7 @@
"node-builtin.url": ["tests/corpus/1794-searchparams-url-live.ts"], "node-builtin.url": ["tests/corpus/1794-searchparams-url-live.ts"],
"node-builtin.util": ["tests/corpus/2440-console-inspect-args.ts", "tests/corpus/2912-promisify-fs-readfile.ts"], "node-builtin.util": ["tests/corpus/2440-console-inspect-args.ts", "tests/corpus/2912-promisify-fs-readfile.ts"],
"node-builtin.worker_threads": ["tests/corpus/957-builtins-namespace.ts"], "node-builtin.worker_threads": ["tests/corpus/957-builtins-namespace.ts"],
"node-builtin.zlib": ["tests/corpus/1404-zlib-crypto-bytes.ts", "tests/corpus/2921-zlib-callbacks.ts"], "node-builtin.zlib": ["tests/corpus/1404-zlib-crypto-bytes.ts", "tests/corpus/2921-zlib-callbacks.ts", "tests/corpus/3115-zlib-static-compression-levels.ts"],
"stdlib.fetch": ["tests/harness/fetch-conformance.test.ts"], "stdlib.fetch": ["tests/harness/fetch-conformance.test.ts"],
"stdlib.headers": ["tests/harness/fetch-conformance.test.ts"], "stdlib.headers": ["tests/harness/fetch-conformance.test.ts"],
"stdlib.request": ["tests/harness/fetch-conformance.test.ts"], "stdlib.request": ["tests/harness/fetch-conformance.test.ts"],
+1 -1
View File
@@ -5,7 +5,7 @@ import { InternalCompilerError } from "../../errors.js";
import type { CEmitter } from "./c-emitter.js"; import type { CEmitter } from "./c-emitter.js";
import { mangleArgPack, mangleAsyncSpawn, mangleChildDataThunk, mangleChildExitThunk, mangleCloseBindThunk, mangleCloseOverrideWrap, mangleConnectResThunk, mangleConnectSockThunk, mangleDgramMsgThunk, mangleDnsLookupThunk, mangleField, mangleFsRenameThunk, mangleFunction, mangleGenDrop, mangleGenResThunk, mangleGenSpawn, mangleGlobal, mangleLocal, mangleRaceThunk, mangleRawParam, mangleNetLookupAnswerThunk, mangleEmitterInvokeThunk, mangleStreamCbThunk, mangleStreamDoneFn, mangleRecordNew, mangleRecordRelease, mangleRecordStruct, mangleResolveThunk, mangleSniAnswerThunk, mangleTrampoline } from "../mangle.js"; import { mangleArgPack, mangleAsyncSpawn, mangleChildDataThunk, mangleChildExitThunk, mangleCloseBindThunk, mangleCloseOverrideWrap, mangleConnectResThunk, mangleConnectSockThunk, mangleDgramMsgThunk, mangleDnsLookupThunk, mangleField, mangleFsRenameThunk, mangleFunction, mangleGenDrop, mangleGenResThunk, mangleGenSpawn, mangleGlobal, mangleLocal, mangleRaceThunk, mangleRawParam, mangleNetLookupAnswerThunk, mangleEmitterInvokeThunk, mangleStreamCbThunk, mangleStreamDoneFn, mangleRecordNew, mangleRecordRelease, mangleRecordStruct, mangleResolveThunk, mangleSniAnswerThunk, mangleTrampoline } from "../mangle.js";
import { cDecl, cType, releaseCallC, retainCallC, vAdapters } from "./types.js"; import { cDecl, cType, releaseCallC, retainCallC, vAdapters } from "./types.js";
import { IrFunction, IrType, isRefCounted, isUnitType, typeEquals, typeKey } from "../../ir/ir.js"; import { type IrFunction, type IrType, isRefCounted, isUnitType, typeEquals, typeKey } from "../../ir/ir.js";
interface ArgPackAndTrampolinePrologue { interface ArgPackAndTrampolinePrologue {
definitions: string[]; definitions: string[];
+15 -31
View File
@@ -64,7 +64,7 @@ import { cCommentText, cFnPtrCast, cType, releaseCallC, cStringLiteral, cDecl, c
import { computeTraced } from "../cycle-analysis.js"; import { computeTraced } from "../cycle-analysis.js";
import { computeMayThrow } from "./may-throw.js"; import { computeMayThrow } from "./may-throw.js";
import { unionTruthyHelper, unionEqHelper, unionToStrHelper, unionJoinHelper, jsonWriteHelper, jsonIndentHelper, dynMatchHelper, dynCheckHelper, dynFuncBoxHelper, dynToStrHelper, caughtToDynHelper, toDynHelper, recordKeyGetHelper, recordKeySetHelper } from "./walkers.js"; import { unionTruthyHelper, unionEqHelper, unionToStrHelper, unionJoinHelper, jsonWriteHelper, jsonIndentHelper, dynMatchHelper, dynCheckHelper, dynFuncBoxHelper, dynToStrHelper, caughtToDynHelper, toDynHelper, recordKeyGetHelper, recordKeySetHelper } from "./walkers.js";
import { VtSlot, ClassMeta, emitStructDefs, vtEntriesFor, vtSlotParams, emitVtableDecls, emitVtableInstances, emitVtAdapterDefs, emitHierarchyClassHelpers, emitClassObjs, emitCtorThunkDefs, errorVtStampLines, emitterVtStampLines, streamVtStampLines, traceAdapterC, traceArgC, boxNewC, arrNewC } from "./shapes.js"; import { type StructShape, type VtSlot, ClassMeta, emitStructDefs, vtEntriesFor, vtSlotParams, emitVtableDecls, emitVtableInstances, emitVtAdapterDefs, emitHierarchyClassHelpers, emitClassObjs, emitCtorThunkDefs, errorVtStampLines, emitterVtStampLines, streamVtStampLines, traceAdapterC, traceArgC, boxNewC, arrNewC } from "./shapes.js";
import { emitAsyncScaffolding, childDataThunkFor, childExitThunkFor, childExitSignalThunkFor, execFileThunkFor, ipcMessageThunkFor, ipcSendThunkFor, closeBindThunkFor, connectResThunkFor, connectSockThunkFor, closeOverrideWrapFor, cryptoBytesThunkFor, dgramMsgThunkFor, dnsLookupThunkFor, fsRenameThunkFor, genResultThunkFor, netLookupAnswerThunkFor, emitterInvokeThunkFor, streamCbThunkFor, streamDataThunkFor, raceAdapterFor, resolveThunkFor, sniAnswerThunkFor, zlibBytesThunkFor } from "./async.js"; import { emitAsyncScaffolding, childDataThunkFor, childExitThunkFor, childExitSignalThunkFor, execFileThunkFor, ipcMessageThunkFor, ipcSendThunkFor, closeBindThunkFor, connectResThunkFor, connectSockThunkFor, closeOverrideWrapFor, cryptoBytesThunkFor, dgramMsgThunkFor, dnsLookupThunkFor, fsRenameThunkFor, genResultThunkFor, netLookupAnswerThunkFor, emitterInvokeThunkFor, streamCbThunkFor, streamDataThunkFor, raceAdapterFor, resolveThunkFor, sniAnswerThunkFor, zlibBytesThunkFor } from "./async.js";
import { emitNpmEmbedding, islandAdapter, islandTypedAdapter } from "./island.js"; import { emitNpmEmbedding, islandAdapter, islandTypedAdapter } from "./island.js";
import { emitFunction, emitBlock, emitStmts, emitStmt, emitTryCatch, emitSwitch, mergeBrace, emitBranchInto, emitCondition } from "./stmts.js"; import { emitFunction, emitBlock, emitStmts, emitStmt, emitTryCatch, emitSwitch, mergeBrace, emitBranchInto, emitCondition } from "./stmts.js";
@@ -196,8 +196,8 @@ export class CEmitter {
* dispatches through. Registered during body emission (the regex * dispatches through. Registered during body emission (the regex
* pattern); the statics and thunks are assembled around the bodies. */ * pattern); the statics and thunks are assembled around the bodies. */
readonly classObjs = new Map<string, string>(); readonly classObjs = new Map<string, string>();
/** Stack of statement frames: refcounted temps not yet released or moved. */ /** Statement frames own expression temps and sequence locals until cleanup. */
frames: Temp[][] = []; frames: ScopeEntry[][] = [];
/** Stack of scopes: refcounted locals (with types) declared in each. */ /** Stack of scopes: refcounted locals (with types) declared in each. */
scopes: ScopeEntry[][] = []; scopes: ScopeEntry[][] = [];
/** The function being emitted: local table (for boxedness) and whether /** The function being emitted: local table (for boxedness) and whether
@@ -383,12 +383,12 @@ export class CEmitter {
usedContinue: boolean; usedContinue: boolean;
endLabel: string | null; endLabel: string | null;
usedEnd: boolean; usedEnd: boolean;
labels?: string[]; labels?: string[] | undefined;
scopeDepth: number; scopeDepth: number;
frameDepth: number; frameDepth: number;
finallyDepth: number; finallyDepth: number;
} }
| { kind: "switch"; endLabel: string; usedEnd: boolean; labels?: string[]; scopeDepth: number; frameDepth: number; finallyDepth: number } | { kind: "switch"; endLabel: string; usedEnd: boolean; labels?: string[] | undefined; scopeDepth: number; frameDepth: number; finallyDepth: number }
| { kind: "block"; endLabel: string; usedEnd: boolean; labels: string[]; scopeDepth: number; frameDepth: number; finallyDepth: number } | { kind: "block"; endLabel: string; usedEnd: boolean; labels: string[]; scopeDepth: number; frameDepth: number; finallyDepth: number }
)[] = []; )[] = [];
labelCounter = 0; labelCounter = 0;
@@ -472,16 +472,7 @@ export class CEmitter {
// redeclares it — never-overridden methods stay direct calls // redeclares it — never-overridden methods stay direct calls
// everywhere (whole-program devirtualization). // everywhere (whole-program devirtualization).
for (const cls of mod.classes ?? []) { for (const cls of mod.classes ?? []) {
const meta: ClassMeta = { const meta = new ClassMeta(cls);
def: cls,
base: null,
children: [],
root: undefined as unknown as ClassMeta,
pre: 0,
post: 0,
hierarchy: false,
slots: [],
};
this.classMeta.set(cls.name, meta); this.classMeta.set(cls.name, meta);
} }
for (const meta of this.classMeta.values()) { for (const meta of this.classMeta.values()) {
@@ -512,7 +503,7 @@ export class CEmitter {
const collectSlots = (m: ClassMeta, root: ClassMeta, seen: Map<string, number>): void => { const collectSlots = (m: ClassMeta, root: ClassMeta, seen: Map<string, number>): void => {
for (const method of m.def.methods ?? []) { for (const method of m.def.methods ?? []) {
let inherited = false; let inherited = false;
for (let a = m.base; a; a = a.base) inherited ||= declares(a, method); for (let a = m.base; a; a = a.base) inherited = inherited || declares(a, method);
if (!inherited && declaredBelow(m, method)) { if (!inherited && declaredBelow(m, method)) {
let fn = this.fnByName.get(`%${m.def.name}.${method}`); let fn = this.fnByName.get(`%${m.def.name}.${method}`);
if (!fn && m.def.abstractMethods?.includes(method)) { if (!fn && m.def.abstractMethods?.includes(method)) {
@@ -560,7 +551,7 @@ export class CEmitter {
if (sourceText !== undefined) { if (sourceText !== undefined) {
this.lineStarts = [0]; this.lineStarts = [0];
for (let i = 0; i < sourceText.length; i++) { for (let i = 0; i < sourceText.length; i++) {
if (sourceText[i] === "\n") this.lineStarts.push(i + 1); if (sourceText.charAt(i) === "\n") this.lineStarts.push(i + 1);
} }
} }
} }
@@ -1279,16 +1270,7 @@ export class CEmitter {
emitHierarchyClassHelpers(out: string[], emitHierarchyClassHelpers(out: string[],
meta: ClassMeta, meta: ClassMeta,
s: { s: StructShape,): void {
struct: string;
newFn: string;
retain: string;
release: string;
trace: string;
gcFree: string;
traced: boolean;
fields: { name: string; type: IrType }[];
},): void {
return emitHierarchyClassHelpers(this, out, meta, s); return emitHierarchyClassHelpers(this, out, meta, s);
} }
@@ -1443,8 +1425,10 @@ export class CEmitter {
`}`, `}`,
); );
} }
for (const elem of ["u8", "u32", "i32", "f32", "f64"] as const) { const elements: ("u8" | "u32" | "i32" | "f32" | "f64")[] = ["u8", "u32", "i32", "f32", "f64"];
for (const mode of ["f64", "u64"] as const) { const modes: ("f64" | "u64")[] = ["f64", "u64"];
for (const elem of elements) {
for (const mode of modes) {
const suffix = mode === "u64" ? "_u64" : ""; const suffix = mode === "u64" ? "_u64" : "";
const indexType = mode === "u64" ? "uint64_t" : "double"; const indexType = mode === "u64" ? "uint64_t" : "double";
const checked = mode === "u64" ? "sc_bytes_index_u64_checked" : "sc_bytes_index_checked"; const checked = mode === "u64" ? "sc_bytes_index_u64_checked" : "sc_bytes_index_checked";
@@ -1546,7 +1530,7 @@ export class CEmitter {
return t; return t;
} }
currentFrame(): Temp[] { currentFrame(): ScopeEntry[] {
const frame = this.frames[this.frames.length - 1]; const frame = this.frames[this.frames.length - 1];
if (!frame) throw new InternalCompilerError("emitter bug: no active statement frame"); if (!frame) throw new InternalCompilerError("emitter bug: no active statement frame");
return frame; return frame;
@@ -1969,12 +1953,12 @@ export class CEmitter {
* (a C break only exits the innermost loop). */ * (a C break only exits the innermost loop). */
loopTarget(continueLabel: string | null, labels: string[] | undefined): (typeof this.jumpTargets)[number] & { kind: "loop" } { loopTarget(continueLabel: string | null, labels: string[] | undefined): (typeof this.jumpTargets)[number] & { kind: "loop" } {
return { return {
labels,
kind: "loop", kind: "loop",
continueLabel: continueLabel === null && labels !== undefined ? `sc_cont_${this.labelCounter++}` : continueLabel, continueLabel: continueLabel === null && labels !== undefined ? `sc_cont_${this.labelCounter++}` : continueLabel,
usedContinue: false, usedContinue: false,
endLabel: labels !== undefined ? `sc_end_${this.labelCounter++}` : null, endLabel: labels !== undefined ? `sc_end_${this.labelCounter++}` : null,
usedEnd: false, usedEnd: false,
...(labels !== undefined && { labels }),
scopeDepth: this.scopes.length, scopeDepth: this.scopes.length,
frameDepth: this.frames.length, frameDepth: this.frames.length,
finallyDepth: this.finallyStack.length, finallyDepth: this.finallyStack.length,
+18 -13
View File
@@ -3,7 +3,7 @@ import { InternalCompilerError } from "../../errors.js";
* expression lands in a fresh C temp, with RC ownership tracked on the * expression lands in a fresh C temp, with RC ownership tracked on the
* emitter's frames (see the discipline comment in emitter core). */ * emitter's frames (see the discipline comment in emitter core). */
import type { CEmitter, Temp } from "./c-emitter.js"; import type { CEmitter, Temp } from "./c-emitter.js";
import { arrayOf, BOOL, BYTES_U8, bytesOf, canMarshalFuncIntoIsland, CHILDSTREAM_T, CHILDWRITER_T, DYN, F64, IrExpr, IrLibFn, IrRecordShape, IrType, islandPromisePayloadTag, isClassOwnEnumerableFieldName, isDynTypedRefType, isFfiCallbackParam, isFfiContextParam, isFfiReleaseParam, isRefCounted, isUnitType, MAY_THROW_LIB_FNS, NETSOCKET_T, RUNTIME_ERROR_CLASSES, STRING, typeEquals, typeKey } from "../../ir/ir.js"; import { arrayOf, BOOL, BYTES_U8, bytesOf, canMarshalFuncIntoIsland, CHILDSTREAM_T, CHILDWRITER_T, DYN, F64, type IrExpr, type IrLibFn, type IrRecordShape, type IrType, islandPromisePayloadTag, isClassOwnEnumerableFieldName, isDynTypedRefType, isFfiCallbackParam, isFfiContextParam, isFfiReleaseParam, isRefCounted, isUnitType, MAY_THROW_LIB_FNS, NETSOCKET_T, RUNTIME_ERROR_CLASSES, STRING, typeEquals, typeKey } from "../../ir/ir.js";
import { BYTES_NUM_KIND_C, BYTES_NUM_VAR_C, bytesElemKindC, cDecl, cFnPtrCast, cNumberLiteral, cStringLiteral, cType, DV_GET_KIND_C, DV_SET_KIND_C, elemAccess, mapKeyAccess, mapKeyKindC, mapValKindC, releaseCallC, retainCallC, vAdapters } from "./types.js"; import { BYTES_NUM_KIND_C, BYTES_NUM_VAR_C, bytesElemKindC, cDecl, cFnPtrCast, cNumberLiteral, cStringLiteral, cType, DV_GET_KIND_C, DV_SET_KIND_C, elemAccess, mapKeyAccess, mapKeyKindC, mapValKindC, releaseCallC, retainCallC, vAdapters } from "./types.js";
import { mangleClassNew, mangleClassRetain, mangleClassStruct, mangleField, mangleFnClosure, mangleFunction, mangleGlobal, mangleLocal, mangleRecordClone, mangleRecordNew, mangleRecordStruct, mangleVtStruct } from "../mangle.js"; import { mangleClassNew, mangleClassRetain, mangleClassStruct, mangleField, mangleFnClosure, mangleFunction, mangleGlobal, mangleLocal, mangleRecordClone, mangleRecordNew, mangleRecordStruct, mangleVtStruct } from "../mangle.js";
import { OVERFLOW_MEMBER } from "./shapes.js"; import { OVERFLOW_MEMBER } from "./shapes.js";
@@ -177,7 +177,8 @@ function nestedTypedRefUnionAdapter(
ctx: StreamTypedRefContext, ctx: StreamTypedRefContext,
): string { ): string {
const key = typeKey(t); const key = typeKey(t);
const unions = (ctx.unions ??= new Map()); if (ctx.unions === undefined) ctx.unions = new Map<string, string>();
const unions = ctx.unions;
const existing = unions.get(key); const existing = unions.get(key);
if (existing) return existing; if (existing) return existing;
const def = emitter.unionsById.get(t.unionId); const def = emitter.unionsById.get(t.unionId);
@@ -993,13 +994,15 @@ function emitOperatorExpr(
return v; return v;
} }
case "seqExpr": { case "seqExpr": {
// Statements mid-expression: C emission is linear, so each // Keep saved operands alive through the enclosing expression:
// statement emits in place (its own frame, exactly statement // tuple spreads and JS arguments reuse them in later call slots.
// position) and the result is an ordinary temp of the current // The current frame still cleans up on the path that created them,
// frame. The validator restricted stmts to straight-line writes — // including a lazy branch that is skipped on later loop iterations.
// no jump can leave the region. emitter.scopes.push([]);
for (const s of e.stmts) emitter.emitStmt(s); for (const s of e.stmts) emitter.emitStmt(s);
return emitter.emitExpr(e.result); const result = emitter.emitExpr(e.result);
emitter.currentFrame().push(...emitter.scopes.pop()!);
return result;
} }
default: { default: {
const _exhaustive: never = e; const _exhaustive: never = e;
@@ -1169,7 +1172,7 @@ function emitControlExpr(
if (e.receiver.type.kind !== "union") throw new InternalCompilerError("emitter bug: optChain receiver is not a union"); if (e.receiver.type.kind !== "union") throw new InternalCompilerError("emitter bug: optChain receiver is not a union");
const def = emitter.unionsById.get(e.receiver.type.unionId); const def = emitter.unionsById.get(e.receiver.type.unionId);
if (!def) throw new InternalCompilerError(`emitter bug: optChain of unknown union ${e.receiver.type.unionId}`); if (!def) throw new InternalCompilerError(`emitter bug: optChain of unknown union ${e.receiver.type.unionId}`);
const unitTags = def.arms.flatMap((a, i) => (isUnitType(a) ? [i] : [])); const unitTags: number[] = def.arms.flatMap((a, i): number[] => (isUnitType(a) ? [i] : []));
const narrowIdx = def.arms.findIndex((a) => !isUnitType(a)); const narrowIdx = def.arms.findIndex((a) => !isUnitType(a));
if (unitTags.length === 0 || narrowIdx < 0) throw new InternalCompilerError("emitter bug: optChain union arms"); if (unitTags.length === 0 || narrowIdx < 0) throw new InternalCompilerError("emitter bug: optChain union arms");
const multiple = def.arms.length - unitTags.length > 1; const multiple = def.arms.length - unitTags.length > 1;
@@ -1340,7 +1343,7 @@ function emitControlExpr(
if (e.left.type.kind !== "union") throw new InternalCompilerError("emitter bug: nullish left is not a union"); if (e.left.type.kind !== "union") throw new InternalCompilerError("emitter bug: nullish left is not a union");
const def = emitter.unionsById.get(e.left.type.unionId); const def = emitter.unionsById.get(e.left.type.unionId);
if (!def) throw new InternalCompilerError(`emitter bug: nullish of unknown union ${e.left.type.unionId}`); if (!def) throw new InternalCompilerError(`emitter bug: nullish of unknown union ${e.left.type.unionId}`);
const unitTags = def.arms.flatMap((a, i) => (isUnitType(a) ? [i] : [])); const unitTags: number[] = def.arms.flatMap((a, i): number[] => (isUnitType(a) ? [i] : []));
if (unitTags.length === 0) throw new InternalCompilerError("emitter bug: nullish union lacks unit arms"); if (unitTags.length === 0) throw new InternalCompilerError("emitter bug: nullish union lacks unit arms");
const l = emitter.emitExpr(e.left); const l = emitter.emitExpr(e.left);
emitter.moveTemp(l); emitter.moveTemp(l);
@@ -2050,7 +2053,7 @@ function emitContainerExpr(
// nargs = the PRESENT index args (omitted ones skip Node's // nargs = the PRESENT index args (omitted ones skip Node's
// validation); the 0 placeholders are never read past nargs. // validation); the 0 placeholders are never read past nargs.
const n = e.args.length - 1; const n = e.args.length - 1;
const idx = [1, 2, 3, 4].map((i) => args[i]?.name ?? "0"); const idx = [1, 2, 3, 4].map((i: number) => args[i]?.name ?? "0");
const t = emitter.newTemp( const t = emitter.newTemp(
e.type, e.type,
`scr_bytes_compare(${r.name}, ${args[0]!.name}, ${n}, ${idx.join(", ")})`, `scr_bytes_compare(${r.name}, ${args[0]!.name}, ${n}, ${idx.join(", ")})`,
@@ -2629,7 +2632,7 @@ function emitCallExpr(
const cast = `(void *(*)(${paramTypes.join(", ") || "void"}))`; const cast = `(void *(*)(${paramTypes.join(", ") || "void"}))`;
const call = `(${cast}${callee.name}->ctor)(${args.map((a) => a.name).join(", ")})`; const call = `(${cast}${callee.name}->ctor)(${args.map((a) => a.name).join(", ")})`;
const t = emitter.newTemp(e.type, `(${cType(e.type).trim()})${call}`); const t = emitter.newTemp(e.type, `(${cType(e.type).trim()})${call}`);
if (newValueMayThrow(cls, emitter.classMeta, emitter.mayThrow)) emitter.emitPendingCheck(); if (newValueMayThrow(cls, emitter.classMeta.get(cls), emitter.mayThrow)) emitter.emitPendingCheck();
return t; return t;
} }
case "instanceOfValue": { case "instanceOfValue": {
@@ -3814,7 +3817,7 @@ function emitAsyncExpr(
emitter.line(`} else {`); emitter.line(`} else {`);
emitter.indent++; emitter.indent++;
emitter.line(`scr_await_hop();`); emitter.line(`scr_await_hop();`);
const unitTags = def.arms.flatMap((a, i) => (isUnitType(a) ? [i] : [])); const unitTags: number[] = def.arms.flatMap((a, i): number[] => (isUnitType(a) ? [i] : []));
if (unitTags.length === 1) { if (unitTags.length === 1) {
emitter.line(`${name} = ${emitter.unitInstanceRef(e.type.unionId, resTagOf(def.arms[unitTags[0]!]!))};`); emitter.line(`${name} = ${emitter.unitInstanceRef(e.type.unionId, resTagOf(def.arms[unitTags[0]!]!))};`);
} else { } else {
@@ -4704,6 +4707,8 @@ function emitFilesystemLibCall(state: LibCallState): Temp {
return finish(`scr_zlib_inflate(${arg(0)})`); return finish(`scr_zlib_inflate(${arg(0)})`);
case "zlib.deflateRawSync": case "zlib.deflateRawSync":
return finish(`scr_zlib_deflate_mode(${arg(0)}, 1.0, -1.0)`); return finish(`scr_zlib_deflate_mode(${arg(0)}, 1.0, -1.0)`);
case "zlib.deflateLevelSync":
return finish(`scr_zlib_deflate_mode(${arg(0)}, ${arg(1)}, ${arg(2)})`);
case "zlib.inflateRawSync": case "zlib.inflateRawSync":
return finish(`scr_zlib_inflate_mode(${arg(0)}, 1.0)`); return finish(`scr_zlib_inflate_mode(${arg(0)}, 1.0)`);
case "zlib.gzipSync": case "zlib.gzipSync":
+1 -1
View File
@@ -7,7 +7,7 @@ import { InternalCompilerError } from "../../errors.js";
import { deflateRawSync } from "node:zlib"; import { deflateRawSync } from "node:zlib";
import type { CEmitter } from "./c-emitter.js"; import type { CEmitter } from "./c-emitter.js";
import { cDecl, cFnPtrCast, cStringLiteral, releaseCallC } from "./types.js"; import { cDecl, cFnPtrCast, cStringLiteral, releaseCallC } from "./types.js";
import { IrType, islandCallbackRet, NPM_COMPRESS_MIN, typeKey } from "../../ir/ir.js"; import { type IrType, islandCallbackRet, NPM_COMPRESS_MIN, typeKey } from "../../ir/ir.js";
import { undefinedArmTag } from "../../ir/analysis.js"; import { undefinedArmTag } from "../../ir/analysis.js";
/** Embedded npm modules (--dynamic): every reached module's SOURCE as a /** Embedded npm modules (--dynamic): every reached module's SOURCE as a
+39 -40
View File
@@ -6,7 +6,7 @@ import { InternalCompilerError } from "../../errors.js";
* VtSlot) the emitter builds up front; emission ORDER is part of the C. */ * VtSlot) the emitter builds up front; emission ORDER is part of the C. */
import type { CEmitter } from "./c-emitter.js"; import type { CEmitter } from "./c-emitter.js";
import type { IrFunction } from "../../ir/ir.js"; import type { IrFunction } from "../../ir/ir.js";
import { IrClassDef, IrType, RUNTIME_EMITTER_CLASS, RUNTIME_ERROR_CLASSES, RUNTIME_STREAM_CLASSES, isRefCounted, mapOf, STRING } from "../../ir/ir.js"; import { type IrClassDef, type IrType, RUNTIME_EMITTER_CLASS, RUNTIME_ERROR_CLASSES, RUNTIME_STREAM_CLASSES, isRefCounted, mapOf, STRING } from "../../ir/ir.js";
import { mangleClassGcFree, mangleClassNew, mangleClassRelease, mangleClassReleaseDirect, mangleClassRetain, mangleClassStruct, mangleClassTrace, mangleCtorThunk, mangleField, mangleFunction, mangleRecordClone, mangleRecordGcFree, mangleRecordNew, mangleRecordRelease, mangleRecordRetain, mangleRecordStruct, mangleRecordTrace, mangleVtAdapter, mangleVtInstance, mangleVtStruct } from "../mangle.js"; import { mangleClassGcFree, mangleClassNew, mangleClassRelease, mangleClassReleaseDirect, mangleClassRetain, mangleClassStruct, mangleClassTrace, mangleCtorThunk, mangleField, mangleFunction, mangleRecordClone, mangleRecordGcFree, mangleRecordNew, mangleRecordRelease, mangleRecordRetain, mangleRecordStruct, mangleRecordTrace, mangleVtAdapter, mangleVtInstance, mangleVtStruct } from "../mangle.js";
import { boxKindC, cCommentText, cDecl, cType, elemKindC, mapValKindC, releaseCallC, retainCallC, vAdapters } from "./types.js"; import { boxKindC, cCommentText, cDecl, cType, elemKindC, mapValKindC, releaseCallC, retainCallC, vAdapters } from "./types.js";
import { streamRooted } from "../../ir/analysis.js"; import { streamRooted } from "../../ir/analysis.js";
@@ -33,16 +33,43 @@ export interface VtSlot {
* are the preorder interval over the whole-program class forest — a class's * are the preorder interval over the whole-program class forest — a class's
* descendants are exactly the classes whose `pre` lies inside it, which is * descendants are exactly the classes whose `pre` lies inside it, which is
* both the instanceof check and the slot-lookup subtree test. */ * both the instanceof check and the slot-lookup subtree test. */
export interface ClassMeta { export class ClassMeta {
def: IrClassDef; def: IrClassDef;
base: ClassMeta | null; base: ClassMeta | null = null;
children: ClassMeta[]; children: ClassMeta[] = [];
root: ClassMeta; root: ClassMeta;
pre: number; pre = 0;
post: number; post = 0;
hierarchy: boolean; hierarchy = false;
/** Root classes: the hierarchy's slots in DFS-declaration order. */ /** Root classes: the hierarchy's slots in DFS-declaration order. */
slots: VtSlot[]; slots: VtSlot[] = [];
constructor(def: IrClassDef) {
this.def = def;
// Every node is a valid root until the forest is linked and numbered.
// No asserted undefined value crosses into native record storage.
this.root = this;
}
}
export interface StructShape {
struct: string;
newFn: string;
retain: string;
release: string;
trace: string;
gcFree: string;
traced: boolean;
fields: { name: string; type: IrType }[];
/** Records with a string index signature: the overflow map's VALUE
* type. The struct carries a trailing `ScrMap *` member the shape's
* new/release/trace treat as one more (map-typed) field. */
indexValue?: IrType;
comment: string;
/** Record shape id; absent for classes. */
recordId?: string;
/** Class shapes only; hierarchy members get the vtable machinery. */
meta: ClassMeta | null;
} }
/** Per-shape C structs + RC helpers for classes AND record shapes (the /** Per-shape C structs + RC helpers for classes AND record shapes (the
@@ -60,31 +87,12 @@ export interface ClassMeta {
* contract in scr_runtime.h) — and their retain/release feed the * contract in scr_runtime.h) — and their retain/release feed the
* candidate-root buffer. Acyclic shapes keep the lean 1-word header. */ * candidate-root buffer. Acyclic shapes keep the lean 1-word header. */
export function emitStructDefs(emitter: CEmitter, out: string[]): void { export function emitStructDefs(emitter: CEmitter, out: string[]): void {
interface StructShape {
struct: string;
newFn: string;
retain: string;
release: string;
trace: string;
gcFree: string;
traced: boolean;
fields: { name: string; type: IrType }[];
/** Records with a string index signature: the overflow map's VALUE
* type. The struct carries a trailing `ScrMap *` member the shape's
* new/release/trace treat as one more (map-typed) field. */
indexValue?: IrType;
comment: string;
/** Record shape id; absent for classes. */
recordId?: string;
/** Class shapes only; hierarchy members get the vtable machinery. */
meta: ClassMeta | null;
}
// Runtime-provided classes (the builtin Error hierarchy) emit NOTHING // Runtime-provided classes (the builtin Error hierarchy) emit NOTHING
// here — struct, RC helpers, and vtables live in the runtime. They keep // here — struct, RC helpers, and vtables live in the runtime. They keep
// their ClassMeta (preorder numbering, instanceof constants, vtable // their ClassMeta (preorder numbering, instanceof constants, vtable
// struct type for user subclasses); main() stamps their intervals. // struct type for user subclasses); main() stamps their intervals.
const shapes: StructShape[] = [ const shapes: StructShape[] = [
...(emitter.mod.classes ?? []).filter((cls) => !cls.runtime).map((cls) => ({ ...(emitter.mod.classes ?? []).filter((cls) => !cls.runtime).map((cls): StructShape => ({
struct: mangleClassStruct(cls.name), struct: mangleClassStruct(cls.name),
newFn: mangleClassNew(cls.name), newFn: mangleClassNew(cls.name),
retain: mangleClassRetain(cls.name), retain: mangleClassRetain(cls.name),
@@ -96,7 +104,7 @@ export interface ClassMeta {
comment: `class ${cls.name}`, comment: `class ${cls.name}`,
meta: emitter.classMeta.get(cls.name) ?? null, meta: emitter.classMeta.get(cls.name) ?? null,
})), })),
...(emitter.mod.records ?? []).map((rec) => ({ ...(emitter.mod.records ?? []).map((rec): StructShape => ({
struct: mangleRecordStruct(rec.id), struct: mangleRecordStruct(rec.id),
newFn: mangleRecordNew(rec.id), newFn: mangleRecordNew(rec.id),
retain: mangleRecordRetain(rec.id), retain: mangleRecordRetain(rec.id),
@@ -365,7 +373,7 @@ function emitRecordCloneC(
* slot also answers null — the class never instantiates (tsc), so its * slot also answers null — the class never instantiates (tsc), so its
* own vtable entry can never dispatch. */ * own vtable entry can never dispatch. */
export function vtEntriesFor(emitter: CEmitter, meta: ClassMeta): { slot: VtSlot; impl: ClassMeta | null }[] { export function vtEntriesFor(emitter: CEmitter, meta: ClassMeta): { slot: VtSlot; impl: ClassMeta | null }[] {
return meta.root.slots.map((slot) => { return meta.root.slots.map((slot): { slot: VtSlot; impl: ClassMeta | null } => {
if (!(slot.declarer.pre <= meta.pre && meta.pre <= slot.declarer.post)) { if (!(slot.declarer.pre <= meta.pre && meta.pre <= slot.declarer.post)) {
return { slot, impl: null }; return { slot, impl: null };
} }
@@ -471,16 +479,7 @@ function emitRecordCloneC(
* class's functions at allocation — the collector needs no vtable. */ * class's functions at allocation — the collector needs no vtable. */
export function emitHierarchyClassHelpers(emitter: CEmitter, out: string[], export function emitHierarchyClassHelpers(emitter: CEmitter, out: string[],
meta: ClassMeta, meta: ClassMeta,
s: { s: StructShape,): void {
struct: string;
newFn: string;
retain: string;
release: string;
trace: string;
gcFree: string;
traced: boolean;
fields: { name: string; type: IrType }[];
},): void {
const reld = mangleClassReleaseDirect(meta.def.name); const reld = mangleClassReleaseDirect(meta.def.name);
const emitterRooted = meta.root.def.name === RUNTIME_EMITTER_CLASS; const emitterRooted = meta.root.def.name === RUNTIME_EMITTER_CLASS;
const isStreamRooted = streamRooted(meta); const isStreamRooted = streamRooted(meta);
+2 -2
View File
@@ -6,7 +6,7 @@ import { InternalCompilerError } from "../../errors.js";
import type { CEmitter, ScopeEntry } from "./c-emitter.js"; import type { CEmitter, ScopeEntry } from "./c-emitter.js";
import type { IrFunction } from "../../ir/ir.js"; import type { IrFunction } from "../../ir/ir.js";
import { mangleField, mangleGlobal, mangleLocal, mangleRawParam } from "../mangle.js"; import { mangleField, mangleGlobal, mangleLocal, mangleRawParam } from "../mangle.js";
import { BOOL, CAUGHT, IrExpr, IrStmt, RUNTIME_ERROR_CLASSES, isRefCounted } from "../../ir/ir.js"; import { BOOL, CAUGHT, type IrExpr, type IrStmt, RUNTIME_ERROR_CLASSES, isRefCounted } from "../../ir/ir.js";
import { boxAccess, cDecl, cStringLiteral, elemAccess, vAdapters } from "./types.js"; import { boxAccess, cDecl, cStringLiteral, elemAccess, vAdapters } from "./types.js";
import { OVERFLOW_MEMBER } from "./shapes.js"; import { OVERFLOW_MEMBER } from "./shapes.js";
import { writeBox } from "./bindings.js"; import { writeBox } from "./bindings.js";
@@ -1017,10 +1017,10 @@ function emitStmtBody(emitter: CEmitter, s: IrStmt): void {
// Bodies in source order: entering one falls through the rest (JS-exact) // Bodies in source order: entering one falls through the rest (JS-exact)
// until a break jumps to the end label. // until a break jumps to the end label.
const target = { const target = {
labels: s.labels,
kind: "switch" as const, kind: "switch" as const,
endLabel, endLabel,
usedEnd: defaultIdx < 0, usedEnd: defaultIdx < 0,
...(s.labels !== undefined && { labels: s.labels }),
scopeDepth: emitter.scopes.length, scopeDepth: emitter.scopes.length,
frameDepth: emitter.frames.length, frameDepth: emitter.frames.length,
finallyDepth: emitter.finallyStack.length, finallyDepth: emitter.finallyStack.length,
+2 -2
View File
@@ -7,7 +7,7 @@ import { InternalCompilerError } from "../../errors.js";
* interning ORDER is part of the emitted C, so the registries stay on * interning ORDER is part of the emitted C, so the registries stay on
* CEmitter and these functions only consult them through it. */ * CEmitter and these functions only consult them through it. */
import type { CEmitter } from "./c-emitter.js"; import type { CEmitter } from "./c-emitter.js";
import { DYN_HANDLE_KINDS, IrType, type IrUnionDef, isDynTypedRefType, isRefCounted, typeEquals, typeKey } from "../../ir/ir.js"; import { DYN_HANDLE_KINDS, type IrType, type IrUnionDef, isDynTypedRefType, isRefCounted, typeEquals, typeKey } from "../../ir/ir.js";
import { dynDesc, undefinedArmTag, unionWideningTags } from "../../ir/analysis.js"; import { dynDesc, undefinedArmTag, unionWideningTags } from "../../ir/analysis.js";
import { cCommentText, cDecl, cStringLiteral, cType, elemAccess, releaseCallC, retainCallC, vAdapters } from "./types.js"; import { cCommentText, cDecl, cStringLiteral, cType, elemAccess, releaseCallC, retainCallC, vAdapters } from "./types.js";
import { mangleField, mangleRecordNew, mangleRecordStruct } from "../mangle.js"; import { mangleField, mangleRecordNew, mangleRecordStruct } from "../mangle.js";
@@ -1962,7 +1962,7 @@ export function jsonWriteHelper(emitter: CEmitter, t: IrType): string {
); );
d.push(` scr_dyn_release(sc_fn);`); d.push(` scr_dyn_release(sc_fn);`);
t.params.forEach((_, i) => d.push(` scr_dyn_release(sc_args[${i}]);`)); t.params.forEach((_, i) => d.push(` scr_dyn_release(sc_args[${i}]);`));
d.push(` if (scr_exc_pending()) return ${dummy};`.replace("return ;", "return;")); d.push(` if (scr_exc_pending()) return${dummy === "" ? "" : ` ${dummy}`};`);
if (t.ret.kind === "void") { if (t.ret.kind === "void") {
d.push(` scr_dyn_release(sc_r);`); d.push(` scr_dyn_release(sc_r);`);
d.push(` return;`); d.push(` return;`);
+17 -1
View File
@@ -2310,7 +2310,12 @@ class LlEmitter {
if (v.slot) { if (v.slot) {
const t = this.B.tmp(); const t = this.B.tmp();
this.B.line(`${t} = load ptr, ptr ${v.name}`); this.B.line(`${t} = load ptr, ptr ${v.name}`);
this.releaseValue(t, v.type); if (v.boxed) {
this.declare(`declare void @scr_box_release(ptr)`);
this.B.line(`call void @scr_box_release(ptr ${t})`);
} else {
this.releaseValue(t, v.type);
}
} else { } else {
this.releaseValue(v.name, v.type); this.releaseValue(v.name, v.type);
} }
@@ -4153,6 +4158,17 @@ class LlEmitter {
} }
private emitOperatorExpr(e: ExprOf<"bin" | "unary" | "incDec" | "fieldIncDec" | "assignExpr" | "seqExpr">): LlValue { private emitOperatorExpr(e: ExprOf<"bin" | "unary" | "incDec" | "fieldIncDec" | "assignExpr" | "seqExpr">): LlValue {
if (e.kind === "seqExpr") {
// Saved operands can feed later call arguments. Transfer ownership
// to this expression's frame so they survive those reads and still
// die on the path that created them, including inside lazy branches.
this.scopes.push([]);
const result = emitOperatorExpr(this.expressionContext(), e);
for (const local of this.scopes.pop()!) {
this.currentFrame().push({ name: local.slot, type: local.type, slot: true, ...(local.boxed ? { boxed: true } : {}) });
}
return result;
}
return emitOperatorExpr(this.expressionContext(), e); return emitOperatorExpr(this.expressionContext(), e);
} }
@@ -480,7 +480,7 @@ export function emitCallExpr(host: LlvmEmitterContext, e: ExprOf<"call" | "ffiCa
const t = B.tmp(); const t = B.tmp();
B.line(`${t} = call ptr ${thunk}(${argList})`); B.line(`${t} = call ptr ${thunk}(${argList})`);
const out = host.own({ name: t, type: e.type }); const out = host.own({ name: t, type: e.type });
if (newValueMayThrow(cls, host.classMeta, host.mayThrow)) host.emitPendingCheck(); if (newValueMayThrow(cls, host.classMeta.get(cls), host.mayThrow)) host.emitPendingCheck();
return out; return out;
} }
case "instanceOfValue": { case "instanceOfValue": {
@@ -15,6 +15,8 @@ export interface LlValue {
name: string; name: string;
type: IrType; type: IrType;
slot?: boolean; slot?: boolean;
/** A sequence local held in a capture box; release the box itself. */
boxed?: boolean;
} }
export type ExprOf<K extends IrExpr["kind"]> = Extract<IrExpr, { kind: K }>; export type ExprOf<K extends IrExpr["kind"]> = Extract<IrExpr, { kind: K }>;
@@ -155,6 +155,13 @@ export function emitFilesystemLibCall(host: LlvmEmitterContext, e: LibCallExpr):
B.line(`call void @scr_zlib_codec_async(ptr ${args[0]!.name}, double ${f64Lit(mode)}, i1 ${compressing}, ptr ${args[1]!.name}, ptr @${adapter})`); B.line(`call void @scr_zlib_codec_async(ptr ${args[0]!.name}, double ${f64Lit(mode)}, i1 ${compressing}, ptr ${args[1]!.name}, ptr @${adapter})`);
return { name: "", type: e.type }; return { name: "", type: e.type };
} }
if (e.fn === "zlib.deflateLevelSync") {
const args = e.args.map((arg) => host.emitExpr(arg));
host.declare("declare ptr @scr_zlib_deflate_mode(ptr, double, double)");
const raw = B.tmp();
B.line(`${raw} = call ptr @scr_zlib_deflate_mode(ptr ${args[0]!.name}, double ${args[1]!.name}, double ${args[2]!.name})`);
return host.own({ name: raw, type: e.type });
}
if ( if (
e.fn === "zlib.deflateRawSync" || e.fn === "zlib.inflateRawSync" || e.fn === "zlib.deflateRawSync" || e.fn === "zlib.inflateRawSync" ||
e.fn === "zlib.gzipSync" || e.fn === "zlib.gunzipSync" || e.fn === "zlib.gzipSync" || e.fn === "zlib.gunzipSync" ||
@@ -409,8 +409,9 @@ try {
* probing their inferred IR layout too early can reject the right arm and * probing their inferred IR layout too early can reject the right arm and
* select a smaller unrelated record that drops fields. Exactly ONE fitting arm * select a smaller unrelated record that drops fields. Exactly ONE fitting arm
* answers it; zero or several answer null and the caller keeps its * answers it; zero or several answer null and the caller keeps its
* fences. Plain property-assignment/shorthand literals only — spreads, * fences. A spread uses the checker's resulting properties so overridden
* accessors, methods, and unfoldable computed keys keep their own paths. */ * discriminants and narrowed recursive source arms keep their identity.
* Accessors, methods, and unfoldable computed keys keep their own paths. */
function literalUnionArmOf( function literalUnionArmOf(
lowerer: Lowerer, lowerer: Lowerer,
expr: ts.ObjectLiteralExpression, expr: ts.ObjectLiteralExpression,
@@ -418,16 +419,28 @@ tsType: ts.Type,
recordArms: (IrType & { kind: "record" })[], recordArms: (IrType & { kind: "record" })[],
): (IrType & { kind: "record" }) | null { ): (IrType & { kind: "record" }) | null {
if (!tsType.isUnionType()) return null; if (!tsType.isUnionType()) return null;
const props: { name: string; node: ts.Expression }[] = []; const props: { name: string; type: ts.Type }[] = [];
let hasSpread = false;
for (const p of expr.properties) { for (const p of expr.properties) {
if (ts.isPropertyAssignment(p) && !ts.isComputedPropertyName(p.name)) { if (ts.isPropertyAssignment(p) && !ts.isComputedPropertyName(p.name)) {
props.push({ name: propNameText(lowerer, p.name), node: p.initializer }); props.push({ name: propNameText(lowerer, p.name), type: lowerer.typeOf(p.initializer) });
} else if (ts.isShorthandPropertyAssignment(p) && ts.isIdentifier(p.name)) { } else if (ts.isShorthandPropertyAssignment(p) && ts.isIdentifier(p.name)) {
props.push({ name: p.name.text, node: p.name }); props.push({ name: p.name.text, type: lowerer.typeOf(p.name) });
} else if (ts.isSpreadAssignment(p)) {
hasSpread = true;
} else { } else {
return null; return null;
} }
} }
if (hasSpread) {
const own = lowerer.typeOf(expr);
// Conditional/union spreads need runtime selection, not one guessed arm.
if (own.isUnionType()) return null;
props.length = 0;
for (const property of lowerer.checker.getPropertiesOfType(own)) {
props.push({ name: property.name, type: lowerer.checker.getTypeOfSymbol(property) });
}
}
/** Cheap literal discriminants avoid checker traffic for unrelated arms. /** Cheap literal discriminants avoid checker traffic for unrelated arms.
* Compound types need the checker's semantic assignability, including * Compound types need the checker's semantic assignability, including
* recursive unions, never intersections, and contextual empty arrays. */ * recursive unions, never intersections, and contextual empty arrays. */
@@ -464,7 +477,7 @@ for (const member of ts.constituentTypes(tsType)) {
const fieldsFit = props.every((p) => { const fieldsFit = props.every((p) => {
const sym = lowerer.checker.getPropertyOfType(member, p.name); const sym = lowerer.checker.getPropertyOfType(member, p.name);
if (!sym) return false; if (!sym) return false;
return fits(lowerer.typeOf(p.node), lowerer.checker.getTypeOfSymbol(sym)); return fits(p.type, lowerer.checker.getTypeOfSymbol(sym));
}); });
if (fieldsFit) candidates.add(mMapped.shapeId); if (fieldsFit) candidates.add(mMapped.shapeId);
} }
@@ -730,7 +743,11 @@ export function lowerObjectLiteral(lowerer: Lowerer, expr: ts.ObjectLiteralExpre
// exactly ONE member fits, build AS that arm — its field types // exactly ONE member fits, build AS that arm — its field types
// drive every property's coercion, exactly the single-record-arm // drive every property's coercion, exactly the single-record-arm
// rule above. Ambiguous literals keep the SC2003 fence. // rule above. Ambiguous literals keep the SC2003 fence.
if (ownShapeId === null || !recordArms.some((a) => a.shapeId === ownShapeId)) { // Recursive arms can have identical stored field layouts while
// retaining distinct nominal knots. An inferred spread shape can
// match the wrong knot, so consult discriminants even if that shape
// already appears in the union.
if (ownShapeId === null || !recordArms.some((a) => a.shapeId === ownShapeId) || expr.properties.some(ts.isSpreadAssignment)) {
const arm = literalUnionArmOf(lowerer, expr, tsType, recordArms); const arm = literalUnionArmOf(lowerer, expr, tsType, recordArms);
if (arm) mapped = arm; if (arm) mapped = arm;
} }
@@ -0,0 +1,70 @@
import * as ts from "../../ts7/adapter.js";
import type { Lowerer } from "../lowerer.js";
import { nodeThrowExpr } from "../lowerer.js";
import { BOOL, VOID, type IrExpr, type IrStmt, type IrType, typeEquals } from "../../../ir/ir.js";
import { varRef } from "../../../ir/build.js";
import { locOf } from "../../program.js";
/** Assign a common data field without projecting the union's records into
* a new structural shape. Each tag writes the original record payload, so
* aliases and identity-keyed collections observe the mutation. Accessors,
* missing fields, and differently typed storage retain their own fences. */
export function lowerUnionFieldWrite(
lowerer: Lowerer,
access: ts.PropertyAccessExpression,
rhs: ts.Expression,
): IrStmt | null {
const mapped = lowerer.mapTypeOf(lowerer.typeOf(access.expression));
if (access.questionDotToken || mapped?.kind !== "union") return null;
const receiver = lowerer.maybeNarrow(lowerer.lowerExpr(access.expression), access.expression);
if (receiver.type.kind !== "union") return null;
const unionId = receiver.type.unionId;
const arms = lowerer.unions.get(unionId)?.arms;
if (!arms?.length) return null;
const fields: ({ shapeId: string; type: IrType } | null)[] = [];
let fieldType: IrType | null = null;
for (const arm of arms) {
if (arm.kind === "undefinedT" || arm.kind === "nullT") {
fields.push(null);
continue;
}
if (arm.kind !== "record") return null;
const field = lowerer.shapes.get(arm.shapeId)?.fields.find((f) => f.name === access.name.text);
if (!field || (fieldType !== null && !typeEquals(fieldType, field.type))) return null;
fieldType = field.type;
fields.push({ shapeId: arm.shapeId, type: field.type });
}
if (fieldType === null) return null;
const loc = locOf(access);
const object = lowerer.declareHiddenLocal("%unionWriteObject", receiver.type);
const objectRef = varRef(object.id, object.type, loc);
const value = lowerer.lowerExprExpecting(rhs, fieldType);
const stored = lowerer.declareHiddenLocal("%unionWriteValue", value.type);
// Snapshot the reference before evaluating the RHS, which can change the
// original binding. Evaluate and retain the value once before dispatch.
const body: IrStmt[] = [
{ kind: "varDecl", localId: object.id, init: receiver, loc },
{ kind: "varDecl", localId: stored.id, init: value, loc },
];
let dispatch: IrStmt[] = [];
for (let tag = fields.length - 1; tag >= 0; tag--) {
const field = fields[tag];
const obj: IrExpr | null = field ? {
kind: "unionNarrow", unionId, tag, value: objectRef,
type: { kind: "record", shapeId: field.shapeId }, loc,
} : null;
const write: IrStmt = field && obj ? {
kind: "recordSet", obj, shapeId: field.shapeId, field: access.name.text,
value: varRef(stored.id, stored.type, loc), loc,
} : {
kind: "exprStmt",
expr: nodeThrowExpr(1, "", `Cannot set properties of ${arms[tag]!.kind === "nullT" ? "null" : "undefined"} (setting '${access.name.text}')`, VOID, loc), loc,
};
dispatch = tag === fields.length - 1 ? [write] : [{
kind: "if",
cond: { kind: "unionIsTag", unionId, tag, value: objectRef, negated: false, type: BOOL, loc },
then: [write], else_: dispatch, loc,
}];
}
return { kind: "block", body: [...body, ...dispatch], loc };
}
@@ -1447,6 +1447,32 @@ function lowerFsSyncBufferWindow(
} }
const syncFn = ZLIB_SYNC_FNS[bi.member]; const syncFn = ZLIB_SYNC_FNS[bi.member];
if (syncFn !== undefined) { if (syncFn !== undefined) {
if (expr.arguments.length === 2 &&
(bi.member === "deflateSync" || bi.member === "deflateRawSync" || bi.member === "gzipSync")) {
const options = expr.arguments[1]!;
// A literal level is enough for build-time compression in the native
// emitters. Other options retain the explicit refusal: silently
// dropping strategy/windowBits/dictionaries changes the wire bytes.
if (ts.isObjectLiteralExpression(options) && options.properties.length === 1) {
const prop = options.properties[0]!;
if (ts.isPropertyAssignment(prop) &&
(ts.isIdentifier(prop.name) || ts.isStringLiteral(prop.name)) && prop.name.text === "level") {
const value = prop.initializer;
const level = ts.isNumericLiteral(value) ? Number(value.text)
: ts.isPrefixUnaryExpression(value) && value.operator === ts.SyntaxKind.MinusToken && ts.isNumericLiteral(value.operand)
? -Number(value.operand.text) : NaN;
if (Number.isInteger(level) && level >= -1 && level <= 9) {
return {
kind: "libCall", fn: "zlib.deflateLevelSync",
args: [zlibInputBytes(lowerer, expr.arguments[0]!, loc), numLit(bi.member === "deflateSync" ? 0 : bi.member === "deflateRawSync" ? 1 : 2, loc), numLit(level, loc)],
type: BYTES_U8, loc,
};
}
}
}
lowerer.noLowering(`${bi.member} with these options`, options,
"the static compression options form is { level: <integer literal from -1 through 9> }");
}
if (expr.arguments.length !== 1) { if (expr.arguments.length !== 1) {
const site = expr.arguments[1] ?? expr; const site = expr.arguments[1] ?? expr;
lowerer.noLowering( lowerer.noLowering(
@@ -565,16 +565,11 @@ function fenceProducedArrayElem(lowerer: Lowerer, node: ts.Node, producer: strin
}; };
} }
if (name === "pop") { if (name === "pop") {
// Like shift(), pop() returns undefined for an empty array. Union // Widen a union payload through the normal array-read machinery
// element arrays remain fenced here because their existing boxed // before removing it. Reinterpreting the stored union's tag as the
// element already carries the result union and a second undefined // result union would confuse arms when undefined changes their order.
// wrapper would lose the element identity contract.
if (elem.kind === "union" && lowerer.armTag(elem.unionId, UNDEFINED_T) < 0) { if (elem.kind === "union" && lowerer.armTag(elem.unionId, UNDEFINED_T) < 0) {
lowerer.unsupported( return lowerUnionArrayRemoval(lowerer, access.expression, elem, false, loc);
"SC1090",
call,
"'.pop()' on union-element arrays without an undefined arm (read the last element and splice it after narrowing instead)",
);
} }
const receiver = lowerer.lowerExpr(access.expression); const receiver = lowerer.lowerExpr(access.expression);
return { return {
@@ -682,18 +677,9 @@ function fenceProducedArrayElem(lowerer: Lowerer, node: ts.Node, producer: strin
return { kind: "arrIntrinsic", method: "spliceInsert", receiver, args: [...args, items], type: receiverIr, loc }; return { kind: "arrIntrinsic", method: "spliceInsert", receiver, args: [...args, items], type: receiverIr, loc };
} }
if (name === "shift") { if (name === "shift") {
// JS shift exactly: undefined on an empty array, else the first // Like pop, a union without undefined needs an explicit tag widening.
// element with the tail sliding down — the result is the interned
// `elem | undefined` union, the env-read convention. Union-element
// arrays are fenced: their shift result would collapse arms
// (`(string | undefined)[]`'s shift is `string | undefined` too, and
// the box can't say which world the undefined came from).
if (elem.kind === "union" && lowerer.armTag(elem.unionId, UNDEFINED_T) < 0) { if (elem.kind === "union" && lowerer.armTag(elem.unionId, UNDEFINED_T) < 0) {
lowerer.unsupported( return lowerUnionArrayRemoval(lowerer, access.expression, elem, true, loc);
"SC1090",
call,
"'.shift()' on union-element arrays without an undefined arm (read [0] and splice(0, 1) with the narrowed value instead)",
);
} }
const receiver = lowerer.lowerExpr(access.expression); const receiver = lowerer.lowerExpr(access.expression);
return { kind: "arrIntrinsic", method: "shift", receiver, args: [], type: arrayValueType(lowerer, elem), loc }; return { kind: "arrIntrinsic", method: "shift", receiver, args: [], type: arrayValueType(lowerer, elem), loc };
@@ -741,6 +727,45 @@ function fenceProducedArrayElem(lowerer: Lowerer, node: ts.Node, producer: strin
return lowerArrayReduceCall(lowerer, call, access, name as "reduce" | "reduceRight", elem); return lowerArrayReduceCall(lowerer, call, access, name as "reduce" | "reduceRight", elem);
} }
/** Read before splice so removal cannot destroy the result's owned payload.
* The source is evaluated once and empty/sparse arrays yield undefined using
* the same slot-state checks as an indexed read. */
function lowerUnionArrayRemoval(
lowerer: Lowerer,
source: ts.Expression,
elem: IrType,
first: boolean,
loc: SrcLoc,
): IrExpr {
const receiver = lowerer.lowerExpr(source);
const arr = lowerer.declareHiddenLocal("%removeArray", receiver.type);
const arrRef = varRef(arr.id, arr.type, loc);
const index = lowerer.declareHiddenLocal("%removeIndex", F64);
const indexRef = varRef(index.id, F64, loc);
const value = arrayValueRead(lowerer, arrRef, indexRef, elem, loc);
const result = lowerer.declareHiddenLocal("%removedValue", value.type);
return {
kind: "seqExpr",
stmts: [
{ kind: "varDecl", localId: arr.id, init: receiver, loc },
{
kind: "varDecl", localId: index.id,
init: first ? numLit(0, loc) : {
kind: "bin", op: "-",
left: { kind: "arrIntrinsic", method: "length", receiver: arrRef, args: [], type: F64, loc },
right: numLit(1, loc), type: F64, loc,
}, loc,
},
{ kind: "varDecl", localId: result.id, init: value, loc },
{
kind: "exprStmt",
expr: { kind: "arrIntrinsic", method: "splice", receiver: arrRef, args: [indexRef, numLit(1, loc)], type: arr.type, loc }, loc,
},
],
result: varRef(result.id, result.type, loc), type: result.type, loc,
};
}
function literalFlatDepth(value: IrExpr): number | null { function literalFlatDepth(value: IrExpr): number | null {
if (value.kind === "numLit") return value.value; if (value.kind === "numLit") return value.value;
if (value.kind === "strLit") return Number(value.value); if (value.kind === "strLit") return Number(value.value);
@@ -2455,6 +2455,10 @@ function lowerExprInner(lowerer: Lowerer, expr: ts.Expression): IrExpr {
return expr; return expr;
} }
if (expr.type.kind !== "union") return expr; if (expr.type.kind !== "union") return expr;
if (ts.isIdentifier(node)) {
const symbol = lowerer.resolveValueSymbol(node);
if (symbol && lowerer.unstableSwitchBindings.has(symbol)) return expr;
}
const narrowedTs = lowerer.typeOf(node); const narrowedTs = lowerer.typeOf(node);
const narrowed = lowerer.mapTypeOf(narrowedTs); const narrowed = lowerer.mapTypeOf(narrowedTs);
// `Array.isArray(u)` on a union with one readonly array/tuple arm: the // `Array.isArray(u)` on a union with one readonly array/tuple arm: the
@@ -4626,7 +4630,10 @@ export function lowerOptionalNumber(
); );
} }
} }
const index = lowerer.lowerExpr(expr.argumentExpression); // Array reads can themselves supply an index. A missing numeric value
// is undefined, whose property lookup misses just like NaN; keep that
// value instead of rejecting a checker-number index with optional IR.
const index = lowerOptionalNumber(lowerer, lowerer.lowerExpr(expr.argumentExpression), locOf(expr.argumentExpression), expr.argumentExpression);
if (index.type.kind !== "f64") { if (index.type.kind !== "f64") {
lowerer.unsupported("SC1090", expr.argumentExpression, "indexing with non-number keys"); lowerer.unsupported("SC1090", expr.argumentExpression, "indexing with non-number keys");
} }
@@ -29,6 +29,7 @@ import { lowerEnumDeclaration } from "./lower-enums.js";
import { abstractPropertyDeclOf, aliasTypeofNarrows, isMatchSliceType, lowerAbsenceProbe, lowerElementCompound, lowerGroupsProjection, lowerOptionalNumber, matchResultNamedGroupsOf, runtimeOptionalTrueIds, symbolFieldInfo, withRuntimeOptionalNarrowed } from "./lower-exprs.js"; import { abstractPropertyDeclOf, aliasTypeofNarrows, isMatchSliceType, lowerAbsenceProbe, lowerElementCompound, lowerGroupsProjection, lowerOptionalNumber, matchResultNamedGroupsOf, runtimeOptionalTrueIds, symbolFieldInfo, withRuntimeOptionalNarrowed } from "./lower-exprs.js";
import { isSafeToRepeat } from "./expressions/evaluation-safety.js"; import { isSafeToRepeat } from "./expressions/evaluation-safety.js";
import { tryLowerExpression } from "./expressions/try-lower-expression.js"; import { tryLowerExpression } from "./expressions/try-lower-expression.js";
import { lowerUnionFieldWrite } from "./expressions/union-field-write.js";
import { UNSUPPORTED, checkerPanicDiag, isCheckerPanic, requiresDynamicDiag } from "../../diagnostics/diagnostic.js"; import { UNSUPPORTED, checkerPanicDiag, isCheckerPanic, requiresDynamicDiag } from "../../diagnostics/diagnostic.js";
import { isParseArgsDynTypeName, isUnitOnlyTsType, unitOnlyUnion } from "../type-mapper.js"; import { isParseArgsDynTypeName, isUnitOnlyTsType, unitOnlyUnion } from "../type-mapper.js";
import { canonicalBuiltinModule } from "../builtin-modules.js"; import { canonicalBuiltinModule } from "../builtin-modules.js";
@@ -4114,14 +4115,13 @@ export function lowerVarDecl(lowerer: Lowerer, decl: ts.VariableDeclaration, isL
* checked case-test comparability (TS2678) — the kind check below is the * checked case-test comparability (TS2678) — the kind check below is the
* backstop for cases tsc lets through (e.g. `unknown as` casts). */ * backstop for cases tsc lets through (e.g. `unknown as` casts). */
export function lowerSwitch(lowerer: Lowerer, stmt: ts.SwitchStatement): IrStmt { export function lowerSwitch(lowerer: Lowerer, stmt: ts.SwitchStatement): IrStmt {
// Labels consume HERE, before any nested statement can see them; the // Consume labels before a nested statement can claim them. All switch
// union desugar drops them (its if/else chain has no switch-end label // forms keep their own break target, including union comparisons.
// point — labeled jumps naming this switch fence at the jump).
const labels = lowerer.takeLabels(); const labels = lowerer.takeLabels();
const disc = lowerer.lowerExpr(stmt.expression); const disc = lowerer.lowerExpr(stmt.expression);
const dk = disc.type.kind; const dk = disc.type.kind;
if (dk === "dyn") return lowerDynSwitch(lowerer, stmt, disc); if (dk === "dyn") return lowerDynSwitch(lowerer, stmt, disc, labels);
if (dk === "union") return lowerUnionSwitch(lowerer, stmt, disc); if (dk === "union") return lowerUnionSwitch(lowerer, stmt, disc, labels);
if (dk !== "f64" && dk !== "string" && dk !== "bool") { if (dk !== "f64" && dk !== "string" && dk !== "bool") {
lowerer.unsupported("SC1090", stmt.expression, "switch on non-primitive values"); lowerer.unsupported("SC1090", stmt.expression, "switch on non-primitive values");
} }
@@ -4150,28 +4150,10 @@ export function lowerVarDecl(lowerer: Lowerer, decl: ts.VariableDeclaration, isL
} }
} }
/** Switch on a UNION-typed discriminant (`switch (m.type)` over /** Compare a union discriminant through a boolean switch. The native switch
* `string | undefined`): desugared to an if/else-if chain of per-union * already implements lazy case tests, shared scope, fallthrough and jumps;
* strict-equality tests (unionEq — the case value wraps into the union, * keeping that control-flow node avoids rebinding an early break to a loop. */
* exactly `disc === test`), because the backend switch compares plain function lowerUnionSwitch(lowerer: Lowerer, stmt: ts.SwitchStatement, disc: IrExpr, labels: string[] | undefined): IrStmt {
* primitives only. The desugar is JS-exact for the shape it accepts and
* fences everything it cannot reproduce:
* - the discriminant evaluates exactly once; effectful values stabilize
* in a hidden local that every generated equality test reuses;
* - tests evaluate lazily in source order (the chain's short-circuit IS
* the switch's test order — grouped empty cases `case a: case b:` OR
* their tests, still in order);
* - each non-final clause must EXIT (a trailing unconditional break —
* dropped, it's the chain's own exit — or a return/throw/continue):
* real fall-through between bodies has no if/else shape;
* - any OTHER unlabeled break binding to this switch (a conditional
* early break) is fenced — desugared, it would bind to an enclosing
* loop instead;
* - `default` may sit anywhere in source (JS tests every case first;
* the chain's final else reproduces that as long as its body exits or
* is last).
* Case bodies share ONE lexical scope, exactly like the real switch. */
function lowerUnionSwitch(lowerer: Lowerer, stmt: ts.SwitchStatement, disc: IrExpr): IrStmt {
if (disc.type.kind !== "union") throw new InternalCompilerError("lowerer bug: non-union disc"); if (disc.type.kind !== "union") throw new InternalCompilerError("lowerer bug: non-union disc");
const unionType = disc.type; const unionType = disc.type;
if (!lowerer.eqComparableUnion(unionType.unionId)) { if (!lowerer.eqComparableUnion(unionType.unionId)) {
@@ -4181,7 +4163,7 @@ export function lowerVarDecl(lowerer: Lowerer, decl: ts.VariableDeclaration, isL
"switch on a union whose arms do not have static equality", "switch on a union whose arms do not have static equality",
); );
} }
return lowerBranchSwitch(lowerer, stmt, disc, (stableDisc, test, expression) => { return lowerBranchSwitch(lowerer, stmt, disc, labels, (stableDisc, test, expression) => {
const unitTest = const unitTest =
test.kind === "unitLit" test.kind === "unitLit"
? lowerer.lowerUnitComparison(stableDisc, test, false, locOf(expression)) ? lowerer.lowerUnitComparison(stableDisc, test, false, locOf(expression))
@@ -4202,8 +4184,8 @@ export function lowerVarDecl(lowerer: Lowerer, decl: ts.VariableDeclaration, isL
/** A checked-dynamic switch whose tests have native strict-equality /** A checked-dynamic switch whose tests have native strict-equality
* answers. Scalar tests use dynScalarEq; null/undefined use dyn kind tests. * answers. Scalar tests use dynScalarEq; null/undefined use dyn kind tests.
* Other case values retain the existing unknown-switch fence. */ * Other case values retain the existing unknown-switch fence. */
function lowerDynSwitch(lowerer: Lowerer, stmt: ts.SwitchStatement, disc: IrExpr): IrStmt { function lowerDynSwitch(lowerer: Lowerer, stmt: ts.SwitchStatement, disc: IrExpr, labels: string[] | undefined): IrStmt {
return lowerBranchSwitch(lowerer, stmt, disc, (stableDisc, test, expression) => { return lowerBranchSwitch(lowerer, stmt, disc, labels, (stableDisc, test, expression) => {
if (test.kind === "unitLit") { if (test.kind === "unitLit") {
return { kind: "dynTest", test: test.unit, value: stableDisc, type: BOOL, loc: locOf(expression) }; return { kind: "dynTest", test: test.unit, value: stableDisc, type: BOOL, loc: locOf(expression) };
} }
@@ -4225,137 +4207,47 @@ function lowerBranchSwitch(
lowerer: Lowerer, lowerer: Lowerer,
stmt: ts.SwitchStatement, stmt: ts.SwitchStatement,
disc: IrExpr, disc: IrExpr,
labels: string[] | undefined,
compare: (disc: IrExpr, test: IrExpr, expression: ts.Expression) => IrExpr, compare: (disc: IrExpr, test: IrExpr, expression: ts.Expression) => IrExpr,
): IrStmt { ): IrStmt {
const loc = locOf(stmt); const loc = locOf(stmt);
const prefix: IrStmt[] = []; // Even a plain variable must be snapshotted: a case expression can write
let stableDisc = disc; // it before the next comparison, without changing the switch value.
if (!isSafeToRepeat(disc)) { const temp = lowerer.declareHiddenLocal("%switch", disc.type);
const temp = lowerer.declareHiddenLocal("%switch", disc.type); const stableDisc = varRef(temp.id, disc.type, loc);
prefix.push({ kind: "varDecl", localId: temp.id, init: disc, loc }); // Calls/assignments in a case test may mutate a captured discriminant
stableDisc = { kind: "varRef", localId: temp.id, type: disc.type, loc }; // binding. The checker still narrows that binding as if it were the
} // snapshot. Keep the original tagged value in those case bodies.
const clauses = stmt.caseBlock.clauses; const effectful = stmt.caseBlock.clauses.some((clause) => ts.isCaseClause(clause) &&
// An unlabeled break at a clause's END exits the switch — the chain's !ts.isStringLiteral(clause.expression) && !ts.isNumericLiteral(clause.expression) &&
// own exit; anywhere else (conditional early breaks) the desugar would !ts.isIdentifier(clause.expression) && clause.expression.kind !== ts.SyntaxKind.NullKeyword &&
// rebind it to an enclosing loop. Walk each clause's statements without clause.expression.kind !== ts.SyntaxKind.TrueKeyword && clause.expression.kind !== ts.SyntaxKind.FalseKeyword);
// descending into nested breakable constructs or functions (their let binding = stmt.expression;
// breaks are their own). while (ts.isParenthesizedExpression(binding)) binding = binding.expression;
const findStrayBreak = (node: ts.Node): ts.Node | null => { const symbol = effectful && ts.isIdentifier(binding) ? lowerer.resolveValueSymbol(binding) : undefined;
if (ts.isBreakStatement(node)) return node; const alreadyUnstable = symbol != null && lowerer.unstableSwitchBindings.has(symbol);
if ( if (symbol) lowerer.unstableSwitchBindings.add(symbol);
ts.isForStatement(node) || ts.isForOfStatement(node) || ts.isForInStatement(node) || lowerer.scopes.push(new Map());
ts.isWhileStatement(node) || ts.isDoStatement(node) || ts.isSwitchStatement(node) || try {
ts.isFunctionLike(node) const cases = stmt.caseBlock.clauses.map((clause) => ({
) { test: ts.isCaseClause(clause)
return null; ? compare(stableDisc, lowerer.lowerExpr(clause.expression), clause.expression)
} : null,
return ts.forEachChild(node, findStrayBreak) ?? null; body: lowerer.inCtl("switch", () => lowerer.lowerStmts(clause.statements), labels),
}));
return {
kind: "block",
body: [
{ kind: "varDecl", localId: temp.id, init: disc, loc },
{ kind: "switch", disc: { kind: "boolLit", value: true, type: BOOL, loc }, cases, ...(labels && { labels }), loc },
],
loc,
}; };
for (const clause of clauses) { } finally {
const last = clause.statements[clause.statements.length - 1]; lowerer.scopes.pop();
for (const s of clause.statements) { if (symbol && !alreadyUnstable) lowerer.unstableSwitchBindings.delete(symbol);
const stray = s === last && ts.isBreakStatement(s) && !s.label ? null : findStrayBreak(s);
if (stray) {
lowerer.unsupported(
"SC1090",
stray,
"early 'break' inside a union-typed switch (only a trailing break exits the desugared chain — restructure with if/else)",
);
}
}
}
const statementExits = (last: ts.Statement | undefined): boolean => {
if (!last) return false;
if (
(ts.isBreakStatement(last) && !last.label) ||
ts.isReturnStatement(last) ||
ts.isThrowStatement(last) ||
ts.isContinueStatement(last)
) return true;
return ts.isBlock(last) && statementExits(last.statements[last.statements.length - 1]);
};
const exits = (clause: ts.CaseOrDefaultClause): boolean => {
const last = clause.statements[clause.statements.length - 1];
return statementExits(last);
};
// The whole case-body sequence is ONE lexical scope, like the real
// switch lowering.
lowerer.scopes.push(new Map());
try {
// Group clauses: consecutive test-only cases (empty statements) share
// the next body, exactly JS's grouped-case idiom.
const groups: { tests: IrExpr[]; body: IrStmt[]; isDefault: boolean }[] = [];
let pendingTests: IrExpr[] = [];
for (let i = 0; i < clauses.length; i++) {
const clause = clauses[i]!;
if (ts.isCaseClause(clause)) {
const test = lowerer.lowerExpr(clause.expression);
// Case tests must be side-effect-free (literals or plain reads):
// the chain evaluates exactly the tests JS would EXCEPT those of
// a default-sharing group (dropped — the shared body is the
// final else, so matching them changes nothing when pure).
if (
test.kind !== "strLit" && test.kind !== "numLit" &&
test.kind !== "boolLit" && test.kind !== "unitLit" &&
!isSafeToRepeat(test)
) {
lowerer.unsupported(
"SC1090",
clause.expression,
"effectful case tests in a union-typed switch (bind the test value to a const first)",
);
}
// A unit-literal test takes the unit-comparison lowering: a tag
// test when the arm exists, the constant FALSE when the union
// lacks it (`case null:` on a `number | undefined` — legal TS,
// never matches; coercing the literal into the union would hit
// the stranded-arm trap and throw where JS just skips the case).
pendingTests.push(compare(stableDisc, test, clause.expression));
}
const isDefault = ts.isDefaultClause(clause);
if (clause.statements.length === 0 && !isDefault && i < clauses.length - 1) {
continue; // grouped with the next clause
}
if (!exits(clause) && i < clauses.length - 1 && (clause.statements.length > 0 || isDefault)) {
// A non-final body that doesn't exit falls into the NEXT body in
// JS — no if/else shape reproduces that (an EMPTY non-final
// default falls through too; empty non-final cases just group).
lowerer.unsupported(
"SC1090",
clause,
"fall-through between case bodies in a union-typed switch (end each case with break/return/throw/continue)",
);
}
const last = clause.statements[clause.statements.length - 1];
const stmts = last && ts.isBreakStatement(last)
? clause.statements.slice(0, -1)
: clause.statements.slice();
const body = lowerer.lowerStmts(stmts);
groups.push({ tests: pendingTests, body, isDefault });
pendingTests = [];
}
// A default clause anywhere lands in the chain's final else; JS
// reaches it only after every case test fails, which the chain
// reproduces because default bodies that don't exit were fenced
// above (unless last in source, where falling out is falling out).
const defaultBody = groups.find((g) => g.isDefault)?.body ?? null;
const caseGroups = groups.filter((g) => !g.isDefault);
let chain: IrStmt[] = defaultBody ?? [];
for (let i = caseGroups.length - 1; i >= 0; i--) {
const g = caseGroups[i]!;
let cond = g.tests[0];
if (!cond) continue; // a default-adjacent group with no tests (defensive)
for (const t of g.tests.slice(1)) {
cond = { kind: "logical", op: "||", left: cond, right: t, type: BOOL, loc };
}
chain = [{ kind: "if", cond, then: g.body, else_: chain.length > 0 ? chain : null, loc }];
}
return { kind: "block", body: [...prefix, ...chain], loc };
} finally {
lowerer.scopes.pop();
}
} }
}
/** try/catch/finally: /** try/catch/finally:
* - `catch { }` (bindingless) discards the thrown value on entry. * - `catch { }` (bindingless) discards the thrown value on entry.
@@ -5230,6 +5122,8 @@ function lowerBranchSwitch(
const viaStream = lowerStreamUnderscoreAssign(lowerer, expr); const viaStream = lowerStreamUnderscoreAssign(lowerer, expr);
if (viaStream) return viaStream; if (viaStream) return viaStream;
} }
const unionWrite = lowerUnionFieldWrite(lowerer, expr.left, expr.right);
if (unionWrite) return unionWrite;
const target = lowerer.fieldTarget(expr.left); const target = lowerer.fieldTarget(expr.left);
if (target) { if (target) {
const value = lowerer.lowerExprExpecting(expr.right, target.fieldType); const value = lowerer.lowerExprExpecting(expr.right, target.fieldType);
@@ -1275,6 +1275,10 @@ export class Lowerer {
* read can be absent at runtime. Bare reads preserve that union until a * read can be absent at runtime. Bare reads preserve that union until a
* surrounding JavaScript guard/default consumes it. */ * surrounding JavaScript guard/default consumes it. */
readonly runtimeOptionalLocals = new Set<IrLocal>(); readonly runtimeOptionalLocals = new Set<IrLocal>();
/** An effectful switch test can change the original binding after the
* discriminant was captured. A case match narrows the captured value,
* not that mutable binding; preserve its tagged representation on reads. */
readonly unstableSwitchBindings = new Set<ts.Symbol>();
/** All storage slots widened for runtime absence, including slots whose /** All storage slots widened for runtime absence, including slots whose
* current control-flow branch has temporarily narrowed the value. */ * current control-flow branch has temporarily narrowed the value. */
readonly runtimeOptionalStorageLocals = new Set<IrLocal>(); readonly runtimeOptionalStorageLocals = new Set<IrLocal>();
+3 -4
View File
@@ -49,10 +49,10 @@ export function matchStringSelfConcat(targetLocalId: string, value: IrExpr): IrE
return value.right; return value.right;
} }
/** The class-graph surface needed by backend-independent hierarchy queries. */ /** Descendant tree needed by constructor queries. Parent links deliberately
* stay out of this structural view: projecting them would revisit cycles. */
export interface IrClassGraphNode { export interface IrClassGraphNode {
readonly def: { readonly name: string }; readonly def: { readonly name: string };
readonly base: IrClassGraphNode | null;
readonly children: readonly IrClassGraphNode[]; readonly children: readonly IrClassGraphNode[];
} }
@@ -158,10 +158,9 @@ export function endsWithJump(stmts: readonly IrStmt[]): boolean {
* the static class's descendant subtree. */ * the static class's descendant subtree. */
export function newValueMayThrow( export function newValueMayThrow(
className: string, className: string,
classes: ReadonlyMap<string, IrClassGraphNode>, meta: IrClassGraphNode | undefined,
mayThrow: ReadonlySet<string>, mayThrow: ReadonlySet<string>,
): boolean { ): boolean {
const meta = classes.get(className);
if (!meta) throw new InternalCompilerError(`IR analysis bug: newValue on unknown class ${className}`); if (!meta) throw new InternalCompilerError(`IR analysis bug: newValue on unknown class ${className}`);
const any = (node: IrClassGraphNode): boolean => const any = (node: IrClassGraphNode): boolean =>
mayThrow.has(`%${node.def.name}.constructor`) || node.children.some(any); mayThrow.has(`%${node.def.name}.constructor`) || node.children.some(any);
+9 -4
View File
@@ -3416,11 +3416,14 @@ export type IrLibFn =
/** node:zlib (scr_zlib.c — native-toolchain.ts compiles/links it ONLY when these /** node:zlib (scr_zlib.c — native-toolchain.ts compiles/links it ONLY when these
* appear on the IR, the regex/libcurl gating precedent): one-shot zlib, * appear on the IR, the regex/libcurl gating precedent): one-shot zlib,
* raw-DEFLATE, gzip, and auto-detect codecs over u8 bytes with Node's * raw-DEFLATE, gzip, and auto-detect codecs over u8 bytes with Node's
* default options. Compression never throws (OOM aborts); decompression * default options, or a validated literal compression level from -1 to 9.
* Compression never throws (OOM aborts); decompression
* of corrupt input THROWS Node's error catchably. */ * of corrupt input THROWS Node's error catchably. */
| "zlib.deflateSync" | "zlib.deflateSync"
| "zlib.inflateSync" | "zlib.inflateSync"
| "zlib.deflateRawSync" | "zlib.deflateRawSync"
/** bytes, mode (0 zlib / 1 raw / 2 gzip), validated compression level. */
| "zlib.deflateLevelSync"
| "zlib.inflateRawSync" | "zlib.inflateRawSync"
| "zlib.gzipSync" | "zlib.gzipSync"
| "zlib.gunzipSync" | "zlib.gunzipSync"
@@ -5332,9 +5335,11 @@ export type IrExpr =
* forms whose statement lowering needs temps and writes (destructuring * forms whose statement lowering needs temps and writes (destructuring
* assignments in value position, keyed dyn writes yielding the RHS). * assignments in value position, keyed dyn writes yielding the RHS).
* `type` IS result's type. Restricted on purpose: stmts must be * `type` IS result's type. Restricted on purpose: stmts must be
* straight-line (varDecl/assign/exprStmt/field-and-record writes — no * local statements (including state-selection blocks/ifs, but no jumps;
* control flow, no jumps; the validator enforces the subset), and any * the validator enforces the subset). Hidden locals retain function-wide
* varDecl-introduced local is a function local like every hidden temp. */ * ids, but their owned values live through the enclosing expression's
* frame: later call arguments may reuse a saved operand. Release them
* on the same path that initialized them, not an outer lexical scope. */
| { kind: "seqExpr"; stmts: IrStmt[]; result: IrExpr; type: IrType; loc: SrcLoc } | { kind: "seqExpr"; stmts: IrStmt[]; result: IrExpr; type: IrType; loc: SrcLoc }
/** RequireObjectCoercible with V8's destructuring TypeError: throws /** RequireObjectCoercible with V8's destructuring TypeError: throws
* "Cannot destructure 'SPELLING' as it is undefined." (or "…null.") on * "Cannot destructure 'SPELLING' as it is undefined." (or "…null.") on
+1
View File
@@ -974,6 +974,7 @@ export const LIB_FN_SIGS: Record<IrLibFn, { argTypes: (IrType | null)[]; result:
"zlib.deflateSync": { argTypes: [BYTES_U8], result: BYTES_U8 }, "zlib.deflateSync": { argTypes: [BYTES_U8], result: BYTES_U8 },
"zlib.inflateSync": { argTypes: [BYTES_U8], result: BYTES_U8 }, "zlib.inflateSync": { argTypes: [BYTES_U8], result: BYTES_U8 },
"zlib.deflateRawSync": { argTypes: [BYTES_U8], result: BYTES_U8 }, "zlib.deflateRawSync": { argTypes: [BYTES_U8], result: BYTES_U8 },
"zlib.deflateLevelSync": { argTypes: [BYTES_U8, F64, F64], result: BYTES_U8 },
"zlib.inflateRawSync": { argTypes: [BYTES_U8], result: BYTES_U8 }, "zlib.inflateRawSync": { argTypes: [BYTES_U8], result: BYTES_U8 },
"zlib.gzipSync": { argTypes: [BYTES_U8], result: BYTES_U8 }, "zlib.gzipSync": { argTypes: [BYTES_U8], result: BYTES_U8 },
"zlib.gunzipSync": { argTypes: [BYTES_U8], result: BYTES_U8 }, "zlib.gunzipSync": { argTypes: [BYTES_U8], result: BYTES_U8 },
@@ -0,0 +1,77 @@
import { mkdtempSync, rmSync, writeFileSync } from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { expect, test } from "vitest";
import { analyze } from "../src/index.js";
function diagnostics(source: string) {
const dir = mkdtempSync(join(process.platform === "win32" ? tmpdir() : "/tmp", "scriptc-native-lowering-"));
try {
const entry = join(dir, "main.ts");
writeFileSync(entry, source);
const { coverage } = analyze(entry, { dynamic: false });
expect(coverage.preflightFailed).toBe(false);
return coverage.diagnostics;
} finally {
rmSync(dir, { recursive: true, force: true });
}
}
for (const options of [
"{ level: 10 }",
"{ level: -2 }",
"{ level: 1.5 }",
"{ level: Number.NaN }",
"{ level }",
"{ level: 9, strategy: 1 }",
"{ level: 9, windowBits: 15 }",
"{ ...{ level: 9 } }",
"{ ['level']: 9 }",
]) {
test(`static zlib refuses unmodeled options ${options}`, () => {
const diags = diagnostics(`
import { deflateRawSync } from "node:zlib";
const level = 9;
console.log(deflateRawSync("payload", ${options}).length);
`);
expect(diags).toHaveLength(1);
expect(diags[0]!.code).toBe("SC2020");
expect(diags[0]!.message).toContain("deflateRawSync with these options");
expect(diags[0]!.hint).toContain("integer literal from -1 through 9");
});
}
test("compression-level support does not silently accept decompression options", () => {
const diags = diagnostics(`
import { inflateRawSync } from "node:zlib";
console.log(inflateRawSync(Buffer.from([3, 0]), { windowBits: 15 }).length);
`);
expect(diags).toHaveLength(1);
expect(diags[0]!.code).toBe("SC2020");
expect(diags[0]!.message).toContain("inflateRawSync with explicit options");
});
test("union writes keep the refusal for incompatible field storage", () => {
const diags = diagnostics(`
type Value = { kind: "text"; value: string } | { kind: "number"; value: number };
function write(value: Value): void { value.value = "changed"; }
write({ kind: "number", value: 1 });
`);
expect(diags).toHaveLength(1);
expect(diags[0]!.code).toBe("SC1090");
expect(diags[0]!.message).toContain("assignment to non-variables");
});
test("ordinary scalar union switches stay static", () => {
const diags = diagnostics(`
function choose(value: string | number): void {
switch (value) {
case "text": break;
case 1: break;
}
}
choose("text");
`);
// Ordinary scalar unions remain entirely static after control-flow changes.
expect(diags).toEqual([]);
});
@@ -7553,6 +7553,48 @@
], ],
"diags": [] "diags": []
}, },
"<repo>/tests/corpus/3112-union-switch-control-flow.ts": {
"order": [
"<repo>/tests/corpus/3112-union-switch-control-flow.ts"
],
"diags": []
},
"<repo>/tests/corpus/3113-union-array-removal.ts": {
"order": [
"<repo>/tests/corpus/3113-union-array-removal.ts"
],
"diags": []
},
"<repo>/tests/corpus/3114-union-record-field-write.ts": {
"order": [
"<repo>/tests/corpus/3114-union-record-field-write.ts"
],
"diags": []
},
"<repo>/tests/corpus/3115-zlib-static-compression-levels.ts": {
"order": [
"<repo>/tests/corpus/3115-zlib-static-compression-levels.ts"
],
"diags": []
},
"<repo>/tests/corpus/3116-sequence-expression-lifetimes.ts": {
"order": [
"<repo>/tests/corpus/3116-sequence-expression-lifetimes.ts"
],
"diags": []
},
"<repo>/tests/corpus/3117-unknown-switch-control-flow.ts": {
"order": [
"<repo>/tests/corpus/3117-unknown-switch-control-flow.ts"
],
"diags": []
},
"<repo>/tests/corpus/3118-recursive-union-spread.ts": {
"order": [
"<repo>/tests/corpus/3118-recursive-union-spread.ts"
],
"diags": []
},
"<repo>/tests/corpus/400-fib.ts": { "<repo>/tests/corpus/400-fib.ts": {
"order": [ "order": [
"<repo>/tests/corpus/400-fib.ts" "<repo>/tests/corpus/400-fib.ts"
@@ -0,0 +1,97 @@
// Union switches preserve case-test order, early exits and real fallthrough.
let trace: string[] = [];
function test(value: string | number | undefined): string | number | undefined {
trace.push(`test:${value}`);
return value;
}
function select(value: string | number | undefined): string {
let out = "";
selection: switch (value) {
case test("a"):
out += "a";
if (trace.length > 0) break selection;
out += "unreachable";
break;
default:
out += "default";
case test(2):
out += ":two";
case test(undefined):
out += ":end";
break;
case test("last"):
out = "last";
}
return out;
}
for (const value of ["a", "missing", 2, undefined, "last"] as (string | number | undefined)[]) {
trace = [];
console.log(select(value), trace.join(","));
}
// A test may change the binding, but the discriminant keeps its old value.
function snapshot(initial: string | number): string {
let value = initial;
function mutate(): string | number {
trace.push("mutate");
value = 42;
return "no";
}
switch ((value)) {
case mutate(): return "wrong";
case "yes": return `yes:${value}`;
case 42: return "forty-two";
default: return "other";
}
}
console.log(snapshot("yes"), snapshot(42));
// An unlabeled break belongs to the switch; continue belongs to the loop.
const values: (string | number)[] = ["skip", 2, "stop", 9];
let total = 0;
outer: for (let i = 0; i < values.length; i++) {
const value = values[i]!;
switch (value) {
case "skip": continue;
case "stop":
try { break outer; } finally { console.log("finally-stop", total); }
case 2:
for (let j = 0; j < 2; j++) {
if (j === 1) break;
total += j + 1;
}
if (total > 0) break;
total += 100;
break;
default: total += 1000;
}
total += 10;
}
console.log("total", total);
// Matching an empty case before default skips all later case tests.
function grouped(value: string | number | undefined): string {
switch (value) {
case test(1):
default:
return "group";
case test(2):
return "two";
}
}
trace = [];
console.log(grouped(1), trace.join(","));
trace = [];
console.log(grouped(2), trace.join(","));
// A thrown case test prevents default and every later test/body.
function explode(): string { throw new Error("case-test"); }
try {
const value: string | number = snapshot("yes");
switch (value) {
default: console.log("wrong-default"); break;
case explode(): console.log("wrong-case"); break;
}
} catch (error) {
if (error instanceof Error) console.log(error.message);
}
+59
View File
@@ -0,0 +1,59 @@
// Removal widens union tags while preserving the identities of payloads.
interface Left { kind: "left"; label: string; count: number }
interface Right { kind: "right"; label: string; enabled: boolean }
type Item = Left | Right;
const left: Left = { kind: "left", label: "left", count: 1 };
const right: Right = { kind: "right", label: "right", enabled: true };
const items: Item[] = [left, right, left];
const identity = new Map<Item, string>();
identity.set(left, "original-left");
identity.set(right, "original-right");
let calls = 0;
function source(): Item[] { calls++; return items; }
const last = source().pop();
if (last !== undefined) {
console.log(identity.get(last), last.kind, items.length, calls);
if (last.kind === "left") last.count = 42;
}
console.log(left.count);
const first = source().shift();
if (first !== undefined) console.log(identity.get(first), first.label, items.length, calls);
const final = items.shift();
if (final !== undefined) console.log(identity.get(final), final.label, items.length);
console.log(items.pop() === undefined, items.shift() === undefined);
// Payload kinds can differ, and the insertion of undefined can reorder tags.
const scalars: (string | number | boolean)[] = ["text", 0, false, 9, "last"];
for (let i = 0; i < 7; i++) {
const value = i % 2 === 0 ? scalars.pop() : scalars.shift();
console.log(typeof value, value, scalars.length);
}
// Holes are missing values, yet the removal still shrinks the receiver.
const sparse: (string | number)[] = ["start", 2];
sparse.length = 5;
console.log(sparse.pop(), sparse.length);
console.log(sparse.shift(), sparse.length);
console.log(sparse.pop(), sparse.length);
console.log(sparse.shift(), sparse.length);
console.log(sparse.shift(), sparse.length);
console.log(sparse.shift(), sparse.length);
// A removed refcounted payload survives after the array drops its ownership.
function fresh(): Item[] {
return [{ kind: "left", label: "fresh", count: 7 }, { kind: "right", label: "fresh-right", enabled: false }];
}
const popped = fresh().pop();
const shifted = fresh().shift();
if (popped !== undefined) console.log(popped.label);
if (shifted !== undefined) console.log(shifted.label);
// Discarded results use the same mutation semantics and release ownership.
for (let i = 0; i < 40; i++) {
const temporary = fresh();
temporary.pop();
temporary.shift();
temporary.pop();
if (temporary.length !== 0) throw new Error("not empty");
}
console.log("removed");
@@ -0,0 +1,58 @@
// A common writable field is updated on the original union arm.
interface Branch { kind: "branch"; value: string; count: number; children: string[] }
interface Leaf { kind: "leaf"; value: string; count: number; enabled: boolean }
type Entry = Branch | Leaf;
const branch: Branch = { kind: "branch", value: "branch", count: 1, children: ["child"] };
const leaf: Leaf = { kind: "leaf", value: "leaf", count: 2, enabled: true };
const index = new Map<Entry, string>();
index.set(branch, "branch-id");
index.set(leaf, "leaf-id");
function update(entry: Entry, value: string, count: number): void {
entry.value = value;
entry.count = count;
console.log(index.get(entry), entry.value, entry.count);
}
update(branch, "updated-branch", 10);
update(leaf, "updated-leaf", 20);
console.log(branch.value, branch.count, leaf.value, leaf.count);
// Receiver evaluation precedes the RHS and is not repeated per union arm.
let trace: string[] = [];
let current: Entry = branch;
function receiver(): Entry { trace.push("receiver"); return current; }
function replacement(): string { trace.push("rhs"); current = leaf; return "snapshot"; }
receiver().value = replacement();
console.log(trace.join(","), branch.value, leaf.value);
// An exception in the RHS leaves the selected record unchanged.
function fail(): string { trace.push("throw"); throw new Error("write-failed"); }
try { receiver().value = fail(); } catch (error) {
if (error instanceof Error) console.log(error.message, leaf.value);
}
console.log(trace.join(","));
// Nested common fields retain their objects; no structural copy is introduced.
interface TextNode { kind: "text"; next: Entry; text: string }
interface NumberNode { kind: "number"; next: Entry; number: number }
type Node = TextNode | NumberNode;
const text: TextNode = { kind: "text", next: branch, text: "t" };
const number: NumberNode = { kind: "number", next: leaf, number: 9 };
function replace(node: Node, next: Entry): void { node.next = next; }
replace(text, leaf);
replace(number, branch);
console.log(index.get(text.next), index.get(number.next));
// An array-read union narrowed by an undefined guard still mutates its arm.
const entries: Entry[] = [branch, leaf];
for (const entry of entries) {
if (entry !== undefined) entry.value = "array-write";
}
console.log(branch.value, leaf.value);
// A missing unchecked array read retains its undefined arm until the write.
// JavaScript evaluates the RHS before PutValue raises the property error.
let missingEffects = 0;
function missingValue(): string { missingEffects++; return "unwritten"; }
try { entries[99].value = missingValue(); } catch (error) {
if (error instanceof Error) console.log(error.name, error.message, missingEffects);
}
@@ -0,0 +1,33 @@
import { deflateSync, deflateRawSync, gzipSync, inflateSync, inflateRawSync, gunzipSync } from "node:zlib";
const text = "native emitter π\u0000".repeat(400);
const data = Buffer.from(text, "utf8");
const raw = [
deflateRawSync(data, { level: -1 }),
deflateRawSync(data, { level: 0 }),
deflateRawSync(data, { level: 1 }),
deflateRawSync(data, { level: 2 }),
deflateRawSync(data, { level: 3 }),
deflateRawSync(data, { level: 4 }),
deflateRawSync(data, { level: 5 }),
deflateRawSync(data, { level: 6 }),
deflateRawSync(data, { level: 7 }),
deflateRawSync(data, { level: 8 }),
deflateRawSync(data, { level: 9 }),
];
for (let i = 0; i < raw.length; i++) {
const bytes = raw[i]!;
console.log(inflateRawSync(bytes).equals(data));
}
// Encoded bytes vary by zlib version. Pin level zero's uncompressed size
// and level nine's compression without requiring a particular bitstream.
console.log(raw[1]!.length > data.length, raw[10]!.length < data.length / 10);
const zlib = deflateSync(data, { level: 9 });
console.log(inflateSync(zlib).equals(data));
const gzip = gzipSync(data, { level: 9 });
console.log(gunzipSync(gzip).equals(data));
console.log(inflateRawSync(deflateRawSync("", { level: 9 })).length);
console.log(inflateSync(deflateSync("hello", { level: 0 })).toString());
console.log(gunzipSync(gzipSync("hello", { level: 1 })).toString());
let calls = 0;
function input(): Buffer { calls++; return data; }
console.log(inflateRawSync(deflateRawSync(input(), { level: 9 })).equals(data), calls);
@@ -0,0 +1,95 @@
// Hidden locals in lazy expressions must be released by the executed branch.
// Reusing a lexical scope across loop iterations must not revisit old temps.
interface Box { text: string; values: number[] }
function box(text: string, n: number): Box { return { text, values: [n] }; }
const boxes: Box[] = [box("a", 1), box("b", 2), box("c", 3)];
const numbers = [1, 2, 3];
let total = 0;
for (let i = 0; i < 100; i++) {
const result = i % 2 === 0
? (numbers[0] += 1)
: (numbers[1] += 2);
total += result;
const selected = i % 2 === 0 ? boxes[0] : boxes[1];
if (selected !== undefined) selected.values.push(result);
}
console.log("compound", total, numbers.join(","), boxes[0]!.values.length, boxes[1]!.values.length);
// Chained destructuring expressions have independent source temporaries.
let first = "";
let second = "";
function strings(n: number): string[] { return [`a${n}`, `b${n}`]; }
for (let i = 0; i < 20; i++) {
const value = i % 2 === 0 ? ([first] = strings(i)) : ([second] = strings(i));
if (i > 17) console.log("destructure", value.join(","), first, second);
}
// Locals created by one lazy arm are not initialized in the other arm.
let label = "start";
for (let i = 0; i < 20; i++) {
const value = i % 3 === 0 ? ({ text: label } = box(`x${i}`, i)) : box(`y${i}`, i);
if (i > 16) console.log("record", value.text, label);
}
// A returned closure retains its capture after the sequence scope closes.
function callback(n: number): () => string {
const text = `callback:${n}`;
return () => text;
}
let fn = callback(-1);
const callbacks = [callback(0)];
for (let i = 0; i < 16; i++) {
const result = i % 2 === 0 ? ([fn] = [callback(i)]) : ([fn] = [callback(i + 100)]);
callbacks.push(result[0]!);
}
console.log("closure", fn(), callbacks[1]!(), callbacks[16]!());
// Abrupt completion releases the current sequence's initialized locals.
let effects = 0;
function failure(): number { effects++; throw new Error("rhs"); }
for (let i = 0; i < 12; i++) {
try {
const result = i % 2 === 0 ? (numbers[0] += failure()) : (numbers[1] += 1);
if (i === 11) console.log("success", result);
} catch (error) {
if (error instanceof Error && i === 10) console.log("caught", error.message);
}
}
console.log("effects", effects);
// Numeric array values remain valid index expressions; missing indices miss.
const indices = [0, 1, 8];
const words = ["zero", "one"];
console.log(indices.map((index) => words[index] ?? "missing").join(","));
for (const index of indices) console.log(words[index!] ?? "missing");
console.log(words[indices[99]] ?? "missing");
// A tuple spread saves its receiver in the first argument's sequence and
// reuses it in later arguments. Both payloads must survive the whole call.
function pair(n: number): [Box, () => string] {
return [box(`pair:${n}`, n), callback(n)];
}
function consume(value: Box, read: () => string): string {
return `${value.text}/${read()}`;
}
for (let i = 0; i < 20; i++) {
const result = i % 2 === 0 ? consume(...pair(i)) : consume(...pair(i + 100));
if (i > 16) console.log("shared-call-temporaries", result);
}
// A later argument can throw after the first has saved owned operands.
function throwingPair(n: number): [Box, () => string] {
if (n % 2 === 0) throw new Error("tuple-argument");
return pair(n);
}
function consumeBoth(first: Box, a: () => string, second: Box, b: () => string): string {
return `${consume(first, a)}/${consume(second, b)}`;
}
for (let i = 0; i < 10; i++) {
try {
const result = consumeBoth(...pair(i), ...throwingPair(i));
if (i === 9) console.log("shared-call-success", result);
} catch (error) {
if (error instanceof Error && i === 8) console.log("shared-call-throw", error.message);
}
}
@@ -0,0 +1,73 @@
// Checked-dynamic switches use strict equality and ordinary switch jumps.
let trace: string[] = [];
function probe(value: unknown): unknown {
trace.push(`test:${typeof value}`);
return value;
}
function describe(value: unknown): string {
let result = "";
choose: switch (value) {
case probe("one"):
result += "string";
if (result.length > 0) break choose;
result += "wrong";
break;
default:
result += "default";
case probe(1):
result += ":number";
case probe(true):
result += ":boolean";
break;
case undefined:
result = "undefined";
break;
case null:
result = "null";
}
return result;
}
const values: unknown[] = ["one", 1, true, false, "1", undefined, null, 2];
for (const value of values) {
trace = [];
console.log(describe(value), trace.join(","));
}
let count = 0;
loop: for (const value of values) {
switch (value) {
case "one": continue;
case 1:
try { count++; break; } finally { count += 10; }
case true:
for (let i = 0; i < 3; i++) { if (i === 1) break; count++; }
break;
case false: break loop;
default: count += 100;
}
}
console.log("count", count);
// Strict equality never coerces a string case into the numeric discriminant.
function equality(value: unknown): string {
switch (value) {
case "1": return "string";
case 1: return "number";
case true: return "boolean";
default: return "other";
}
}
console.log(equality("1"), equality(1), equality(true), equality(false));
// Defaults are chosen after testing every case, regardless of their position.
function choose(value: unknown): number {
switch (value) {
default: return 0;
case probe(1): return 1;
case probe(2): return 2;
}
}
trace = [];
console.log(choose(2), trace.join(","));
trace = [];
console.log(choose(3), trace.join(","));
@@ -0,0 +1,84 @@
// Recursive union arms can have identical stored layouts while carrying
// different literal discriminants. Rebuilding one must keep its own arm.
type Expression =
| { kind: "number"; value: number }
| { kind: "binary"; op: "+" | "-"; left: Expression; right: Expression }
| { kind: "logical"; op: "and" | "or"; left: Expression; right: Expression };
type Binary = Extract<Expression, { kind: "binary" }>;
type Logical = Extract<Expression, { kind: "logical" }>;
function binary(expr: Binary): number {
const left = evaluate(expr.left);
const right = evaluate(expr.right);
return expr.op === "+" ? left + right : left - right;
}
function logical(expr: Logical): number {
const left = evaluate(expr.left);
return expr.op === "and" ? (left ? evaluate(expr.right) : left) : (left || evaluate(expr.right));
}
function evaluate(expr: Expression): number {
switch (expr.kind) {
case "number": return expr.value;
case "binary": return binary(expr);
case "logical": return logical(expr);
}
}
function rewrite(expr: Expression): Expression {
switch (expr.kind) {
case "number": return { ...expr, value: expr.value + 1 };
case "binary": return { ...expr, left: rewrite(expr.left), right: rewrite(expr.right) };
case "logical": return { ...expr, left: rewrite(expr.left), right: rewrite(expr.right) };
}
}
const left: Expression = { kind: "number", value: 3 };
const right: Expression = { kind: "number", value: 5 };
const sum: Binary = { kind: "binary", op: "+", left, right };
const difference: Binary = { kind: "binary", op: "-", left: sum, right };
const both: Logical = { kind: "logical", op: "and", left: difference, right: sum };
const either: Logical = { kind: "logical", op: "or", left: both, right };
for (const input of [sum, difference, both, either] as Expression[]) {
const output = rewrite(input);
console.log(input.kind, evaluate(input), output.kind, evaluate(output));
}
// Last-write-wins applies to discriminants as well as ordinary fields.
function asBinary(expr: Logical): Expression {
return { ...expr, kind: "binary", op: "+" };
}
function asLogical(expr: Binary): Expression {
return { ...expr, kind: "logical", op: "and" };
}
console.log("overridden", evaluate(asBinary(both)), evaluate(asLogical(sum)));
// Explicit fields before the spread are replaced by the source's fields.
function before(expr: Binary): Expression {
const initial = { kind: "logical" as const, op: "or" as const };
return { ...initial, ...expr };
}
console.log("spread-order", evaluate(before(difference)));
// Rebuilds allocate a new parent and retain the original child objects.
function shallow(expr: Expression): Expression {
switch (expr.kind) {
case "number": return { ...expr };
case "binary": return { ...expr, op: "+" };
case "logical": return { ...expr, op: "or" };
}
}
const copied = shallow(sum);
if (copied.kind === "binary") {
console.log("identity", copied === sum, copied.left === left, copied.right === right);
}
// Selecting the destination arm must not evaluate a source or override.
const effects: string[] = [];
function source(): Binary { effects.push("source"); return sum; }
function operand(name: string, value: number): Expression {
effects.push(name);
return { kind: "number", value };
}
function ordered(): Expression {
return { ...source(), left: operand("left", 10), right: operand("right", 20) };
}
console.log("evaluation", evaluate(ordered()), effects.join(","));
+3 -3
View File
@@ -29,9 +29,9 @@ const tmp = mkdtempSync("/tmp/scr-");
const raw = readFileSync("/etc/hosts").toString(tty ? "hex" : "latin1"); // runtime BufferEncoding selection lowers too const raw = readFileSync("/etc/hosts").toString(tty ? "hex" : "latin1"); // runtime BufferEncoding selection lowers too
const intervalWithSignal = promiseInterval(1, "tick", {}); // AbortSignal options stay fenced const intervalWithSignal = promiseInterval(1, "tick", {}); // AbortSignal options stay fenced
// The computed encoding is now part of the static Buffer surface. The // The computed encoding is now part of the static Buffer surface. The
// one-shot zlib/raw/gzip codecs lower for strings and Buffers; explicit // one-shot zlib/raw/gzip codecs also accept literal compression levels;
// options stay fenced, while Brotli remains an explicit member fence. // other options stay fenced, while Brotli remains an explicit member fence.
import { brotliCompressSync, deflateSync } from "node:zlib"; import { brotliCompressSync, deflateSync } from "node:zlib";
const packed = deflateSync("data", { level: 9 }); // options remain fenced const packed = deflateSync("data", { level: 9, strategy: 1 }); // strategy remains fenced
const brotli = brotliCompressSync(Buffer.from("data")); // outside the lowered family: fenced const brotli = brotliCompressSync(Buffer.from("data")); // outside the lowered family: fenced
// The remaining imports continue the declared-but-not-lowered surface. // The remaining imports continue the declared-but-not-lowered surface.
+3 -3
View File
@@ -46,11 +46,11 @@ console.log(win32.sep);
const u = new URL("https://example.com/x?a=1"); const u = new URL("https://example.com/x?a=1");
console.log(u.toJSON()); console.log(u.toJSON());
u.searchParams.get("a"); u.searchParams.get("a");
/* The one-shot zlib/raw/gzip codecs lower for strings and Buffers; /* The one-shot zlib/raw/gzip codecs also accept literal compression levels;
* explicit options remain fenced, while Brotli remains a member-qualified * other options remain fenced, while Brotli remains a member-qualified
* fence with the lowered family named. */ * fence with the lowered family named. */
import { brotliCompressSync, deflateSync } from "zlib"; import { brotliCompressSync, deflateSync } from "zlib";
deflateSync("data", { level: 9 }); deflateSync("data", { level: 9, strategy: 1 });
brotliCompressSync(Buffer.from("data")); brotliCompressSync(Buffer.from("data"));
/* The http2 compatibility slice's @types/node-world fences (divergence /* The http2 compatibility slice's @types/node-world fences (divergence
* 56): the SNICallback option fences by name with the serve-one-pair * 56): the SNICallback option fences by name with the serve-one-pair
+13
View File
@@ -0,0 +1,13 @@
import { readFileSync, writeFileSync } from "node:fs";
import { deserializeModule } from "../../../packages/compiler/src/ir/serialize.js";
import { emitCModule } from "../../../packages/compiler/src/backend/c/c-emitter.js";
// Exercise the production entry point, including scalarization, metadata,
// ownership analysis, helper discovery and complete translation-unit output.
const module = deserializeModule(readFileSync(process.argv[2]!, "utf8"));
const sources = new Map<string, string>();
if (process.argv.length > 4) {
const inputs = JSON.parse(readFileSync(process.argv[4]!, "utf8")) as { file: string; text: string }[];
for (const input of inputs) sources.set(input.file, input.text);
}
writeFileSync(process.argv[3]!, emitCModule(module, undefined, { debugSources: sources }));
+6 -6
View File
@@ -1,4 +1,4 @@
import { streamRooted, unionWideningTags, type IrClassGraphNode } from "../../../packages/compiler/src/ir/analysis.js"; import { streamRooted, unionWideningTags, type IrClassAncestry } from "../../../packages/compiler/src/ir/analysis.js";
import { BOOL, F64, STRING, VOID, isRefCounted, typeEquals, type IrType } from "../../../packages/compiler/src/ir/ir.js"; import { BOOL, F64, STRING, VOID, isRefCounted, typeEquals, type IrType } from "../../../packages/compiler/src/ir/ir.js";
// These are the compiler's real type comparison and ownership queries. // These are the compiler's real type comparison and ownership queries.
@@ -82,9 +82,9 @@ console.log("empty", unionWideningTags([], wider)?.length);
// Hierarchy traversal uses real recursive graph interfaces and nullable // Hierarchy traversal uses real recursive graph interfaces and nullable
// parents. Query several depths and distinct runtime/user roots. // parents. Query several depths and distinct runtime/user roots.
const plain: IrClassGraphNode = { def: { name: "Plain" }, base: null, children: [] }; const plain: IrClassAncestry = { def: { name: "Plain" }, base: null };
const stream: IrClassGraphNode = { def: { name: "%Readable" }, base: null, children: [] }; const stream: IrClassAncestry = { def: { name: "%Readable" }, base: null };
const child: IrClassGraphNode = { def: { name: "Child" }, base: stream, children: [] }; const child: IrClassAncestry = { def: { name: "Child" }, base: stream };
const grandchild: IrClassGraphNode = { def: { name: "Grandchild" }, base: child, children: [] }; const grandchild: IrClassAncestry = { def: { name: "Grandchild" }, base: child };
const unrelated: IrClassGraphNode = { def: { name: "Unrelated" }, base: plain, children: [] }; const unrelated: IrClassAncestry = { def: { name: "Unrelated" }, base: plain };
console.log("roots", streamRooted(plain), streamRooted(stream), streamRooted(child), streamRooted(grandchild), streamRooted(unrelated)); console.log("roots", streamRooted(plain), streamRooted(stream), streamRooted(child), streamRooted(grandchild), streamRooted(unrelated));
@@ -25,18 +25,18 @@ node-fallback-fence.ts:30:28 - error SC2020: 'timers/promises.setInterval with 3
hint: the lowered form is setInterval(delay, value) with an explicit yielded value; AbortSignal options are not supported yet hint: the lowered form is setInterval(delay, value) with an explicit yielded value; AbortSignal options are not supported yet
node-fallback-fence.ts:35:36 - error SC2020: 'deflateSync with explicit options' is part of the standard library types but has no scriptc lowering yet node-fallback-fence.ts:35:36 - error SC2020: 'deflateSync with these options' is part of the standard library types but has no scriptc lowering yet
34 | import { brotliCompressSync, deflateSync } from "node:zlib"; 34 | import { brotliCompressSync, deflateSync } from "node:zlib";
35 | const packed = deflateSync("data", { level: 9 }); // options remain fenced 35 | const packed = deflateSync("data", { level: 9, strategy: 1 }); // strategy remains fenced
| ^~~~~~~~~~~~ | ^~~~~~~~~~~~~~~~~~~~~~~~~
36 | const brotli = brotliCompressSync(Buffer.from("data")); // outside the lowered family: fenced 36 | const brotli = brotliCompressSync(Buffer.from("data")); // outside the lowered family: fenced
hint: deflateSync(data) with Node's default options is supported hint: the static compression options form is { level: <integer literal from -1 through 9> }
node-fallback-fence.ts:36:16 - error SC2020: 'zlib.brotliCompressSync' is part of the standard library types but has no scriptc lowering yet node-fallback-fence.ts:36:16 - error SC2020: 'zlib.brotliCompressSync' is part of the standard library types but has no scriptc lowering yet
35 | const packed = deflateSync("data", { level: 9 }); // options remain fenced 35 | const packed = deflateSync("data", { level: 9, strategy: 1 }); // strategy remains fenced
36 | const brotli = brotliCompressSync(Buffer.from("data")); // outside the lowered family: fenced 36 | const brotli = brotliCompressSync(Buffer.from("data")); // outside the lowered family: fenced
| ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ | ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
37 | // The remaining imports continue the declared-but-not-lowered surface. 37 | // The remaining imports continue the declared-but-not-lowered surface.
@@ -88,18 +88,18 @@ fenced.ts:47:13 - error SC2020: 'URL.toJSON' is typed by @types/node but has no
hint: protocol, origin, username, password, pathname, href, host, hostname, port, search, hash, searchParams, and toString() are the supported URL members hint: protocol, origin, username, password, pathname, href, host, hostname, port, search, hash, searchParams, and toString() are the supported URL members
fenced.ts:53:21 - error SC2020: 'deflateSync with explicit options' is part of the standard library types but has no scriptc lowering yet fenced.ts:53:21 - error SC2020: 'deflateSync with these options' is part of the standard library types but has no scriptc lowering yet
52 | import { brotliCompressSync, deflateSync } from "zlib"; 52 | import { brotliCompressSync, deflateSync } from "zlib";
53 | deflateSync("data", { level: 9 }); 53 | deflateSync("data", { level: 9, strategy: 1 });
| ^~~~~~~~~~~~ | ^~~~~~~~~~~~~~~~~~~~~~~~~
54 | brotliCompressSync(Buffer.from("data")); 54 | brotliCompressSync(Buffer.from("data"));
hint: deflateSync(data) with Node's default options is supported hint: the static compression options form is { level: <integer literal from -1 through 9> }
fenced.ts:54:1 - error SC2020: 'zlib.brotliCompressSync' is typed by @types/node but has no scriptc lowering yet fenced.ts:54:1 - error SC2020: 'zlib.brotliCompressSync' is typed by @types/node but has no scriptc lowering yet
53 | deflateSync("data", { level: 9 }); 53 | deflateSync("data", { level: 9, strategy: 1 });
54 | brotliCompressSync(Buffer.from("data")); 54 | brotliCompressSync(Buffer.from("data"));
| ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ | ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
55 | /* The http2 compatibility slice's @types/node-world fences (divergence 55 | /* The http2 compatibility slice's @types/node-world fences (divergence
@@ -0,0 +1,53 @@
import { deflateRawSync } from "node:zlib";
import { expect, test } from "vitest";
import { cStringLiteral } from "../../packages/compiler/src/backend/c/types.js";
import { normalizedEmbeddingC } from "./self-hosting-c-embedding.js";
import { cEmitterCases } from "./self-hosting-c-emitter-cases.js";
import { emitCModule } from "@scriptc/compiler";
const item = cEmitterCases().find((item) => item.name === "level-nine module and facade compression")!;
const original = emitCModule(item.module);
const declaration = /(static const char sc_npm_src_0\[\] = [^\n]*\n)((?:\s*"(?:[^"\\]|\\.)*"\s*)+);/;
function withSource(bytes: Buffer): string {
return original.replace(declaration, (_match, prefix: string) => `${prefix} ${cStringLiteral(bytes)};`);
}
test("different valid DEFLATE encodings compare by their full source bytes", () => {
const source = item.module.embedded!.modules[0]!.source;
const stored = deflateRawSync(source, { level: 0 });
const compressed = deflateRawSync(source, { level: 9 });
expect(stored.equals(compressed)).toBe(false);
expect(normalizedEmbeddingC(withSource(stored), item.module)).toBe(normalizedEmbeddingC(withSource(compressed), item.module));
});
test("embedding comparison rejects a corrupted compressed payload", () => {
expect(() => normalizedEmbeddingC(withSource(Buffer.from([255, 255, 255])), item.module)).toThrow();
});
test("embedding comparison rejects a valid stream containing the wrong source", () => {
const wrong = deflateRawSync("a different module", { level: 9 });
expect(() => normalizedEmbeddingC(withSource(wrong), item.module)).toThrow("embedded source differs");
});
test("embedding comparison checks the raw byte length including UTF-8", () => {
const wrong = original.replace(/(sc_npm_src_0, sizeof sc_npm_src_0 - 1, )\d+/, "$11");
expect(() => normalizedEmbeddingC(wrong, item.module)).toThrow("incorrect raw length");
});
test("embedding comparison checks every facade as well as the module", () => {
const changed = { ...item.module, embedded: {
...item.module.embedded!,
modules: [{ ...item.module.embedded!.modules[0]!, esm: "export default 'wrong';" }],
} };
expect(() => normalizedEmbeddingC(original, changed)).toThrow("embedded source differs for sc_npm_esm_0");
});
test("normalization preserves runtime setup and edge condition metadata", () => {
const actual = normalizedEmbeddingC(original, item.module);
expect(actual).toContain("scr_zlib_inflate_exact");
expect(actual).toContain("sc_npm_edges");
expect(actual).toContain('"/app/main.js", "large", "/node_modules/large/index.js", 2');
const changed = original.replace("scr_zlib_inflate_exact", "incorrect_inflater");
expect(normalizedEmbeddingC(changed, item.module)).not.toBe(actual);
});
+68
View File
@@ -0,0 +1,68 @@
import { inflateRawSync } from "node:zlib";
import type { IrModule } from "../../packages/compiler/src/ir/ir.js";
/** Decode the C backend's byte-string literal grammar. This intentionally
* accepts only its emitted forms, so a malformed escape or lost chunk fails
* the native-emitter comparison instead of being normalized away. */
function literalBytes(literals: string): Buffer {
const bytes: number[] = [];
let offset = 0;
while (offset < literals.length) {
if (/\s/.test(literals[offset]!)) { offset++; continue; }
if (literals[offset++] !== '"') throw new Error("expected C string chunk");
let closed = false;
while (offset < literals.length) {
const ch = literals[offset++]!;
if (ch === '"') { closed = true; break; }
if (ch !== "\\") {
const code = ch.charCodeAt(0);
if (code < 32 || code > 126) throw new Error("non-ASCII C literal byte");
bytes.push(code);
continue;
}
const escape = literals[offset++]!;
if (/[0-7]/.test(escape)) {
const octal = escape + literals.slice(offset, offset + 2);
if (!/^[0-7]{3}$/.test(octal)) throw new Error("invalid C octal byte");
const byte = parseInt(octal, 8);
if (byte > 255) throw new Error("C octal escape exceeds one byte");
bytes.push(byte);
offset += 2;
} else if (escape === '"' || escape === "\\" || escape === "?") {
bytes.push(escape.charCodeAt(0));
} else {
throw new Error(`unexpected C escape ${escape}`);
}
}
if (!closed) throw new Error("unterminated C string chunk");
}
return Buffer.from(bytes);
}
/** zlib implementations can choose different valid DEFLATE streams. Verify
* every byte of each inflated source/facade and its raw-length metadata,
* then normalize only the encoded literal for the C text comparison. All
* declarations, table rows, edge conditions and runtime setup remain exact. */
export function normalizedEmbeddingC(text: string, mod: IrModule): string {
let normalized = text;
const check = (name: string, source: string): void => {
const declaration = new RegExp(`(static const char ${name}\\[\\] = [^\\n]*\\n)((?:\\s*"(?:[^"\\\\]|\\\\.)*"\\s*)+);`);
const match = declaration.exec(normalized);
if (!match) throw new Error(`missing embedded declaration ${name}`);
const metadata = new RegExp(`\\b${name}, sizeof ${name} - 1, (\\d+)`).exec(normalized);
if (!metadata) throw new Error(`missing raw-length metadata for ${name}`);
const rawLength = Number(metadata[1]);
const encoded = literalBytes(match[2]!);
const bytes = rawLength === 0 ? encoded : inflateRawSync(encoded);
const expected = Buffer.from(source, "utf8");
if (!bytes.equals(expected)) throw new Error(`embedded source differs for ${name}`);
if (rawLength !== 0 && rawLength !== bytes.length) throw new Error(`incorrect raw length for ${name}`);
const canonical = `"${bytes.toString("base64")}"`;
normalized = normalized.replace(declaration, (_all, prefix: string) => `${prefix} ${canonical};`);
};
for (const [index, module] of (mod.embedded?.modules ?? []).entries()) {
check(`sc_npm_src_${index}`, module.source);
if (module.esm !== undefined) check(`sc_npm_esm_${index}`, module.esm);
}
return normalized;
}
@@ -0,0 +1,233 @@
import {
BOOL, F64, STRING, UNDEFINED_T, VOID, arrayOf, mapOf,
type IrClassDef, type IrExpr, type IrFunction, type IrModule, type IrStmt, type IrType,
} from "../../packages/compiler/src/ir/ir.js";
import { boolLit, numLit, strLit, varRef } from "../../packages/compiler/src/ir/build.js";
import { IR_VERSION } from "../../packages/compiler/src/ir/serialize.js";
export interface CEmitterCase {
name: string;
module: IrModule;
sources: { file: string; text: string }[];
contains: string[];
excludes?: string[];
}
const loc = { file: "native emitter/日本.ts", start: 0, end: 1 };
const record = (shapeId: string): IrType => ({ kind: "record", shapeId });
const object = (className: string): IrType => ({ kind: "object", className });
const union = (unionId: string): IrType => ({ kind: "union", unionId });
function module(): IrModule {
return {
irVersion: IR_VERSION, sourceFile: loc.file, entry: "main",
functions: [{ name: "main", params: [], locals: [], returnType: VOID, body: [], loc }],
};
}
function effect(expr: IrExpr): IrStmt {
return { kind: "exprStmt", expr, loc: expr.loc };
}
function method(cls: string, member: string): IrFunction {
return {
name: `%${cls}.${member}`,
params: [{ localId: "this", name: "this", type: object(cls) }],
locals: [{ id: "this", name: "this", type: object(cls), mutable: false }],
returnType: VOID, body: [], loc,
};
}
/** Small, hand-built inputs cover code-generation branches that ordinary
* TypeScript sources cannot request independently (unused layouts, exact
* library ABI metadata, module compression and literal interning). The
* execution cases in the harness separately check emitted program behavior. */
export function cEmitterCases(): CEmitterCase[] {
const cases: CEmitterCase[] = [];
const add = (name: string, mod: IrModule, contains: string[], sources: CEmitterCase["sources"] = []): CEmitterCase => {
const item = { name, module: mod, contains, sources };
cases.push(item);
return item;
};
add("empty executable", module(), ["int main(", "scr_runtime.h"]);
const strings = module();
strings.functions[0]!.body = [
effect(strLit("quote\" slash\\ newline\n tab\t nul\u0000 end", loc)),
effect(strLit("日本 π 😀 \ud800", loc)),
effect(strLit("trigraph??/comment*/", loc)),
effect(strLit("repeat", loc)),
effect(strLit("repeat", loc)),
];
add("literal escaping and interning", strings, ["sc_lit_", "ScrStr", "scr_str_retain"]);
const numbers = module();
numbers.functions[0]!.body = [-0, 0, 1, -1, Number.MAX_VALUE, Number.MIN_VALUE, Infinity, -Infinity]
.map((value) => effect(numLit(value, loc)));
numbers.functions[0]!.body.push(effect({ kind: "bin", op: "/", left: numLit(0, loc), right: numLit(0, loc), type: F64, loc }));
add("floating point constants", numbers, ["INFINITY", " / ", "-0.0"]);
const source = module();
source.functions[0]!.body = [effect(strLit("line two", { ...loc, start: 12, end: 20 }))];
add("source locations with multibyte text", source, ["#line 2", "native emitter"], [
{ file: loc.file, text: "// 日本\nconsole.log('line two');\n" },
]);
const records = module();
records.records = [
{ id: "scalar", fields: [{ name: "flag", type: BOOL }, { name: "value", type: F64 }] },
{ id: "references", fields: [
{ name: "list", type: arrayOf(STRING) },
{ name: "text", type: STRING },
] },
{ id: "tuple", tuple: true, fields: [{ name: "0", type: STRING }, { name: "1", type: F64 }] },
];
add("scalar, reference and tuple layouts", records, ["scr_str_release", "scr_arr_release", "record tuple"]);
const recursive = module();
recursive.records = [
{ id: "node", fields: [{ name: "children", type: arrayOf(record("node")) }, { name: "text", type: STRING }] },
];
add("recursive record trace and teardown", recursive, ["scr_cyc_alloc", "visit(o->sc_fld_children, ctx)", "scr_str_release"]);
const mutual = module();
mutual.records = [
{ id: "left", fields: [{ name: "next", type: union("rightMaybe") }] },
{ id: "right", fields: [{ name: "next", type: union("leftMaybe") }] },
];
mutual.unions = [
{ id: "rightMaybe", arms: [record("right"), UNDEFINED_T] },
{ id: "leftMaybe", arms: [record("left"), UNDEFINED_T] },
];
add("mutually recursive optional records", mutual, ["visit(o->sc_fld_next, ctx)", "scr_union_release", "scr_cyc_free"]);
const overflow = module();
overflow.records = [{ id: "dictionary", fields: [{ name: "label", type: STRING }], indexValue: record("dictionary") }];
add("recursive overflow maps", overflow, ["sc_ovf", "visit(o->sc_ovf, ctx)", "scr_map_release"]);
const cls = (name: string, base?: string): IrClassDef => ({
name, ...(base === undefined ? {} : { base }),
fields: [{ name: "label", type: STRING }], methods: ["visit"], loc,
});
const classes = module();
classes.classes = [cls("Root"), cls("Left", "Root"), cls("Right", "Root"), cls("Leaf", "Left")];
for (const c of classes.classes) classes.functions.push(method(c.name, "visit"));
add("class forest and virtual dispatch tables", classes, ["sc_vtable_", "sc_vm_", "sc_vtt_", "class Root", "class Leaf"]);
const abstract = module();
abstract.classes = [
{ ...cls("Abstract"), abstract: true, abstractMethods: ["visit"] },
cls("Concrete", "Abstract"),
];
abstract.functions.push(method("Concrete", "visit"));
add("abstract method ABI from descendant", abstract, ["class Abstract", "class Concrete", "sc_vm_"]);
const globals = module();
globals.globals = [
{ id: "%g.text", name: "text", type: STRING, mutable: true },
{ id: "%g.values", name: "values", type: arrayOf(F64), mutable: true },
];
globals.functions[0]!.body = [
{ kind: "assign", localId: "%g.text", value: strLit("global", loc), loc },
{ kind: "assign", localId: "%g.values", value: { kind: "arrayLit", elems: [numLit(1, loc)], type: arrayOf(F64), loc }, loc },
];
add("global initialization and shutdown ownership", globals, ["scr_str_release", "scr_arr_release", "global"]);
const constantTable = module();
constantTable.globals = [{ id: "%g.table", name: "table", type: arrayOf(F64), mutable: false }];
constantTable.functions[0]!.body = [
{ kind: "assign", localId: "%g.table", value: { kind: "arrayLit", elems: [1, 4, 9, 16].map((n) => numLit(n, loc)), type: arrayOf(F64), loc }, loc },
effect({ kind: "arrayGet", arr: varRef("%g.table", arrayOf(F64), loc), index: numLit(2, loc), type: F64, loc }),
];
add("constant numeric array storage", constantTable, ["static", "scr_arr_push_f64"]);
const labels = module();
labels.functions[0]!.body = [{
kind: "block", labels: ["outer"], body: [{
kind: "switch", disc: boolLit(true, loc), labels: ["choose"], cases: [
{ test: boolLit(false, loc), body: [effect(strLit("skipped", loc))] },
{ test: null, body: [{ kind: "break", label: "outer", loc }] },
], loc,
}], loc,
}];
add("switch and block jump labels", labels, ["goto sc_end_", "sc_swcase_"]);
const closure = module();
const closureType: IrType = { kind: "func", params: [], ret: STRING };
closure.functions.push({
name: "reader", params: [], locals: [{ id: "message", name: "message", type: STRING, mutable: true, boxed: true }], returnType: STRING,
captures: [{ localId: "message", name: "message", type: STRING }],
body: [{ kind: "return", value: varRef("message", STRING, loc), loc }], loc,
});
closure.functions[0]!.locals = [{ id: "message", name: "message", type: STRING, mutable: true, boxed: true }];
closure.functions[0]!.body = [
{ kind: "varDecl", localId: "message", init: strLit("capture", loc), loc },
effect({ kind: "closure", fnName: "reader", captures: ["message"], type: closureType, loc }),
];
add("closure capture boxes", closure, ["scr_box_get_ref", "scr_box_retain", "scr_closure_new"]);
const unions = module();
unions.unions = [{ id: "scalar", arms: [BOOL, F64, STRING, UNDEFINED_T] }];
unions.functions[0]!.body = [
effect({ kind: "unionWrap", unionId: "scalar", tag: 2, value: strLit("payload", loc), type: union("scalar"), loc }),
effect({ kind: "unionWrap", unionId: "scalar", tag: 3, value: { kind: "unitLit", unit: "undefined", type: UNDEFINED_T, loc }, type: union("scalar"), loc }),
];
add("union scalar payloads and immortal units", unions, ["scr_union_new_ref", "sc_unit_", "SIZE_MAX"]);
const dictionary = module();
const mapType = mapOf(STRING, F64);
dictionary.functions[0]!.body = [effect({ kind: "mapNew", type: mapType, loc })];
add("typed map construction", dictionary, ["scr_map_new"]);
const shortNpm = module();
shortNpm.embedded = {
modules: [
{ key: "/node_modules/tiny/index.js", format: "esm", source: "export const n = 1;" },
{ key: "/node_modules/tiny/config.json", format: "json", source: '{"name":"π"}' },
{ key: "/node_modules/tiny/empty.js", format: "cjs", source: "", esm: "export default {};" },
],
edges: [
{ from: "/node_modules/tiny/index.js", specifier: "./config.json", to: "/node_modules/tiny/config.json", kind: "import" },
{ from: "/node_modules/tiny/index.js", specifier: "node:path", to: "node:path", kind: "any" },
],
};
add("uncompressed npm, JSON and CJS facade tables", shortNpm, ["sc_npm_src_0", "sc_npm_esm_2", "sc_npm_edges", "export const n"]);
const compressed = module();
compressed.embedded = {
modules: [{ key: "/node_modules/large/index.js", format: "cjs", source: "exports.value = 'repeat π';\n".repeat(2000), esm: "export const value = 'repeat π';\n".repeat(2000) }],
edges: [
{ from: "/app/main.js", specifier: "large", to: "/node_modules/large/index.js", kind: "require" },
{ from: "/app/main.js", specifier: "large", to: "/node_modules/large/index.js", kind: "import" },
],
};
add("level-nine module and facade compression", compressed, ["sc_npm_src_0", "sc_npm_esm_0", "scr_zlib_inflate_exact"]);
const chunked = module();
// A deterministic byte distribution prevents this source from being a
// single repeated run while forcing several source-literal chunks.
let state = 1;
let text = "";
for (let i = 0; i < 12_000; i++) {
state = (Math.imul(state, 1664525) + 1013904223) >>> 0;
text += String.fromCharCode(32 + (state % 95));
}
chunked.embedded = { modules: [{ key: "/node_modules/random/data.json", format: "json", source: JSON.stringify(text) }], edges: [] };
add("large source literal chunk boundaries", chunked, ["sc_npm_src_0", "ScrIslandModule"]);
const library = module();
library.lib = {
profileName: "native-test", prefix: "native_", initSymbol: "native_init",
sinkRegisterSymbol: "native_sink", collectSymbol: "native_collect", resultResetSymbol: "native_reset",
threadInstances: false, exports: [], trapOverlays: [],
identity: { buildIdSymbol: "native_build_id", abiVersionSymbol: "native_abi", buildId: "0123456789abcdef", abiVersion: 7 },
};
add("library entry points and identity constants", library, ["native_init", "native_collect", "native_reset", "native_build_id", "0123456789abcdef"]).excludes = ["int main("];
const threaded = module();
threaded.lib = { ...library.lib, threadInstances: true };
threaded.globals = [{ id: "%g.counter", name: "counter", type: F64, mutable: true }];
threaded.functions[0]!.body = [{ kind: "assign", localId: "%g.counter", value: numLit(0, loc), loc }];
add("thread-local library globals", threaded, ["_Thread_local", "native_init"]);
return cases;
}
@@ -0,0 +1,154 @@
import { execFile, spawnSync } from "node:child_process";
import { mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { fileURLToPath, pathToFileURL } from "node:url";
import { promisify } from "node:util";
import { expect, test } from "vitest";
import { compile, compileC, deserializeModule, emitCModule, serializeModule, validateModule } from "@scriptc/compiler";
import {
moduleUsesAssert, moduleUsesBigInt, moduleUsesCopying, moduleUsesDynInvoke, moduleUsesInspect,
moduleUsesRegex, moduleUsesSymbol, moduleUsesZlib, type IrModule,
} from "../../packages/compiler/src/ir/ir.js";
import { cEmitterCases } from "./self-hosting-c-emitter-cases.js";
import { normalizedEmbeddingC } from "./self-hosting-c-embedding.js";
const root = fileURLToPath(new URL("../..", import.meta.url));
const entry = join(root, "tests/fixtures/self-hosting/c-emitter.ts");
const execFileAsync = promisify(execFile);
const runOptions = { cwd: root, timeout: 60_000, maxBuffer: 128 * 1024 * 1024 };
const sanitize = process.env["SCRIPTC_SAN"] === "1";
function nativeFeatures(mod: IrModule) {
return {
regex: moduleUsesRegex(mod), copying: moduleUsesCopying(mod),
inspect: moduleUsesInspect(mod), dynInvoke: moduleUsesDynInvoke(mod),
symbol: moduleUsesSymbol(mod), bigint: moduleUsesBigInt(mod), zlib: moduleUsesZlib(mod),
assert: moduleUsesAssert(mod),
};
}
// This list exercises generated code, not only helper text. It spans the
// emitter's statement, expression, ownership, async, class and dynamic-value
// paths while keeping bootstrap validation a focused test of this stage.
const programs = [
"001-hello.ts",
"101-arithmetic.ts",
"600-closures-basic.ts",
"711-inheritance-dispatch.ts",
"756-cycle-inheritance.ts",
"803-switch-rc-stress.ts",
"1005-json-nested.ts",
"1023-async-rc-stress.ts",
"1401-typedarray-slice-set.ts",
"1452-return-through-finally.ts",
"1672-symbol-containers.ts",
"2012-generators-return-throw.ts",
"2537-destructuring-assign-member-targets.ts",
"2840-finally-completions.ts",
"2905-bigint-edges.ts",
"3103-scalar-record-nested-control-flow.ts",
"3104-native-analysis-tables-loops.ts",
"3109-identity-union-collections.ts",
"3112-union-switch-control-flow.ts",
"3113-union-array-removal.ts",
"3114-union-record-field-write.ts",
"3115-zlib-static-compression-levels.ts",
"3116-sequence-expression-lifetimes.ts",
"3117-unknown-switch-control-flow.ts",
"3118-recursive-union-spread.ts",
];
for (const backend of ["c", "llvm"] as const) {
test(`the complete C emitter runs natively (${backend})`, async () => {
const dir = mkdtempSync(join(process.platform === "win32" ? tmpdir() : "/tmp", "scriptc-c-emitter-"));
const executable = (name: string) => join(dir, name + (process.platform === "win32" ? ".exe" : ""));
const stage = executable("emitter");
try {
// The frontend can spend longer than Vitest's worker-RPC deadline in
// synchronous lowering. Run the same source API in an awaited child.
// Compile without dynamic fallback and keep its IR for the fixed point.
const api = pathToFileURL(join(root, "packages/compiler/src/index.ts")).href;
const { stdout } = await execFileAsync(process.execPath, [
"--import", "tsx", "--input-type=module", "--eval",
`import { compile } from ${JSON.stringify(api)};
const result = await compile(process.argv[1], {
outDir: process.argv[2], outPath: process.argv[3], backend: process.argv[4],
dynamic: false, optimization: 'dev', sanitize: process.argv[5] === '1', emitIr: true,
});
console.log(JSON.stringify(result));`,
entry, dir, stage, backend, sanitize ? "1" : "0",
], { ...runOptions, timeout: 600_000 });
const built = JSON.parse(stdout) as Awaited<ReturnType<typeof compile>>;
if (!built.ok) throw new Error(built.diagnostics.map((d) => `${d.code}: ${d.message}`).join("\n"));
if (!("binaryPath" in built)) throw new Error("bootstrap did not produce an executable");
expect(built.backend).toBe(backend);
expect(built.llvmRefusal).toBeUndefined();
expect(built.irPath).toBeDefined();
const ownIr = deserializeModule(readFileSync(built.irPath!, "utf8"));
expect(ownIr.functions.length).toBeGreaterThan(500);
expect(validateModule(ownIr)).toEqual([]);
const emit = async (mod: IrModule, name: string, sources: { file: string; text: string }[] = []): Promise<string> => {
const input = join(dir, "input.json");
const output = join(dir, "native.c");
const sourcePath = join(dir, "sources.json");
writeFileSync(input, serializeModule(mod));
writeFileSync(sourcePath, JSON.stringify(sources));
const result = await execFileAsync(stage, [input, output, sourcePath], { ...runOptions, timeout: 300_000 })
.catch((cause: unknown) => { throw new Error(`native C emission failed for ${name}`, { cause }); });
expect(result.stdout, name).toBe("");
expect(result.stderr, name).toBe("");
const text = readFileSync(output, "utf8");
const expected = emitCModule(mod, undefined, { debugSources: new Map(sources.map((s) => [s.file, s.text])) });
expect(normalizedEmbeddingC(text, mod), name).toBe(normalizedEmbeddingC(expected, mod));
return text;
};
for (const item of cEmitterCases()) {
expect(validateModule(item.module), item.name).toEqual([]);
const text = await emit(item.module, item.name, item.sources);
for (const fragment of item.contains) expect(text, item.name).toContain(fragment);
for (const fragment of item.excludes ?? []) expect(text, item.name).not.toContain(fragment);
}
for (const source of programs) {
const sourcePath = join(root, "tests/corpus", source);
const irPath = join(dir, "program.ir.json");
const lowered = await compile(sourcePath, { outDir: dir, outPath: irPath, outputKind: "ir", dynamic: false });
if (!lowered.ok) throw new Error(`${source}: ${lowered.diagnostics.map((d) => d.message).join("\n")}`);
const mod = deserializeModule(readFileSync(irPath, "utf8"));
const text = await emit(mod, source, [{ file: sourcePath, text: readFileSync(sourcePath, "utf8") }]);
const cPath = join(dir, "program.c");
const outPath = executable("program");
writeFileSync(cPath, text);
await compileC({ cPath, outPath, sanitize, optimization: "dev", ...nativeFeatures(mod) });
const node = spawnSync(process.execPath, [sourcePath], runOptions);
const native = spawnSync(outPath, [], runOptions);
for (const result of [node, native]) {
expect(result.error, source).toBeUndefined();
expect(result.signal, `${source}: ${result.stderr}`).toBeNull();
expect(result.status, `${source}: ${result.stderr}`).toBe(0);
}
expect(native.stdout, source).toEqual(node.stdout);
expect(native.stderr, source).toEqual(node.stderr);
}
// Bootstrap the backend itself: the native stage emits its own IR,
// then the resulting executable emits that IR again. Exact C equality
// checks the second generation without depending on linker timestamps.
const selfC = await emit(ownIr, "emitter emits itself");
const selfPath = join(dir, "self.c");
const secondStage = executable("emitter-second");
writeFileSync(selfPath, selfC);
await compileC({ cPath: selfPath, outPath: secondStage, sanitize, optimization: "dev", ...nativeFeatures(ownIr) });
const again = join(dir, "again.c");
const second = await execFileAsync(secondStage, [join(dir, "input.json"), again, join(dir, "sources.json")], { ...runOptions, timeout: 300_000 });
expect(second.stdout).toBe("");
expect(second.stderr).toBe("");
expect(readFileSync(again, "utf8")).toBe(selfC);
} finally {
rmSync(dir, { recursive: true, force: true });
}
}, 900_000);
}